123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573 |
- /*
- * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Distribution License v. 1.0 which is available at
- * https://www.eclipse.org/org/documents/edl-v10.php.
- *
- * SPDX-License-Identifier: BSD-3-Clause
- */
- package org.eclipse.jgit.junit.ssh;
-
- import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENTIFICATION_LINES;
- import static org.apache.sshd.core.CoreModuleProperties.SERVER_EXTRA_IDENT_LINES_SEPARATOR;
-
- import java.io.ByteArrayInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.io.OutputStream;
- import java.nio.charset.StandardCharsets;
- import java.nio.file.Files;
- import java.nio.file.Path;
- import java.security.GeneralSecurityException;
- import java.security.KeyPair;
- import java.security.PublicKey;
- import java.text.MessageFormat;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Collections;
- import java.util.List;
- import java.util.Locale;
- import java.util.concurrent.TimeUnit;
-
- import org.apache.sshd.common.NamedFactory;
- import org.apache.sshd.common.NamedResource;
- import org.apache.sshd.common.PropertyResolver;
- import org.apache.sshd.common.SshConstants;
- import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
- import org.apache.sshd.common.config.keys.KeyUtils;
- import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
- import org.apache.sshd.common.file.virtualfs.VirtualFileSystemFactory;
- import org.apache.sshd.common.signature.BuiltinSignatures;
- import org.apache.sshd.common.signature.Signature;
- import org.apache.sshd.common.util.buffer.Buffer;
- import org.apache.sshd.common.util.security.SecurityUtils;
- import org.apache.sshd.common.util.threads.CloseableExecutorService;
- import org.apache.sshd.common.util.threads.ThreadUtils;
- import org.apache.sshd.server.ServerAuthenticationManager;
- import org.apache.sshd.server.ServerBuilder;
- import org.apache.sshd.server.SshServer;
- import org.apache.sshd.server.auth.UserAuth;
- import org.apache.sshd.server.auth.UserAuthFactory;
- import org.apache.sshd.server.auth.gss.GSSAuthenticator;
- import org.apache.sshd.server.auth.gss.UserAuthGSS;
- import org.apache.sshd.server.auth.gss.UserAuthGSSFactory;
- import org.apache.sshd.server.auth.keyboard.DefaultKeyboardInteractiveAuthenticator;
- import org.apache.sshd.server.command.AbstractCommandSupport;
- import org.apache.sshd.server.session.ServerSession;
- import org.apache.sshd.server.shell.UnknownCommand;
- import org.apache.sshd.server.subsystem.SubsystemFactory;
- import org.apache.sshd.sftp.server.SftpSubsystemFactory;
- import org.eclipse.jgit.annotations.NonNull;
- import org.eclipse.jgit.annotations.Nullable;
- import org.eclipse.jgit.lib.Repository;
- import org.eclipse.jgit.transport.ReceivePack;
- import org.eclipse.jgit.transport.RemoteConfig;
- import org.eclipse.jgit.transport.UploadPack;
-
- /**
- * A simple ssh/sftp git <em>test</em> server based on Apache MINA sshd.
- * <p>
- * Supports only a single repository. Authenticates only the given test user
- * against his given test public key. Supports fetch and push.
- * </p>
- *
- * @since 5.2
- */
- public class SshTestGitServer {
-
- /**
- * Simple echo test command. Replies with the command string as passed. If
- * of the form "echo [int] anything", takes the integer value as a delay in
- * seconds before replying, which may be useful to test various
- * timeout-related things.
- *
- * @since 5.9
- */
- public static final String ECHO_COMMAND = "echo";
-
- @NonNull
- protected final String testUser;
-
- @NonNull
- protected final Repository repository;
-
- @NonNull
- protected final List<KeyPair> hostKeys = new ArrayList<>();
-
- protected final SshServer server;
-
- @NonNull
- protected PublicKey testKey;
-
- private final CloseableExecutorService executorService = ThreadUtils
- .newFixedThreadPool("SshTestGitServerPool", 2);
-
- /**
- * Creates a ssh git <em>test</em> server. It serves one single repository,
- * and accepts public-key authentication for exactly one test user.
- *
- * @param testUser
- * user name of the test user
- * @param testKey
- * public key file of the test user
- * @param repository
- * to serve
- * @param hostKey
- * the unencrypted private key to use as host key
- * @throws IOException
- * @throws GeneralSecurityException
- */
- public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey,
- @NonNull Repository repository, @NonNull byte[] hostKey)
- throws IOException, GeneralSecurityException {
- this(testUser, readPublicKey(testKey), repository,
- readKeyPair(hostKey));
- }
-
- /**
- * Creates a ssh git <em>test</em> server. It serves one single repository,
- * and accepts public-key authentication for exactly one test user.
- *
- * @param testUser
- * user name of the test user
- * @param testKey
- * public key file of the test user
- * @param repository
- * to serve
- * @param hostKey
- * the unencrypted private key to use as host key
- * @throws IOException
- * @throws GeneralSecurityException
- * @since 5.9
- */
- public SshTestGitServer(@NonNull String testUser, @NonNull Path testKey,
- @NonNull Repository repository, @NonNull KeyPair hostKey)
- throws IOException, GeneralSecurityException {
- this(testUser, readPublicKey(testKey), repository, hostKey);
- }
-
- /**
- * Creates a ssh git <em>test</em> server. It serves one single repository,
- * and accepts public-key authentication for exactly one test user.
- *
- * @param testUser
- * user name of the test user
- * @param testKey
- * the {@link PublicKey} of the test user
- * @param repository
- * to serve
- * @param hostKey
- * the {@link KeyPair} to use as host key
- * @since 5.9
- */
- public SshTestGitServer(@NonNull String testUser,
- @NonNull PublicKey testKey, @NonNull Repository repository,
- @NonNull KeyPair hostKey) {
- this.testUser = testUser;
- setTestUserPublicKey(testKey);
- this.repository = repository;
- ServerBuilder builder = ServerBuilder.builder()
- .signatureFactories(getSignatureFactories());
- server = builder.build();
- hostKeys.add(hostKey);
- server.setKeyPairProvider((session) -> hostKeys);
-
- configureAuthentication();
-
- List<SubsystemFactory> subsystems = configureSubsystems();
- if (!subsystems.isEmpty()) {
- server.setSubsystemFactories(subsystems);
- }
-
- configureShell();
-
- server.setCommandFactory((channel, command) -> {
- if (command.startsWith(RemoteConfig.DEFAULT_UPLOAD_PACK)) {
- return new GitUploadPackCommand(command, executorService);
- } else if (command.startsWith(RemoteConfig.DEFAULT_RECEIVE_PACK)) {
- return new GitReceivePackCommand(command, executorService);
- } else if (command.startsWith(ECHO_COMMAND)) {
- return new EchoCommand(command, executorService);
- }
- return new UnknownCommand(command);
- });
- }
-
- /**
- * Apache MINA sshd 2.6.0 has removed DSA, DSA_CERT and RSA_CERT. We have to
- * set it up explicitly to still allow users to connect with DSA keys.
- *
- * @return a list of supported signature factories
- */
- @SuppressWarnings("deprecation")
- private static List<NamedFactory<Signature>> getSignatureFactories() {
- // @formatter:off
- return Arrays.asList(
- BuiltinSignatures.nistp256_cert,
- BuiltinSignatures.nistp384_cert,
- BuiltinSignatures.nistp521_cert,
- BuiltinSignatures.ed25519_cert,
- BuiltinSignatures.rsaSHA512_cert,
- BuiltinSignatures.rsaSHA256_cert,
- BuiltinSignatures.rsa_cert,
- BuiltinSignatures.nistp256,
- BuiltinSignatures.nistp384,
- BuiltinSignatures.nistp521,
- BuiltinSignatures.ed25519,
- BuiltinSignatures.sk_ecdsa_sha2_nistp256,
- BuiltinSignatures.sk_ssh_ed25519,
- BuiltinSignatures.rsaSHA512,
- BuiltinSignatures.rsaSHA256,
- BuiltinSignatures.rsa,
- BuiltinSignatures.dsa_cert,
- BuiltinSignatures.dsa);
- // @formatter:on
- }
-
- private static PublicKey readPublicKey(Path key)
- throws IOException, GeneralSecurityException {
- return AuthorizedKeyEntry.readAuthorizedKeys(key).get(0)
- .resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
- }
-
- private static KeyPair readKeyPair(byte[] keyMaterial)
- throws IOException, GeneralSecurityException {
- try (ByteArrayInputStream in = new ByteArrayInputStream(keyMaterial)) {
- return SecurityUtils.loadKeyPairIdentities(null, null, in, null)
- .iterator().next();
- }
- }
-
- private static class FakeUserAuthGSS extends UserAuthGSS {
- @Override
- protected @Nullable Boolean doAuth(Buffer buffer, boolean initial)
- throws Exception {
- // We always reply that we did do this, but then we fail at the
- // first token message. That way we can test that the client-side
- // sends the correct initial request and then is skipped correctly,
- // even if it causes a GSSException if Kerberos isn't configured at
- // all.
- if (initial) {
- ServerSession session = getServerSession();
- Buffer b = session.createBuffer(
- SshConstants.SSH_MSG_USERAUTH_INFO_REQUEST);
- b.putBytes(KRB5_MECH.getDER());
- session.writePacket(b);
- return null;
- }
- return Boolean.FALSE;
- }
- }
-
- private List<UserAuthFactory> getAuthFactories() {
- List<UserAuthFactory> authentications = new ArrayList<>();
- authentications.add(new UserAuthGSSFactory() {
- @Override
- public UserAuth createUserAuth(ServerSession session)
- throws IOException {
- return new FakeUserAuthGSS();
- }
- });
- authentications.add(
- ServerAuthenticationManager.DEFAULT_USER_AUTH_PUBLIC_KEY_FACTORY);
- authentications.add(
- ServerAuthenticationManager.DEFAULT_USER_AUTH_KB_INTERACTIVE_FACTORY);
- authentications.add(
- ServerAuthenticationManager.DEFAULT_USER_AUTH_PASSWORD_FACTORY);
- return authentications;
- }
-
- /**
- * Configures the authentication mechanisms of this test server. Invoked
- * from the constructor. The default sets up public key authentication for
- * the test user, and a gssapi-with-mic authenticator that pretends to
- * support this mechanism, but that then refuses to authenticate anyone.
- */
- protected void configureAuthentication() {
- server.setUserAuthFactories(getAuthFactories());
- // Disable some authentications
- server.setPasswordAuthenticator(null);
- server.setKeyboardInteractiveAuthenticator(null);
- server.setHostBasedAuthenticator(null);
- // Pretend we did gssapi-with-mic.
- server.setGSSAuthenticator(new GSSAuthenticator() {
- @Override
- public boolean validateInitialUser(ServerSession session,
- String user) {
- return false;
- }
- });
- // Accept only the test user/public key
- server.setPublickeyAuthenticator((userName, publicKey, session) -> {
- return SshTestGitServer.this.testUser.equals(userName) && KeyUtils
- .compareKeys(SshTestGitServer.this.testKey, publicKey);
- });
- }
-
- /**
- * Configures the test server's subsystems (sftp, scp). Invoked from the
- * constructor. The default provides a simple SFTP setup with the root
- * directory as the given repository's .git directory's parent. (I.e., at
- * the directory containing the .git directory.)
- *
- * @return A possibly empty collection of subsystems.
- */
- @NonNull
- protected List<SubsystemFactory> configureSubsystems() {
- // SFTP.
- server.setFileSystemFactory(new VirtualFileSystemFactory(repository
- .getDirectory().getParentFile().getAbsoluteFile().toPath()));
- return Collections
- .singletonList((new SftpSubsystemFactory.Builder()).build());
- }
-
- /**
- * Configures shell access for the test server. The default provides no
- * shell at all.
- */
- protected void configureShell() {
- // No shell
- server.setShellFactory(null);
- }
-
- /**
- * Adds an additional host key to the server.
- *
- * @param key
- * path to the private key file; should not be encrypted
- * @param inFront
- * whether to add the new key before other existing keys
- * @throws IOException
- * if the file denoted by the {@link Path} {@code key} cannot be
- * read
- * @throws GeneralSecurityException
- * if the key contained in the file cannot be read
- */
- public void addHostKey(@NonNull Path key, boolean inFront)
- throws IOException, GeneralSecurityException {
- try (InputStream in = Files.newInputStream(key)) {
- KeyPair pair = SecurityUtils
- .loadKeyPairIdentities(null,
- NamedResource.ofName(key.toString()), in, null)
- .iterator().next();
- addHostKey(pair, inFront);
- }
- }
-
- /**
- * Adds an additional host key to the server.
- *
- * @param key
- * {@link KeyPair} to add
- * @param inFront
- * whether to add the new key before other existing keys
- * @since 5.8
- */
- public void addHostKey(@NonNull KeyPair key, boolean inFront) {
- if (inFront) {
- hostKeys.add(0, key);
- } else {
- hostKeys.add(key);
- }
- }
-
- /**
- * Enable password authentication. The server will accept the test user's
- * name, converted to all upper-case, as password.
- */
- public void enablePasswordAuthentication() {
- server.setPasswordAuthenticator((user, pwd, session) -> {
- return testUser.equals(user)
- && testUser.toUpperCase(Locale.ROOT).equals(pwd);
- });
- }
-
- /**
- * Enable keyboard-interactive authentication. The server will accept the
- * test user's name, converted to all upper-case, as password.
- */
- public void enableKeyboardInteractiveAuthentication() {
- server.setPasswordAuthenticator((user, pwd, session) -> {
- return testUser.equals(user)
- && testUser.toUpperCase(Locale.ROOT).equals(pwd);
- });
- server.setKeyboardInteractiveAuthenticator(
- DefaultKeyboardInteractiveAuthenticator.INSTANCE);
- }
-
- /**
- * Retrieves the server's {@link PropertyResolver}, giving access to server
- * properties.
- *
- * @return the {@link PropertyResolver}
- * @since 5.9
- */
- public PropertyResolver getPropertyResolver() {
- return server;
- }
-
- /**
- * Starts the test server, listening on a random port.
- *
- * @return the port the server listens on; test clients should connect to
- * that port
- * @throws IOException
- */
- public int start() throws IOException {
- server.start();
- return server.getPort();
- }
-
- /**
- * Stops the test server.
- *
- * @throws IOException
- */
- public void stop() throws IOException {
- executorService.shutdownNow();
- server.stop(true);
- }
-
- /**
- * Sets the test user's public key on the server.
- *
- * @param key
- * to set
- * @throws IOException
- * if the file cannot be read
- * @throws GeneralSecurityException
- * if the public key cannot be extracted from the file
- */
- public void setTestUserPublicKey(Path key)
- throws IOException, GeneralSecurityException {
- this.testKey = readPublicKey(key);
- }
-
- /**
- * Sets the test user's public key on the server.
- *
- * @param key
- * to set
- *
- * @since 5.8
- */
- public void setTestUserPublicKey(@NonNull PublicKey key) {
- this.testKey = key;
- }
-
- /**
- * Sets the lines the server sends before its server identification in the
- * initial protocol version exchange.
- *
- * @param lines
- * to send
- * @since 5.5
- */
- public void setPreamble(String... lines) {
- if (lines != null && lines.length > 0) {
- SERVER_EXTRA_IDENTIFICATION_LINES.set(server, String.join(
- String.valueOf(SERVER_EXTRA_IDENT_LINES_SEPARATOR), lines));
- }
- }
-
- private class GitUploadPackCommand extends AbstractCommandSupport {
-
- protected GitUploadPackCommand(String command,
- CloseableExecutorService executorService) {
- super(command, ThreadUtils.noClose(executorService));
- }
-
- @Override
- public void run() {
- UploadPack uploadPack = new UploadPack(repository);
- String gitProtocol = getEnvironment().getEnv().get("GIT_PROTOCOL");
- if (gitProtocol != null) {
- uploadPack
- .setExtraParameters(Collections.singleton(gitProtocol));
- }
- try {
- uploadPack.upload(getInputStream(), getOutputStream(),
- getErrorStream());
- onExit(0);
- } catch (IOException e) {
- log.warn(
- MessageFormat.format("Could not run {0}", getCommand()),
- e);
- onExit(-1, e.toString());
- }
- }
-
- }
-
- private class GitReceivePackCommand extends AbstractCommandSupport {
-
- protected GitReceivePackCommand(String command,
- CloseableExecutorService executorService) {
- super(command, ThreadUtils.noClose(executorService));
- }
-
- @Override
- public void run() {
- try {
- new ReceivePack(repository).receive(getInputStream(),
- getOutputStream(), getErrorStream());
- onExit(0);
- } catch (IOException e) {
- log.warn(
- MessageFormat.format("Could not run {0}", getCommand()),
- e);
- onExit(-1, e.toString());
- }
- }
-
- }
-
- /**
- * Simple echo command that echoes back the command string. If the first
- * argument is a positive integer, it's taken as a delay (in seconds) before
- * replying. Assumes UTF-8 character encoding.
- */
- private static class EchoCommand extends AbstractCommandSupport {
-
- protected EchoCommand(String command,
- CloseableExecutorService executorService) {
- super(command, ThreadUtils.noClose(executorService));
- }
-
- @Override
- public void run() {
- String[] parts = getCommand().split(" ");
- int timeout = 0;
- if (parts.length >= 2) {
- try {
- timeout = Integer.parseInt(parts[1]);
- } catch (NumberFormatException e) {
- // No timeout.
- }
- if (timeout > 0) {
- try {
- Thread.sleep(TimeUnit.SECONDS.toMillis(timeout));
- } catch (InterruptedException e) {
- // Ignore.
- }
- }
- }
- try {
- doEcho(getCommand(), getOutputStream());
- onExit(0);
- } catch (IOException e) {
- log.warn(
- MessageFormat.format("Could not run {0}", getCommand()),
- e);
- onExit(-1, e.toString());
- }
- }
-
- private void doEcho(String text, OutputStream stream)
- throws IOException {
- stream.write(text.getBytes(StandardCharsets.UTF_8));
- stream.flush();
- }
- }
- }
|