summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java3
-rw-r--r--org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF3
-rw-r--r--org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParserTest.java146
-rw-r--r--org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java3
-rw-r--r--org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF12
-rw-r--r--org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties29
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiMechanisms.java7
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java7
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java101
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java72
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java2
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java27
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/AbstractAuthenticationHandler.java89
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/AuthenticationHandler.java121
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/BasicAuthentication.java167
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/GssApiAuthentication.java147
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/AbstractClientProxyConnector.java209
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/AuthenticationChallenge.java123
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpClientConnector.java403
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParser.java346
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/Socks5ClientConnector.java642
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/StatefulProxyConnector.java89
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/StatusLine.java99
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java103
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ProxyData.java136
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ProxyDataFactory.java70
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SessionCloseListener.java2
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java23
28 files changed, 3157 insertions, 24 deletions
diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
index 1ca35a24e6..c4b4018b8f 100644
--- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
+++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/TextBuiltin.java
@@ -70,6 +70,7 @@ import org.eclipse.jgit.pgm.internal.SshDriver;
import org.eclipse.jgit.pgm.opt.CmdLineParser;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.transport.sshd.DefaultProxyDataFactory;
import org.eclipse.jgit.transport.sshd.JGitKeyCache;
import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
import org.eclipse.jgit.util.io.ThrowingPrintWriter;
@@ -249,7 +250,7 @@ public abstract class TextBuiltin {
switch (sshDriver) {
case APACHE: {
SshdSessionFactory factory = new SshdSessionFactory(
- new JGitKeyCache());
+ new JGitKeyCache(), new DefaultProxyDataFactory());
Runtime.getRuntime()
.addShutdownHook(new Thread(() -> factory.close()));
SshSessionFactory.setInstance(factory);
diff --git a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
index c8f53c4c15..38dc190679 100644
--- a/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache.test/META-INF/MANIFEST.MF
@@ -6,7 +6,8 @@ Bundle-SymbolicName: org.eclipse.jgit.ssh.apache.test
Bundle-Version: 5.2.0.qualifier
Bundle-Vendor: %Provider-Name
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
-Import-Package: org.eclipse.jgit.junit;version="[5.2.0,5.3.0)",
+Import-Package: org.eclipse.jgit.internal.transport.sshd.proxy;version="[5.2.0,5.3.0)",
+ org.eclipse.jgit.junit;version="[5.2.0,5.3.0)",
org.eclipse.jgit.lib;version="[5.2.0,5.3.0)",
org.eclipse.jgit.transport;version="[5.2.0,5.3.0)",
org.eclipse.jgit.transport.ssh;version="[5.2.0,5.3.0)",
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParserTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParserTest.java
new file mode 100644
index 0000000000..b8e85493aa
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParserTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.proxy;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+public class HttpParserTest {
+
+ private static final String STATUS_LINE = "HTTP/1.1. 407 Authentication required";
+
+ @Test
+ public void testEmpty() throws Exception {
+ String[] lines = { STATUS_LINE };
+ List<AuthenticationChallenge> challenges = HttpParser
+ .getAuthenticationHeaders(Arrays.asList(lines),
+ "WWW-Authenticate:");
+ assertTrue("No challenges expected", challenges.isEmpty());
+ }
+
+ @Test
+ public void testRFC7235Example() throws Exception {
+ // The example from RFC 7235, sec. 4.1, slightly modified ("kind"
+ // argument with whitespace around '=')
+ String[] lines = { STATUS_LINE,
+ "WWW-Authenticate: Newauth realm=\"apps\", type=1 , kind = \t2 ",
+ " \t title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"" };
+ List<AuthenticationChallenge> challenges = HttpParser
+ .getAuthenticationHeaders(Arrays.asList(lines),
+ "WWW-Authenticate:");
+ assertEquals("Unexpected number of challenges", 2, challenges.size());
+ assertNull("No token expected", challenges.get(0).getToken());
+ assertNull("No token expected", challenges.get(1).getToken());
+ assertEquals("Unexpected mechanism", "Newauth",
+ challenges.get(0).getMechanism());
+ assertEquals("Unexpected mechanism", "Basic",
+ challenges.get(1).getMechanism());
+ Map<String, String> expectedArguments = new LinkedHashMap<>();
+ expectedArguments.put("realm", "apps");
+ expectedArguments.put("type", "1");
+ expectedArguments.put("kind", "2");
+ expectedArguments.put("title", "Login to \"apps\"");
+ assertEquals("Unexpected arguments", expectedArguments,
+ challenges.get(0).getArguments());
+ expectedArguments.clear();
+ expectedArguments.put("realm", "simple");
+ assertEquals("Unexpected arguments", expectedArguments,
+ challenges.get(1).getArguments());
+ }
+
+ @Test
+ public void testMultipleHeaders() {
+ String[] lines = { STATUS_LINE,
+ "Server: Apache",
+ "WWW-Authenticate: Newauth realm=\"apps\", type=1 , kind = \t2 ",
+ " \t title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"",
+ "Content-Type: text/plain",
+ "WWW-Authenticate: Other 0123456789=== , YetAnother, ",
+ "WWW-Authenticate: Negotiate ",
+ "WWW-Authenticate: Negotiate a87421000492aa874209af8bc028" };
+ List<AuthenticationChallenge> challenges = HttpParser
+ .getAuthenticationHeaders(Arrays.asList(lines),
+ "WWW-Authenticate:");
+ assertEquals("Unexpected number of challenges", 6, challenges.size());
+ assertEquals("Mismatched challenge", "Other",
+ challenges.get(2).getMechanism());
+ assertEquals("Token expected", "0123456789===",
+ challenges.get(2).getToken());
+ assertEquals("Mismatched challenge", "YetAnother",
+ challenges.get(3).getMechanism());
+ assertNull("No token expected", challenges.get(3).getToken());
+ assertTrue("No arguments expected",
+ challenges.get(3).getArguments().isEmpty());
+ assertEquals("Mismatched challenge", "Negotiate",
+ challenges.get(4).getMechanism());
+ assertNull("No token expected", challenges.get(4).getToken());
+ assertEquals("Mismatched challenge", "Negotiate",
+ challenges.get(5).getMechanism());
+ assertEquals("Token expected", "a87421000492aa874209af8bc028",
+ challenges.get(5).getToken());
+ }
+
+ @Test
+ public void testStopOnEmptyLine() {
+ String[] lines = { STATUS_LINE, "Server: Apache",
+ "WWW-Authenticate: Newauth realm=\"apps\", type=1 , kind = \t2 ",
+ " \t title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"",
+ "Content-Type: text/plain",
+ "WWW-Authenticate: Other 0123456789===", "",
+ // Not headers anymore; this would be the body
+ "WWW-Authenticate: Negotiate ",
+ "WWW-Authenticate: Negotiate a87421000492aa874209af8bc028" };
+ List<AuthenticationChallenge> challenges = HttpParser
+ .getAuthenticationHeaders(Arrays.asList(lines),
+ "WWW-Authenticate:");
+ assertEquals("Unexpected number of challenges", 3, challenges.size());
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
index cbbc6386f0..69a9165aa7 100644
--- a/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
+++ b/org.eclipse.jgit.ssh.apache.test/tst/org/eclipse/jgit/transport/sshd/ApacheSshTest.java
@@ -61,7 +61,8 @@ public class ApacheSshTest extends SshTestBase {
@Override
protected SshSessionFactory createSessionFactory() {
- SshdSessionFactory result = new SshdSessionFactory(new JGitKeyCache());
+ SshdSessionFactory result = new SshdSessionFactory(new JGitKeyCache(),
+ null);
// The home directory is mocked at this point!
result.setHomeDirectory(FS.DETECTED.userHome());
result.setSshDirectory(sshDir);
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
index caeff53634..e5d66536fc 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
@@ -22,14 +22,15 @@ Export-Package: org.eclipse.jgit.internal.transport.sshd;version="5.2.0";x-inter
org.apache.sshd.common.signature,
org.apache.sshd.common.util.buffer,
org.eclipse.jgit.transport",
+ org.eclipse.jgit.internal.transport.sshd.auth;version="5.2.0";x-internal:=true,
+ org.eclipse.jgit.internal.transport.sshd.proxy;version="5.2.0";x-friends:="org.eclipse.jgit.ssh.apache.test",
org.eclipse.jgit.transport.sshd;version="5.2.0";
- uses:="org.apache.sshd.client,
+ uses:="org.eclipse.jgit.transport,
org.apache.sshd.client.config.hosts,
org.apache.sshd.common.keyprovider,
- org.apache.sshd.client.keyverifier,
- org.eclipse.jgit.internal.transport.sshd,
- org.eclipse.jgit.transport,
- org.eclipse.jgit.util"
+ org.eclipse.jgit.util,
+ org.apache.sshd.client.session,
+ org.apache.sshd.client.keyverifier"
Import-Package: org.apache.sshd.agent;version="[2.0.0,2.1.0)",
org.apache.sshd.client;version="[2.0.0,2.1.0)",
org.apache.sshd.client.auth;version="[2.0.0,2.1.0)",
@@ -64,6 +65,7 @@ Import-Package: org.apache.sshd.agent;version="[2.0.0,2.1.0)",
org.apache.sshd.common.subsystem.sftp;version="[2.0.0,2.1.0)",
org.apache.sshd.common.util;version="[2.0.0,2.1.0)",
org.apache.sshd.common.util.buffer;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.util.closeable;version="[2.0.0,2.1.0)",
org.apache.sshd.common.util.io;version="[2.0.0,2.1.0)",
org.apache.sshd.common.util.logging;version="[2.0.0,2.1.0)",
org.apache.sshd.common.util.net;version="[2.0.0,2.1.0)",
diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
index 12afedd8b2..f9ff02b40c 100644
--- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
+++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
@@ -14,6 +14,7 @@ identityFileCannotDecrypt=Given passphrase cannot decrypt identity {0}
identityFileNoKey=No keys found in identity {0}
identityFileMultipleKeys=Multiple key pairs found in identity {0}
identityFileUnsupportedFormat=Unsupported format in identity {0}
+kexServerKeyInvalid=Server key did not validate
keyEncryptedMsg=Key ''{0}'' is encrypted. Enter the passphrase to decrypt it.
keyEncryptedPrompt=Passphrase
keyEncryptedRetry=Encrypted key ''{0}'' could not be decrypted. Enter the passphrase again.
@@ -43,7 +44,33 @@ knownHostsUnknownKeyPrompt=Accept and store this key, and continue connecting?
knownHostsUnknownKeyType=Cannot read server key from known hosts file {0}; line {1}
knownHostsUserAskCreationMsg=File {0} does not exist.
knownHostsUserAskCreationPrompt=Create file {0} ?
+proxyCannotAuthenticate=Cannot authenticate to proxy {0}
+proxyHttpFailure=HTTP Proxy connection to {0} failed with code {1}: {2}
+proxyHttpInvalidUserName=HTTP proxy connection {0} with invalid user name; must not contain colons: {1}
+proxyHttpUnexpectedReply=Unexpected HTTP proxy response from {0}: {1}
+proxyHttpUnspecifiedFailureReason=unspecified reason
+proxyPasswordPrompt=Proxy password
+proxySocksAuthenticationFailed=Authentication to SOCKS5 proxy {0} failed
+proxySocksFailureForbidden=SOCKS5 proxy {0}: connection to {1} not allowed by ruleset
+proxySocksFailureGeneral=SOCKS5 proxy {0}: general failure
+proxySocksFailureHostUnreachable=SOCKS5 proxy {0}: host unreachable {1}
+proxySocksFailureNetworkUnreachable=SOCKS5 proxy {0}: network unreachable {1}
+proxySocksFailureRefused=SOCKS5 proxy {0}: connection refused {1}
+proxySocksFailureTTL=TTL expired in SOCKS5 proxy connection {0}
+proxySocksFailureUnspecified=Unspecified failure in SOCKS5 proxy connection {0}
+proxySocksFailureUnsupportedAddress=SOCKS5 proxy {0} does not support address type
+proxySocksFailureUnsupportedCommand=SOCKS5 proxy {0} does not support CONNECT command
+proxySocksGssApiFailure=Cannot authenticate with GSS-API to SOCKS5 proxy {0}
+proxySocksGssApiMessageTooShort=SOCKS5 proxy {0} sent too short message
+proxySocksGssApiUnknownMessage=SOCKS5 proxy {0} sent unexpected GSS-API message type, expected 1, got {1}
+proxySocksGssApiVersionMismatch=SOCKS5 proxy {0} sent wrong GSS-API version number, expected 1, got {1}
+proxySocksNoRemoteHostName=Could not send remote address {0}
+proxySocksPasswordTooLong=Password for proxy {0} must be at most 255 bytes long, is {1} bytes
+proxySocksUnexpectedMessage=Unexpected message received from SOCKS5 proxy {0}; client state {1}: {2}
+proxySocksUnexpectedVersion=Expected SOCKS version 5, got {0}
+proxySocksUsernameTooLong=User name for proxy {0} must be at most 255 bytes long, is {1} bytes: {2}
sessionCloseFailed=Closing the session failed
sshClosingDown=Apache MINA sshd session factory is closing down; cannot create new ssh sessions on this factory
sshCommandTimeout={0} timed out after {1} seconds while opening the channel
-sshProcessStillRunning={0} is not yet completed, cannot get exit code \ No newline at end of file
+sshProcessStillRunning={0} is not yet completed, cannot get exit code
+unknownProxyProtocol=Ignoring unknown proxy protocol {0} \ No newline at end of file
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiMechanisms.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiMechanisms.java
index 834a503096..cf68eac5a7 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiMechanisms.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiMechanisms.java
@@ -74,7 +74,7 @@ public class GssApiMechanisms {
public static final Oid KERBEROS_5 = createOid("1.2.840.113554.1.2.2"); //$NON-NLS-1$
/** SGNEGO is not to be used with ssh. */
- private static final Oid SPNEGO = createOid("1.3.6.1.5.5.2"); //$NON-NLS-1$
+ public static final Oid SPNEGO = createOid("1.3.6.1.5.5.2"); //$NON-NLS-1$
/** Protects {@link #supportedMechanisms}. */
private static final Object LOCK = new Object();
@@ -99,10 +99,7 @@ public class GssApiMechanisms {
Map<Oid, Boolean> mechanisms = new LinkedHashMap<>();
if (mechs != null) {
for (Oid oid : mechs) {
- // RFC 4462 states that SPNEGO must not be used with ssh
- if (!SPNEGO.equals(oid)) {
- mechanisms.put(oid, Boolean.FALSE);
- }
+ mechanisms.put(oid, Boolean.FALSE);
}
}
supportedMechanisms = mechanisms;
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java
index fe6671489c..aef263d7f4 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java
@@ -109,6 +109,13 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth {
}
state = ProtocolState.STARTED;
currentMechanism = nextMechanism.next();
+ // RFC 4462 states that SPNEGO must not be used with ssh
+ while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) {
+ if (!nextMechanism.hasNext()) {
+ return false;
+ }
+ currentMechanism = nextMechanism.next();
+ }
try {
String hostName = getHostName(session);
context = GssApiMechanisms.createContext(currentMechanism,
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
index 3e2a1aa6df..9b4694c450 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
@@ -44,6 +44,8 @@ package org.eclipse.jgit.internal.transport.sshd;
import static java.text.MessageFormat.format;
+import java.io.IOException;
+import java.net.SocketAddress;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Iterator;
@@ -57,10 +59,14 @@ import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier.HostEntryP
import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
import org.apache.sshd.client.session.ClientSessionImpl;
import org.apache.sshd.common.FactoryManager;
+import org.apache.sshd.common.SshException;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.io.IoWriteFuture;
+import org.apache.sshd.common.util.Readable;
import org.eclipse.jgit.errors.InvalidPatternException;
import org.eclipse.jgit.fnmatch.FileNameMatcher;
+import org.eclipse.jgit.internal.transport.sshd.proxy.StatefulProxyConnector;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshConstants;
@@ -79,6 +85,8 @@ public class JGitClientSession extends ClientSessionImpl {
private CredentialsProvider credentialsProvider;
+ private StatefulProxyConnector proxyHandler;
+
/**
* @param manager
* @param session
@@ -127,6 +135,95 @@ public class JGitClientSession extends ClientSessionImpl {
return credentialsProvider;
}
+ /**
+ * Sets a {@link StatefulProxyConnector} to handle proxy connection
+ * protocols.
+ *
+ * @param handler
+ * to set
+ */
+ public void setProxyHandler(StatefulProxyConnector handler) {
+ proxyHandler = handler;
+ }
+
+ @Override
+ protected IoWriteFuture sendIdentification(String ident)
+ throws IOException {
+ // Nothing; we do this below together with the KEX init in
+ // sendStartSsh(). Called only from the ClientSessionImpl constructor,
+ // where the return value is ignored.
+ return null;
+ }
+
+ @Override
+ protected byte[] sendKexInit() throws IOException {
+ StatefulProxyConnector proxy = proxyHandler;
+ if (proxy != null) {
+ try {
+ // We must not block here; the framework starts reading messages
+ // from the peer only once sendKexInit() has returned!
+ proxy.runWhenDone(() -> {
+ sendStartSsh();
+ return null;
+ });
+ // sendKexInit() is called only from the ClientSessionImpl
+ // constructor, where the return value is ignored.
+ return null;
+ } catch (IOException e) {
+ throw e;
+ } catch (Exception other) {
+ throw new IOException(other.getLocalizedMessage(), other);
+ }
+ } else {
+ return sendStartSsh();
+ }
+ }
+
+ /**
+ * Sends the initial messages starting the ssh setup: the client
+ * identification and the KEX init message.
+ *
+ * @return the client's KEX seed
+ * @throws IOException
+ * if something goes wrong
+ */
+ private byte[] sendStartSsh() throws IOException {
+ super.sendIdentification(clientVersion);
+ return super.sendKexInit();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * As long as we're still setting up the proxy connection, diverts messages
+ * to the {@link StatefulProxyConnector}.
+ */
+ @Override
+ public void messageReceived(Readable buffer) throws Exception {
+ StatefulProxyConnector proxy = proxyHandler;
+ if (proxy != null) {
+ proxy.messageReceived(getIoSession(), buffer);
+ } else {
+ super.messageReceived(buffer);
+ }
+ }
+
+ @Override
+ protected void checkKeys() throws SshException {
+ ServerKeyVerifier serverKeyVerifier = getServerKeyVerifier();
+ // The super implementation always uses
+ // getIoSession().getRemoteAddress(). In case of a proxy connection,
+ // that would be the address of the proxy!
+ SocketAddress remoteAddress = getConnectAddress();
+ PublicKey serverKey = getKex().getServerKey();
+ if (!serverKeyVerifier.verifyServerKey(this, remoteAddress,
+ serverKey)) {
+ throw new SshException(
+ org.apache.sshd.common.SshConstants.SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE,
+ SshdText.get().kexServerKeyInvalid);
+ }
+ }
+
@Override
protected String resolveAvailableSignaturesProposal(
FactoryManager manager) {
@@ -175,8 +272,10 @@ public class JGitClientSession extends ClientSessionImpl {
// keys first.
ServerKeyVerifier verifier = getServerKeyVerifier();
if (verifier instanceof ServerKeyLookup) {
+ SocketAddress remoteAddress = resolvePeerAddress(
+ resolveAttribute(JGitSshClient.ORIGINAL_REMOTE_ADDRESS));
List<HostEntryPair> allKnownKeys = ((ServerKeyLookup) verifier)
- .lookup(this, this.getIoSession().getRemoteAddress());
+ .lookup(this, remoteAddress);
Set<String> reordered = new LinkedHashSet<>();
for (HostEntryPair h : allKnownKeys) {
PublicKey key = h.getServerKey();
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 915b696b99..9e9340482f 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
@@ -47,6 +47,7 @@ import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive
import java.io.IOException;
import java.net.InetSocketAddress;
+import java.net.Proxy;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
@@ -73,9 +74,15 @@ import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.session.helpers.AbstractSession;
import org.apache.sshd.common.util.ValidateUtils;
+import org.eclipse.jgit.internal.transport.sshd.proxy.HttpClientConnector;
+import org.eclipse.jgit.internal.transport.sshd.proxy.Socks5ClientConnector;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshConstants;
import org.eclipse.jgit.transport.sshd.KeyCache;
+import org.eclipse.jgit.transport.sshd.ProxyData;
+import org.eclipse.jgit.transport.sshd.ProxyDataFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/**
* Customized {@link SshClient} for JGit. It creates specialized
@@ -83,7 +90,7 @@ import org.eclipse.jgit.transport.sshd.KeyCache;
* were created for, and it loads all KeyPair identities lazily.
*/
public class JGitSshClient extends SshClient {
-
+ private static Logger LOG = LoggerFactory.getLogger(JGitSshClient.class);
/**
* We need access to this during the constructor of the ClientSession,
* before setConnectAddress() can have been called. So we have to remember
@@ -91,6 +98,8 @@ public class JGitSshClient extends SshClient {
*/
static final AttributeKey<HostConfigEntry> HOST_CONFIG_ENTRY = new AttributeKey<>();
+ static final AttributeKey<InetSocketAddress> ORIGINAL_REMOTE_ADDRESS = new AttributeKey<>();
+
/**
* An attribute key for the comma-separated list of default preferred
* authentication mechanisms.
@@ -101,6 +110,8 @@ public class JGitSshClient extends SshClient {
private CredentialsProvider credentialsProvider;
+ private ProxyDataFactory proxyDatabase;
+
@Override
protected SessionFactory createSessionFactory() {
// Override the parent's default
@@ -133,6 +144,13 @@ public class JGitSshClient extends SshClient {
getAttribute(PREFERRED_AUTHENTICATIONS)),
PREFERRED_AUTHS);
setAttribute(HOST_CONFIG_ENTRY, hostConfig);
+ setAttribute(ORIGINAL_REMOTE_ADDRESS, address);
+ // Proxy support
+ ProxyData proxy = getProxyData(hostConfig, address);
+ if (proxy != null) {
+ address = configureProxy(proxy, address);
+ proxy.clearPassword();
+ }
connector.connect(address).addListener(listener);
return connectFuture;
}
@@ -143,6 +161,38 @@ public class JGitSshClient extends SshClient {
}
}
+ private ProxyData getProxyData(HostConfigEntry hostConfig,
+ InetSocketAddress remoteAddress) {
+ ProxyDataFactory factory = getProxyDatabase();
+ return factory == null ? null : factory.get(hostConfig, remoteAddress);
+ }
+
+ private InetSocketAddress configureProxy(ProxyData proxyData,
+ InetSocketAddress remoteAddress) {
+ Proxy proxy = proxyData.getProxy();
+ if (proxy.type() == Proxy.Type.DIRECT
+ || !(proxy.address() instanceof InetSocketAddress)) {
+ return remoteAddress;
+ }
+ InetSocketAddress address = (InetSocketAddress) proxy.address();
+ switch (proxy.type()) {
+ case HTTP:
+ setClientProxyConnector(
+ new HttpClientConnector(address, remoteAddress,
+ proxyData.getUser(), proxyData.getPassword()));
+ return address;
+ case SOCKS:
+ setClientProxyConnector(
+ new Socks5ClientConnector(address, remoteAddress,
+ proxyData.getUser(), proxyData.getPassword()));
+ return address;
+ default:
+ LOG.warn(format(SshdText.get().unknownProxyProtocol,
+ proxy.type().name()));
+ return remoteAddress;
+ }
+ }
+
private SshFutureListener<IoConnectFuture> createConnectCompletionListener(
ConnectFuture connectFuture, String username,
InetSocketAddress address, HostConfigEntry hostConfig) {
@@ -261,6 +311,26 @@ public class JGitSshClient extends SshClient {
}
/**
+ * Sets a {@link ProxyDataFactory} for connecting through proxies.
+ *
+ * @param factory
+ * to use, or {@code null} if proxying is not desired or
+ * supported
+ */
+ public void setProxyDatabase(ProxyDataFactory factory) {
+ proxyDatabase = factory;
+ }
+
+ /**
+ * Retrieves the {@link ProxyDataFactory}.
+ *
+ * @return the factory, or {@code null} if none is set
+ */
+ protected ProxyDataFactory getProxyDatabase() {
+ return proxyDatabase;
+ }
+
+ /**
* Sets the {@link CredentialsProvider} for this client.
*
* @param provider
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
index a96a6962cc..27380db33b 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
@@ -119,7 +119,7 @@ public class JGitUserInteraction implements UserInteraction {
return prompt; // Is known to have length zero here
}
URIish uri = toURI(session.getUsername(),
- (InetSocketAddress) session.getIoSession().getRemoteAddress());
+ (InetSocketAddress) session.getConnectAddress());
if (provider.get(uri, items)) {
return items.stream().map(i -> {
if (i instanceof CredentialItem.Password) {
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 bd9b2a2544..d4b6593ef6 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
@@ -34,6 +34,7 @@ public final class SshdText extends TranslationBundle {
/***/ public String identityFileNoKey;
/***/ public String identityFileMultipleKeys;
/***/ public String identityFileUnsupportedFormat;
+ /***/ public String kexServerKeyInvalid;
/***/ public String keyEncryptedMsg;
/***/ public String keyEncryptedPrompt;
/***/ public String keyEncryptedRetry;
@@ -55,9 +56,35 @@ public final class SshdText extends TranslationBundle {
/***/ public String knownHostsUnknownKeyType;
/***/ public String knownHostsUserAskCreationMsg;
/***/ public String knownHostsUserAskCreationPrompt;
+ /***/ public String proxyCannotAuthenticate;
+ /***/ public String proxyHttpFailure;
+ /***/ public String proxyHttpInvalidUserName;
+ /***/ public String proxyHttpUnexpectedReply;
+ /***/ public String proxyHttpUnspecifiedFailureReason;
+ /***/ public String proxyPasswordPrompt;
+ /***/ public String proxySocksAuthenticationFailed;
+ /***/ public String proxySocksFailureForbidden;
+ /***/ public String proxySocksFailureGeneral;
+ /***/ public String proxySocksFailureHostUnreachable;
+ /***/ public String proxySocksFailureNetworkUnreachable;
+ /***/ public String proxySocksFailureRefused;
+ /***/ public String proxySocksFailureTTL;
+ /***/ public String proxySocksFailureUnspecified;
+ /***/ public String proxySocksFailureUnsupportedAddress;
+ /***/ public String proxySocksFailureUnsupportedCommand;
+ /***/ public String proxySocksGssApiFailure;
+ /***/ public String proxySocksGssApiMessageTooShort;
+ /***/ public String proxySocksGssApiUnknownMessage;
+ /***/ public String proxySocksGssApiVersionMismatch;
+ /***/ public String proxySocksNoRemoteHostName;
+ /***/ public String proxySocksPasswordTooLong;
+ /***/ public String proxySocksUnexpectedMessage;
+ /***/ public String proxySocksUnexpectedVersion;
+ /***/ public String proxySocksUsernameTooLong;
/***/ public String sessionCloseFailed;
/***/ public String sshClosingDown;
/***/ public String sshCommandTimeout;
/***/ public String sshProcessStillRunning;
+ /***/ public String unknownProxyProtocol;
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/AbstractAuthenticationHandler.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/AbstractAuthenticationHandler.java
new file mode 100644
index 0000000000..6caa1b6aa1
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/AbstractAuthenticationHandler.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.auth;
+
+import java.net.InetSocketAddress;
+
+/**
+ * Abstract base class for {@link AuthenticationHandler}s encapsulating basic
+ * common things.
+ *
+ * @param <ParameterType>
+ * defining the parameter type for the authentication
+ * @param <TokenType>
+ * defining the token type for the authentication
+ */
+public abstract class AbstractAuthenticationHandler<ParameterType, TokenType>
+ implements AuthenticationHandler<ParameterType, TokenType> {
+
+ /** The {@link InetSocketAddress} or the proxy to connect to. */
+ protected InetSocketAddress proxy;
+
+ /** The last set parameters. */
+ protected ParameterType params;
+
+ /** A flag telling whether this authentication is done. */
+ protected boolean done;
+
+ /**
+ * Creates a new {@link AbstractAuthenticationHandler} to authenticate with
+ * the given {@code proxy}.
+ *
+ * @param proxy
+ * the {@link InetSocketAddress} of the proxy to connect to
+ */
+ public AbstractAuthenticationHandler(InetSocketAddress proxy) {
+ this.proxy = proxy;
+ }
+
+ @Override
+ public final void setParams(ParameterType input) {
+ params = input;
+ }
+
+ @Override
+ public final boolean isDone() {
+ return done;
+ }
+
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/AuthenticationHandler.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/AuthenticationHandler.java
new file mode 100644
index 0000000000..34724687a2
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/AuthenticationHandler.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.auth;
+
+import java.io.Closeable;
+
+/**
+ * An {@code AuthenticationHandler} encapsulates a possibly multi-step
+ * authentication protocol. Intended usage:
+ *
+ * <pre>
+ * setParams(something);
+ * start();
+ * sendToken(getToken());
+ * while (!isDone()) {
+ * setParams(receiveMessageAndExtractParams());
+ * process();
+ * Object t = getToken();
+ * if (t != null) {
+ * sendToken(t);
+ * }
+ * }
+ * </pre>
+ *
+ * An {@code AuthenticationHandler} may be stateful and therefore is a
+ * {@link Closeable}.
+ *
+ * @param <ParameterType>
+ * defining the parameter type for {@link #setParams(Object)}
+ * @param <TokenType>
+ * defining the token type for {@link #getToken()}
+ */
+public interface AuthenticationHandler<ParameterType, TokenType>
+ extends Closeable {
+
+ /**
+ * Produces the initial authentication token that can be then retrieved via
+ * {@link #getToken()}.
+ *
+ * @throws Exception
+ * if an error occurs
+ */
+ void start() throws Exception;
+
+ /**
+ * Produces the next authentication token, if any.
+ *
+ * @throws Exception
+ * if an error occurs
+ */
+ void process() throws Exception;
+
+ /**
+ * Sets the parameters for the next token generation via {@link #start()} or
+ * {@link #process()}.
+ *
+ * @param input
+ * to set, may be {@code null}
+ */
+ void setParams(ParameterType input);
+
+ /**
+ * Retrieves the last token generated.
+ *
+ * @return the token, or {@code null} if there is none
+ * @throws Exception
+ * if an error occurs
+ */
+ TokenType getToken() throws Exception;
+
+ /**
+ * Tells whether is authentication mechanism is done (successfully or
+ * unsuccessfully).
+ *
+ * @return whether this authentication is done
+ */
+ boolean isDone();
+
+ @Override
+ public void close();
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/BasicAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/BasicAuthentication.java
new file mode 100644
index 0000000000..efb1f55867
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/BasicAuthentication.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.auth;
+
+import java.net.Authenticator;
+import java.net.Authenticator.RequestorType;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Arrays;
+import java.util.concurrent.CancellationException;
+
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.transport.SshConstants;
+
+/**
+ * An abstract implementation of a username-password authentication. It can be
+ * given an initial known username-password pair; if so, this will be tried
+ * first. Subsequent rounds will then try to obtain a user name and password via
+ * the global {@link Authenticator}.
+ *
+ * @param <ParameterType>
+ * defining the parameter type for the authentication
+ * @param <TokenType>
+ * defining the token type for the authentication
+ */
+public abstract class BasicAuthentication<ParameterType, TokenType>
+ extends AbstractAuthenticationHandler<ParameterType, TokenType> {
+
+ /** The current user name. */
+ protected String user;
+
+ /** The current password. */
+ protected byte[] password;
+
+ /**
+ * Creates a new {@link BasicAuthentication} to authenticate with the given
+ * {@code proxy}.
+ *
+ * @param proxy
+ * {@link InetSocketAddress} of the proxy to connect to
+ * @param initialUser
+ * initial user name to try; may be {@code null}
+ * @param initialPassword
+ * initial password to try, may be {@code null}
+ */
+ public BasicAuthentication(InetSocketAddress proxy, String initialUser,
+ char[] initialPassword) {
+ super(proxy);
+ this.user = initialUser;
+ this.password = convert(initialPassword);
+ }
+
+ private byte[] convert(char[] pass) {
+ if (pass == null) {
+ return new byte[0];
+ }
+ ByteBuffer bytes = StandardCharsets.UTF_8.encode(CharBuffer.wrap(pass));
+ byte[] pwd = new byte[bytes.remaining()];
+ bytes.get(pwd);
+ if (bytes.hasArray()) {
+ Arrays.fill(bytes.array(), (byte) 0);
+ }
+ Arrays.fill(pass, '\000');
+ return pwd;
+ }
+
+ /**
+ * Clears the {@link #password}.
+ */
+ protected void clearPassword() {
+ if (password != null) {
+ Arrays.fill(password, (byte) 0);
+ }
+ password = new byte[0];
+ }
+
+ @Override
+ public final void close() {
+ clearPassword();
+ done = true;
+ }
+
+ @Override
+ public final void start() throws Exception {
+ if (user != null && !user.isEmpty()
+ || password != null && password.length > 0) {
+ return;
+ }
+ askCredentials();
+ }
+
+ @Override
+ public void process() throws Exception {
+ askCredentials();
+ }
+
+ /**
+ * Asks for credentials via the global {@link Authenticator}.
+ */
+ protected void askCredentials() {
+ clearPassword();
+ PasswordAuthentication auth = AccessController
+ .doPrivileged(new PrivilegedAction<PasswordAuthentication>() {
+
+ @Override
+ public PasswordAuthentication run() {
+ return Authenticator.requestPasswordAuthentication(
+ proxy.getHostString(), proxy.getAddress(),
+ proxy.getPort(), SshConstants.SSH_SCHEME,
+ SshdText.get().proxyPasswordPrompt, "Basic", //$NON-NLS-1$
+ null, RequestorType.PROXY);
+ }
+ });
+ if (auth == null) {
+ user = ""; //$NON-NLS-1$
+ throw new CancellationException(
+ SshdText.get().authenticationCanceled);
+ }
+ user = auth.getUserName();
+ password = convert(auth.getPassword());
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/GssApiAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/GssApiAuthentication.java
new file mode 100644
index 0000000000..63cc954479
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/auth/GssApiAuthentication.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.auth;
+
+import static java.text.MessageFormat.format;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.ietf.jgss.GSSContext;
+
+/**
+ * An abstract implementation of a GSS-API multi-round authentication.
+ *
+ * @param <ParameterType>
+ * defining the parameter type for the authentication
+ * @param <TokenType>
+ * defining the token type for the authentication
+ */
+public abstract class GssApiAuthentication<ParameterType, TokenType>
+ extends AbstractAuthenticationHandler<ParameterType, TokenType> {
+
+ private GSSContext context;
+
+ /** The last token generated. */
+ protected byte[] token;
+
+ /**
+ * Creates a new {@link GssApiAuthentication} to authenticate with the given
+ * {@code proxy}.
+ *
+ * @param proxy
+ * the {@link InetSocketAddress} of the proxy to connect to
+ */
+ public GssApiAuthentication(InetSocketAddress proxy) {
+ super(proxy);
+ }
+
+ @Override
+ public void close() {
+ GssApiMechanisms.closeContextSilently(context);
+ context = null;
+ done = true;
+ }
+
+ @Override
+ public final void start() throws Exception {
+ try {
+ context = createContext();
+ context.requestMutualAuth(true);
+ context.requestConf(false);
+ context.requestInteg(false);
+ byte[] empty = new byte[0];
+ token = context.initSecContext(empty, 0, 0);
+ } catch (Exception e) {
+ close();
+ throw e;
+ }
+ }
+
+ @Override
+ public final void process() throws Exception {
+ if (context == null) {
+ throw new IOException(
+ format(SshdText.get().proxyCannotAuthenticate, proxy));
+ }
+ try {
+ byte[] received = extractToken(params);
+ token = context.initSecContext(received, 0, received.length);
+ checkDone();
+ } catch (Exception e) {
+ close();
+ throw e;
+ }
+ }
+
+ private void checkDone() throws Exception {
+ done = context.isEstablished();
+ if (done) {
+ context.dispose();
+ context = null;
+ }
+ }
+
+ /**
+ * Creates the {@link GSSContext} to use.
+ *
+ * @return a fresh {@link GSSContext} to use
+ * @throws Exception
+ * if the context cannot be created
+ */
+ protected abstract GSSContext createContext() throws Exception;
+
+ /**
+ * Extracts the token from the last set parameters.
+ *
+ * @param input
+ * to extract the token from
+ * @return the extracted token, or {@code null} if none
+ * @throws Exception
+ * if an error occurs
+ */
+ protected abstract byte[] extractToken(ParameterType input)
+ throws Exception;
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/AbstractClientProxyConnector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/AbstractClientProxyConnector.java
new file mode 100644
index 0000000000..444fbb62ed
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/AbstractClientProxyConnector.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.proxy;
+
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.transport.sshd.JGitClientSession;
+
+/**
+ * Basic common functionality for a {@link StatefulProxyConnector}.
+ */
+public abstract class AbstractClientProxyConnector
+ implements StatefulProxyConnector {
+
+ private static final long DEFAULT_PROXY_TIMEOUT_MILLIS = TimeUnit.SECONDS
+ .toMillis(30L);
+
+ /** Guards {@link #done} and {@link #startSsh}. */
+ private Object lock = new Object();
+
+ private boolean done;
+
+ private Callable<Void> startSsh;
+
+ private AtomicReference<Runnable> unregister = new AtomicReference<>();
+
+ private long remainingProxyProtocolTime = DEFAULT_PROXY_TIMEOUT_MILLIS;
+
+ private long lastProxyOperationTime = 0L;
+
+ /** The ultimate remote address to connect to. */
+ protected final InetSocketAddress remoteAddress;
+
+ /** The proxy address. */
+ protected final InetSocketAddress proxyAddress;
+
+ /** The user to authenticate at the proxy with. */
+ protected String proxyUser;
+
+ /** The password to use for authentication at the proxy. */
+ protected char[] proxyPassword;
+
+ /**
+ * Creates a new {@link AbstractClientProxyConnector}.
+ *
+ * @param proxyAddress
+ * of the proxy server we're connecting to
+ * @param remoteAddress
+ * of the target server to connect to
+ * @param proxyUser
+ * to authenticate at the proxy with; may be {@code null}
+ * @param proxyPassword
+ * to authenticate at the proxy with; may be {@code null}
+ */
+ public AbstractClientProxyConnector(@NonNull InetSocketAddress proxyAddress,
+ @NonNull InetSocketAddress remoteAddress, String proxyUser,
+ char[] proxyPassword) {
+ this.proxyAddress = proxyAddress;
+ this.remoteAddress = remoteAddress;
+ this.proxyUser = proxyUser;
+ this.proxyPassword = proxyPassword == null ? new char[0]
+ : proxyPassword;
+ }
+
+ /**
+ * Initializes this instance. Installs itself as proxy handler on the
+ * session.
+ *
+ * @param session
+ * to initialize for
+ */
+ protected void init(ClientSession session) {
+ remainingProxyProtocolTime = session.getLongProperty(
+ StatefulProxyConnector.TIMEOUT_PROPERTY,
+ DEFAULT_PROXY_TIMEOUT_MILLIS);
+ if (remainingProxyProtocolTime <= 0L) {
+ remainingProxyProtocolTime = DEFAULT_PROXY_TIMEOUT_MILLIS;
+ }
+ if (session instanceof JGitClientSession) {
+ JGitClientSession s = (JGitClientSession) session;
+ unregister.set(() -> s.setProxyHandler(null));
+ s.setProxyHandler(this);
+ } else {
+ // Internal error, no translation
+ throw new IllegalStateException(
+ "Not a JGit session: " + session.getClass().getName()); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Obtains the timeout for the whole rest of the proxy connection protocol.
+ *
+ * @return the timeout in milliseconds, always > 0L
+ */
+ protected long getTimeout() {
+ long last = lastProxyOperationTime;
+ long now = System.nanoTime();
+ lastProxyOperationTime = now;
+ long remaining = remainingProxyProtocolTime;
+ if (last != 0L) {
+ long elapsed = now - last;
+ remaining -= elapsed;
+ if (remaining < 0L) {
+ remaining = 10L; // Give it grace period.
+ }
+ }
+ remainingProxyProtocolTime = remaining;
+ return remaining;
+ }
+
+ /**
+ * Adjusts the timeout calculation to not account of elapsed time since the
+ * last time the timeout was gotten. Can be used for instance to ignore time
+ * spent in user dialogs be counted against the overall proxy connection
+ * protocol timeout.
+ */
+ protected void adjustTimeout() {
+ lastProxyOperationTime = System.nanoTime();
+ }
+
+ /**
+ * Sets the "done" flag.
+ *
+ * @param success
+ * whether the connector terminated successfully.
+ * @throws Exception
+ * if starting ssh fails
+ */
+ protected void setDone(boolean success) throws Exception {
+ Callable<Void> starter;
+ Runnable unset = unregister.getAndSet(null);
+ if (unset != null) {
+ unset.run();
+ }
+ synchronized (lock) {
+ done = true;
+ starter = startSsh;
+ startSsh = null;
+ }
+ if (success && starter != null) {
+ starter.call();
+ }
+ }
+
+ @Override
+ public void runWhenDone(Callable<Void> starter) throws Exception {
+ synchronized (lock) {
+ if (!done) {
+ this.startSsh = starter;
+ return;
+ }
+ }
+ starter.call();
+ }
+
+ /**
+ * Clears the proxy password.
+ */
+ protected void clearPassword() {
+ Arrays.fill(proxyPassword, '\000');
+ proxyPassword = new char[0];
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/AuthenticationChallenge.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/AuthenticationChallenge.java
new file mode 100644
index 0000000000..4a6572d45b
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/AuthenticationChallenge.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.proxy;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A simple representation of an authentication challenge as sent in a
+ * "WWW-Authenticate" or "Proxy-Authenticate" header. Such challenges start with
+ * a mechanism name, followed either by one single token, or by a list of
+ * key=value pairs.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7235#section-2.1">RFC 7235, sec.
+ * 2.1</a>
+ */
+public class AuthenticationChallenge {
+
+ private final String mechanism;
+
+ private String token;
+
+ private Map<String, String> arguments;
+
+ /**
+ * Create a new {@link AuthenticationChallenge} with the given mechanism.
+ *
+ * @param mechanism
+ * for the challenge
+ */
+ public AuthenticationChallenge(String mechanism) {
+ this.mechanism = mechanism;
+ }
+
+ /**
+ * Retrieves the authentication mechanism specified by this challenge, for
+ * instance "Basic".
+ *
+ * @return the mechanism name
+ */
+ public String getMechanism() {
+ return mechanism;
+ }
+
+ /**
+ * Retrieves the token of the challenge, if any.
+ *
+ * @return the token, or {@code null} if there is none.
+ */
+ public String getToken() {
+ return token;
+ }
+
+ /**
+ * Retrieves the arguments of the challenge.
+ *
+ * @return a possibly empty map of the key=value arguments of the challenge
+ */
+ @NonNull
+ public Map<String, String> getArguments() {
+ return arguments == null ? Collections.emptyMap() : arguments;
+ }
+
+ void addArgument(String key, String value) {
+ if (arguments == null) {
+ arguments = new LinkedHashMap<>();
+ }
+ arguments.put(key, value);
+ }
+
+ void setToken(String token) {
+ this.token = token;
+ }
+
+ @Override
+ public String toString() {
+ return "AuthenticationChallenge[" + mechanism + ',' + token + ',' //$NON-NLS-1$
+ + (arguments == null ? "<none>" : arguments.toString()) + ']'; //$NON-NLS-1$
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpClientConnector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpClientConnector.java
new file mode 100644
index 0000000000..46cdd52f5f
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpClientConnector.java
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.proxy;
+
+import static java.text.MessageFormat.format;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.util.Readable;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.internal.transport.sshd.auth.AuthenticationHandler;
+import org.eclipse.jgit.internal.transport.sshd.auth.BasicAuthentication;
+import org.eclipse.jgit.internal.transport.sshd.auth.GssApiAuthentication;
+import org.eclipse.jgit.util.Base64;
+import org.ietf.jgss.GSSContext;
+
+/**
+ * Simple HTTP proxy connector using Basic Authentication.
+ */
+public class HttpClientConnector extends AbstractClientProxyConnector {
+
+ private static final String HTTP_HEADER_PROXY_AUTHENTICATION = "Proxy-Authentication:"; //$NON-NLS-1$
+
+ private static final String HTTP_HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization:"; //$NON-NLS-1$
+
+ private HttpAuthenticationHandler basic;
+
+ private HttpAuthenticationHandler negotiate;
+
+ private List<HttpAuthenticationHandler> availableAuthentications;
+
+ private Iterator<HttpAuthenticationHandler> clientAuthentications;
+
+ private HttpAuthenticationHandler authenticator;
+
+ private boolean ongoing;
+
+ /**
+ * Creates a new {@link HttpClientConnector}. The connector supports
+ * anonymous proxy connections as well as Basic and Negotiate
+ * authentication.
+ *
+ * @param proxyAddress
+ * of the proxy server we're connecting to
+ * @param remoteAddress
+ * of the target server to connect to
+ */
+ public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
+ @NonNull InetSocketAddress remoteAddress) {
+ this(proxyAddress, remoteAddress, null, null);
+ }
+
+ /**
+ * Creates a new {@link HttpClientConnector}. The connector supports
+ * anonymous proxy connections as well as Basic and Negotiate
+ * authentication. If a user name and password are given, the connector
+ * tries pre-emptive Basic authentication.
+ *
+ * @param proxyAddress
+ * of the proxy server we're connecting to
+ * @param remoteAddress
+ * of the target server to connect to
+ * @param proxyUser
+ * to authenticate at the proxy with
+ * @param proxyPassword
+ * to authenticate at the proxy with
+ */
+ public HttpClientConnector(@NonNull InetSocketAddress proxyAddress,
+ @NonNull InetSocketAddress remoteAddress, String proxyUser,
+ char[] proxyPassword) {
+ super(proxyAddress, remoteAddress, proxyUser, proxyPassword);
+ basic = new HttpBasicAuthentication();
+ negotiate = new NegotiateAuthentication();
+ availableAuthentications = new ArrayList<>(2);
+ availableAuthentications.add(negotiate);
+ availableAuthentications.add(basic);
+ clientAuthentications = availableAuthentications.iterator();
+ }
+
+ private void close() {
+ HttpAuthenticationHandler current = authenticator;
+ authenticator = null;
+ if (current != null) {
+ current.close();
+ }
+ }
+
+ @Override
+ public void sendClientProxyMetadata(ClientSession sshSession)
+ throws Exception {
+ init(sshSession);
+ IoSession session = sshSession.getIoSession();
+ session.addCloseFutureListener(f -> close());
+ StringBuilder msg = connect();
+ if (proxyUser != null && !proxyUser.isEmpty()
+ || proxyPassword != null && proxyPassword.length > 0) {
+ authenticator = basic;
+ basic.setParams(null);
+ basic.start();
+ msg = authenticate(msg, basic.getToken());
+ clearPassword();
+ proxyUser = null;
+ }
+ ongoing = true;
+ try {
+ send(msg, session);
+ } catch (Exception e) {
+ ongoing = false;
+ throw e;
+ }
+ }
+
+ private void send(StringBuilder msg, IoSession session) throws Exception {
+ byte[] data = eol(msg).toString().getBytes(StandardCharsets.US_ASCII);
+ Buffer buffer = new ByteArrayBuffer(data.length, false);
+ buffer.putRawBytes(data);
+ session.writePacket(buffer).verify(getTimeout());
+ }
+
+ private StringBuilder connect() {
+ StringBuilder msg = new StringBuilder();
+ // Persistent connections are the default in HTTP 1.1 (see RFC 2616),
+ // but let's be explicit.
+ return msg.append(format(
+ "CONNECT {0}:{1} HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\nHost: {0}:{1}\r\n", //$NON-NLS-1$
+ remoteAddress.getHostString(),
+ Integer.toString(remoteAddress.getPort())));
+ }
+
+ private StringBuilder authenticate(StringBuilder msg, String token) {
+ msg.append(HTTP_HEADER_PROXY_AUTHORIZATION).append(' ').append(token);
+ return eol(msg);
+ }
+
+ private StringBuilder eol(StringBuilder msg) {
+ return msg.append('\r').append('\n');
+ }
+
+ @Override
+ public void messageReceived(IoSession session, Readable buffer)
+ throws Exception {
+ try {
+ int length = buffer.available();
+ byte[] data = new byte[length];
+ buffer.getRawBytes(data, 0, length);
+ String[] reply = new String(data, StandardCharsets.US_ASCII)
+ .split("\r\n"); //$NON-NLS-1$
+ handleMessage(session, Arrays.asList(reply));
+ } catch (Exception e) {
+ if (authenticator != null) {
+ authenticator.close();
+ authenticator = null;
+ }
+ ongoing = false;
+ try {
+ setDone(false);
+ } catch (Exception inner) {
+ e.addSuppressed(inner);
+ }
+ throw e;
+ }
+ }
+
+ private void handleMessage(IoSession session, List<String> reply)
+ throws Exception {
+ if (reply.isEmpty() || reply.get(0).isEmpty()) {
+ throw new IOException(
+ format(SshdText.get().proxyHttpUnexpectedReply,
+ proxyAddress, "<empty>")); //$NON-NLS-1$
+ }
+ try {
+ StatusLine status = HttpParser.parseStatusLine(reply.get(0));
+ if (!ongoing) {
+ throw new IOException(format(
+ SshdText.get().proxyHttpUnexpectedReply, proxyAddress,
+ Integer.toString(status.getResultCode()),
+ status.getReason()));
+ }
+ switch (status.getResultCode()) {
+ case HttpURLConnection.HTTP_OK:
+ if (authenticator != null) {
+ authenticator.close();
+ }
+ authenticator = null;
+ ongoing = false;
+ setDone(true);
+ break;
+ case HttpURLConnection.HTTP_PROXY_AUTH:
+ List<AuthenticationChallenge> challenges = HttpParser
+ .getAuthenticationHeaders(reply,
+ HTTP_HEADER_PROXY_AUTHENTICATION);
+ authenticator = selectProtocol(challenges, authenticator);
+ if (authenticator == null) {
+ throw new IOException(
+ format(SshdText.get().proxyCannotAuthenticate,
+ proxyAddress));
+ }
+ String token = authenticator.getToken();
+ if (token == null) {
+ throw new IOException(
+ format(SshdText.get().proxyCannotAuthenticate,
+ proxyAddress));
+ }
+ send(authenticate(connect(), token), session);
+ break;
+ default:
+ throw new IOException(format(SshdText.get().proxyHttpFailure,
+ proxyAddress, Integer.toString(status.getResultCode()),
+ status.getReason()));
+ }
+ } catch (HttpParser.ParseException e) {
+ throw new IOException(
+ format(SshdText.get().proxyHttpUnexpectedReply,
+ proxyAddress, reply.get(0)));
+ }
+ }
+
+ private HttpAuthenticationHandler selectProtocol(
+ List<AuthenticationChallenge> challenges,
+ HttpAuthenticationHandler current) throws Exception {
+ if (current != null && !current.isDone()) {
+ AuthenticationChallenge challenge = getByName(challenges,
+ current.getName());
+ if (challenge != null) {
+ current.setParams(challenge);
+ current.process();
+ return current;
+ }
+ }
+ if (current != null) {
+ current.close();
+ }
+ while (clientAuthentications.hasNext()) {
+ HttpAuthenticationHandler next = clientAuthentications.next();
+ if (!next.isDone()) {
+ AuthenticationChallenge challenge = getByName(challenges,
+ next.getName());
+ if (challenge != null) {
+ next.setParams(challenge);
+ next.start();
+ return next;
+ }
+ }
+ }
+ return null;
+ }
+
+ private AuthenticationChallenge getByName(
+ List<AuthenticationChallenge> challenges,
+ String name) {
+ return challenges.stream()
+ .filter(c -> c.getMechanism().equalsIgnoreCase(name))
+ .findFirst().orElse(null);
+ }
+
+ private interface HttpAuthenticationHandler
+ extends AuthenticationHandler<AuthenticationChallenge, String> {
+
+ public String getName();
+ }
+
+ /**
+ * @see <a href="https://tools.ietf.org/html/rfc7617">RFC 7617</a>
+ */
+ private class HttpBasicAuthentication
+ extends BasicAuthentication<AuthenticationChallenge, String>
+ implements HttpAuthenticationHandler {
+
+ private boolean asked;
+
+ public HttpBasicAuthentication() {
+ super(proxyAddress, proxyUser, proxyPassword);
+ }
+
+ @Override
+ public String getName() {
+ return "Basic"; //$NON-NLS-1$
+ }
+
+ @Override
+ protected void askCredentials() {
+ // We ask only once.
+ if (asked) {
+ throw new IllegalStateException(
+ "Basic auth: already asked user for password"); //$NON-NLS-1$
+ }
+ asked = true;
+ super.askCredentials();
+ done = true;
+ }
+
+ @Override
+ public String getToken() throws Exception {
+ if (user.indexOf(':') >= 0) {
+ throw new IOException(format(
+ SshdText.get().proxyHttpInvalidUserName, proxy, user));
+ }
+ byte[] rawUser = user.getBytes(StandardCharsets.UTF_8);
+ byte[] toEncode = new byte[rawUser.length + 1 + password.length];
+ System.arraycopy(rawUser, 0, toEncode, 0, rawUser.length);
+ toEncode[rawUser.length] = ':';
+ System.arraycopy(password, 0, toEncode, rawUser.length + 1,
+ password.length);
+ Arrays.fill(password, (byte) 0);
+ String result = Base64.encodeBytes(toEncode);
+ Arrays.fill(toEncode, (byte) 0);
+ return getName() + ' ' + result;
+ }
+
+ }
+
+ /**
+ * @see <a href="https://tools.ietf.org/html/rfc4559">RFC 4559</a>
+ */
+ private class NegotiateAuthentication
+ extends GssApiAuthentication<AuthenticationChallenge, String>
+ implements HttpAuthenticationHandler {
+
+ public NegotiateAuthentication() {
+ super(proxyAddress);
+ }
+
+ @Override
+ public String getName() {
+ return "Negotiate"; //$NON-NLS-1$
+ }
+
+ @Override
+ public String getToken() throws Exception {
+ return getName() + ' ' + Base64.encodeBytes(token);
+ }
+
+ @Override
+ protected GSSContext createContext() throws Exception {
+ return GssApiMechanisms.createContext(GssApiMechanisms.SPNEGO,
+ GssApiMechanisms.getCanonicalName(proxyAddress));
+ }
+
+ @Override
+ protected byte[] extractToken(AuthenticationChallenge input)
+ throws Exception {
+ String received = input.getToken();
+ if (received == null) {
+ return new byte[0];
+ }
+ return Base64.decode(received);
+ }
+
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParser.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParser.java
new file mode 100644
index 0000000000..b9b32b1300
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/HttpParser.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.proxy;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A basic parser for HTTP response headers. Handles status lines and
+ * authentication headers (WWW-Authenticate, Proxy-Authenticate).
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc7230">RFC 7230</a>
+ * @see <a href="https://tools.ietf.org/html/rfc7235">RFC 7235</a>
+ */
+public final class HttpParser {
+
+ /**
+ * An exception indicating some problem parsing HTPP headers.
+ */
+ public static class ParseException extends Exception {
+
+ private static final long serialVersionUID = -1634090143702048640L;
+
+ }
+
+ private HttpParser() {
+ // No instantiation
+ }
+
+ /**
+ * Parse a HTTP response status line.
+ *
+ * @param line
+ * to parse
+ * @return the {@link StatusLine}
+ * @throws ParseException
+ * if the line cannot be parsed or has the wrong HTTP version
+ */
+ public static StatusLine parseStatusLine(String line)
+ throws ParseException {
+ // Format is HTTP/<version> Code Reason
+ int firstBlank = line.indexOf(' ');
+ if (firstBlank < 0) {
+ throw new ParseException();
+ }
+ int secondBlank = line.indexOf(' ', firstBlank + 1);
+ if (secondBlank < 0) {
+ // Accept the line even if the (according to RFC 2616 mandatory)
+ // reason is missing.
+ secondBlank = line.length();
+ }
+ int resultCode;
+ try {
+ resultCode = Integer.parseUnsignedInt(
+ line.substring(firstBlank + 1, secondBlank));
+ } catch (NumberFormatException e) {
+ throw new ParseException();
+ }
+ // Again, accept even if the reason is missing
+ String reason = ""; //$NON-NLS-1$
+ if (secondBlank < line.length()) {
+ reason = line.substring(secondBlank + 1);
+ }
+ return new StatusLine(line.substring(0, firstBlank), resultCode,
+ reason);
+ }
+
+ /**
+ * Extract the authentication headers from the header lines. It is assumed
+ * that the first element in {@code reply} is the raw status line as
+ * received from the server. It is skipped. Line processing stops on the
+ * first empty line thereafter.
+ *
+ * @param reply
+ * The complete (header) lines of the HTTP response
+ * @param authenticationHeader
+ * to look for (including the terminating ':'!)
+ * @return a list of {@link AuthenticationChallenge}s found.
+ */
+ public static List<AuthenticationChallenge> getAuthenticationHeaders(
+ List<String> reply, String authenticationHeader) {
+ List<AuthenticationChallenge> challenges = new ArrayList<>();
+ Iterator<String> lines = reply.iterator();
+ // We know we have at least one line. Skip the response line.
+ lines.next();
+ StringBuilder value = null;
+ while (lines.hasNext()) {
+ String line = lines.next();
+ if (line.isEmpty()) {
+ break;
+ }
+ if (Character.isWhitespace(line.charAt(0))) {
+ // Continuation line.
+ if (value == null) {
+ // Skip if we have no current value
+ continue;
+ }
+ // Skip leading whitespace
+ int i = skipWhiteSpace(line, 1);
+ value.append(' ').append(line, i, line.length());
+ continue;
+ }
+ if (value != null) {
+ parseChallenges(challenges, value.toString());
+ value = null;
+ }
+ int firstColon = line.indexOf(':');
+ if (firstColon > 0 && authenticationHeader
+ .equalsIgnoreCase(line.substring(0, firstColon + 1))) {
+ value = new StringBuilder(line.substring(firstColon + 1));
+ }
+ }
+ if (value != null) {
+ parseChallenges(challenges, value.toString());
+ }
+ return challenges;
+ }
+
+ private static void parseChallenges(
+ List<AuthenticationChallenge> challenges,
+ String header) {
+ // Comma-separated list of challenges, each itself a scheme name
+ // followed optionally by either: a comma-separated list of key=value
+ // pairs, where the value may be a quoted string with backslash escapes,
+ // or a single token value, which itself may end in zero or more '='
+ // characters. Ugh.
+ int length = header.length();
+ for (int i = 0; i < length;) {
+ int start = skipWhiteSpace(header, i);
+ int end = scanToken(header, start);
+ if (end <= start) {
+ break;
+ }
+ AuthenticationChallenge challenge = new AuthenticationChallenge(
+ header.substring(start, end));
+ challenges.add(challenge);
+ i = parseChallenge(challenge, header, end);
+ }
+ }
+
+ private static int parseChallenge(AuthenticationChallenge challenge,
+ String header, int from) {
+ int length = header.length();
+ boolean first = true;
+ for (int start = from; start <= length; first = false) {
+ // Now we have either a single token, which may end in zero or more
+ // equal signs, or a comma-separated list of key=value pairs (with
+ // optional legacy whitespace around the equals sign), where the
+ // value can be either a token or a quoted string.
+ start = skipWhiteSpace(header, start);
+ int end = scanToken(header, start);
+ if (end == start) {
+ // Nothing found. Either at end or on a comma.
+ if (start < header.length() && header.charAt(start) == ',') {
+ return start + 1;
+ }
+ return start;
+ }
+ int next = skipWhiteSpace(header, end);
+ // Comma, or equals sign, or end of string
+ if (next >= length || header.charAt(next) != '=') {
+ if (first) {
+ // It must be a token
+ challenge.setToken(header.substring(start, end));
+ if (next < length && header.charAt(next) == ',') {
+ next++;
+ }
+ return next;
+ } else {
+ // This token must be the name of the next authentication
+ // scheme.
+ return start;
+ }
+ }
+ int nextStart = skipWhiteSpace(header, next + 1);
+ if (nextStart >= length) {
+ if (next == end) {
+ // '=' immediately after the key, no value: key must be the
+ // token, and the equals sign is part of the token
+ challenge.setToken(header.substring(start, end + 1));
+ } else {
+ // Key without value...
+ challenge.addArgument(header.substring(start, end), null);
+ }
+ return nextStart;
+ }
+ if (nextStart == end + 1 && header.charAt(nextStart) == '=') {
+ // More than one equals sign: must be the single token.
+ end = nextStart + 1;
+ while (end < length && header.charAt(end) == '=') {
+ end++;
+ }
+ challenge.setToken(header.substring(start, end));
+ end = skipWhiteSpace(header, end);
+ if (end < length && header.charAt(end) == ',') {
+ end++;
+ }
+ return end;
+ }
+ if (header.charAt(nextStart) == ',') {
+ if (next == end) {
+ // '=' immediately after the key, no value: key must be the
+ // token, and the equals sign is part of the token
+ challenge.setToken(header.substring(start, end + 1));
+ return nextStart + 1;
+ } else {
+ // Key without value...
+ challenge.addArgument(header.substring(start, end), null);
+ start = nextStart + 1;
+ }
+ } else {
+ if (header.charAt(nextStart) == '"') {
+ int nextEnd[] = { nextStart + 1 };
+ String value = scanQuotedString(header, nextStart + 1,
+ nextEnd);
+ challenge.addArgument(header.substring(start, end), value);
+ start = nextEnd[0];
+ } else {
+ int nextEnd = scanToken(header, nextStart);
+ challenge.addArgument(header.substring(start, end),
+ header.substring(nextStart, nextEnd));
+ start = nextEnd;
+ }
+ start = skipWhiteSpace(header, start);
+ if (start < length && header.charAt(start) == ',') {
+ start++;
+ }
+ }
+ }
+ return length;
+ }
+
+ private static int skipWhiteSpace(String header, int i) {
+ int length = header.length();
+ while (i < length && Character.isWhitespace(header.charAt(i))) {
+ i++;
+ }
+ return i;
+ }
+
+ private static int scanToken(String header, int from) {
+ int length = header.length();
+ int i = from;
+ while (i < length) {
+ char c = header.charAt(i);
+ switch (c) {
+ case '!':
+ case '#':
+ case '$':
+ case '%':
+ case '&':
+ case '\'':
+ case '*':
+ case '+':
+ case '-':
+ case '.':
+ case '^':
+ case '_':
+ case '`':
+ case '|':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ i++;
+ break;
+ default:
+ if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') {
+ i++;
+ break;
+ }
+ return i;
+ }
+ }
+ return i;
+ }
+
+ private static String scanQuotedString(String header, int from, int[] to) {
+ StringBuilder result = new StringBuilder();
+ int length = header.length();
+ boolean quoted = false;
+ int i = from;
+ while (i < length) {
+ char c = header.charAt(i++);
+ if (quoted) {
+ result.append(c);
+ quoted = false;
+ } else if (c == '\\') {
+ quoted = true;
+ } else if (c == '"') {
+ break;
+ } else {
+ result.append(c);
+ }
+ }
+ to[0] = i;
+ return result.toString();
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/Socks5ClientConnector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/Socks5ClientConnector.java
new file mode 100644
index 0000000000..1844fdc794
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/Socks5ClientConnector.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.proxy;
+
+import static java.text.MessageFormat.format;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.util.Readable;
+import org.apache.sshd.common.util.buffer.Buffer;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms;
+import org.eclipse.jgit.internal.transport.sshd.SshdText;
+import org.eclipse.jgit.internal.transport.sshd.auth.AuthenticationHandler;
+import org.eclipse.jgit.internal.transport.sshd.auth.BasicAuthentication;
+import org.eclipse.jgit.internal.transport.sshd.auth.GssApiAuthentication;
+import org.eclipse.jgit.transport.SshConstants;
+import org.ietf.jgss.GSSContext;
+
+/**
+ * A {@link AbstractClientProxyConnector} to connect through a SOCKS5 proxy.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc1928">RFC 1928</a>
+ */
+public class Socks5ClientConnector extends AbstractClientProxyConnector {
+
+ // private static final byte SOCKS_VERSION_4 = 4;
+ private static final byte SOCKS_VERSION_5 = 5;
+
+ private static final byte SOCKS_CMD_CONNECT = 1;
+ // private static final byte SOCKS5_CMD_BIND = 2;
+ // private static final byte SOCKS5_CMD_UDP_ASSOCIATE = 3;
+
+ // Address types
+
+ private static final byte SOCKS_ADDRESS_IPv4 = 1;
+
+ private static final byte SOCKS_ADDRESS_FQDN = 3;
+
+ private static final byte SOCKS_ADDRESS_IPv6 = 4;
+
+ // Reply codes
+
+ private static final byte SOCKS_REPLY_SUCCESS = 0;
+
+ private static final byte SOCKS_REPLY_FAILURE = 1;
+
+ private static final byte SOCKS_REPLY_FORBIDDEN = 2;
+
+ private static final byte SOCKS_REPLY_NETWORK_UNREACHABLE = 3;
+
+ private static final byte SOCKS_REPLY_HOST_UNREACHABLE = 4;
+
+ private static final byte SOCKS_REPLY_CONNECTION_REFUSED = 5;
+
+ private static final byte SOCKS_REPLY_TTL_EXPIRED = 6;
+
+ private static final byte SOCKS_REPLY_COMMAND_UNSUPPORTED = 7;
+
+ private static final byte SOCKS_REPLY_ADDRESS_UNSUPPORTED = 8;
+
+ /**
+ * Authentication methods for SOCKS5.
+ *
+ * @see <a href=
+ * "https://www.iana.org/assignments/socks-methods/socks-methods.xhtml">SOCKS
+ * Methods, IANA.org</a>
+ */
+ private enum SocksAuthenticationMethod {
+
+ ANONYMOUS(0),
+ GSSAPI(1),
+ PASSWORD(2),
+ // CHALLENGE_HANDSHAKE(3),
+ // CHALLENGE_RESPONSE(5),
+ // SSL(6),
+ // NDS(7),
+ // MULTI_AUTH(8),
+ // JSON(9),
+ NONE_ACCEPTABLE(0xFF);
+
+ private byte value;
+
+ SocksAuthenticationMethod(int value) {
+ this.value = (byte) value;
+ }
+
+ public byte getValue() {
+ return value;
+ }
+ }
+
+ private enum ProtocolState {
+ NONE,
+
+ INIT {
+ @Override
+ public void handleMessage(Socks5ClientConnector connector,
+ IoSession session, Buffer data) throws Exception {
+ connector.versionCheck(data.getByte());
+ SocksAuthenticationMethod authMethod = connector.getAuthMethod(
+ data.getByte());
+ switch (authMethod) {
+ case ANONYMOUS:
+ connector.sendConnectInfo(session);
+ break;
+ case PASSWORD:
+ connector.doPasswordAuth(session);
+ break;
+ case GSSAPI:
+ connector.doGssApiAuth(session);
+ break;
+ default:
+ throw new IOException(
+ format(SshdText.get().proxyCannotAuthenticate,
+ connector.proxyAddress));
+ }
+ }
+ },
+
+ AUTHENTICATING {
+ @Override
+ public void handleMessage(Socks5ClientConnector connector,
+ IoSession session, Buffer data) throws Exception {
+ connector.authStep(session, data);
+ }
+ },
+
+ CONNECTING {
+ @Override
+ public void handleMessage(Socks5ClientConnector connector,
+ IoSession session, Buffer data) throws Exception {
+ // Special case: when GSS-API authentication completes, the
+ // client moves into CONNECTING as soon as the GSS context is
+ // established and sends the connect request. This is per RFC
+ // 1961. But for the server, RFC 1961 says it _should_ send an
+ // empty token even if none generated when its server side
+ // context is established. That means we may actually get an
+ // empty token here. That message is 4 bytes long (and has
+ // content 0x01, 0x01, 0x00, 0x00). We simply skip this message
+ // if we get it here. If the server for whatever reason sends
+ // back a "GSS failed" message (it shouldn't, at this point)
+ // it will be two bytes 0x01 0xFF, which will fail the version
+ // check.
+ if (data.available() != 4) {
+ connector.versionCheck(data.getByte());
+ connector.establishConnection(data);
+ }
+ }
+ },
+
+ CONNECTED,
+
+ FAILED;
+
+ public void handleMessage(Socks5ClientConnector connector,
+ @SuppressWarnings("unused") IoSession session, Buffer data)
+ throws Exception {
+ throw new IOException(
+ format(SshdText.get().proxySocksUnexpectedMessage,
+ connector.proxyAddress, this,
+ BufferUtils.toHex(data.array())));
+ }
+ }
+
+ private ProtocolState state;
+
+ private AuthenticationHandler<Buffer, Buffer> authenticator;
+
+ private GSSContext context;
+
+ private byte[] authenticationProposals;
+
+ /**
+ * Creates a new {@link Socks5ClientConnector}. The connector supports
+ * anonymous connections as well as username-password or Kerberos5 (GSS-API)
+ * authentication.
+ *
+ * @param proxyAddress
+ * of the proxy server we're connecting to
+ * @param remoteAddress
+ * of the target server to connect to
+ */
+ public Socks5ClientConnector(@NonNull InetSocketAddress proxyAddress,
+ @NonNull InetSocketAddress remoteAddress) {
+ this(proxyAddress, remoteAddress, null, null);
+ }
+
+ /**
+ * Creates a new {@link Socks5ClientConnector}. The connector supports
+ * anonymous connections as well as username-password or Kerberos5 (GSS-API)
+ * authentication.
+ *
+ * @param proxyAddress
+ * of the proxy server we're connecting to
+ * @param remoteAddress
+ * of the target server to connect to
+ * @param proxyUser
+ * to authenticate at the proxy with
+ * @param proxyPassword
+ * to authenticate at the proxy with
+ */
+ public Socks5ClientConnector(@NonNull InetSocketAddress proxyAddress,
+ @NonNull InetSocketAddress remoteAddress,
+ String proxyUser, char[] proxyPassword) {
+ super(proxyAddress, remoteAddress, proxyUser, proxyPassword);
+ this.state = ProtocolState.NONE;
+ }
+
+ @Override
+ public void sendClientProxyMetadata(ClientSession sshSession)
+ throws Exception {
+ init(sshSession);
+ IoSession session = sshSession.getIoSession();
+ // Send the initial request
+ Buffer buffer = new ByteArrayBuffer(5, false);
+ buffer.putByte(SOCKS_VERSION_5);
+ context = getGSSContext(remoteAddress);
+ authenticationProposals = getAuthenticationProposals();
+ buffer.putByte((byte) authenticationProposals.length);
+ buffer.putRawBytes(authenticationProposals);
+ state = ProtocolState.INIT;
+ session.writePacket(buffer).verify(getTimeout());
+ }
+
+ private byte[] getAuthenticationProposals() {
+ byte[] proposals = new byte[3];
+ int i = 0;
+ proposals[i++] = SocksAuthenticationMethod.ANONYMOUS.getValue();
+ proposals[i++] = SocksAuthenticationMethod.PASSWORD.getValue();
+ if (context != null) {
+ proposals[i++] = SocksAuthenticationMethod.GSSAPI.getValue();
+ }
+ if (i == proposals.length) {
+ return proposals;
+ } else {
+ byte[] result = new byte[i];
+ System.arraycopy(proposals, 0, result, 0, i);
+ return result;
+ }
+ }
+
+ private void sendConnectInfo(IoSession session) throws Exception {
+ GssApiMechanisms.closeContextSilently(context);
+
+ byte[] rawAddress = getRawAddress(remoteAddress);
+ byte[] remoteName = null;
+ byte type;
+ int length = 0;
+ if (rawAddress == null) {
+ remoteName = remoteAddress.getHostString()
+ .getBytes(StandardCharsets.US_ASCII);
+ if (remoteName == null || remoteName.length == 0) {
+ throw new IOException(
+ format(SshdText.get().proxySocksNoRemoteHostName,
+ remoteAddress));
+ } else if (remoteName.length > 255) {
+ // Should not occur; host names must not be longer than 255
+ // US_ASCII characters. Internal error, no translation.
+ throw new IOException(format(
+ "Proxy host name too long for SOCKS (at most 255 characters): {0}", //$NON-NLS-1$
+ remoteAddress.getHostString()));
+ }
+ type = SOCKS_ADDRESS_FQDN;
+ length = remoteName.length + 1;
+ } else {
+ length = rawAddress.length;
+ type = length == 4 ? SOCKS_ADDRESS_IPv4 : SOCKS_ADDRESS_IPv6;
+ }
+ Buffer buffer = new ByteArrayBuffer(4 + length + 2, false);
+ buffer.putByte(SOCKS_VERSION_5);
+ buffer.putByte(SOCKS_CMD_CONNECT);
+ buffer.putByte((byte) 0); // Reserved
+ buffer.putByte(type);
+ if (remoteName != null) {
+ buffer.putByte((byte) remoteName.length);
+ buffer.putRawBytes(remoteName);
+ } else {
+ buffer.putRawBytes(rawAddress);
+ }
+ int port = remoteAddress.getPort();
+ if (port <= 0) {
+ port = SshConstants.SSH_DEFAULT_PORT;
+ }
+ buffer.putByte((byte) ((port >> 8) & 0xFF));
+ buffer.putByte((byte) (port & 0xFF));
+ state = ProtocolState.CONNECTING;
+ session.writePacket(buffer).verify(getTimeout());
+ }
+
+ private void doPasswordAuth(IoSession session) throws Exception {
+ GssApiMechanisms.closeContextSilently(context);
+ authenticator = new SocksBasicAuthentication();
+ session.addCloseFutureListener(f -> close());
+ startAuth(session);
+ }
+
+ private void doGssApiAuth(IoSession session) throws Exception {
+ authenticator = new SocksGssApiAuthentication();
+ session.addCloseFutureListener(f -> close());
+ startAuth(session);
+ }
+
+ private void close() {
+ AuthenticationHandler<?, ?> handler = authenticator;
+ authenticator = null;
+ if (handler != null) {
+ handler.close();
+ }
+ }
+
+ private void startAuth(IoSession session) throws Exception {
+ Buffer buffer = null;
+ try {
+ authenticator.setParams(null);
+ authenticator.start();
+ buffer = authenticator.getToken();
+ state = ProtocolState.AUTHENTICATING;
+ if (buffer == null) {
+ // Internal error; no translation
+ throw new IOException(
+ "No data for proxy authentication with " //$NON-NLS-1$
+ + proxyAddress);
+ }
+ session.writePacket(buffer).verify(getTimeout());
+ } finally {
+ if (buffer != null) {
+ buffer.clear(true);
+ }
+ }
+ }
+
+ private void authStep(IoSession session, Buffer input) throws Exception {
+ Buffer buffer = null;
+ try {
+ authenticator.setParams(input);
+ authenticator.process();
+ buffer = authenticator.getToken();
+ if (buffer != null) {
+ session.writePacket(buffer).verify(getTimeout());
+ }
+ } finally {
+ if (buffer != null) {
+ buffer.clear(true);
+ }
+ }
+ if (authenticator.isDone()) {
+ sendConnectInfo(session);
+ }
+ }
+
+ private void establishConnection(Buffer data) throws Exception {
+ byte reply = data.getByte();
+ switch (reply) {
+ case SOCKS_REPLY_SUCCESS:
+ state = ProtocolState.CONNECTED;
+ setDone(true);
+ return;
+ case SOCKS_REPLY_FAILURE:
+ throw new IOException(format(
+ SshdText.get().proxySocksFailureGeneral, proxyAddress));
+ case SOCKS_REPLY_FORBIDDEN:
+ throw new IOException(
+ format(SshdText.get().proxySocksFailureForbidden,
+ proxyAddress, remoteAddress));
+ case SOCKS_REPLY_NETWORK_UNREACHABLE:
+ throw new IOException(
+ format(SshdText.get().proxySocksFailureNetworkUnreachable,
+ proxyAddress, remoteAddress));
+ case SOCKS_REPLY_HOST_UNREACHABLE:
+ throw new IOException(
+ format(SshdText.get().proxySocksFailureHostUnreachable,
+ proxyAddress, remoteAddress));
+ case SOCKS_REPLY_CONNECTION_REFUSED:
+ throw new IOException(
+ format(SshdText.get().proxySocksFailureRefused,
+ proxyAddress, remoteAddress));
+ case SOCKS_REPLY_TTL_EXPIRED:
+ throw new IOException(
+ format(SshdText.get().proxySocksFailureTTL, proxyAddress));
+ case SOCKS_REPLY_COMMAND_UNSUPPORTED:
+ throw new IOException(
+ format(SshdText.get().proxySocksFailureUnsupportedCommand,
+ proxyAddress));
+ case SOCKS_REPLY_ADDRESS_UNSUPPORTED:
+ throw new IOException(
+ format(SshdText.get().proxySocksFailureUnsupportedAddress,
+ proxyAddress));
+ default:
+ throw new IOException(format(
+ SshdText.get().proxySocksFailureUnspecified, proxyAddress));
+ }
+ }
+
+ @Override
+ public void messageReceived(IoSession session, Readable buffer)
+ throws Exception {
+ try {
+ // Dispatch according to protocol state
+ ByteArrayBuffer data = new ByteArrayBuffer(buffer.available(),
+ false);
+ data.putBuffer(buffer);
+ data.compact();
+ state.handleMessage(this, session, data);
+ } catch (Exception e) {
+ state = ProtocolState.FAILED;
+ if (authenticator != null) {
+ authenticator.close();
+ authenticator = null;
+ }
+ try {
+ setDone(false);
+ } catch (Exception inner) {
+ e.addSuppressed(inner);
+ }
+ throw e;
+ }
+ }
+
+ private void versionCheck(byte version) throws Exception {
+ if (version != SOCKS_VERSION_5) {
+ throw new IOException(
+ format(SshdText.get().proxySocksUnexpectedVersion,
+ Integer.toString(version & 0xFF)));
+ }
+ }
+
+ private SocksAuthenticationMethod getAuthMethod(byte value) {
+ if (value != SocksAuthenticationMethod.NONE_ACCEPTABLE.getValue()) {
+ for (byte proposed : authenticationProposals) {
+ if (proposed == value) {
+ for (SocksAuthenticationMethod method : SocksAuthenticationMethod
+ .values()) {
+ if (method.getValue() == value) {
+ return method;
+ }
+ }
+ break;
+ }
+ }
+ }
+ return SocksAuthenticationMethod.NONE_ACCEPTABLE;
+ }
+
+ private static byte[] getRawAddress(@NonNull InetSocketAddress address) {
+ InetAddress ipAddress = GssApiMechanisms.resolve(address);
+ return ipAddress == null ? null : ipAddress.getAddress();
+ }
+
+ private static GSSContext getGSSContext(
+ @NonNull InetSocketAddress address) {
+ if (!GssApiMechanisms.getSupportedMechanisms()
+ .contains(GssApiMechanisms.KERBEROS_5)) {
+ return null;
+ }
+ return GssApiMechanisms.createContext(GssApiMechanisms.KERBEROS_5,
+ GssApiMechanisms.getCanonicalName(address));
+ }
+
+ /**
+ * @see <a href="https://tools.ietf.org/html/rfc1929">RFC 1929</a>
+ */
+ private class SocksBasicAuthentication
+ extends BasicAuthentication<Buffer, Buffer> {
+
+ private static final byte SOCKS_BASIC_PROTOCOL_VERSION = 1;
+
+ private static final byte SOCKS_BASIC_AUTH_SUCCESS = 0;
+
+ public SocksBasicAuthentication() {
+ super(proxyAddress, proxyUser, proxyPassword);
+ }
+
+ @Override
+ public void process() throws Exception {
+ // Retries impossible. RFC 1929 specifies that the server MUST
+ // close the connection if authentication is unsuccessful.
+ done = true;
+ if (params.getByte() != SOCKS_BASIC_PROTOCOL_VERSION
+ || params.getByte() != SOCKS_BASIC_AUTH_SUCCESS) {
+ throw new IOException(format(
+ SshdText.get().proxySocksAuthenticationFailed, proxy));
+ }
+ }
+
+ @Override
+ protected void askCredentials() {
+ super.askCredentials();
+ adjustTimeout();
+ }
+
+ @Override
+ public Buffer getToken() throws IOException {
+ if (done) {
+ return null;
+ }
+ try {
+ byte[] rawUser = user.getBytes(StandardCharsets.UTF_8);
+ if (rawUser.length > 255) {
+ throw new IOException(format(
+ SshdText.get().proxySocksUsernameTooLong, proxy,
+ Integer.toString(rawUser.length), user));
+ }
+
+ if (password.length > 255) {
+ throw new IOException(
+ format(SshdText.get().proxySocksPasswordTooLong,
+ proxy, Integer.toString(password.length)));
+ }
+ ByteArrayBuffer buffer = new ByteArrayBuffer(
+ 3 + rawUser.length + password.length, false);
+ buffer.putByte(SOCKS_BASIC_PROTOCOL_VERSION);
+ buffer.putByte((byte) rawUser.length);
+ buffer.putRawBytes(rawUser);
+ buffer.putByte((byte) password.length);
+ buffer.putRawBytes(password);
+ return buffer;
+ } finally {
+ clearPassword();
+ done = true;
+ }
+ }
+ }
+
+ /**
+ * @see <a href="https://tools.ietf.org/html/rfc1961">RFC 1961</a>
+ */
+ private class SocksGssApiAuthentication
+ extends GssApiAuthentication<Buffer, Buffer> {
+
+ private static final byte SOCKS5_GSSAPI_VERSION = 1;
+
+ private static final byte SOCKS5_GSSAPI_TOKEN = 1;
+
+ private static final int SOCKS5_GSSAPI_FAILURE = 0xFF;
+
+ public SocksGssApiAuthentication() {
+ super(proxyAddress);
+ }
+
+ @Override
+ protected GSSContext createContext() throws Exception {
+ return context;
+ }
+
+ @Override
+ public Buffer getToken() throws Exception {
+ if (token == null) {
+ return null;
+ }
+ Buffer buffer = new ByteArrayBuffer(4 + token.length, false);
+ buffer.putByte(SOCKS5_GSSAPI_VERSION);
+ buffer.putByte(SOCKS5_GSSAPI_TOKEN);
+ buffer.putByte((byte) ((token.length >> 8) & 0xFF));
+ buffer.putByte((byte) (token.length & 0xFF));
+ buffer.putRawBytes(token);
+ return buffer;
+ }
+
+ @Override
+ protected byte[] extractToken(Buffer input) throws Exception {
+ if (context == null) {
+ return null;
+ }
+ int version = input.getUByte();
+ if (version != SOCKS5_GSSAPI_VERSION) {
+ throw new IOException(
+ format(SshdText.get().proxySocksGssApiVersionMismatch,
+ remoteAddress, Integer.toString(version)));
+ }
+ int msgType = input.getUByte();
+ if (msgType == SOCKS5_GSSAPI_FAILURE) {
+ throw new IOException(format(
+ SshdText.get().proxySocksGssApiFailure, remoteAddress));
+ } else if (msgType != SOCKS5_GSSAPI_TOKEN) {
+ throw new IOException(format(
+ SshdText.get().proxySocksGssApiUnknownMessage,
+ remoteAddress, Integer.toHexString(msgType & 0xFF)));
+ }
+ if (input.available() >= 2) {
+ int length = (input.getUByte() << 8) + input.getUByte();
+ if (input.available() >= length) {
+ byte[] value = new byte[length];
+ if (length > 0) {
+ input.getRawBytes(value);
+ }
+ return value;
+ }
+ }
+ throw new IOException(
+ format(SshdText.get().proxySocksGssApiMessageTooShort,
+ remoteAddress));
+ }
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/StatefulProxyConnector.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/StatefulProxyConnector.java
new file mode 100644
index 0000000000..0d8e0f93e5
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/StatefulProxyConnector.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.proxy;
+
+import java.util.concurrent.Callable;
+
+import org.apache.sshd.client.session.ClientProxyConnector;
+import org.apache.sshd.common.io.IoSession;
+import org.apache.sshd.common.util.Readable;
+
+/**
+ * Some proxy connections are stateful and require the exchange of multiple
+ * request-reply messages. The default {@link ClientProxyConnector} has only
+ * support for sending a message; replies get routed through the Ssh session,
+ * and don't get back to this proxy connector. Augment the interface so that the
+ * session can know when to route messages received to the proxy connector, and
+ * when to start handling them itself.
+ */
+public interface StatefulProxyConnector extends ClientProxyConnector {
+
+ /**
+ * A property key for a session property defining the timeout for setting up
+ * the proxy connection.
+ */
+ static final String TIMEOUT_PROPERTY = StatefulProxyConnector.class
+ .getName() + "-timeout"; //$NON-NLS-1$
+
+ /**
+ * Handle a received message.
+ *
+ * @param session
+ * to use for writing data
+ * @param buffer
+ * received data
+ * @throws Exception
+ * if data cannot be read, or the connection attempt fails
+ */
+ void messageReceived(IoSession session, Readable buffer) throws Exception;
+
+ /**
+ * Runs {@code startSsh} once the proxy connection is established.
+ *
+ * @param startSsh
+ * operation to run
+ * @throws Exception
+ * if the operation is run synchronously and throws an exception
+ */
+ void runWhenDone(Callable<Void> startSsh) throws Exception;
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/StatusLine.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/StatusLine.java
new file mode 100644
index 0000000000..7ff0183b22
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/proxy/StatusLine.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd.proxy;
+
+/**
+ * A very simple representation of a HTTP status line.
+ */
+public class StatusLine {
+
+ private final String version;
+
+ private final int resultCode;
+
+ private final String reason;
+
+ /**
+ * Create a new {@link StatusLine} with the given response code and reason
+ * string.
+ *
+ * @param version
+ * the version string (normally "HTTP/1.1" or "HTTP/1.0")
+ * @param resultCode
+ * the HTTP response code (200, 401, etc.)
+ * @param reason
+ * the reason phrase for the code
+ */
+ public StatusLine(String version, int resultCode, String reason) {
+ this.version = version;
+ this.resultCode = resultCode;
+ this.reason = reason;
+ }
+
+ /**
+ * Retrieves the version string.
+ *
+ * @return the version string
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * Retrieves the HTTP response code.
+ *
+ * @return the code
+ */
+ public int getResultCode() {
+ return resultCode;
+ }
+
+ /**
+ * Retrieves the HTTP reason phrase.
+ *
+ * @return the reason
+ */
+ public String getReason() {
+ return reason;
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java
new file mode 100644
index 0000000000..d83e31fa2b
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/DefaultProxyDataFactory.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport.sshd;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+import org.apache.sshd.client.config.hosts.HostConfigEntry;
+
+/**
+ * A default implementation of a {@link ProxyDataFactory} based on the standard
+ * {@link java.net.ProxySelector}.
+ *
+ * @since 5.2
+ */
+public class DefaultProxyDataFactory implements ProxyDataFactory {
+
+ @Override
+ public ProxyData get(HostConfigEntry hostConfig,
+ InetSocketAddress remoteAddress) {
+ try {
+ List<Proxy> proxies = ProxySelector.getDefault()
+ .select(new URI(Proxy.Type.SOCKS.name(),
+ "//" + remoteAddress.getHostString(), null)); //$NON-NLS-1$
+ ProxyData data = getData(proxies, Proxy.Type.SOCKS);
+ if (data == null) {
+ proxies = ProxySelector.getDefault()
+ .select(new URI(Proxy.Type.HTTP.name(),
+ "//" + remoteAddress.getHostString(), //$NON-NLS-1$
+ null));
+ data = getData(proxies, Proxy.Type.HTTP);
+ }
+ return data;
+ } catch (URISyntaxException e) {
+ return null;
+ }
+ }
+
+ private ProxyData getData(List<Proxy> proxies, Proxy.Type type) {
+ Proxy proxy = proxies.stream().filter(p -> type == p.type()).findFirst()
+ .orElse(null);
+ if (proxy == null) {
+ return null;
+ }
+ SocketAddress address = proxy.address();
+ if (!(address instanceof InetSocketAddress)) {
+ return null;
+ }
+ switch (type) {
+ case HTTP:
+ return new ProxyData(proxy);
+ case SOCKS:
+ return new ProxyData(proxy);
+ default:
+ return null;
+ }
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ProxyData.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ProxyData.java
new file mode 100644
index 0000000000..39b1e02aec
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ProxyData.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport.sshd;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.util.Arrays;
+
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A DTO encapsulating the data needed to connect through a proxy server.
+ *
+ * @since 5.2
+ */
+public class ProxyData {
+
+ private final @NonNull Proxy proxy;
+
+ private final String proxyUser;
+
+ private final char[] proxyPassword;
+
+ /**
+ * Creates a new {@link ProxyData} instance without user name or password.
+ *
+ * @param proxy
+ * to connect to; must not be {@link java.net.Proxy.Type#DIRECT}
+ * and must have an {@link InetSocketAddress}.
+ */
+ public ProxyData(@NonNull Proxy proxy) {
+ this(proxy, null, null);
+ }
+
+ /**
+ * Creates a new {@link ProxyData} instance.
+ *
+ * @param proxy
+ * to connect to; must not be {@link java.net.Proxy.Type#DIRECT}
+ * and must have an {@link InetSocketAddress}.
+ * @param proxyUser
+ * to use for log-in to the proxy, may be {@code null}
+ * @param proxyPassword
+ * to use for log-in to the proxy, may be {@code null}
+ */
+ public ProxyData(@NonNull Proxy proxy, String proxyUser,
+ char[] proxyPassword) {
+ this.proxy = proxy;
+ if (!(proxy.address() instanceof InetSocketAddress)) {
+ // Internal error not translated
+ throw new IllegalArgumentException(
+ "Proxy does not have an InetSocketAddress"); //$NON-NLS-1$
+ }
+ this.proxyUser = proxyUser;
+ this.proxyPassword = proxyPassword == null ? null
+ : proxyPassword.clone();
+ }
+
+ /**
+ * Obtains the remote {@link InetSocketAddress} of the proxy to connect to.
+ *
+ * @return the remote address of the proxy
+ */
+ @NonNull
+ public Proxy getProxy() {
+ return proxy;
+ }
+
+ /**
+ * Obtains the user to log in at the proxy with.
+ *
+ * @return the user name, or {@code null} if none
+ */
+ public String getUser() {
+ return proxyUser;
+ }
+
+ /**
+ * Obtains a copy of the internally stored password.
+ *
+ * @return the password or {@code null} if none
+ */
+ public char[] getPassword() {
+ return proxyPassword == null ? null : proxyPassword.clone();
+ }
+
+ /**
+ * Clears the stored password, if any.
+ */
+ public void clearPassword() {
+ if (proxyPassword != null) {
+ Arrays.fill(proxyPassword, '\000');
+ }
+ }
+
+} \ No newline at end of file
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ProxyDataFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ProxyDataFactory.java
new file mode 100644
index 0000000000..1446d6ecea
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/ProxyDataFactory.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport.sshd;
+
+import java.net.InetSocketAddress;
+
+import org.apache.sshd.client.config.hosts.HostConfigEntry;
+
+/**
+ * Interface for obtaining {@link ProxyData} to connect through some proxy.
+ *
+ * @since 5.2
+ */
+public interface ProxyDataFactory {
+
+ /**
+ * Get the {@link ProxyData} to connect to a proxy. It should return a
+ * <em>new</em> {@link ProxyData} instance every time; if the returned
+ * {@link ProxyData} contains a password, the {@link SshdSession} will clear
+ * it once it is no longer needed.
+ *
+ * @param hostConfig
+ * from the ssh config that we're going to connect for
+ * @param remoteAddress
+ * to connect to
+ * @return the {@link ProxyData} or {@code null} if a direct connection is
+ * to be made
+ */
+ ProxyData get(HostConfigEntry hostConfig, InetSocketAddress remoteAddress);
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SessionCloseListener.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SessionCloseListener.java
index 1707c7079d..31fc61f82c 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SessionCloseListener.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SessionCloseListener.java
@@ -45,6 +45,8 @@ package org.eclipse.jgit.transport.sshd;
/**
* A {@code SessionCloseListener} is invoked when a {@link SshdSession} is
* closed.
+ *
+ * @since 5.2
*/
@FunctionalInterface
public interface SessionCloseListener {
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 302ba09cc8..f5d46d3d86 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
@@ -107,22 +107,25 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
private final KeyCache keyCache;
+ private final ProxyDataFactory proxies;
+
private File sshDirectory;
private File homeDirectory;
/**
- * Creates a new {@link SshdSessionFactory} without {@link KeyCache}.
+ * Creates a new {@link SshdSessionFactory} without key cache and a
+ * {@link DefaultProxyDataFactory}.
*/
public SshdSessionFactory() {
- this(null);
+ this(null, new DefaultProxyDataFactory());
}
/**
- * Creates a new {@link SshdSessionFactory} using the given
- * {@link KeyCache}. The {@code keyCache} is used for all sessions created
- * through this session factory; cached keys are destroyed when the session
- * factory is {@link #close() closed}.
+ * Creates a new {@link SshdSessionFactory} using the given {@link KeyCache}
+ * and {@link ProxyDataFactory}. The {@code keyCache} is used for all sessions
+ * created through this session factory; cached keys are destroyed when the
+ * session factory is {@link #close() closed}.
* <p>
* Caching ssh keys in memory for an extended period of time is generally
* considered bad practice, but there may be circumstances where using a
@@ -143,10 +146,15 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
* @param keyCache
* {@link KeyCache} to use for caching ssh keys, or {@code null}
* to not use a key cache
+ * @param proxies
+ * {@link ProxyDataFactory} to use, or {@code null} to not use a
+ * proxy database (in which case connections through proxies will
+ * not be possible)
*/
- public SshdSessionFactory(KeyCache keyCache) {
+ public SshdSessionFactory(KeyCache keyCache, ProxyDataFactory proxies) {
super();
this.keyCache = keyCache;
+ this.proxies = proxies;
}
/** A simple general map key. */
@@ -222,6 +230,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
JGitSshClient jgitClient = (JGitSshClient) client;
jgitClient.setKeyCache(getKeyCache());
jgitClient.setCredentialsProvider(credentialsProvider);
+ jgitClient.setProxyDatabase(proxies);
String defaultAuths = getDefaultPreferredAuthentications();
if (defaultAuths != null) {
jgitClient.setAttribute(