123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- /*
- * 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 java.io.ByteArrayInputStream;
- import java.io.IOException;
- import java.io.InputStream;
- 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.Collections;
- import java.util.List;
- import java.util.Locale;
-
- import org.apache.sshd.common.NamedResource;
- import org.apache.sshd.common.PropertyResolverUtils;
- 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.session.Session;
- 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.ServerFactoryManager;
- 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.server.subsystem.sftp.SftpSubsystemFactory;
- import org.eclipse.jgit.annotations.NonNull;
- 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 {
-
- @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
- * <em>private</em> key file of the test user; the server will
- * only user the public key from it
- * @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 = testUser;
- setTestUserPublicKey(testKey);
- this.repository = repository;
- server = SshServer.setUpDefaultServer();
- // Set host key
- try (ByteArrayInputStream in = new ByteArrayInputStream(hostKey)) {
- SecurityUtils.loadKeyPairIdentities(null, null, in, null)
- .forEach((k) -> hostKeys.add(k));
- } catch (IOException | GeneralSecurityException e) {
- // Ignore.
- }
- 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);
- }
- return new UnknownCommand(command);
- });
- }
-
- private static class FakeUserAuthGSS extends UserAuthGSS {
- @Override
- protected 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() {
-
- @Override
- protected Path computeRootDir(Session session) throws IOException {
- return SshTestGitServer.this.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);
- }
-
- /**
- * 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 = AuthorizedKeyEntry.readAuthorizedKeys(key).get(0)
- .resolvePublicKey(null, PublicKeyEntryResolver.IGNORING);
- }
-
- /**
- * 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) {
- PropertyResolverUtils.updateProperty(this.server,
- ServerFactoryManager.SERVER_EXTRA_IDENTIFICATION_LINES,
- String.join("|", 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());
- }
- }
-
- }
- }
|