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 1ca35a24e..c4b4018b8 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 c8f53c4c1..38dc19067 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 000000000..b8e85493a --- /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 + * 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 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 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 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 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 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 cbbc6386f..69a9165aa 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 caeff5363..e5d66536f 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 12afedd8b..f9ff02b40 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 834a50309..cf68eac5a 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 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 fe6671489..aef263d7f 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 3e2a1aa6d..9b4694c45 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 allKnownKeys = ((ServerKeyLookup) verifier) - .lookup(this, this.getIoSession().getRemoteAddress()); + .lookup(this, remoteAddress); Set 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 915b696b9..9e9340482 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 HOST_CONFIG_ENTRY = new AttributeKey<>(); + static final AttributeKey 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 createConnectCompletionListener( ConnectFuture connectFuture, String username, InetSocketAddress address, HostConfigEntry hostConfig) { @@ -260,6 +310,26 @@ public class JGitSshClient extends SshClient { keyCache = cache; } + /** + * 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. * 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 a96a6962c..27380db33 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 bd9b2a254..d4b6593ef 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 000000000..6caa1b6aa --- /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 + * 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 + * defining the parameter type for the authentication + * @param + * defining the token type for the authentication + */ +public abstract class AbstractAuthenticationHandler + implements AuthenticationHandler { + + /** 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 000000000..34724687a --- /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 + * 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: + * + *
+ * setParams(something);
+ * start();
+ * sendToken(getToken());
+ * while (!isDone()) {
+ * 	setParams(receiveMessageAndExtractParams());
+ * 	process();
+ * 	Object t = getToken();
+ * 	if (t != null) {
+ * 		sendToken(t);
+ * 	}
+ * }
+ * 
+ * + * An {@code AuthenticationHandler} may be stateful and therefore is a + * {@link Closeable}. + * + * @param + * defining the parameter type for {@link #setParams(Object)} + * @param + * defining the token type for {@link #getToken()} + */ +public interface AuthenticationHandler + 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 000000000..efb1f5586 --- /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 + * 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 + * defining the parameter type for the authentication + * @param + * defining the token type for the authentication + */ +public abstract class BasicAuthentication + extends AbstractAuthenticationHandler { + + /** 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() { + + @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 000000000..63cc95447 --- /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 + * 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 + * defining the parameter type for the authentication + * @param + * defining the token type for the authentication + */ +public abstract class GssApiAuthentication + extends AbstractAuthenticationHandler { + + 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 000000000..444fbb62e --- /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 + * 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 startSsh; + + private AtomicReference 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 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 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 000000000..4a6572d45 --- /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 + * 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 RFC 7235, sec. + * 2.1 + */ +public class AuthenticationChallenge { + + private final String mechanism; + + private String token; + + private Map 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 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 ? "" : 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 000000000..46cdd52f5 --- /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 + * 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 availableAuthentications; + + private Iterator 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 reply) + throws Exception { + if (reply.isEmpty() || reply.get(0).isEmpty()) { + throw new IOException( + format(SshdText.get().proxyHttpUnexpectedReply, + proxyAddress, "")); //$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 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 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 challenges, + String name) { + return challenges.stream() + .filter(c -> c.getMechanism().equalsIgnoreCase(name)) + .findFirst().orElse(null); + } + + private interface HttpAuthenticationHandler + extends AuthenticationHandler { + + public String getName(); + } + + /** + * @see RFC 7617 + */ + private class HttpBasicAuthentication + extends BasicAuthentication + 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 RFC 4559 + */ + private class NegotiateAuthentication + extends GssApiAuthentication + 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 000000000..b9b32b130 --- /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 + * 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 RFC 7230 + * @see RFC 7235 + */ +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/ 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 getAuthenticationHeaders( + List reply, String authenticationHeader) { + List challenges = new ArrayList<>(); + Iterator 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 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 000000000..1844fdc79 --- /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 + * 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 RFC 1928 + */ +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 SOCKS + * Methods, IANA.org + */ + 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 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 RFC 1929 + */ + private class SocksBasicAuthentication + extends BasicAuthentication { + + 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 RFC 1961 + */ + private class SocksGssApiAuthentication + extends GssApiAuthentication { + + 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 000000000..0d8e0f93e --- /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 + * 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 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 000000000..7ff0183b2 --- /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 + * 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 000000000..d83e31fa2 --- /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 + * 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 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 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 000000000..39b1e02ae --- /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 + * 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 000000000..1446d6ece --- /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 + * 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 + * new {@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 1707c7079..31fc61f82 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 302ba09cc..f5d46d3d8 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}. *

* 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(