summaryrefslogtreecommitdiffstats
path: root/org.eclipse.jgit.ssh.apache/src/org
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit.ssh.apache/src/org')
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java20
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java14
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/ConnectorFactoryProvider.java51
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java72
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java246
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java21
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactoryBuilder.java55
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/AbstractConnector.java116
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/Connector.java61
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/agent/ConnectorFactory.java58
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();
+
+}