diff options
Diffstat (limited to 'org.eclipse.jgit.ssh.apache/src/org')
10 files changed, 711 insertions, 3 deletions
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java index fdb8cde670..71e8e61585 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java @@ -32,8 +32,10 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.function.Supplier; import java.util.stream.Collectors; +import org.apache.sshd.agent.SshAgentFactory; import org.apache.sshd.client.SshClient; import org.apache.sshd.client.config.hosts.HostConfigEntry; import org.apache.sshd.client.future.ConnectFuture; @@ -100,6 +102,8 @@ public class JGitSshClient extends SshClient { private ProxyDataFactory proxyDatabase; + private Supplier<SshAgentFactory> agentFactorySupplier = () -> null; + @Override protected SessionFactory createSessionFactory() { // Override the parent's default @@ -368,6 +372,22 @@ public class JGitSshClient extends SshClient { return credentialsProvider; } + @Override + public SshAgentFactory getAgentFactory() { + return agentFactorySupplier.get(); + } + + @Override + protected void checkConfig() { + // The super class requires channel factories for agent forwarding if a + // factory for an SSH agent is set. We haven't implemented this yet, and + // we don't do SSH agent forwarding for now. Unfortunately, there is no + // way to bypass this check in the super class except making + // getAgentFactory() return null until after the check. + super.checkConfig(); + agentFactorySupplier = super::getAgentFactory; + } + /** * A {@link SessionFactory} to create our own specialized * {@link JGitClientSession}s. diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java index c0f5719629..00ee62d6dd 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java @@ -1,3 +1,12 @@ +/* + * Copyright (C) 2018, 2021 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.internal.transport.sshd; import org.eclipse.jgit.nls.NLS; @@ -39,6 +48,7 @@ public final class SshdText extends TranslationBundle { /***/ public String identityFileMultipleKeys; /***/ public String identityFileNotFound; /***/ public String identityFileUnsupportedFormat; + /***/ public String invalidSignatureAlgorithm; /***/ public String kexServerKeyInvalid; /***/ public String keyEncryptedMsg; /***/ public String keyEncryptedPrompt; @@ -96,6 +106,10 @@ public final class SshdText extends TranslationBundle { /***/ public String serverIdWithNul; /***/ public String sessionCloseFailed; /***/ public String sessionWithoutUsername; + /***/ public String sshAgentReplyLengthError; + /***/ public String sshAgentReplyUnexpected; + /***/ public String sshAgentShortReadBuffer; + /***/ public String sshAgentWrongNumberOfKeys; /***/ public String sshClosingDown; /***/ public String sshCommandTimeout; /***/ public String sshProcessStillRunning; diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/ConnectorFactoryProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/ConnectorFactoryProvider.java new file mode 100644 index 0000000000..9984f99763 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/ConnectorFactoryProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2021, 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.internal.transport.sshd.agent; + +import java.util.Iterator; +import java.util.ServiceLoader; + +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; + +/** + * Provides a {@link ConnectorFactory} obtained via the {@link ServiceLoader}. + */ +public final class ConnectorFactoryProvider { + + private static final ConnectorFactory FACTORY = loadDefaultFactory(); + + private static ConnectorFactory loadDefaultFactory() { + ServiceLoader<ConnectorFactory> loader = ServiceLoader + .load(ConnectorFactory.class); + Iterator<ConnectorFactory> iter = loader.iterator(); + while (iter.hasNext()) { + ConnectorFactory candidate = iter.next(); + if (candidate.isSupported()) { + return candidate; + } + } + return null; + + } + + private ConnectorFactoryProvider() { + // No instantiation + } + + /** + * Retrieves the default {@link ConnectorFactory} obtained via the + * {@link ServiceLoader}. + * + * @return the {@link ConnectorFactory}, or {@code null} if none. + */ + public static ConnectorFactory getDefaultFactory() { + return FACTORY; + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java new file mode 100644 index 0000000000..1ed2ab9d78 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021, 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.internal.transport.sshd.agent; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.agent.SshAgentFactory; +import org.apache.sshd.agent.SshAgentServer; +import org.apache.sshd.common.FactoryManager; +import org.apache.sshd.common.channel.ChannelFactory; +import org.apache.sshd.common.session.ConnectionService; +import org.eclipse.jgit.annotations.NonNull; +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; + +/** + * A factory for creating {@link SshAgentClient}s. + */ +public class JGitSshAgentFactory implements SshAgentFactory { + + private final @NonNull ConnectorFactory factory; + + private final File homeDir; + + /** + * Creates a new {@link JGitSshAgentFactory}. + * + * @param factory + * {@link JGitSshAgentFactory} to wrap + * @param homeDir + * for obtaining the current local user's home directory + */ + public JGitSshAgentFactory(@NonNull ConnectorFactory factory, + File homeDir) { + this.factory = factory; + this.homeDir = homeDir; + } + + @Override + public List<ChannelFactory> getChannelForwardingFactories( + FactoryManager manager) { + // No agent forwarding supported yet. + return Collections.emptyList(); + } + + @Override + public SshAgent createClient(FactoryManager manager) throws IOException { + // sshd 2.8.0 will pass us the session here. At that point, we can get + // the HostConfigEntry and extract and handle the IdentityAgent setting. + // For now, pass null to let the ConnectorFactory do its default + // behavior (Pageant on Windows, SSH_AUTH_SOCK on Unixes with the + // jgit-builtin factory). + return new SshAgentClient(factory.create(null, homeDir)); + } + + @Override + public SshAgentServer createServer(ConnectionService service) + throws IOException { + // This should be called in a server only. + return null; + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java new file mode 100644 index 0000000000..08483e4c20 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java @@ -0,0 +1,246 @@ +/* + * Copyright (C) 2021, 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.internal.transport.sshd.agent; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.PublicKey; +import java.text.MessageFormat; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.agent.SshAgentConstants; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.session.SessionContext; +import org.apache.sshd.common.util.buffer.Buffer; +import org.apache.sshd.common.util.buffer.BufferException; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.buffer.ByteArrayBuffer; +import org.eclipse.jgit.internal.transport.sshd.SshdText; +import org.eclipse.jgit.transport.sshd.agent.Connector; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A client for an SSH2 agent. This client supports only querying identities and + * signature requests. + * + * @see <a href="https://tools.ietf.org/html/draft-miller-ssh-agent-04">SSH + * Agent Protocol, RFC draft</a> + */ +public class SshAgentClient implements SshAgent { + + private static final Logger LOG = LoggerFactory + .getLogger(SshAgentClient.class); + + // OpenSSH limit + private static final int MAX_NUMBER_OF_KEYS = 2048; + + private final AtomicBoolean closed = new AtomicBoolean(); + + private final Connector connector; + + /** + * Creates a new {@link SshAgentClient} implementing the SSH2 ssh agent + * protocol, using the given {@link Connector} to connect to the SSH agent + * and to exchange messages. + * + * @param connector + * {@link Connector} to use + */ + public SshAgentClient(Connector connector) { + this.connector = connector; + } + + private boolean open(boolean debugging) throws IOException { + if (closed.get()) { + if (debugging) { + LOG.debug("SSH agent connection already closed"); //$NON-NLS-1$ + } + return false; + } + boolean connected = connector != null && connector.connect(); + if (!connected) { + if (debugging) { + LOG.debug("No SSH agent (SSH_AUTH_SOCK not set)"); //$NON-NLS-1$ + } + } + return connected; + } + + @Override + public void close() throws IOException { + if (!closed.getAndSet(true) && connector != null) { + connector.close(); + } + } + + @Override + public Iterable<? extends Map.Entry<PublicKey, String>> getIdentities() + throws IOException { + boolean debugging = LOG.isDebugEnabled(); + if (!open(debugging)) { + return Collections.emptyList(); + } + if (debugging) { + LOG.debug("Requesting identities from SSH agent"); //$NON-NLS-1$ + } + try { + Buffer reply = rpc( + SshAgentConstants.SSH2_AGENTC_REQUEST_IDENTITIES); + byte cmd = reply.getByte(); + if (cmd != SshAgentConstants.SSH2_AGENT_IDENTITIES_ANSWER) { + throw new SshException(MessageFormat.format( + SshdText.get().sshAgentReplyUnexpected, + SshAgentConstants.getCommandMessageName(cmd))); + } + int numberOfKeys = reply.getInt(); + if (numberOfKeys < 0 || numberOfKeys > MAX_NUMBER_OF_KEYS) { + throw new SshException(MessageFormat.format( + SshdText.get().sshAgentWrongNumberOfKeys, + Integer.toString(numberOfKeys))); + } + if (numberOfKeys == 0) { + if (debugging) { + LOG.debug("SSH agent has no keys"); //$NON-NLS-1$ + } + return Collections.emptyList(); + } + if (debugging) { + LOG.debug("Got {} key(s) from the SSH agent", //$NON-NLS-1$ + Integer.toString(numberOfKeys)); + } + boolean tracing = LOG.isTraceEnabled(); + List<Map.Entry<PublicKey, String>> keys = new ArrayList<>( + numberOfKeys); + for (int i = 0; i < numberOfKeys; i++) { + PublicKey key = reply.getPublicKey(); + String comment = reply.getString(); + if (tracing) { + LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$ + KeyUtils.getKeyType(key), + KeyUtils.getFingerPrint(key), comment); + } + keys.add(new AbstractMap.SimpleImmutableEntry<>(key, comment)); + } + return keys; + } catch (BufferException e) { + throw new SshException(SshdText.get().sshAgentShortReadBuffer, e); + } + } + + @Override + public Map.Entry<String, byte[]> sign(SessionContext session, PublicKey key, + String algorithm, byte[] data) throws IOException { + boolean debugging = LOG.isDebugEnabled(); + String keyType = KeyUtils.getKeyType(key); + String signatureAlgorithm; + if (algorithm != null) { + if (!KeyUtils.getCanonicalKeyType(algorithm).equals(keyType)) { + throw new IllegalArgumentException(MessageFormat.format( + SshdText.get().invalidSignatureAlgorithm, algorithm, + keyType)); + } + signatureAlgorithm = algorithm; + } else { + signatureAlgorithm = keyType; + } + if (!open(debugging)) { + return null; + } + int flags = 0; + switch (signatureAlgorithm) { + case KeyUtils.RSA_SHA512_KEY_TYPE_ALIAS: + case KeyUtils.RSA_SHA512_CERT_TYPE_ALIAS: + flags = 4; + break; + case KeyUtils.RSA_SHA256_KEY_TYPE_ALIAS: + case KeyUtils.RSA_SHA256_CERT_TYPE_ALIAS: + flags = 2; + break; + default: + break; + } + ByteArrayBuffer msg = new ByteArrayBuffer(); + msg.putInt(0); + msg.putByte(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST); + msg.putPublicKey(key); + msg.putBytes(data); + msg.putInt(flags); + if (debugging) { + LOG.debug( + "sign({}): signing request to SSH agent for {} key, {} signature; flags={}", //$NON-NLS-1$ + session, keyType, signatureAlgorithm, + Integer.toString(flags)); + } + Buffer reply = rpc(SshAgentConstants.SSH2_AGENTC_SIGN_REQUEST, + msg.getCompactData()); + byte cmd = reply.getByte(); + if (cmd != SshAgentConstants.SSH2_AGENT_SIGN_RESPONSE) { + throw new SshException( + MessageFormat.format(SshdText.get().sshAgentReplyUnexpected, + SshAgentConstants.getCommandMessageName(cmd))); + } + try { + Buffer signatureReply = new ByteArrayBuffer(reply.getBytes()); + String actualAlgorithm = signatureReply.getString(); + byte[] signature = signatureReply.getBytes(); + if (LOG.isTraceEnabled()) { + LOG.trace( + "sign({}): signature reply from SSH agent for {} key: {} signature={}", //$NON-NLS-1$ + session, keyType, actualAlgorithm, + BufferUtils.toHex(':', signature)); + + } else if (LOG.isDebugEnabled()) { + LOG.debug( + "sign({}): signature reply from SSH agent for {} key, {} signature", //$NON-NLS-1$ + session, keyType, actualAlgorithm); + } + return new AbstractMap.SimpleImmutableEntry<>(actualAlgorithm, + signature); + } catch (BufferException e) { + throw new SshException(SshdText.get().sshAgentShortReadBuffer, e); + } + } + + private Buffer rpc(byte command, byte[] message) throws IOException { + return new ByteArrayBuffer(connector.rpc(command, message)); + } + + private Buffer rpc(byte command) throws IOException { + return new ByteArrayBuffer(connector.rpc(command)); + } + + @Override + public boolean isOpen() { + return !closed.get(); + } + + @Override + public void addIdentity(KeyPair key, String comment) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void removeIdentity(PublicKey key) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void removeAllIdentities() throws IOException { + throw new UnsupportedOperationException(); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java index cad959c904..da99f56cb8 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2018, 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2018, 2021 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 @@ -56,11 +56,14 @@ import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction; import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyDatabase; import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper; import org.eclipse.jgit.internal.transport.sshd.SshdText; +import org.eclipse.jgit.internal.transport.sshd.agent.ConnectorFactoryProvider; +import org.eclipse.jgit.internal.transport.sshd.agent.JGitSshAgentFactory; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.SshConfigStore; import org.eclipse.jgit.transport.SshConstants; import org.eclipse.jgit.transport.SshSessionFactory; import org.eclipse.jgit.transport.URIish; +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; import org.eclipse.jgit.util.FS; /** @@ -216,6 +219,11 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { new JGitUserInteraction(credentialsProvider)); client.setUserAuthFactories(getUserAuthFactories()); client.setKeyIdentityProvider(defaultKeysProvider); + ConnectorFactory connectors = getConnectorFactory(); + if (connectors != null) { + client.setAgentFactory( + new JGitSshAgentFactory(connectors, home)); + } // JGit-specific things: JGitSshClient jgitClient = (JGitSshClient) client; jgitClient.setKeyCache(getKeyCache()); @@ -437,6 +445,17 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable { } /** + * Gets a {@link ConnectorFactory}. If this returns {@code null}, SSH agents + * are not supported. + * + * @return the factory, or {@code null} if no SSH agent support is desired + * @since 6.0 + */ + protected ConnectorFactory getConnectorFactory() { + return ConnectorFactoryProvider.getDefaultFactory(); + } + + /** * Gets the list of default user known hosts files. The default returns * ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config * {@code UserKnownHostsFile} overrides this default. diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java index 2147c2bd58..7ed9b5ea3b 100644 --- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> and others + * Copyright (C) 2020, 2021 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 @@ -20,6 +20,7 @@ import java.util.function.Function; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.SshConfigStore; +import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory; import org.eclipse.jgit.util.StringUtils; /** @@ -114,7 +115,7 @@ public final class SshdSessionFactoryBuilder { } /** - * A factory interface for creating a @link SshConfigStore}. + * A factory interface for creating a {@link SshConfigStore}. */ @FunctionalInterface public interface ConfigStoreFactory { @@ -233,6 +234,41 @@ public final class SshdSessionFactoryBuilder { } /** + * Sets an explicit {@link ConnectorFactory}. If {@code null}, there will be + * no support for SSH agents. + * <p> + * If not set, the created {@link SshdSessionFactory} will use the + * {@link java.util.ServiceLoader} to find an {@link ConnectorFactory}. + * </p> + * + * @param factory + * {@link ConnectorFactory} to use + * @return this {@link SshdSessionFactoryBuilder} + * @since 6.0 + */ + public SshdSessionFactoryBuilder setConnectorFactory( + ConnectorFactory factory) { + this.state.connectorFactory = factory; + this.state.connectorFactorySet = true; + return this; + } + + /** + * Removes a previously set {@link ConnectorFactory}. The created + * {@link SshdSessionFactory} will use the {@link java.util.ServiceLoader} + * to find an {@link ConnectorFactory}. This is also the default if + * {@link #setConnectorFactory(ConnectorFactory)} isn't called at all. + * + * @return this {@link SshdSessionFactoryBuilder} + * @since 6.0 + */ + public SshdSessionFactoryBuilder withDefaultConnectorFactory() { + this.state.connectorFactory = null; + this.state.connectorFactorySet = false; + return this; + } + + /** * Builds a {@link SshdSessionFactory} as configured, using the given * {@link KeyCache} for caching keys. * <p> @@ -277,6 +313,10 @@ public final class SshdSessionFactoryBuilder { BiFunction<File, File, ServerKeyDatabase> serverKeyDatabaseCreator; + ConnectorFactory connectorFactory; + + boolean connectorFactorySet; + State copy() { State c = new State(); c.proxyDataFactory = proxyDataFactory; @@ -290,6 +330,8 @@ public final class SshdSessionFactoryBuilder { c.defaultKeyFileFinder = defaultKeyFileFinder; c.defaultKeysProvider = defaultKeysProvider; c.serverKeyDatabaseCreator = serverKeyDatabaseCreator; + c.connectorFactory = connectorFactory; + c.connectorFactorySet = connectorFactorySet; return c; } @@ -388,6 +430,15 @@ public final class SshdSessionFactoryBuilder { return super.createSshConfigStore(homeDir, configFile, localUserName); } + + @Override + protected ConnectorFactory getConnectorFactory() { + if (connectorFactorySet) { + return connectorFactory; + } + // Use default via ServiceLoader + return super.getConnectorFactory(); + } } } } diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/AbstractConnector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/AbstractConnector.java new file mode 100644 index 0000000000..71ddc3b003 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/AbstractConnector.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2021, 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.transport.sshd.agent; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Objects; + +import org.apache.sshd.agent.SshAgentConstants; +import org.apache.sshd.common.SshException; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.eclipse.jgit.internal.transport.sshd.SshdText; + +/** + * Provides some utility methods for implementing {@link Connector}s. + * + * @since 6.0 + */ +public abstract class AbstractConnector implements Connector { + + // A somewhat sane lower bound for the maximum reply length + private static final int MIN_REPLY_LENGTH = 8 * 1024; + + /** + * Default maximum reply length. 256kB is the OpenSSH limit. + */ + protected static final int DEFAULT_MAX_REPLY_LENGTH = 256 * 1024; + + private final int maxReplyLength; + + /** + * Creates a new instance using the {@link #DEFAULT_MAX_REPLY_LENGTH}. + */ + protected AbstractConnector() { + this(DEFAULT_MAX_REPLY_LENGTH); + } + + /** + * Creates a new instance. + * + * @param maxReplyLength + * maximum number of payload bytes we're ready to accept + */ + protected AbstractConnector(int maxReplyLength) { + if (maxReplyLength < MIN_REPLY_LENGTH) { + throw new IllegalArgumentException( + "Maximum payload length too small"); //$NON-NLS-1$ + } + this.maxReplyLength = maxReplyLength; + } + + /** + * Retrieves the maximum message length this {@link AbstractConnector} is + * configured for. + * + * @return the maximum message length + */ + protected int getMaximumMessageLength() { + return this.maxReplyLength; + } + + /** + * Prepares a message for sending by inserting the command and message + * length. + * + * @param command + * SSH agent command the request is for + * @param message + * about to be sent, including the 5 spare bytes at the front + * @throws IllegalArgumentException + * if {@code message} has less than 5 bytes + */ + protected void prepareMessage(byte command, byte[] message) + throws IllegalArgumentException { + Objects.requireNonNull(message); + if (message.length < 5) { + // No translation; internal error + throw new IllegalArgumentException("Message buffer for " //$NON-NLS-1$ + + SshAgentConstants.getCommandMessageName(command) + + " must have at least 5 bytes; have only " //$NON-NLS-1$ + + message.length); + } + BufferUtils.putUInt(message.length - 4, message); + message[4] = command; + } + + /** + * Checks the received length of a reply. + * + * @param command + * SSH agent command the reply is for + * @param length + * length as received: number of payload bytes + * @return the length as an {@code int} + * @throws IOException + * if the length is invalid + */ + protected int toLength(byte command, byte[] length) + throws IOException { + long l = BufferUtils.getUInt(length); + if (l <= 0 || l > maxReplyLength - 4) { + throw new SshException(MessageFormat.format( + SshdText.get().sshAgentReplyLengthError, + Long.toString(l), + SshAgentConstants.getCommandMessageName(command))); + } + return (int) l; + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/Connector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/Connector.java new file mode 100644 index 0000000000..b6da0866a0 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/Connector.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021, 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.transport.sshd.agent; + +import java.io.Closeable; +import java.io.IOException; + +/** + * Simple interface for connecting to something and making RPC-style + * request-reply calls. + * + * @since 6.0 + */ +public interface Connector extends Closeable { + + /** + * Connects to an SSH agent if there is one running. If called when already + * connected just returns {@code true}. + * + * @return {@code true} if an SSH agent is available and connected, + * {@false} if no SSH agent is available + * @throws IOException + * if connecting to the SSH agent failed + */ + boolean connect() throws IOException; + + /** + * Performs a remote call to the SSH agent and returns the result. + * + * @param command + * to send + * @param message + * to send; must have at least 5 bytes, and must have 5 unused + * bytes at the front. + * @return the result received + * @throws IOException + * if an error occurs + */ + byte[] rpc(byte command, byte[] message) throws IOException; + + /** + * Performs a remote call sending only a command without any parameters to + * the SSH agent and returns the result. + * + * @param command + * to send + * @return the result received + * @throws IOException + * if an error occurs + */ + default byte[] rpc(byte command) throws IOException { + return rpc(command, new byte[5]); + } +} diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/ConnectorFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/ConnectorFactory.java new file mode 100644 index 0000000000..fa725ab858 --- /dev/null +++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/ConnectorFactory.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021, 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.transport.sshd.agent; + +import java.io.File; +import java.io.IOException; + +import org.eclipse.jgit.annotations.NonNull; + +/** + * A factory for creating {@link Connector}s. + * + * @since 6.0 + */ +public interface ConnectorFactory { + + /** + * Creates a new {@link Connector}. + * + * @param identityAgent + * identifies the wanted agent connection; if {@code null}, the + * factory is free to provide a {@link Connector} to a default + * agent. The value will typically come from the IdentityAgent + * setting in ~/.ssh/config. + * @param homeDir + * the current local user's home directory as configured in the + * {@link org.eclipse.jgit.transport.sshd.SshdSessionFactory} + * @return a new {@link Connector} + * @throws IOException + * if no connector can be created + */ + @NonNull + Connector create(String identityAgent, File homeDir) + throws IOException; + + /** + * Tells whether this {@link ConnectorFactory} is applicable on the + * currently running platform. + * + * @return {@code true} if the factory can be used, {@code false} otherwise + */ + boolean isSupported(); + + /** + * Retrieves a name for this factory. + * + * @return the name + */ + String getName(); + +} |