xref: /JGit/org.eclipse.jgit.junit.ssh/src/org/eclipse/jgit/junit/ssh/SshTestGitServer.java (revision 4560bdf7e2e3c16a7c7bb3f2fcf067bb1eee26fb)
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