11316d43eSThomas Wolf /* 25a5d85a4SThomas Wolf * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others 31316d43eSThomas Wolf * 45c5f7c6bSMatthias Sohn * This program and the accompanying materials are made available under the 55c5f7c6bSMatthias Sohn * terms of the Eclipse Distribution License v. 1.0 which is available at 65c5f7c6bSMatthias Sohn * https://www.eclipse.org/org/documents/edl-v10.php. 71316d43eSThomas Wolf * 85c5f7c6bSMatthias Sohn * SPDX-License-Identifier: BSD-3-Clause 91316d43eSThomas Wolf */ 101316d43eSThomas Wolf package org.eclipse.jgit.junit.ssh; 111316d43eSThomas Wolf 12*4560bdf7SDavid Ostrovsky import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENTIFICATION_LINES; 13*4560bdf7SDavid Ostrovsky import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENT_LINES_SEPARATOR; 14*4560bdf7SDavid Ostrovsky 151316d43eSThomas Wolf import java.io.ByteArrayInputStream; 161316d43eSThomas Wolf import java.io.IOException; 171316d43eSThomas Wolf import java.io.InputStream; 1824fdc1d0SThomas Wolf import java.io.OutputStream; 1924fdc1d0SThomas Wolf import java.nio.charset.StandardCharsets; 201316d43eSThomas Wolf import java.nio.file.Files; 211316d43eSThomas Wolf import java.nio.file.Path; 221316d43eSThomas Wolf import java.security.GeneralSecurityException; 231316d43eSThomas Wolf import java.security.KeyPair; 241316d43eSThomas Wolf import java.security.PublicKey; 251316d43eSThomas Wolf import java.text.MessageFormat; 261316d43eSThomas Wolf import java.util.ArrayList; 27*4560bdf7SDavid Ostrovsky import java.util.Arrays; 281316d43eSThomas Wolf import java.util.Collections; 291316d43eSThomas Wolf import java.util.List; 3000b235f0SThomas Wolf import java.util.Locale; 3124fdc1d0SThomas Wolf import java.util.concurrent.TimeUnit; 321316d43eSThomas Wolf 33*4560bdf7SDavid Ostrovsky import org.apache.sshd.common.NamedFactory; 3486cee68eSThomas Wolf import org.apache.sshd.common.NamedResource; 35835e3225SThomas Wolf import org.apache.sshd.common.PropertyResolver; 361316d43eSThomas Wolf import org.apache.sshd.common.SshConstants; 371316d43eSThomas Wolf import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; 381316d43eSThomas Wolf import org.apache.sshd.common.config.keys.KeyUtils; 391316d43eSThomas Wolf import org.apache.sshd.common.config.keys.PublicKeyEntryResolver; 401316d43eSThomas Wolf import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory; 41*4560bdf7SDavid Ostrovsky import org.apache.sshd.common.signature.BuiltinSignatures; 42*4560bdf7SDavid Ostrovsky import org.apache.sshd.common.signature.Signature; 431316d43eSThomas Wolf import org.apache.sshd.common.util.buffer.Buffer; 441316d43eSThomas Wolf import org.apache.sshd.common.util.security.SecurityUtils; 4586cee68eSThomas Wolf import org.apache.sshd.common.util.threads.CloseableExecutorService; 4686cee68eSThomas Wolf import org.apache.sshd.common.util.threads.ThreadUtils; 471316d43eSThomas Wolf import org.apache.sshd.server.ServerAuthenticationManager; 48*4560bdf7SDavid Ostrovsky import org.apache.sshd.server.ServerBuilder; 491316d43eSThomas Wolf import org.apache.sshd.server.SshServer; 501316d43eSThomas Wolf import org.apache.sshd.server.auth.UserAuth; 51fd3778b9SThomas Wolf import org.apache.sshd.server.auth.UserAuthFactory; 521316d43eSThomas Wolf import org.apache.sshd.server.auth.gss.GSSAuthenticator; 531316d43eSThomas Wolf import org.apache.sshd.server.auth.gss.UserAuthGSS; 541316d43eSThomas Wolf import org.apache.sshd.server.auth.gss.UserAuthGSSFactory; 5500b235f0SThomas Wolf import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator; 561316d43eSThomas Wolf import org.apache.sshd.server.command.AbstractCommandSupport; 571316d43eSThomas Wolf import org.apache.sshd.server.session.ServerSession; 581316d43eSThomas Wolf import org.apache.sshd.server.shell.UnknownCommand; 59fd3778b9SThomas Wolf import org.apache.sshd.server.subsystem.SubsystemFactory; 60*4560bdf7SDavid Ostrovsky import org.apache.sshd.sftp.server.SftpSubsystemFactory; 611316d43eSThomas Wolf import org.eclipse.jgit.annotations.NonNull; 6221262e98SMatthias Sohn import org.eclipse.jgit.annotations.Nullable; 631316d43eSThomas Wolf import org.eclipse.jgit.lib.Repository; 641316d43eSThomas Wolf import org.eclipse.jgit.transport.ReceivePack; 651316d43eSThomas Wolf import org.eclipse.jgit.transport.RemoteConfig; 661316d43eSThomas Wolf import org.eclipse.jgit.transport.UploadPack; 671316d43eSThomas Wolf 681316d43eSThomas Wolf /** 691316d43eSThomas Wolf * A simple ssh/sftp git <em>test</em> server based on Apache MINA sshd. 701316d43eSThomas Wolf * <p> 711316d43eSThomas Wolf * Supports only a single repository. Authenticates only the given test user 721316d43eSThomas Wolf * against his given test public key. Supports fetch and push. 731316d43eSThomas Wolf * </p> 741316d43eSThomas Wolf * 751316d43eSThomas Wolf * @since 5.2 761316d43eSThomas Wolf */ 771316d43eSThomas Wolf public class SshTestGitServer { 781316d43eSThomas Wolf 7924fdc1d0SThomas Wolf /** 8024fdc1d0SThomas Wolf * Simple echo test command. Replies with the command string as passed. If 8124fdc1d0SThomas Wolf * of the form "echo [int] anything", takes the integer value as a delay in 8224fdc1d0SThomas Wolf * seconds before replying, which may be useful to test various 8324fdc1d0SThomas Wolf * timeout-related things. 8424fdc1d0SThomas Wolf * 8524fdc1d0SThomas Wolf * @since 5.9 8624fdc1d0SThomas Wolf */ 8724fdc1d0SThomas Wolf public static final String ECHO_COMMAND = "echo"; 8824fdc1d0SThomas Wolf 891316d43eSThomas Wolf @NonNull 901316d43eSThomas Wolf protected final String testUser; 911316d43eSThomas Wolf 921316d43eSThomas Wolf @NonNull 931316d43eSThomas Wolf protected final Repository repository; 941316d43eSThomas Wolf 951316d43eSThomas Wolf @NonNull 961316d43eSThomas Wolf protected final List<KeyPair> hostKeys = new ArrayList<>(); 971316d43eSThomas Wolf 981316d43eSThomas Wolf protected final SshServer server; 991316d43eSThomas Wolf 1001316d43eSThomas Wolf @NonNull 1011316d43eSThomas Wolf protected PublicKey testKey; 1021316d43eSThomas Wolf 10386cee68eSThomas Wolf private final CloseableExecutorService executorService = ThreadUtils 10486cee68eSThomas Wolf .newFixedThreadPool("SshTestGitServerPool", 2); 1051316d43eSThomas Wolf 1061316d43eSThomas Wolf /** 1071316d43eSThomas Wolf * Creates a ssh git <em>test</em> server. It serves one single repository, 1081316d43eSThomas Wolf * and accepts public-key authentication for exactly one test user. 1091316d43eSThomas Wolf * 1101316d43eSThomas Wolf * @param testUser 1111316d43eSThomas Wolf * user name of the test user 1121316d43eSThomas Wolf * @param testKey 113eb67862cSThomas Wolf * public key file of the test user 1141316d43eSThomas Wolf * @param repository 1151316d43eSThomas Wolf * to serve 1161316d43eSThomas Wolf * @param hostKey 1171316d43eSThomas Wolf * the unencrypted private key to use as host key 1181316d43eSThomas Wolf * @throws IOException 1191316d43eSThomas Wolf * @throws GeneralSecurityException 1201316d43eSThomas Wolf */ SshTestGitServer(@onNull String testUser, @NonNull Path testKey, @NonNull Repository repository, @NonNull byte[] hostKey)1211316d43eSThomas Wolf public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey, 1221316d43eSThomas Wolf @NonNull Repository repository, @NonNull byte[] hostKey) 1231316d43eSThomas Wolf throws IOException, GeneralSecurityException { 124eb67862cSThomas Wolf this(testUser, readPublicKey(testKey), repository, 125eb67862cSThomas Wolf readKeyPair(hostKey)); 126eb67862cSThomas Wolf } 127eb67862cSThomas Wolf 128eb67862cSThomas Wolf /** 129eb67862cSThomas Wolf * Creates a ssh git <em>test</em> server. It serves one single repository, 130eb67862cSThomas Wolf * and accepts public-key authentication for exactly one test user. 131eb67862cSThomas Wolf * 132eb67862cSThomas Wolf * @param testUser 133eb67862cSThomas Wolf * user name of the test user 134eb67862cSThomas Wolf * @param testKey 135eb67862cSThomas Wolf * public key file of the test user 136eb67862cSThomas Wolf * @param repository 137eb67862cSThomas Wolf * to serve 138eb67862cSThomas Wolf * @param hostKey 139eb67862cSThomas Wolf * the unencrypted private key to use as host key 140eb67862cSThomas Wolf * @throws IOException 141eb67862cSThomas Wolf * @throws GeneralSecurityException 142eb67862cSThomas Wolf * @since 5.9 143eb67862cSThomas Wolf */ SshTestGitServer(@onNull String testUser, @NonNull Path testKey, @NonNull Repository repository, @NonNull KeyPair hostKey)144eb67862cSThomas Wolf public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey, 145eb67862cSThomas Wolf @NonNull Repository repository, @NonNull KeyPair hostKey) 146eb67862cSThomas Wolf throws IOException, GeneralSecurityException { 147eb67862cSThomas Wolf this(testUser, readPublicKey(testKey), repository, hostKey); 148eb67862cSThomas Wolf } 149eb67862cSThomas Wolf 150eb67862cSThomas Wolf /** 151eb67862cSThomas Wolf * Creates a ssh git <em>test</em> server. It serves one single repository, 152eb67862cSThomas Wolf * and accepts public-key authentication for exactly one test user. 153eb67862cSThomas Wolf * 154eb67862cSThomas Wolf * @param testUser 155eb67862cSThomas Wolf * user name of the test user 156eb67862cSThomas Wolf * @param testKey 157eb67862cSThomas Wolf * the {@link PublicKey} of the test user 158eb67862cSThomas Wolf * @param repository 159eb67862cSThomas Wolf * to serve 160eb67862cSThomas Wolf * @param hostKey 161eb67862cSThomas Wolf * the {@link KeyPair} to use as host key 162eb67862cSThomas Wolf * @since 5.9 163eb67862cSThomas Wolf */ SshTestGitServer(@onNull String testUser, @NonNull PublicKey testKey, @NonNull Repository repository, @NonNull KeyPair hostKey)164eb67862cSThomas Wolf public SshTestGitServer(@NonNull String testUser, 165eb67862cSThomas Wolf @NonNull PublicKey testKey, @NonNull Repository repository, 166eb67862cSThomas Wolf @NonNull KeyPair hostKey) { 1671316d43eSThomas Wolf this.testUser = testUser; 1681316d43eSThomas Wolf setTestUserPublicKey(testKey); 1691316d43eSThomas Wolf this.repository = repository; 170*4560bdf7SDavid Ostrovsky ServerBuilder builder = ServerBuilder.builder() 171*4560bdf7SDavid Ostrovsky .signatureFactories(getSignatureFactories()); 172*4560bdf7SDavid Ostrovsky server = builder.build(); 173eb67862cSThomas Wolf hostKeys.add(hostKey); 17486cee68eSThomas Wolf server.setKeyPairProvider((session) -> hostKeys); 1751316d43eSThomas Wolf 1761316d43eSThomas Wolf configureAuthentication(); 1771316d43eSThomas Wolf 178fd3778b9SThomas Wolf List<SubsystemFactory> subsystems = configureSubsystems(); 1791316d43eSThomas Wolf if (!subsystems.isEmpty()) { 1801316d43eSThomas Wolf server.setSubsystemFactories(subsystems); 1811316d43eSThomas Wolf } 1821316d43eSThomas Wolf 1831316d43eSThomas Wolf configureShell(); 1841316d43eSThomas Wolf 185fd3778b9SThomas Wolf server.setCommandFactory((channel, command) -> { 1861316d43eSThomas Wolf if (command.startsWith(RemoteConfig.DEFAULT_UPLOAD_PACK)) { 1871316d43eSThomas Wolf return new GitUploadPackCommand(command, executorService); 1881316d43eSThomas Wolf } else if (command.startsWith(RemoteConfig.DEFAULT_RECEIVE_PACK)) { 1891316d43eSThomas Wolf return new GitReceivePackCommand(command, executorService); 19024fdc1d0SThomas Wolf } else if (command.startsWith(ECHO_COMMAND)) { 19124fdc1d0SThomas Wolf return new EchoCommand(command, executorService); 1921316d43eSThomas Wolf } 1931316d43eSThomas Wolf return new UnknownCommand(command); 1941316d43eSThomas Wolf }); 1951316d43eSThomas Wolf } 1961316d43eSThomas Wolf 197*4560bdf7SDavid Ostrovsky /** 198*4560bdf7SDavid Ostrovsky * Apache MINA sshd 2.6.0 has removed DSA, DSA_CERT and RSA_CERT. We have to 199*4560bdf7SDavid Ostrovsky * set it up explicitly to still allow users to connect with DSA keys. 200*4560bdf7SDavid Ostrovsky * 201*4560bdf7SDavid Ostrovsky * @return a list of supported signature factories 202*4560bdf7SDavid Ostrovsky */ 203*4560bdf7SDavid Ostrovsky @SuppressWarnings("deprecation") getSignatureFactories()204*4560bdf7SDavid Ostrovsky private static List<NamedFactory<Signature>> getSignatureFactories() { 205*4560bdf7SDavid Ostrovsky // @formatter:off 206*4560bdf7SDavid Ostrovsky return Arrays.asList( 207*4560bdf7SDavid Ostrovsky BuiltinSignatures.nistp256_cert, 208*4560bdf7SDavid Ostrovsky BuiltinSignatures.nistp384_cert, 209*4560bdf7SDavid Ostrovsky BuiltinSignatures.nistp521_cert, 210*4560bdf7SDavid Ostrovsky BuiltinSignatures.ed25519_cert, 211*4560bdf7SDavid Ostrovsky BuiltinSignatures.rsaSHA512_cert, 212*4560bdf7SDavid Ostrovsky BuiltinSignatures.rsaSHA256_cert, 213*4560bdf7SDavid Ostrovsky BuiltinSignatures.rsa_cert, 214*4560bdf7SDavid Ostrovsky BuiltinSignatures.nistp256, 215*4560bdf7SDavid Ostrovsky BuiltinSignatures.nistp384, 216*4560bdf7SDavid Ostrovsky BuiltinSignatures.nistp521, 217*4560bdf7SDavid Ostrovsky BuiltinSignatures.ed25519, 218*4560bdf7SDavid Ostrovsky BuiltinSignatures.sk_ecdsa_sha2_nistp256, 219*4560bdf7SDavid Ostrovsky BuiltinSignatures.sk_ssh_ed25519, 220*4560bdf7SDavid Ostrovsky BuiltinSignatures.rsaSHA512, 221*4560bdf7SDavid Ostrovsky BuiltinSignatures.rsaSHA256, 222*4560bdf7SDavid Ostrovsky BuiltinSignatures.rsa, 223*4560bdf7SDavid Ostrovsky BuiltinSignatures.dsa_cert, 224*4560bdf7SDavid Ostrovsky BuiltinSignatures.dsa); 225*4560bdf7SDavid Ostrovsky // @formatter:on 226*4560bdf7SDavid Ostrovsky } 227*4560bdf7SDavid Ostrovsky readPublicKey(Path key)228eb67862cSThomas Wolf private static PublicKey readPublicKey(Path key) 229eb67862cSThomas Wolf throws IOException, GeneralSecurityException { 230eb67862cSThomas Wolf return AuthorizedKeyEntry.readAuthorizedKeys(key).get(0) 231eb67862cSThomas Wolf .resolvePublicKey(null, PublicKeyEntryResolver.IGNORING); 232eb67862cSThomas Wolf } 233eb67862cSThomas Wolf readKeyPair(byte[] keyMaterial)234eb67862cSThomas Wolf private static KeyPair readKeyPair(byte[] keyMaterial) 235eb67862cSThomas Wolf throws IOException, GeneralSecurityException { 236eb67862cSThomas Wolf try (ByteArrayInputStream in = new ByteArrayInputStream(keyMaterial)) { 237eb67862cSThomas Wolf return SecurityUtils.loadKeyPairIdentities(null, null, in, null) 238eb67862cSThomas Wolf .iterator().next(); 239eb67862cSThomas Wolf } 240eb67862cSThomas Wolf } 241eb67862cSThomas Wolf 2421316d43eSThomas Wolf private static class FakeUserAuthGSS extends UserAuthGSS { 2431316d43eSThomas Wolf @Override doAuth(Buffer buffer, boolean initial)24421262e98SMatthias Sohn protected @Nullable Boolean doAuth(Buffer buffer, boolean initial) 2451316d43eSThomas Wolf throws Exception { 2461316d43eSThomas Wolf // We always reply that we did do this, but then we fail at the 2471316d43eSThomas Wolf // first token message. That way we can test that the client-side 2481316d43eSThomas Wolf // sends the correct initial request and then is skipped correctly, 2491316d43eSThomas Wolf // even if it causes a GSSException if Kerberos isn't configured at 2501316d43eSThomas Wolf // all. 2511316d43eSThomas Wolf if (initial) { 2521316d43eSThomas Wolf ServerSession session = getServerSession(); 2531316d43eSThomas Wolf Buffer b = session.createBuffer( 2541316d43eSThomas Wolf SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST); 2551316d43eSThomas Wolf b.putBytes(KRB5_MECH.getDER()); 2561316d43eSThomas Wolf session.writePacket(b); 2571316d43eSThomas Wolf return null; 2581316d43eSThomas Wolf } 2591316d43eSThomas Wolf return Boolean.FALSE; 2601316d43eSThomas Wolf } 2611316d43eSThomas Wolf } 2621316d43eSThomas Wolf getAuthFactories()263fd3778b9SThomas Wolf private List<UserAuthFactory> getAuthFactories() { 264fd3778b9SThomas Wolf List<UserAuthFactory> authentications = new ArrayList<>(); 2651316d43eSThomas Wolf authentications.add(new UserAuthGSSFactory() { 2661316d43eSThomas Wolf @Override 267fd3778b9SThomas Wolf public UserAuth createUserAuth(ServerSession session) 268fd3778b9SThomas Wolf throws IOException { 2691316d43eSThomas Wolf return new FakeUserAuthGSS(); 2701316d43eSThomas Wolf } 2711316d43eSThomas Wolf }); 27200b235f0SThomas Wolf authentications.add( 27300b235f0SThomas Wolf ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY); 27400b235f0SThomas Wolf authentications.add( 27500b235f0SThomas Wolf ServerAuthenticationManager.DEFAULT_USER_AUTH_KB_INTERACTIVE_FACTORY); 27600b235f0SThomas Wolf authentications.add( 27700b235f0SThomas Wolf ServerAuthenticationManager.DEFAULT_USER_AUTH_PASSWORD_FACTORY); 2781316d43eSThomas Wolf return authentications; 2791316d43eSThomas Wolf } 2801316d43eSThomas Wolf 2811316d43eSThomas Wolf /** 2821316d43eSThomas Wolf * Configures the authentication mechanisms of this test server. Invoked 2831316d43eSThomas Wolf * from the constructor. The default sets up public key authentication for 2841316d43eSThomas Wolf * the test user, and a gssapi-with-mic authenticator that pretends to 2851316d43eSThomas Wolf * support this mechanism, but that then refuses to authenticate anyone. 2861316d43eSThomas Wolf */ configureAuthentication()2871316d43eSThomas Wolf protected void configureAuthentication() { 2881316d43eSThomas Wolf server.setUserAuthFactories(getAuthFactories()); 2891316d43eSThomas Wolf // Disable some authentications 2901316d43eSThomas Wolf server.setPasswordAuthenticator(null); 2911316d43eSThomas Wolf server.setKeyboardInteractiveAuthenticator(null); 2921316d43eSThomas Wolf server.setHostBasedAuthenticator(null); 2931316d43eSThomas Wolf // Pretend we did gssapi-with-mic. 2941316d43eSThomas Wolf server.setGSSAuthenticator(new GSSAuthenticator() { 2951316d43eSThomas Wolf @Override 2961316d43eSThomas Wolf public boolean validateInitialUser(ServerSession session, 2971316d43eSThomas Wolf String user) { 2981316d43eSThomas Wolf return false; 2991316d43eSThomas Wolf } 3001316d43eSThomas Wolf }); 3011316d43eSThomas Wolf // Accept only the test user/public key 3021316d43eSThomas Wolf server.setPublickeyAuthenticator((userName, publicKey, session) -> { 3031316d43eSThomas Wolf return SshTestGitServer.this.testUser.equals(userName) && KeyUtils 3041316d43eSThomas Wolf .compareKeys(SshTestGitServer.this.testKey, publicKey); 3051316d43eSThomas Wolf }); 3061316d43eSThomas Wolf } 3071316d43eSThomas Wolf 3081316d43eSThomas Wolf /** 3091316d43eSThomas Wolf * Configures the test server's subsystems (sftp, scp). Invoked from the 3101316d43eSThomas Wolf * constructor. The default provides a simple SFTP setup with the root 3111316d43eSThomas Wolf * directory as the given repository's .git directory's parent. (I.e., at 3121316d43eSThomas Wolf * the directory containing the .git directory.) 3131316d43eSThomas Wolf * 3141316d43eSThomas Wolf * @return A possibly empty collection of subsystems. 3151316d43eSThomas Wolf */ 3161316d43eSThomas Wolf @NonNull configureSubsystems()317fd3778b9SThomas Wolf protected List<SubsystemFactory> configureSubsystems() { 3181316d43eSThomas Wolf // SFTP. 319*4560bdf7SDavid Ostrovsky server.setFileSystemFactory(new VirtualFileSystemFactory(repository 320*4560bdf7SDavid Ostrovsky .getDirectory().getParentFile().getAbsoluteFile().toPath())); 3211316d43eSThomas Wolf return Collections 3221316d43eSThomas Wolf .singletonList((new SftpSubsystemFactory.Builder()).build()); 3231316d43eSThomas Wolf } 3241316d43eSThomas Wolf 3251316d43eSThomas Wolf /** 3261316d43eSThomas Wolf * Configures shell access for the test server. The default provides no 3271316d43eSThomas Wolf * shell at all. 3281316d43eSThomas Wolf */ configureShell()3291316d43eSThomas Wolf protected void configureShell() { 3301316d43eSThomas Wolf // No shell 3311316d43eSThomas Wolf server.setShellFactory(null); 3321316d43eSThomas Wolf } 3331316d43eSThomas Wolf 3341316d43eSThomas Wolf /** 3351316d43eSThomas Wolf * Adds an additional host key to the server. 3361316d43eSThomas Wolf * 3371316d43eSThomas Wolf * @param key 3381316d43eSThomas Wolf * path to the private key file; should not be encrypted 3391316d43eSThomas Wolf * @param inFront 3401316d43eSThomas Wolf * whether to add the new key before other existing keys 3411316d43eSThomas Wolf * @throws IOException 3421316d43eSThomas Wolf * if the file denoted by the {@link Path} {@code key} cannot be 3431316d43eSThomas Wolf * read 3441316d43eSThomas Wolf * @throws GeneralSecurityException 3451316d43eSThomas Wolf * if the key contained in the file cannot be read 3461316d43eSThomas Wolf */ addHostKey(@onNull Path key, boolean inFront)3471316d43eSThomas Wolf public void addHostKey(@NonNull Path key, boolean inFront) 3481316d43eSThomas Wolf throws IOException, GeneralSecurityException { 3491316d43eSThomas Wolf try (InputStream in = Files.newInputStream(key)) { 35086cee68eSThomas Wolf KeyPair pair = SecurityUtils 35186cee68eSThomas Wolf .loadKeyPairIdentities(null, 35286cee68eSThomas Wolf NamedResource.ofName(key.toString()), in, null) 35386cee68eSThomas Wolf .iterator().next(); 3545a5d85a4SThomas Wolf addHostKey(pair, inFront); 3551316d43eSThomas Wolf } 3561316d43eSThomas Wolf } 3575a5d85a4SThomas Wolf 3585a5d85a4SThomas Wolf /** 3595a5d85a4SThomas Wolf * Adds an additional host key to the server. 3605a5d85a4SThomas Wolf * 3615a5d85a4SThomas Wolf * @param key 3625a5d85a4SThomas Wolf * {@link KeyPair} to add 3635a5d85a4SThomas Wolf * @param inFront 3645a5d85a4SThomas Wolf * whether to add the new key before other existing keys 3655a5d85a4SThomas Wolf * @since 5.8 3665a5d85a4SThomas Wolf */ addHostKey(@onNull KeyPair key, boolean inFront)3675a5d85a4SThomas Wolf public void addHostKey(@NonNull KeyPair key, boolean inFront) { 3685a5d85a4SThomas Wolf if (inFront) { 3695a5d85a4SThomas Wolf hostKeys.add(0, key); 3705a5d85a4SThomas Wolf } else { 3715a5d85a4SThomas Wolf hostKeys.add(key); 3725a5d85a4SThomas Wolf } 3731316d43eSThomas Wolf } 3741316d43eSThomas Wolf 3751316d43eSThomas Wolf /** 37600b235f0SThomas Wolf * Enable password authentication. The server will accept the test user's 37700b235f0SThomas Wolf * name, converted to all upper-case, as password. 37800b235f0SThomas Wolf */ enablePasswordAuthentication()37900b235f0SThomas Wolf public void enablePasswordAuthentication() { 38000b235f0SThomas Wolf server.setPasswordAuthenticator((user, pwd, session) -> { 38100b235f0SThomas Wolf return testUser.equals(user) 38200b235f0SThomas Wolf && testUser.toUpperCase(Locale.ROOT).equals(pwd); 38300b235f0SThomas Wolf }); 38400b235f0SThomas Wolf } 38500b235f0SThomas Wolf 38600b235f0SThomas Wolf /** 38700b235f0SThomas Wolf * Enable keyboard-interactive authentication. The server will accept the 38800b235f0SThomas Wolf * test user's name, converted to all upper-case, as password. 38900b235f0SThomas Wolf */ enableKeyboardInteractiveAuthentication()39000b235f0SThomas Wolf public void enableKeyboardInteractiveAuthentication() { 39100b235f0SThomas Wolf server.setPasswordAuthenticator((user, pwd, session) -> { 39200b235f0SThomas Wolf return testUser.equals(user) 39300b235f0SThomas Wolf && testUser.toUpperCase(Locale.ROOT).equals(pwd); 39400b235f0SThomas Wolf }); 39500b235f0SThomas Wolf server.setKeyboardInteractiveAuthenticator( 39600b235f0SThomas Wolf DefaultKeyboardInteractiveAuthenticator.INSTANCE); 39700b235f0SThomas Wolf } 39800b235f0SThomas Wolf 39900b235f0SThomas Wolf /** 400835e3225SThomas Wolf * Retrieves the server's {@link PropertyResolver}, giving access to server 401835e3225SThomas Wolf * properties. 402151f0cb8SThomas Wolf * 403835e3225SThomas Wolf * @return the {@link PropertyResolver} 404151f0cb8SThomas Wolf * @since 5.9 405151f0cb8SThomas Wolf */ getPropertyResolver()406835e3225SThomas Wolf public PropertyResolver getPropertyResolver() { 407835e3225SThomas Wolf return server; 408151f0cb8SThomas Wolf } 409151f0cb8SThomas Wolf 410151f0cb8SThomas Wolf /** 4111316d43eSThomas Wolf * Starts the test server, listening on a random port. 4121316d43eSThomas Wolf * 4131316d43eSThomas Wolf * @return the port the server listens on; test clients should connect to 4141316d43eSThomas Wolf * that port 4151316d43eSThomas Wolf * @throws IOException 4161316d43eSThomas Wolf */ start()4171316d43eSThomas Wolf public int start() throws IOException { 4181316d43eSThomas Wolf server.start(); 4191316d43eSThomas Wolf return server.getPort(); 4201316d43eSThomas Wolf } 4211316d43eSThomas Wolf 4221316d43eSThomas Wolf /** 4231316d43eSThomas Wolf * Stops the test server. 4241316d43eSThomas Wolf * 4251316d43eSThomas Wolf * @throws IOException 4261316d43eSThomas Wolf */ stop()4271316d43eSThomas Wolf public void stop() throws IOException { 4281316d43eSThomas Wolf executorService.shutdownNow(); 4291316d43eSThomas Wolf server.stop(true); 4301316d43eSThomas Wolf } 4311316d43eSThomas Wolf 4323a3c8de8SThomas Wolf /** 4333a3c8de8SThomas Wolf * Sets the test user's public key on the server. 4343a3c8de8SThomas Wolf * 4353a3c8de8SThomas Wolf * @param key 4363a3c8de8SThomas Wolf * to set 4373a3c8de8SThomas Wolf * @throws IOException 4383a3c8de8SThomas Wolf * if the file cannot be read 4393a3c8de8SThomas Wolf * @throws GeneralSecurityException 4403a3c8de8SThomas Wolf * if the public key cannot be extracted from the file 4413a3c8de8SThomas Wolf */ setTestUserPublicKey(Path key)4421316d43eSThomas Wolf public void setTestUserPublicKey(Path key) 4431316d43eSThomas Wolf throws IOException, GeneralSecurityException { 444eb67862cSThomas Wolf this.testKey = readPublicKey(key); 4451316d43eSThomas Wolf } 4461316d43eSThomas Wolf 447b8a514fdSThomas Wolf /** 4485a5d85a4SThomas Wolf * Sets the test user's public key on the server. 4495a5d85a4SThomas Wolf * 4505a5d85a4SThomas Wolf * @param key 4515a5d85a4SThomas Wolf * to set 4525a5d85a4SThomas Wolf * 4535a5d85a4SThomas Wolf * @since 5.8 4545a5d85a4SThomas Wolf */ setTestUserPublicKey(@onNull PublicKey key)4555a5d85a4SThomas Wolf public void setTestUserPublicKey(@NonNull PublicKey key) { 4565a5d85a4SThomas Wolf this.testKey = key; 4575a5d85a4SThomas Wolf } 4585a5d85a4SThomas Wolf 4595a5d85a4SThomas Wolf /** 460b8a514fdSThomas Wolf * Sets the lines the server sends before its server identification in the 461b8a514fdSThomas Wolf * initial protocol version exchange. 462b8a514fdSThomas Wolf * 463b8a514fdSThomas Wolf * @param lines 464b8a514fdSThomas Wolf * to send 465b8a514fdSThomas Wolf * @since 5.5 466b8a514fdSThomas Wolf */ setPreamble(String... lines)467b8a514fdSThomas Wolf public void setPreamble(String... lines) { 468b8a514fdSThomas Wolf if (lines != null && lines.length > 0) { 469*4560bdf7SDavid Ostrovsky SERVER_EXTRA_IDENTIFICATION_LINES.set(server, String.join( 470*4560bdf7SDavid Ostrovsky String.valueOf(SERVER_EXTRA_IDENT_LINES_SEPARATOR), lines)); 471b8a514fdSThomas Wolf } 472b8a514fdSThomas Wolf } 473b8a514fdSThomas Wolf 4741316d43eSThomas Wolf private class GitUploadPackCommand extends AbstractCommandSupport { 4751316d43eSThomas Wolf GitUploadPackCommand(String command, CloseableExecutorService executorService)4761316d43eSThomas Wolf protected GitUploadPackCommand(String command, 47786cee68eSThomas Wolf CloseableExecutorService executorService) { 47886cee68eSThomas Wolf super(command, ThreadUtils.noClose(executorService)); 4791316d43eSThomas Wolf } 4801316d43eSThomas Wolf 4811316d43eSThomas Wolf @Override run()4821316d43eSThomas Wolf public void run() { 4831316d43eSThomas Wolf UploadPack uploadPack = new UploadPack(repository); 4841316d43eSThomas Wolf String gitProtocol = getEnvironment().getEnv().get("GIT_PROTOCOL"); 4851316d43eSThomas Wolf if (gitProtocol != null) { 4861316d43eSThomas Wolf uploadPack 4871316d43eSThomas Wolf .setExtraParameters(Collections.singleton(gitProtocol)); 4881316d43eSThomas Wolf } 4891316d43eSThomas Wolf try { 4901316d43eSThomas Wolf uploadPack.upload(getInputStream(), getOutputStream(), 4911316d43eSThomas Wolf getErrorStream()); 4921316d43eSThomas Wolf onExit(0); 4931316d43eSThomas Wolf } catch (IOException e) { 4941316d43eSThomas Wolf log.warn( 4951316d43eSThomas Wolf MessageFormat.format("Could not run {0}", getCommand()), 4961316d43eSThomas Wolf e); 4971316d43eSThomas Wolf onExit(-1, e.toString()); 4981316d43eSThomas Wolf } 4991316d43eSThomas Wolf } 5001316d43eSThomas Wolf 5011316d43eSThomas Wolf } 5021316d43eSThomas Wolf 5031316d43eSThomas Wolf private class GitReceivePackCommand extends AbstractCommandSupport { 5041316d43eSThomas Wolf GitReceivePackCommand(String command, CloseableExecutorService executorService)5051316d43eSThomas Wolf protected GitReceivePackCommand(String command, 50686cee68eSThomas Wolf CloseableExecutorService executorService) { 50786cee68eSThomas Wolf super(command, ThreadUtils.noClose(executorService)); 5081316d43eSThomas Wolf } 5091316d43eSThomas Wolf 5101316d43eSThomas Wolf @Override run()5111316d43eSThomas Wolf public void run() { 5121316d43eSThomas Wolf try { 5131316d43eSThomas Wolf new ReceivePack(repository).receive(getInputStream(), 5141316d43eSThomas Wolf getOutputStream(), getErrorStream()); 5151316d43eSThomas Wolf onExit(0); 5161316d43eSThomas Wolf } catch (IOException e) { 5171316d43eSThomas Wolf log.warn( 5181316d43eSThomas Wolf MessageFormat.format("Could not run {0}", getCommand()), 5191316d43eSThomas Wolf e); 5201316d43eSThomas Wolf onExit(-1, e.toString()); 5211316d43eSThomas Wolf } 5221316d43eSThomas Wolf } 5231316d43eSThomas Wolf 5241316d43eSThomas Wolf } 52524fdc1d0SThomas Wolf 52624fdc1d0SThomas Wolf /** 52724fdc1d0SThomas Wolf * Simple echo command that echoes back the command string. If the first 52824fdc1d0SThomas Wolf * argument is a positive integer, it's taken as a delay (in seconds) before 52924fdc1d0SThomas Wolf * replying. Assumes UTF-8 character encoding. 53024fdc1d0SThomas Wolf */ 53124fdc1d0SThomas Wolf private static class EchoCommand extends AbstractCommandSupport { 53224fdc1d0SThomas Wolf EchoCommand(String command, CloseableExecutorService executorService)53324fdc1d0SThomas Wolf protected EchoCommand(String command, 53424fdc1d0SThomas Wolf CloseableExecutorService executorService) { 53524fdc1d0SThomas Wolf super(command, ThreadUtils.noClose(executorService)); 53624fdc1d0SThomas Wolf } 53724fdc1d0SThomas Wolf 53824fdc1d0SThomas Wolf @Override run()53924fdc1d0SThomas Wolf public void run() { 54024fdc1d0SThomas Wolf String[] parts = getCommand().split(" "); 54124fdc1d0SThomas Wolf int timeout = 0; 54224fdc1d0SThomas Wolf if (parts.length >= 2) { 54324fdc1d0SThomas Wolf try { 54424fdc1d0SThomas Wolf timeout = Integer.parseInt(parts[1]); 54524fdc1d0SThomas Wolf } catch (NumberFormatException e) { 54624fdc1d0SThomas Wolf // No timeout. 54724fdc1d0SThomas Wolf } 54824fdc1d0SThomas Wolf if (timeout > 0) { 54924fdc1d0SThomas Wolf try { 55024fdc1d0SThomas Wolf Thread.sleep(TimeUnit.SECONDS.toMillis(timeout)); 55124fdc1d0SThomas Wolf } catch (InterruptedException e) { 55224fdc1d0SThomas Wolf // Ignore. 55324fdc1d0SThomas Wolf } 55424fdc1d0SThomas Wolf } 55524fdc1d0SThomas Wolf } 55624fdc1d0SThomas Wolf try { 55724fdc1d0SThomas Wolf doEcho(getCommand(), getOutputStream()); 55824fdc1d0SThomas Wolf onExit(0); 55924fdc1d0SThomas Wolf } catch (IOException e) { 56024fdc1d0SThomas Wolf log.warn( 56124fdc1d0SThomas Wolf MessageFormat.format("Could not run {0}", getCommand()), 56224fdc1d0SThomas Wolf e); 56324fdc1d0SThomas Wolf onExit(-1, e.toString()); 56424fdc1d0SThomas Wolf } 56524fdc1d0SThomas Wolf } 56624fdc1d0SThomas Wolf doEcho(String text, OutputStream stream)56724fdc1d0SThomas Wolf private void doEcho(String text, OutputStream stream) 56824fdc1d0SThomas Wolf throws IOException { 56924fdc1d0SThomas Wolf stream.write(text.getBytes(StandardCharsets.UTF_8)); 57024fdc1d0SThomas Wolf stream.flush(); 57124fdc1d0SThomas Wolf } 57224fdc1d0SThomas Wolf } 5731316d43eSThomas Wolf } 574