Browse Source
This is not about the ssh config ProxyCommand but about programmatic support for HTTP and SOCKS5 proxies. Eclipse allows the user to specify such proxies, and JSch at least contains code to connect through proxies. So our Apache MINA sshd client also should be able to do this. Add interfaces and provide two implementations for HTTP and SOCKS5 proxies. Adapt the core code to be able to deal with proxy connections at all. The built-in client-side support for this in sshd 2.0.0 is woefully inadequate. Tested manually by running proxies and then fetching various real- world repositories via these proxies from different servers. Proxies tested: ssh -D (SOCKS, anonymous), tinyproxy (HTTP, anonymous), and 3proxy (SOCKS & HTTP, username-password authentication). The GSS-API authentication is untested since I have no Kerberos setup. Bug: 520927 Change-Id: I1a5c34687d439b3ef8373c5d58e24004f93e63ae Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>stable-5.2
Thomas Wolf
6 years ago
committed by
Matthias Sohn
28 changed files with 3157 additions and 24 deletions
@ -0,0 +1,146 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.proxy; |
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals; |
||||||
|
import static org.junit.Assert.assertNull; |
||||||
|
import static org.junit.Assert.assertTrue; |
||||||
|
|
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.LinkedHashMap; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import org.junit.Test; |
||||||
|
|
||||||
|
public class HttpParserTest { |
||||||
|
|
||||||
|
private static final String STATUS_LINE = "HTTP/1.1. 407 Authentication required"; |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testEmpty() throws Exception { |
||||||
|
String[] lines = { STATUS_LINE }; |
||||||
|
List<AuthenticationChallenge> challenges = HttpParser |
||||||
|
.getAuthenticationHeaders(Arrays.asList(lines), |
||||||
|
"WWW-Authenticate:"); |
||||||
|
assertTrue("No challenges expected", challenges.isEmpty()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testRFC7235Example() throws Exception { |
||||||
|
// The example from RFC 7235, sec. 4.1, slightly modified ("kind"
|
||||||
|
// argument with whitespace around '=')
|
||||||
|
String[] lines = { STATUS_LINE, |
||||||
|
"WWW-Authenticate: Newauth realm=\"apps\", type=1 , kind = \t2 ", |
||||||
|
" \t title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"" }; |
||||||
|
List<AuthenticationChallenge> challenges = HttpParser |
||||||
|
.getAuthenticationHeaders(Arrays.asList(lines), |
||||||
|
"WWW-Authenticate:"); |
||||||
|
assertEquals("Unexpected number of challenges", 2, challenges.size()); |
||||||
|
assertNull("No token expected", challenges.get(0).getToken()); |
||||||
|
assertNull("No token expected", challenges.get(1).getToken()); |
||||||
|
assertEquals("Unexpected mechanism", "Newauth", |
||||||
|
challenges.get(0).getMechanism()); |
||||||
|
assertEquals("Unexpected mechanism", "Basic", |
||||||
|
challenges.get(1).getMechanism()); |
||||||
|
Map<String, String> expectedArguments = new LinkedHashMap<>(); |
||||||
|
expectedArguments.put("realm", "apps"); |
||||||
|
expectedArguments.put("type", "1"); |
||||||
|
expectedArguments.put("kind", "2"); |
||||||
|
expectedArguments.put("title", "Login to \"apps\""); |
||||||
|
assertEquals("Unexpected arguments", expectedArguments, |
||||||
|
challenges.get(0).getArguments()); |
||||||
|
expectedArguments.clear(); |
||||||
|
expectedArguments.put("realm", "simple"); |
||||||
|
assertEquals("Unexpected arguments", expectedArguments, |
||||||
|
challenges.get(1).getArguments()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testMultipleHeaders() { |
||||||
|
String[] lines = { STATUS_LINE, |
||||||
|
"Server: Apache", |
||||||
|
"WWW-Authenticate: Newauth realm=\"apps\", type=1 , kind = \t2 ", |
||||||
|
" \t title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"", |
||||||
|
"Content-Type: text/plain", |
||||||
|
"WWW-Authenticate: Other 0123456789=== , YetAnother, ", |
||||||
|
"WWW-Authenticate: Negotiate ", |
||||||
|
"WWW-Authenticate: Negotiate a87421000492aa874209af8bc028" }; |
||||||
|
List<AuthenticationChallenge> challenges = HttpParser |
||||||
|
.getAuthenticationHeaders(Arrays.asList(lines), |
||||||
|
"WWW-Authenticate:"); |
||||||
|
assertEquals("Unexpected number of challenges", 6, challenges.size()); |
||||||
|
assertEquals("Mismatched challenge", "Other", |
||||||
|
challenges.get(2).getMechanism()); |
||||||
|
assertEquals("Token expected", "0123456789===", |
||||||
|
challenges.get(2).getToken()); |
||||||
|
assertEquals("Mismatched challenge", "YetAnother", |
||||||
|
challenges.get(3).getMechanism()); |
||||||
|
assertNull("No token expected", challenges.get(3).getToken()); |
||||||
|
assertTrue("No arguments expected", |
||||||
|
challenges.get(3).getArguments().isEmpty()); |
||||||
|
assertEquals("Mismatched challenge", "Negotiate", |
||||||
|
challenges.get(4).getMechanism()); |
||||||
|
assertNull("No token expected", challenges.get(4).getToken()); |
||||||
|
assertEquals("Mismatched challenge", "Negotiate", |
||||||
|
challenges.get(5).getMechanism()); |
||||||
|
assertEquals("Token expected", "a87421000492aa874209af8bc028", |
||||||
|
challenges.get(5).getToken()); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void testStopOnEmptyLine() { |
||||||
|
String[] lines = { STATUS_LINE, "Server: Apache", |
||||||
|
"WWW-Authenticate: Newauth realm=\"apps\", type=1 , kind = \t2 ", |
||||||
|
" \t title=\"Login to \\\"apps\\\"\", Basic realm=\"simple\"", |
||||||
|
"Content-Type: text/plain", |
||||||
|
"WWW-Authenticate: Other 0123456789===", "", |
||||||
|
// Not headers anymore; this would be the body
|
||||||
|
"WWW-Authenticate: Negotiate ", |
||||||
|
"WWW-Authenticate: Negotiate a87421000492aa874209af8bc028" }; |
||||||
|
List<AuthenticationChallenge> challenges = HttpParser |
||||||
|
.getAuthenticationHeaders(Arrays.asList(lines), |
||||||
|
"WWW-Authenticate:"); |
||||||
|
assertEquals("Unexpected number of challenges", 3, challenges.size()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,89 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.auth; |
||||||
|
|
||||||
|
import java.net.InetSocketAddress; |
||||||
|
|
||||||
|
/** |
||||||
|
* Abstract base class for {@link AuthenticationHandler}s encapsulating basic |
||||||
|
* common things. |
||||||
|
* |
||||||
|
* @param <ParameterType> |
||||||
|
* defining the parameter type for the authentication |
||||||
|
* @param <TokenType> |
||||||
|
* defining the token type for the authentication |
||||||
|
*/ |
||||||
|
public abstract class AbstractAuthenticationHandler<ParameterType, TokenType> |
||||||
|
implements AuthenticationHandler<ParameterType, TokenType> { |
||||||
|
|
||||||
|
/** The {@link InetSocketAddress} or the proxy to connect to. */ |
||||||
|
protected InetSocketAddress proxy; |
||||||
|
|
||||||
|
/** The last set parameters. */ |
||||||
|
protected ParameterType params; |
||||||
|
|
||||||
|
/** A flag telling whether this authentication is done. */ |
||||||
|
protected boolean done; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link AbstractAuthenticationHandler} to authenticate with |
||||||
|
* the given {@code proxy}. |
||||||
|
* |
||||||
|
* @param proxy |
||||||
|
* the {@link InetSocketAddress} of the proxy to connect to |
||||||
|
*/ |
||||||
|
public AbstractAuthenticationHandler(InetSocketAddress proxy) { |
||||||
|
this.proxy = proxy; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void setParams(ParameterType input) { |
||||||
|
params = input; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final boolean isDone() { |
||||||
|
return done; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,121 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.auth; |
||||||
|
|
||||||
|
import java.io.Closeable; |
||||||
|
|
||||||
|
/** |
||||||
|
* An {@code AuthenticationHandler} encapsulates a possibly multi-step |
||||||
|
* authentication protocol. Intended usage: |
||||||
|
* |
||||||
|
* <pre> |
||||||
|
* setParams(something); |
||||||
|
* start(); |
||||||
|
* sendToken(getToken()); |
||||||
|
* while (!isDone()) { |
||||||
|
* setParams(receiveMessageAndExtractParams()); |
||||||
|
* process(); |
||||||
|
* Object t = getToken(); |
||||||
|
* if (t != null) { |
||||||
|
* sendToken(t); |
||||||
|
* } |
||||||
|
* } |
||||||
|
* </pre> |
||||||
|
* |
||||||
|
* An {@code AuthenticationHandler} may be stateful and therefore is a |
||||||
|
* {@link Closeable}. |
||||||
|
* |
||||||
|
* @param <ParameterType> |
||||||
|
* defining the parameter type for {@link #setParams(Object)} |
||||||
|
* @param <TokenType> |
||||||
|
* defining the token type for {@link #getToken()} |
||||||
|
*/ |
||||||
|
public interface AuthenticationHandler<ParameterType, TokenType> |
||||||
|
extends Closeable { |
||||||
|
|
||||||
|
/** |
||||||
|
* Produces the initial authentication token that can be then retrieved via |
||||||
|
* {@link #getToken()}. |
||||||
|
* |
||||||
|
* @throws Exception |
||||||
|
* if an error occurs |
||||||
|
*/ |
||||||
|
void start() throws Exception; |
||||||
|
|
||||||
|
/** |
||||||
|
* Produces the next authentication token, if any. |
||||||
|
* |
||||||
|
* @throws Exception |
||||||
|
* if an error occurs |
||||||
|
*/ |
||||||
|
void process() throws Exception; |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the parameters for the next token generation via {@link #start()} or |
||||||
|
* {@link #process()}. |
||||||
|
* |
||||||
|
* @param input |
||||||
|
* to set, may be {@code null} |
||||||
|
*/ |
||||||
|
void setParams(ParameterType input); |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieves the last token generated. |
||||||
|
* |
||||||
|
* @return the token, or {@code null} if there is none |
||||||
|
* @throws Exception |
||||||
|
* if an error occurs |
||||||
|
*/ |
||||||
|
TokenType getToken() throws Exception; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tells whether is authentication mechanism is done (successfully or |
||||||
|
* unsuccessfully). |
||||||
|
* |
||||||
|
* @return whether this authentication is done |
||||||
|
*/ |
||||||
|
boolean isDone(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public void close(); |
||||||
|
} |
@ -0,0 +1,167 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.auth; |
||||||
|
|
||||||
|
import java.net.Authenticator; |
||||||
|
import java.net.Authenticator.RequestorType; |
||||||
|
import java.net.InetSocketAddress; |
||||||
|
import java.net.PasswordAuthentication; |
||||||
|
import java.nio.ByteBuffer; |
||||||
|
import java.nio.CharBuffer; |
||||||
|
import java.nio.charset.StandardCharsets; |
||||||
|
import java.security.AccessController; |
||||||
|
import java.security.PrivilegedAction; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.concurrent.CancellationException; |
||||||
|
|
||||||
|
import org.eclipse.jgit.internal.transport.sshd.SshdText; |
||||||
|
import org.eclipse.jgit.transport.SshConstants; |
||||||
|
|
||||||
|
/** |
||||||
|
* An abstract implementation of a username-password authentication. It can be |
||||||
|
* given an initial known username-password pair; if so, this will be tried |
||||||
|
* first. Subsequent rounds will then try to obtain a user name and password via |
||||||
|
* the global {@link Authenticator}. |
||||||
|
* |
||||||
|
* @param <ParameterType> |
||||||
|
* defining the parameter type for the authentication |
||||||
|
* @param <TokenType> |
||||||
|
* defining the token type for the authentication |
||||||
|
*/ |
||||||
|
public abstract class BasicAuthentication<ParameterType, TokenType> |
||||||
|
extends AbstractAuthenticationHandler<ParameterType, TokenType> { |
||||||
|
|
||||||
|
/** The current user name. */ |
||||||
|
protected String user; |
||||||
|
|
||||||
|
/** The current password. */ |
||||||
|
protected byte[] password; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link BasicAuthentication} to authenticate with the given |
||||||
|
* {@code proxy}. |
||||||
|
* |
||||||
|
* @param proxy |
||||||
|
* {@link InetSocketAddress} of the proxy to connect to |
||||||
|
* @param initialUser |
||||||
|
* initial user name to try; may be {@code null} |
||||||
|
* @param initialPassword |
||||||
|
* initial password to try, may be {@code null} |
||||||
|
*/ |
||||||
|
public BasicAuthentication(InetSocketAddress proxy, String initialUser, |
||||||
|
char[] initialPassword) { |
||||||
|
super(proxy); |
||||||
|
this.user = initialUser; |
||||||
|
this.password = convert(initialPassword); |
||||||
|
} |
||||||
|
|
||||||
|
private byte[] convert(char[] pass) { |
||||||
|
if (pass == null) { |
||||||
|
return new byte[0]; |
||||||
|
} |
||||||
|
ByteBuffer bytes = StandardCharsets.UTF_8.encode(CharBuffer.wrap(pass)); |
||||||
|
byte[] pwd = new byte[bytes.remaining()]; |
||||||
|
bytes.get(pwd); |
||||||
|
if (bytes.hasArray()) { |
||||||
|
Arrays.fill(bytes.array(), (byte) 0); |
||||||
|
} |
||||||
|
Arrays.fill(pass, '\000'); |
||||||
|
return pwd; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clears the {@link #password}. |
||||||
|
*/ |
||||||
|
protected void clearPassword() { |
||||||
|
if (password != null) { |
||||||
|
Arrays.fill(password, (byte) 0); |
||||||
|
} |
||||||
|
password = new byte[0]; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void close() { |
||||||
|
clearPassword(); |
||||||
|
done = true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void start() throws Exception { |
||||||
|
if (user != null && !user.isEmpty() |
||||||
|
|| password != null && password.length > 0) { |
||||||
|
return; |
||||||
|
} |
||||||
|
askCredentials(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void process() throws Exception { |
||||||
|
askCredentials(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Asks for credentials via the global {@link Authenticator}. |
||||||
|
*/ |
||||||
|
protected void askCredentials() { |
||||||
|
clearPassword(); |
||||||
|
PasswordAuthentication auth = AccessController |
||||||
|
.doPrivileged(new PrivilegedAction<PasswordAuthentication>() { |
||||||
|
|
||||||
|
@Override |
||||||
|
public PasswordAuthentication run() { |
||||||
|
return Authenticator.requestPasswordAuthentication( |
||||||
|
proxy.getHostString(), proxy.getAddress(), |
||||||
|
proxy.getPort(), SshConstants.SSH_SCHEME, |
||||||
|
SshdText.get().proxyPasswordPrompt, "Basic", //$NON-NLS-1$
|
||||||
|
null, RequestorType.PROXY); |
||||||
|
} |
||||||
|
}); |
||||||
|
if (auth == null) { |
||||||
|
user = ""; //$NON-NLS-1$
|
||||||
|
throw new CancellationException( |
||||||
|
SshdText.get().authenticationCanceled); |
||||||
|
} |
||||||
|
user = auth.getUserName(); |
||||||
|
password = convert(auth.getPassword()); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,147 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.auth; |
||||||
|
|
||||||
|
import static java.text.MessageFormat.format; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.net.InetSocketAddress; |
||||||
|
|
||||||
|
import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.SshdText; |
||||||
|
import org.ietf.jgss.GSSContext; |
||||||
|
|
||||||
|
/** |
||||||
|
* An abstract implementation of a GSS-API multi-round authentication. |
||||||
|
* |
||||||
|
* @param <ParameterType> |
||||||
|
* defining the parameter type for the authentication |
||||||
|
* @param <TokenType> |
||||||
|
* defining the token type for the authentication |
||||||
|
*/ |
||||||
|
public abstract class GssApiAuthentication<ParameterType, TokenType> |
||||||
|
extends AbstractAuthenticationHandler<ParameterType, TokenType> { |
||||||
|
|
||||||
|
private GSSContext context; |
||||||
|
|
||||||
|
/** The last token generated. */ |
||||||
|
protected byte[] token; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link GssApiAuthentication} to authenticate with the given |
||||||
|
* {@code proxy}. |
||||||
|
* |
||||||
|
* @param proxy |
||||||
|
* the {@link InetSocketAddress} of the proxy to connect to |
||||||
|
*/ |
||||||
|
public GssApiAuthentication(InetSocketAddress proxy) { |
||||||
|
super(proxy); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void close() { |
||||||
|
GssApiMechanisms.closeContextSilently(context); |
||||||
|
context = null; |
||||||
|
done = true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void start() throws Exception { |
||||||
|
try { |
||||||
|
context = createContext(); |
||||||
|
context.requestMutualAuth(true); |
||||||
|
context.requestConf(false); |
||||||
|
context.requestInteg(false); |
||||||
|
byte[] empty = new byte[0]; |
||||||
|
token = context.initSecContext(empty, 0, 0); |
||||||
|
} catch (Exception e) { |
||||||
|
close(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public final void process() throws Exception { |
||||||
|
if (context == null) { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxyCannotAuthenticate, proxy)); |
||||||
|
} |
||||||
|
try { |
||||||
|
byte[] received = extractToken(params); |
||||||
|
token = context.initSecContext(received, 0, received.length); |
||||||
|
checkDone(); |
||||||
|
} catch (Exception e) { |
||||||
|
close(); |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void checkDone() throws Exception { |
||||||
|
done = context.isEstablished(); |
||||||
|
if (done) { |
||||||
|
context.dispose(); |
||||||
|
context = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates the {@link GSSContext} to use. |
||||||
|
* |
||||||
|
* @return a fresh {@link GSSContext} to use |
||||||
|
* @throws Exception |
||||||
|
* if the context cannot be created |
||||||
|
*/ |
||||||
|
protected abstract GSSContext createContext() throws Exception; |
||||||
|
|
||||||
|
/** |
||||||
|
* Extracts the token from the last set parameters. |
||||||
|
* |
||||||
|
* @param input |
||||||
|
* to extract the token from |
||||||
|
* @return the extracted token, or {@code null} if none |
||||||
|
* @throws Exception |
||||||
|
* if an error occurs |
||||||
|
*/ |
||||||
|
protected abstract byte[] extractToken(ParameterType input) |
||||||
|
throws Exception; |
||||||
|
} |
@ -0,0 +1,209 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.proxy; |
||||||
|
|
||||||
|
import java.net.InetSocketAddress; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.concurrent.Callable; |
||||||
|
import java.util.concurrent.TimeUnit; |
||||||
|
import java.util.concurrent.atomic.AtomicReference; |
||||||
|
|
||||||
|
import org.apache.sshd.client.session.ClientSession; |
||||||
|
import org.eclipse.jgit.annotations.NonNull; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.JGitClientSession; |
||||||
|
|
||||||
|
/** |
||||||
|
* Basic common functionality for a {@link StatefulProxyConnector}. |
||||||
|
*/ |
||||||
|
public abstract class AbstractClientProxyConnector |
||||||
|
implements StatefulProxyConnector { |
||||||
|
|
||||||
|
private static final long DEFAULT_PROXY_TIMEOUT_MILLIS = TimeUnit.SECONDS |
||||||
|
.toMillis(30L); |
||||||
|
|
||||||
|
/** Guards {@link #done} and {@link #startSsh}. */ |
||||||
|
private Object lock = new Object(); |
||||||
|
|
||||||
|
private boolean done; |
||||||
|
|
||||||
|
private Callable<Void> startSsh; |
||||||
|
|
||||||
|
private AtomicReference<Runnable> unregister = new AtomicReference<>(); |
||||||
|
|
||||||
|
private long remainingProxyProtocolTime = DEFAULT_PROXY_TIMEOUT_MILLIS; |
||||||
|
|
||||||
|
private long lastProxyOperationTime = 0L; |
||||||
|
|
||||||
|
/** The ultimate remote address to connect to. */ |
||||||
|
protected final InetSocketAddress remoteAddress; |
||||||
|
|
||||||
|
/** The proxy address. */ |
||||||
|
protected final InetSocketAddress proxyAddress; |
||||||
|
|
||||||
|
/** The user to authenticate at the proxy with. */ |
||||||
|
protected String proxyUser; |
||||||
|
|
||||||
|
/** The password to use for authentication at the proxy. */ |
||||||
|
protected char[] proxyPassword; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link AbstractClientProxyConnector}. |
||||||
|
* |
||||||
|
* @param proxyAddress |
||||||
|
* of the proxy server we're connecting to |
||||||
|
* @param remoteAddress |
||||||
|
* of the target server to connect to |
||||||
|
* @param proxyUser |
||||||
|
* to authenticate at the proxy with; may be {@code null} |
||||||
|
* @param proxyPassword |
||||||
|
* to authenticate at the proxy with; may be {@code null} |
||||||
|
*/ |
||||||
|
public AbstractClientProxyConnector(@NonNull InetSocketAddress proxyAddress, |
||||||
|
@NonNull InetSocketAddress remoteAddress, String proxyUser, |
||||||
|
char[] proxyPassword) { |
||||||
|
this.proxyAddress = proxyAddress; |
||||||
|
this.remoteAddress = remoteAddress; |
||||||
|
this.proxyUser = proxyUser; |
||||||
|
this.proxyPassword = proxyPassword == null ? new char[0] |
||||||
|
: proxyPassword; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Initializes this instance. Installs itself as proxy handler on the |
||||||
|
* session. |
||||||
|
* |
||||||
|
* @param session |
||||||
|
* to initialize for |
||||||
|
*/ |
||||||
|
protected void init(ClientSession session) { |
||||||
|
remainingProxyProtocolTime = session.getLongProperty( |
||||||
|
StatefulProxyConnector.TIMEOUT_PROPERTY, |
||||||
|
DEFAULT_PROXY_TIMEOUT_MILLIS); |
||||||
|
if (remainingProxyProtocolTime <= 0L) { |
||||||
|
remainingProxyProtocolTime = DEFAULT_PROXY_TIMEOUT_MILLIS; |
||||||
|
} |
||||||
|
if (session instanceof JGitClientSession) { |
||||||
|
JGitClientSession s = (JGitClientSession) session; |
||||||
|
unregister.set(() -> s.setProxyHandler(null)); |
||||||
|
s.setProxyHandler(this); |
||||||
|
} else { |
||||||
|
// Internal error, no translation
|
||||||
|
throw new IllegalStateException( |
||||||
|
"Not a JGit session: " + session.getClass().getName()); //$NON-NLS-1$
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Obtains the timeout for the whole rest of the proxy connection protocol. |
||||||
|
* |
||||||
|
* @return the timeout in milliseconds, always > 0L |
||||||
|
*/ |
||||||
|
protected long getTimeout() { |
||||||
|
long last = lastProxyOperationTime; |
||||||
|
long now = System.nanoTime(); |
||||||
|
lastProxyOperationTime = now; |
||||||
|
long remaining = remainingProxyProtocolTime; |
||||||
|
if (last != 0L) { |
||||||
|
long elapsed = now - last; |
||||||
|
remaining -= elapsed; |
||||||
|
if (remaining < 0L) { |
||||||
|
remaining = 10L; // Give it grace period.
|
||||||
|
} |
||||||
|
} |
||||||
|
remainingProxyProtocolTime = remaining; |
||||||
|
return remaining; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Adjusts the timeout calculation to not account of elapsed time since the |
||||||
|
* last time the timeout was gotten. Can be used for instance to ignore time |
||||||
|
* spent in user dialogs be counted against the overall proxy connection |
||||||
|
* protocol timeout. |
||||||
|
*/ |
||||||
|
protected void adjustTimeout() { |
||||||
|
lastProxyOperationTime = System.nanoTime(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the "done" flag. |
||||||
|
* |
||||||
|
* @param success |
||||||
|
* whether the connector terminated successfully. |
||||||
|
* @throws Exception |
||||||
|
* if starting ssh fails |
||||||
|
*/ |
||||||
|
protected void setDone(boolean success) throws Exception { |
||||||
|
Callable<Void> starter; |
||||||
|
Runnable unset = unregister.getAndSet(null); |
||||||
|
if (unset != null) { |
||||||
|
unset.run(); |
||||||
|
} |
||||||
|
synchronized (lock) { |
||||||
|
done = true; |
||||||
|
starter = startSsh; |
||||||
|
startSsh = null; |
||||||
|
} |
||||||
|
if (success && starter != null) { |
||||||
|
starter.call(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void runWhenDone(Callable<Void> starter) throws Exception { |
||||||
|
synchronized (lock) { |
||||||
|
if (!done) { |
||||||
|
this.startSsh = starter; |
||||||
|
return; |
||||||
|
} |
||||||
|
} |
||||||
|
starter.call(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clears the proxy password. |
||||||
|
*/ |
||||||
|
protected void clearPassword() { |
||||||
|
Arrays.fill(proxyPassword, '\000'); |
||||||
|
proxyPassword = new char[0]; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,123 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.proxy; |
||||||
|
|
||||||
|
import java.util.Collections; |
||||||
|
import java.util.LinkedHashMap; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import org.eclipse.jgit.annotations.NonNull; |
||||||
|
|
||||||
|
/** |
||||||
|
* A simple representation of an authentication challenge as sent in a |
||||||
|
* "WWW-Authenticate" or "Proxy-Authenticate" header. Such challenges start with |
||||||
|
* a mechanism name, followed either by one single token, or by a list of |
||||||
|
* key=value pairs. |
||||||
|
* |
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc7235#section-2.1">RFC 7235, sec. |
||||||
|
* 2.1</a> |
||||||
|
*/ |
||||||
|
public class AuthenticationChallenge { |
||||||
|
|
||||||
|
private final String mechanism; |
||||||
|
|
||||||
|
private String token; |
||||||
|
|
||||||
|
private Map<String, String> arguments; |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link AuthenticationChallenge} with the given mechanism. |
||||||
|
* |
||||||
|
* @param mechanism |
||||||
|
* for the challenge |
||||||
|
*/ |
||||||
|
public AuthenticationChallenge(String mechanism) { |
||||||
|
this.mechanism = mechanism; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieves the authentication mechanism specified by this challenge, for |
||||||
|
* instance "Basic". |
||||||
|
* |
||||||
|
* @return the mechanism name |
||||||
|
*/ |
||||||
|
public String getMechanism() { |
||||||
|
return mechanism; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieves the token of the challenge, if any. |
||||||
|
* |
||||||
|
* @return the token, or {@code null} if there is none. |
||||||
|
*/ |
||||||
|
public String getToken() { |
||||||
|
return token; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieves the arguments of the challenge. |
||||||
|
* |
||||||
|
* @return a possibly empty map of the key=value arguments of the challenge |
||||||
|
*/ |
||||||
|
@NonNull |
||||||
|
public Map<String, String> getArguments() { |
||||||
|
return arguments == null ? Collections.emptyMap() : arguments; |
||||||
|
} |
||||||
|
|
||||||
|
void addArgument(String key, String value) { |
||||||
|
if (arguments == null) { |
||||||
|
arguments = new LinkedHashMap<>(); |
||||||
|
} |
||||||
|
arguments.put(key, value); |
||||||
|
} |
||||||
|
|
||||||
|
void setToken(String token) { |
||||||
|
this.token = token; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return "AuthenticationChallenge[" + mechanism + ',' + token + ',' //$NON-NLS-1$
|
||||||
|
+ (arguments == null ? "<none>" : arguments.toString()) + ']'; //$NON-NLS-1$
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,403 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.proxy; |
||||||
|
|
||||||
|
import static java.text.MessageFormat.format; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.net.HttpURLConnection; |
||||||
|
import java.net.InetSocketAddress; |
||||||
|
import java.nio.charset.StandardCharsets; |
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Iterator; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.apache.sshd.client.session.ClientSession; |
||||||
|
import org.apache.sshd.common.io.IoSession; |
||||||
|
import org.apache.sshd.common.util.Readable; |
||||||
|
import org.apache.sshd.common.util.buffer.Buffer; |
||||||
|
import org.apache.sshd.common.util.buffer.ByteArrayBuffer; |
||||||
|
import org.eclipse.jgit.annotations.NonNull; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.SshdText; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.auth.AuthenticationHandler; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.auth.BasicAuthentication; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.auth.GssApiAuthentication; |
||||||
|
import org.eclipse.jgit.util.Base64; |
||||||
|
import org.ietf.jgss.GSSContext; |
||||||
|
|
||||||
|
/** |
||||||
|
* Simple HTTP proxy connector using Basic Authentication. |
||||||
|
*/ |
||||||
|
public class HttpClientConnector extends AbstractClientProxyConnector { |
||||||
|
|
||||||
|
private static final String HTTP_HEADER_PROXY_AUTHENTICATION = "Proxy-Authentication:"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
private static final String HTTP_HEADER_PROXY_AUTHORIZATION = "Proxy-Authorization:"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
private HttpAuthenticationHandler basic; |
||||||
|
|
||||||
|
private HttpAuthenticationHandler negotiate; |
||||||
|
|
||||||
|
private List<HttpAuthenticationHandler> availableAuthentications; |
||||||
|
|
||||||
|
private Iterator<HttpAuthenticationHandler> clientAuthentications; |
||||||
|
|
||||||
|
private HttpAuthenticationHandler authenticator; |
||||||
|
|
||||||
|
private boolean ongoing; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link HttpClientConnector}. The connector supports |
||||||
|
* anonymous proxy connections as well as Basic and Negotiate |
||||||
|
* authentication. |
||||||
|
* |
||||||
|
* @param proxyAddress |
||||||
|
* of the proxy server we're connecting to |
||||||
|
* @param remoteAddress |
||||||
|
* of the target server to connect to |
||||||
|
*/ |
||||||
|
public HttpClientConnector(@NonNull InetSocketAddress proxyAddress, |
||||||
|
@NonNull InetSocketAddress remoteAddress) { |
||||||
|
this(proxyAddress, remoteAddress, null, null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link HttpClientConnector}. The connector supports |
||||||
|
* anonymous proxy connections as well as Basic and Negotiate |
||||||
|
* authentication. If a user name and password are given, the connector |
||||||
|
* tries pre-emptive Basic authentication. |
||||||
|
* |
||||||
|
* @param proxyAddress |
||||||
|
* of the proxy server we're connecting to |
||||||
|
* @param remoteAddress |
||||||
|
* of the target server to connect to |
||||||
|
* @param proxyUser |
||||||
|
* to authenticate at the proxy with |
||||||
|
* @param proxyPassword |
||||||
|
* to authenticate at the proxy with |
||||||
|
*/ |
||||||
|
public HttpClientConnector(@NonNull InetSocketAddress proxyAddress, |
||||||
|
@NonNull InetSocketAddress remoteAddress, String proxyUser, |
||||||
|
char[] proxyPassword) { |
||||||
|
super(proxyAddress, remoteAddress, proxyUser, proxyPassword); |
||||||
|
basic = new HttpBasicAuthentication(); |
||||||
|
negotiate = new NegotiateAuthentication(); |
||||||
|
availableAuthentications = new ArrayList<>(2); |
||||||
|
availableAuthentications.add(negotiate); |
||||||
|
availableAuthentications.add(basic); |
||||||
|
clientAuthentications = availableAuthentications.iterator(); |
||||||
|
} |
||||||
|
|
||||||
|
private void close() { |
||||||
|
HttpAuthenticationHandler current = authenticator; |
||||||
|
authenticator = null; |
||||||
|
if (current != null) { |
||||||
|
current.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void sendClientProxyMetadata(ClientSession sshSession) |
||||||
|
throws Exception { |
||||||
|
init(sshSession); |
||||||
|
IoSession session = sshSession.getIoSession(); |
||||||
|
session.addCloseFutureListener(f -> close()); |
||||||
|
StringBuilder msg = connect(); |
||||||
|
if (proxyUser != null && !proxyUser.isEmpty() |
||||||
|
|| proxyPassword != null && proxyPassword.length > 0) { |
||||||
|
authenticator = basic; |
||||||
|
basic.setParams(null); |
||||||
|
basic.start(); |
||||||
|
msg = authenticate(msg, basic.getToken()); |
||||||
|
clearPassword(); |
||||||
|
proxyUser = null; |
||||||
|
} |
||||||
|
ongoing = true; |
||||||
|
try { |
||||||
|
send(msg, session); |
||||||
|
} catch (Exception e) { |
||||||
|
ongoing = false; |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void send(StringBuilder msg, IoSession session) throws Exception { |
||||||
|
byte[] data = eol(msg).toString().getBytes(StandardCharsets.US_ASCII); |
||||||
|
Buffer buffer = new ByteArrayBuffer(data.length, false); |
||||||
|
buffer.putRawBytes(data); |
||||||
|
session.writePacket(buffer).verify(getTimeout()); |
||||||
|
} |
||||||
|
|
||||||
|
private StringBuilder connect() { |
||||||
|
StringBuilder msg = new StringBuilder(); |
||||||
|
// Persistent connections are the default in HTTP 1.1 (see RFC 2616),
|
||||||
|
// but let's be explicit.
|
||||||
|
return msg.append(format( |
||||||
|
"CONNECT {0}:{1} HTTP/1.1\r\nProxy-Connection: keep-alive\r\nConnection: keep-alive\r\nHost: {0}:{1}\r\n", //$NON-NLS-1$
|
||||||
|
remoteAddress.getHostString(), |
||||||
|
Integer.toString(remoteAddress.getPort()))); |
||||||
|
} |
||||||
|
|
||||||
|
private StringBuilder authenticate(StringBuilder msg, String token) { |
||||||
|
msg.append(HTTP_HEADER_PROXY_AUTHORIZATION).append(' ').append(token); |
||||||
|
return eol(msg); |
||||||
|
} |
||||||
|
|
||||||
|
private StringBuilder eol(StringBuilder msg) { |
||||||
|
return msg.append('\r').append('\n'); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void messageReceived(IoSession session, Readable buffer) |
||||||
|
throws Exception { |
||||||
|
try { |
||||||
|
int length = buffer.available(); |
||||||
|
byte[] data = new byte[length]; |
||||||
|
buffer.getRawBytes(data, 0, length); |
||||||
|
String[] reply = new String(data, StandardCharsets.US_ASCII) |
||||||
|
.split("\r\n"); //$NON-NLS-1$
|
||||||
|
handleMessage(session, Arrays.asList(reply)); |
||||||
|
} catch (Exception e) { |
||||||
|
if (authenticator != null) { |
||||||
|
authenticator.close(); |
||||||
|
authenticator = null; |
||||||
|
} |
||||||
|
ongoing = false; |
||||||
|
try { |
||||||
|
setDone(false); |
||||||
|
} catch (Exception inner) { |
||||||
|
e.addSuppressed(inner); |
||||||
|
} |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void handleMessage(IoSession session, List<String> reply) |
||||||
|
throws Exception { |
||||||
|
if (reply.isEmpty() || reply.get(0).isEmpty()) { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxyHttpUnexpectedReply, |
||||||
|
proxyAddress, "<empty>")); //$NON-NLS-1$
|
||||||
|
} |
||||||
|
try { |
||||||
|
StatusLine status = HttpParser.parseStatusLine(reply.get(0)); |
||||||
|
if (!ongoing) { |
||||||
|
throw new IOException(format( |
||||||
|
SshdText.get().proxyHttpUnexpectedReply, proxyAddress, |
||||||
|
Integer.toString(status.getResultCode()), |
||||||
|
status.getReason())); |
||||||
|
} |
||||||
|
switch (status.getResultCode()) { |
||||||
|
case HttpURLConnection.HTTP_OK: |
||||||
|
if (authenticator != null) { |
||||||
|
authenticator.close(); |
||||||
|
} |
||||||
|
authenticator = null; |
||||||
|
ongoing = false; |
||||||
|
setDone(true); |
||||||
|
break; |
||||||
|
case HttpURLConnection.HTTP_PROXY_AUTH: |
||||||
|
List<AuthenticationChallenge> challenges = HttpParser |
||||||
|
.getAuthenticationHeaders(reply, |
||||||
|
HTTP_HEADER_PROXY_AUTHENTICATION); |
||||||
|
authenticator = selectProtocol(challenges, authenticator); |
||||||
|
if (authenticator == null) { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxyCannotAuthenticate, |
||||||
|
proxyAddress)); |
||||||
|
} |
||||||
|
String token = authenticator.getToken(); |
||||||
|
if (token == null) { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxyCannotAuthenticate, |
||||||
|
proxyAddress)); |
||||||
|
} |
||||||
|
send(authenticate(connect(), token), session); |
||||||
|
break; |
||||||
|
default: |
||||||
|
throw new IOException(format(SshdText.get().proxyHttpFailure, |
||||||
|
proxyAddress, Integer.toString(status.getResultCode()), |
||||||
|
status.getReason())); |
||||||
|
} |
||||||
|
} catch (HttpParser.ParseException e) { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxyHttpUnexpectedReply, |
||||||
|
proxyAddress, reply.get(0))); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private HttpAuthenticationHandler selectProtocol( |
||||||
|
List<AuthenticationChallenge> challenges, |
||||||
|
HttpAuthenticationHandler current) throws Exception { |
||||||
|
if (current != null && !current.isDone()) { |
||||||
|
AuthenticationChallenge challenge = getByName(challenges, |
||||||
|
current.getName()); |
||||||
|
if (challenge != null) { |
||||||
|
current.setParams(challenge); |
||||||
|
current.process(); |
||||||
|
return current; |
||||||
|
} |
||||||
|
} |
||||||
|
if (current != null) { |
||||||
|
current.close(); |
||||||
|
} |
||||||
|
while (clientAuthentications.hasNext()) { |
||||||
|
HttpAuthenticationHandler next = clientAuthentications.next(); |
||||||
|
if (!next.isDone()) { |
||||||
|
AuthenticationChallenge challenge = getByName(challenges, |
||||||
|
next.getName()); |
||||||
|
if (challenge != null) { |
||||||
|
next.setParams(challenge); |
||||||
|
next.start(); |
||||||
|
return next; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
private AuthenticationChallenge getByName( |
||||||
|
List<AuthenticationChallenge> challenges, |
||||||
|
String name) { |
||||||
|
return challenges.stream() |
||||||
|
.filter(c -> c.getMechanism().equalsIgnoreCase(name)) |
||||||
|
.findFirst().orElse(null); |
||||||
|
} |
||||||
|
|
||||||
|
private interface HttpAuthenticationHandler |
||||||
|
extends AuthenticationHandler<AuthenticationChallenge, String> { |
||||||
|
|
||||||
|
public String getName(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc7617">RFC 7617</a> |
||||||
|
*/ |
||||||
|
private class HttpBasicAuthentication |
||||||
|
extends BasicAuthentication<AuthenticationChallenge, String> |
||||||
|
implements HttpAuthenticationHandler { |
||||||
|
|
||||||
|
private boolean asked; |
||||||
|
|
||||||
|
public HttpBasicAuthentication() { |
||||||
|
super(proxyAddress, proxyUser, proxyPassword); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getName() { |
||||||
|
return "Basic"; //$NON-NLS-1$
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void askCredentials() { |
||||||
|
// We ask only once.
|
||||||
|
if (asked) { |
||||||
|
throw new IllegalStateException( |
||||||
|
"Basic auth: already asked user for password"); //$NON-NLS-1$
|
||||||
|
} |
||||||
|
asked = true; |
||||||
|
super.askCredentials(); |
||||||
|
done = true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getToken() throws Exception { |
||||||
|
if (user.indexOf(':') >= 0) { |
||||||
|
throw new IOException(format( |
||||||
|
SshdText.get().proxyHttpInvalidUserName, proxy, user)); |
||||||
|
} |
||||||
|
byte[] rawUser = user.getBytes(StandardCharsets.UTF_8); |
||||||
|
byte[] toEncode = new byte[rawUser.length + 1 + password.length]; |
||||||
|
System.arraycopy(rawUser, 0, toEncode, 0, rawUser.length); |
||||||
|
toEncode[rawUser.length] = ':'; |
||||||
|
System.arraycopy(password, 0, toEncode, rawUser.length + 1, |
||||||
|
password.length); |
||||||
|
Arrays.fill(password, (byte) 0); |
||||||
|
String result = Base64.encodeBytes(toEncode); |
||||||
|
Arrays.fill(toEncode, (byte) 0); |
||||||
|
return getName() + ' ' + result; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc4559">RFC 4559</a> |
||||||
|
*/ |
||||||
|
private class NegotiateAuthentication |
||||||
|
extends GssApiAuthentication<AuthenticationChallenge, String> |
||||||
|
implements HttpAuthenticationHandler { |
||||||
|
|
||||||
|
public NegotiateAuthentication() { |
||||||
|
super(proxyAddress); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getName() { |
||||||
|
return "Negotiate"; //$NON-NLS-1$
|
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getToken() throws Exception { |
||||||
|
return getName() + ' ' + Base64.encodeBytes(token); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected GSSContext createContext() throws Exception { |
||||||
|
return GssApiMechanisms.createContext(GssApiMechanisms.SPNEGO, |
||||||
|
GssApiMechanisms.getCanonicalName(proxyAddress)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected byte[] extractToken(AuthenticationChallenge input) |
||||||
|
throws Exception { |
||||||
|
String received = input.getToken(); |
||||||
|
if (received == null) { |
||||||
|
return new byte[0]; |
||||||
|
} |
||||||
|
return Base64.decode(received); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
@ -0,0 +1,346 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.proxy; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Iterator; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* A basic parser for HTTP response headers. Handles status lines and |
||||||
|
* authentication headers (WWW-Authenticate, Proxy-Authenticate). |
||||||
|
* |
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc7230">RFC 7230</a> |
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc7235">RFC 7235</a> |
||||||
|
*/ |
||||||
|
public final class HttpParser { |
||||||
|
|
||||||
|
/** |
||||||
|
* An exception indicating some problem parsing HTPP headers. |
||||||
|
*/ |
||||||
|
public static class ParseException extends Exception { |
||||||
|
|
||||||
|
private static final long serialVersionUID = -1634090143702048640L; |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private HttpParser() { |
||||||
|
// No instantiation
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parse a HTTP response status line. |
||||||
|
* |
||||||
|
* @param line |
||||||
|
* to parse |
||||||
|
* @return the {@link StatusLine} |
||||||
|
* @throws ParseException |
||||||
|
* if the line cannot be parsed or has the wrong HTTP version |
||||||
|
*/ |
||||||
|
public static StatusLine parseStatusLine(String line) |
||||||
|
throws ParseException { |
||||||
|
// Format is HTTP/<version> Code Reason
|
||||||
|
int firstBlank = line.indexOf(' '); |
||||||
|
if (firstBlank < 0) { |
||||||
|
throw new ParseException(); |
||||||
|
} |
||||||
|
int secondBlank = line.indexOf(' ', firstBlank + 1); |
||||||
|
if (secondBlank < 0) { |
||||||
|
// Accept the line even if the (according to RFC 2616 mandatory)
|
||||||
|
// reason is missing.
|
||||||
|
secondBlank = line.length(); |
||||||
|
} |
||||||
|
int resultCode; |
||||||
|
try { |
||||||
|
resultCode = Integer.parseUnsignedInt( |
||||||
|
line.substring(firstBlank + 1, secondBlank)); |
||||||
|
} catch (NumberFormatException e) { |
||||||
|
throw new ParseException(); |
||||||
|
} |
||||||
|
// Again, accept even if the reason is missing
|
||||||
|
String reason = ""; //$NON-NLS-1$
|
||||||
|
if (secondBlank < line.length()) { |
||||||
|
reason = line.substring(secondBlank + 1); |
||||||
|
} |
||||||
|
return new StatusLine(line.substring(0, firstBlank), resultCode, |
||||||
|
reason); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Extract the authentication headers from the header lines. It is assumed |
||||||
|
* that the first element in {@code reply} is the raw status line as |
||||||
|
* received from the server. It is skipped. Line processing stops on the |
||||||
|
* first empty line thereafter. |
||||||
|
* |
||||||
|
* @param reply |
||||||
|
* The complete (header) lines of the HTTP response |
||||||
|
* @param authenticationHeader |
||||||
|
* to look for (including the terminating ':'!) |
||||||
|
* @return a list of {@link AuthenticationChallenge}s found. |
||||||
|
*/ |
||||||
|
public static List<AuthenticationChallenge> getAuthenticationHeaders( |
||||||
|
List<String> reply, String authenticationHeader) { |
||||||
|
List<AuthenticationChallenge> challenges = new ArrayList<>(); |
||||||
|
Iterator<String> lines = reply.iterator(); |
||||||
|
// We know we have at least one line. Skip the response line.
|
||||||
|
lines.next(); |
||||||
|
StringBuilder value = null; |
||||||
|
while (lines.hasNext()) { |
||||||
|
String line = lines.next(); |
||||||
|
if (line.isEmpty()) { |
||||||
|
break; |
||||||
|
} |
||||||
|
if (Character.isWhitespace(line.charAt(0))) { |
||||||
|
// Continuation line.
|
||||||
|
if (value == null) { |
||||||
|
// Skip if we have no current value
|
||||||
|
continue; |
||||||
|
} |
||||||
|
// Skip leading whitespace
|
||||||
|
int i = skipWhiteSpace(line, 1); |
||||||
|
value.append(' ').append(line, i, line.length()); |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (value != null) { |
||||||
|
parseChallenges(challenges, value.toString()); |
||||||
|
value = null; |
||||||
|
} |
||||||
|
int firstColon = line.indexOf(':'); |
||||||
|
if (firstColon > 0 && authenticationHeader |
||||||
|
.equalsIgnoreCase(line.substring(0, firstColon + 1))) { |
||||||
|
value = new StringBuilder(line.substring(firstColon + 1)); |
||||||
|
} |
||||||
|
} |
||||||
|
if (value != null) { |
||||||
|
parseChallenges(challenges, value.toString()); |
||||||
|
} |
||||||
|
return challenges; |
||||||
|
} |
||||||
|
|
||||||
|
private static void parseChallenges( |
||||||
|
List<AuthenticationChallenge> challenges, |
||||||
|
String header) { |
||||||
|
// Comma-separated list of challenges, each itself a scheme name
|
||||||
|
// followed optionally by either: a comma-separated list of key=value
|
||||||
|
// pairs, where the value may be a quoted string with backslash escapes,
|
||||||
|
// or a single token value, which itself may end in zero or more '='
|
||||||
|
// characters. Ugh.
|
||||||
|
int length = header.length(); |
||||||
|
for (int i = 0; i < length;) { |
||||||
|
int start = skipWhiteSpace(header, i); |
||||||
|
int end = scanToken(header, start); |
||||||
|
if (end <= start) { |
||||||
|
break; |
||||||
|
} |
||||||
|
AuthenticationChallenge challenge = new AuthenticationChallenge( |
||||||
|
header.substring(start, end)); |
||||||
|
challenges.add(challenge); |
||||||
|
i = parseChallenge(challenge, header, end); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static int parseChallenge(AuthenticationChallenge challenge, |
||||||
|
String header, int from) { |
||||||
|
int length = header.length(); |
||||||
|
boolean first = true; |
||||||
|
for (int start = from; start <= length; first = false) { |
||||||
|
// Now we have either a single token, which may end in zero or more
|
||||||
|
// equal signs, or a comma-separated list of key=value pairs (with
|
||||||
|
// optional legacy whitespace around the equals sign), where the
|
||||||
|
// value can be either a token or a quoted string.
|
||||||
|
start = skipWhiteSpace(header, start); |
||||||
|
int end = scanToken(header, start); |
||||||
|
if (end == start) { |
||||||
|
// Nothing found. Either at end or on a comma.
|
||||||
|
if (start < header.length() && header.charAt(start) == ',') { |
||||||
|
return start + 1; |
||||||
|
} |
||||||
|
return start; |
||||||
|
} |
||||||
|
int next = skipWhiteSpace(header, end); |
||||||
|
// Comma, or equals sign, or end of string
|
||||||
|
if (next >= length || header.charAt(next) != '=') { |
||||||
|
if (first) { |
||||||
|
// It must be a token
|
||||||
|
challenge.setToken(header.substring(start, end)); |
||||||
|
if (next < length && header.charAt(next) == ',') { |
||||||
|
next++; |
||||||
|
} |
||||||
|
return next; |
||||||
|
} else { |
||||||
|
// This token must be the name of the next authentication
|
||||||
|
// scheme.
|
||||||
|
return start; |
||||||
|
} |
||||||
|
} |
||||||
|
int nextStart = skipWhiteSpace(header, next + 1); |
||||||
|
if (nextStart >= length) { |
||||||
|
if (next == end) { |
||||||
|
// '=' immediately after the key, no value: key must be the
|
||||||
|
// token, and the equals sign is part of the token
|
||||||
|
challenge.setToken(header.substring(start, end + 1)); |
||||||
|
} else { |
||||||
|
// Key without value...
|
||||||
|
challenge.addArgument(header.substring(start, end), null); |
||||||
|
} |
||||||
|
return nextStart; |
||||||
|
} |
||||||
|
if (nextStart == end + 1 && header.charAt(nextStart) == '=') { |
||||||
|
// More than one equals sign: must be the single token.
|
||||||
|
end = nextStart + 1; |
||||||
|
while (end < length && header.charAt(end) == '=') { |
||||||
|
end++; |
||||||
|
} |
||||||
|
challenge.setToken(header.substring(start, end)); |
||||||
|
end = skipWhiteSpace(header, end); |
||||||
|
if (end < length && header.charAt(end) == ',') { |
||||||
|
end++; |
||||||
|
} |
||||||
|
return end; |
||||||
|
} |
||||||
|
if (header.charAt(nextStart) == ',') { |
||||||
|
if (next == end) { |
||||||
|
// '=' immediately after the key, no value: key must be the
|
||||||
|
// token, and the equals sign is part of the token
|
||||||
|
challenge.setToken(header.substring(start, end + 1)); |
||||||
|
return nextStart + 1; |
||||||
|
} else { |
||||||
|
// Key without value...
|
||||||
|
challenge.addArgument(header.substring(start, end), null); |
||||||
|
start = nextStart + 1; |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (header.charAt(nextStart) == '"') { |
||||||
|
int nextEnd[] = { nextStart + 1 }; |
||||||
|
String value = scanQuotedString(header, nextStart + 1, |
||||||
|
nextEnd); |
||||||
|
challenge.addArgument(header.substring(start, end), value); |
||||||
|
start = nextEnd[0]; |
||||||
|
} else { |
||||||
|
int nextEnd = scanToken(header, nextStart); |
||||||
|
challenge.addArgument(header.substring(start, end), |
||||||
|
header.substring(nextStart, nextEnd)); |
||||||
|
start = nextEnd; |
||||||
|
} |
||||||
|
start = skipWhiteSpace(header, start); |
||||||
|
if (start < length && header.charAt(start) == ',') { |
||||||
|
start++; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return length; |
||||||
|
} |
||||||
|
|
||||||
|
private static int skipWhiteSpace(String header, int i) { |
||||||
|
int length = header.length(); |
||||||
|
while (i < length && Character.isWhitespace(header.charAt(i))) { |
||||||
|
i++; |
||||||
|
} |
||||||
|
return i; |
||||||
|
} |
||||||
|
|
||||||
|
private static int scanToken(String header, int from) { |
||||||
|
int length = header.length(); |
||||||
|
int i = from; |
||||||
|
while (i < length) { |
||||||
|
char c = header.charAt(i); |
||||||
|
switch (c) { |
||||||
|
case '!': |
||||||
|
case '#': |
||||||
|
case '$': |
||||||
|
case '%': |
||||||
|
case '&': |
||||||
|
case '\'': |
||||||
|
case '*': |
||||||
|
case '+': |
||||||
|
case '-': |
||||||
|
case '.': |
||||||
|
case '^': |
||||||
|
case '_': |
||||||
|
case '`': |
||||||
|
case '|': |
||||||
|
case '0': |
||||||
|
case '1': |
||||||
|
case '2': |
||||||
|
case '3': |
||||||
|
case '4': |
||||||
|
case '5': |
||||||
|
case '6': |
||||||
|
case '7': |
||||||
|
case '8': |
||||||
|
case '9': |
||||||
|
i++; |
||||||
|
break; |
||||||
|
default: |
||||||
|
if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z') { |
||||||
|
i++; |
||||||
|
break; |
||||||
|
} |
||||||
|
return i; |
||||||
|
} |
||||||
|
} |
||||||
|
return i; |
||||||
|
} |
||||||
|
|
||||||
|
private static String scanQuotedString(String header, int from, int[] to) { |
||||||
|
StringBuilder result = new StringBuilder(); |
||||||
|
int length = header.length(); |
||||||
|
boolean quoted = false; |
||||||
|
int i = from; |
||||||
|
while (i < length) { |
||||||
|
char c = header.charAt(i++); |
||||||
|
if (quoted) { |
||||||
|
result.append(c); |
||||||
|
quoted = false; |
||||||
|
} else if (c == '\\') { |
||||||
|
quoted = true; |
||||||
|
} else if (c == '"') { |
||||||
|
break; |
||||||
|
} else { |
||||||
|
result.append(c); |
||||||
|
} |
||||||
|
} |
||||||
|
to[0] = i; |
||||||
|
return result.toString(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,642 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.proxy; |
||||||
|
|
||||||
|
import static java.text.MessageFormat.format; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.net.InetAddress; |
||||||
|
import java.net.InetSocketAddress; |
||||||
|
import java.nio.charset.StandardCharsets; |
||||||
|
|
||||||
|
import org.apache.sshd.client.session.ClientSession; |
||||||
|
import org.apache.sshd.common.io.IoSession; |
||||||
|
import org.apache.sshd.common.util.Readable; |
||||||
|
import org.apache.sshd.common.util.buffer.Buffer; |
||||||
|
import org.apache.sshd.common.util.buffer.BufferUtils; |
||||||
|
import org.apache.sshd.common.util.buffer.ByteArrayBuffer; |
||||||
|
import org.eclipse.jgit.annotations.NonNull; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.GssApiMechanisms; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.SshdText; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.auth.AuthenticationHandler; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.auth.BasicAuthentication; |
||||||
|
import org.eclipse.jgit.internal.transport.sshd.auth.GssApiAuthentication; |
||||||
|
import org.eclipse.jgit.transport.SshConstants; |
||||||
|
import org.ietf.jgss.GSSContext; |
||||||
|
|
||||||
|
/** |
||||||
|
* A {@link AbstractClientProxyConnector} to connect through a SOCKS5 proxy. |
||||||
|
* |
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc1928">RFC 1928</a> |
||||||
|
*/ |
||||||
|
public class Socks5ClientConnector extends AbstractClientProxyConnector { |
||||||
|
|
||||||
|
// private static final byte SOCKS_VERSION_4 = 4;
|
||||||
|
private static final byte SOCKS_VERSION_5 = 5; |
||||||
|
|
||||||
|
private static final byte SOCKS_CMD_CONNECT = 1; |
||||||
|
// private static final byte SOCKS5_CMD_BIND = 2;
|
||||||
|
// private static final byte SOCKS5_CMD_UDP_ASSOCIATE = 3;
|
||||||
|
|
||||||
|
// Address types
|
||||||
|
|
||||||
|
private static final byte SOCKS_ADDRESS_IPv4 = 1; |
||||||
|
|
||||||
|
private static final byte SOCKS_ADDRESS_FQDN = 3; |
||||||
|
|
||||||
|
private static final byte SOCKS_ADDRESS_IPv6 = 4; |
||||||
|
|
||||||
|
// Reply codes
|
||||||
|
|
||||||
|
private static final byte SOCKS_REPLY_SUCCESS = 0; |
||||||
|
|
||||||
|
private static final byte SOCKS_REPLY_FAILURE = 1; |
||||||
|
|
||||||
|
private static final byte SOCKS_REPLY_FORBIDDEN = 2; |
||||||
|
|
||||||
|
private static final byte SOCKS_REPLY_NETWORK_UNREACHABLE = 3; |
||||||
|
|
||||||
|
private static final byte SOCKS_REPLY_HOST_UNREACHABLE = 4; |
||||||
|
|
||||||
|
private static final byte SOCKS_REPLY_CONNECTION_REFUSED = 5; |
||||||
|
|
||||||
|
private static final byte SOCKS_REPLY_TTL_EXPIRED = 6; |
||||||
|
|
||||||
|
private static final byte SOCKS_REPLY_COMMAND_UNSUPPORTED = 7; |
||||||
|
|
||||||
|
private static final byte SOCKS_REPLY_ADDRESS_UNSUPPORTED = 8; |
||||||
|
|
||||||
|
/** |
||||||
|
* Authentication methods for SOCKS5. |
||||||
|
* |
||||||
|
* @see <a href= |
||||||
|
* "https://www.iana.org/assignments/socks-methods/socks-methods.xhtml">SOCKS |
||||||
|
* Methods, IANA.org</a> |
||||||
|
*/ |
||||||
|
private enum SocksAuthenticationMethod { |
||||||
|
|
||||||
|
ANONYMOUS(0), |
||||||
|
GSSAPI(1), |
||||||
|
PASSWORD(2), |
||||||
|
// CHALLENGE_HANDSHAKE(3),
|
||||||
|
// CHALLENGE_RESPONSE(5),
|
||||||
|
// SSL(6),
|
||||||
|
// NDS(7),
|
||||||
|
// MULTI_AUTH(8),
|
||||||
|
// JSON(9),
|
||||||
|
NONE_ACCEPTABLE(0xFF); |
||||||
|
|
||||||
|
private byte value; |
||||||
|
|
||||||
|
SocksAuthenticationMethod(int value) { |
||||||
|
this.value = (byte) value; |
||||||
|
} |
||||||
|
|
||||||
|
public byte getValue() { |
||||||
|
return value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private enum ProtocolState { |
||||||
|
NONE, |
||||||
|
|
||||||
|
INIT { |
||||||
|
@Override |
||||||
|
public void handleMessage(Socks5ClientConnector connector, |
||||||
|
IoSession session, Buffer data) throws Exception { |
||||||
|
connector.versionCheck(data.getByte()); |
||||||
|
SocksAuthenticationMethod authMethod = connector.getAuthMethod( |
||||||
|
data.getByte()); |
||||||
|
switch (authMethod) { |
||||||
|
case ANONYMOUS: |
||||||
|
connector.sendConnectInfo(session); |
||||||
|
break; |
||||||
|
case PASSWORD: |
||||||
|
connector.doPasswordAuth(session); |
||||||
|
break; |
||||||
|
case GSSAPI: |
||||||
|
connector.doGssApiAuth(session); |
||||||
|
break; |
||||||
|
default: |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxyCannotAuthenticate, |
||||||
|
connector.proxyAddress)); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
AUTHENTICATING { |
||||||
|
@Override |
||||||
|
public void handleMessage(Socks5ClientConnector connector, |
||||||
|
IoSession session, Buffer data) throws Exception { |
||||||
|
connector.authStep(session, data); |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
CONNECTING { |
||||||
|
@Override |
||||||
|
public void handleMessage(Socks5ClientConnector connector, |
||||||
|
IoSession session, Buffer data) throws Exception { |
||||||
|
// Special case: when GSS-API authentication completes, the
|
||||||
|
// client moves into CONNECTING as soon as the GSS context is
|
||||||
|
// established and sends the connect request. This is per RFC
|
||||||
|
// 1961. But for the server, RFC 1961 says it _should_ send an
|
||||||
|
// empty token even if none generated when its server side
|
||||||
|
// context is established. That means we may actually get an
|
||||||
|
// empty token here. That message is 4 bytes long (and has
|
||||||
|
// content 0x01, 0x01, 0x00, 0x00). We simply skip this message
|
||||||
|
// if we get it here. If the server for whatever reason sends
|
||||||
|
// back a "GSS failed" message (it shouldn't, at this point)
|
||||||
|
// it will be two bytes 0x01 0xFF, which will fail the version
|
||||||
|
// check.
|
||||||
|
if (data.available() != 4) { |
||||||
|
connector.versionCheck(data.getByte()); |
||||||
|
connector.establishConnection(data); |
||||||
|
} |
||||||
|
} |
||||||
|
}, |
||||||
|
|
||||||
|
CONNECTED, |
||||||
|
|
||||||
|
FAILED; |
||||||
|
|
||||||
|
public void handleMessage(Socks5ClientConnector connector, |
||||||
|
@SuppressWarnings("unused") IoSession session, Buffer data) |
||||||
|
throws Exception { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksUnexpectedMessage, |
||||||
|
connector.proxyAddress, this, |
||||||
|
BufferUtils.toHex(data.array()))); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private ProtocolState state; |
||||||
|
|
||||||
|
private AuthenticationHandler<Buffer, Buffer> authenticator; |
||||||
|
|
||||||
|
private GSSContext context; |
||||||
|
|
||||||
|
private byte[] authenticationProposals; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link Socks5ClientConnector}. The connector supports |
||||||
|
* anonymous connections as well as username-password or Kerberos5 (GSS-API) |
||||||
|
* authentication. |
||||||
|
* |
||||||
|
* @param proxyAddress |
||||||
|
* of the proxy server we're connecting to |
||||||
|
* @param remoteAddress |
||||||
|
* of the target server to connect to |
||||||
|
*/ |
||||||
|
public Socks5ClientConnector(@NonNull InetSocketAddress proxyAddress, |
||||||
|
@NonNull InetSocketAddress remoteAddress) { |
||||||
|
this(proxyAddress, remoteAddress, null, null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link Socks5ClientConnector}. The connector supports |
||||||
|
* anonymous connections as well as username-password or Kerberos5 (GSS-API) |
||||||
|
* authentication. |
||||||
|
* |
||||||
|
* @param proxyAddress |
||||||
|
* of the proxy server we're connecting to |
||||||
|
* @param remoteAddress |
||||||
|
* of the target server to connect to |
||||||
|
* @param proxyUser |
||||||
|
* to authenticate at the proxy with |
||||||
|
* @param proxyPassword |
||||||
|
* to authenticate at the proxy with |
||||||
|
*/ |
||||||
|
public Socks5ClientConnector(@NonNull InetSocketAddress proxyAddress, |
||||||
|
@NonNull InetSocketAddress remoteAddress, |
||||||
|
String proxyUser, char[] proxyPassword) { |
||||||
|
super(proxyAddress, remoteAddress, proxyUser, proxyPassword); |
||||||
|
this.state = ProtocolState.NONE; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void sendClientProxyMetadata(ClientSession sshSession) |
||||||
|
throws Exception { |
||||||
|
init(sshSession); |
||||||
|
IoSession session = sshSession.getIoSession(); |
||||||
|
// Send the initial request
|
||||||
|
Buffer buffer = new ByteArrayBuffer(5, false); |
||||||
|
buffer.putByte(SOCKS_VERSION_5); |
||||||
|
context = getGSSContext(remoteAddress); |
||||||
|
authenticationProposals = getAuthenticationProposals(); |
||||||
|
buffer.putByte((byte) authenticationProposals.length); |
||||||
|
buffer.putRawBytes(authenticationProposals); |
||||||
|
state = ProtocolState.INIT; |
||||||
|
session.writePacket(buffer).verify(getTimeout()); |
||||||
|
} |
||||||
|
|
||||||
|
private byte[] getAuthenticationProposals() { |
||||||
|
byte[] proposals = new byte[3]; |
||||||
|
int i = 0; |
||||||
|
proposals[i++] = SocksAuthenticationMethod.ANONYMOUS.getValue(); |
||||||
|
proposals[i++] = SocksAuthenticationMethod.PASSWORD.getValue(); |
||||||
|
if (context != null) { |
||||||
|
proposals[i++] = SocksAuthenticationMethod.GSSAPI.getValue(); |
||||||
|
} |
||||||
|
if (i == proposals.length) { |
||||||
|
return proposals; |
||||||
|
} else { |
||||||
|
byte[] result = new byte[i]; |
||||||
|
System.arraycopy(proposals, 0, result, 0, i); |
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void sendConnectInfo(IoSession session) throws Exception { |
||||||
|
GssApiMechanisms.closeContextSilently(context); |
||||||
|
|
||||||
|
byte[] rawAddress = getRawAddress(remoteAddress); |
||||||
|
byte[] remoteName = null; |
||||||
|
byte type; |
||||||
|
int length = 0; |
||||||
|
if (rawAddress == null) { |
||||||
|
remoteName = remoteAddress.getHostString() |
||||||
|
.getBytes(StandardCharsets.US_ASCII); |
||||||
|
if (remoteName == null || remoteName.length == 0) { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksNoRemoteHostName, |
||||||
|
remoteAddress)); |
||||||
|
} else if (remoteName.length > 255) { |
||||||
|
// Should not occur; host names must not be longer than 255
|
||||||
|
// US_ASCII characters. Internal error, no translation.
|
||||||
|
throw new IOException(format( |
||||||
|
"Proxy host name too long for SOCKS (at most 255 characters): {0}", //$NON-NLS-1$
|
||||||
|
remoteAddress.getHostString())); |
||||||
|
} |
||||||
|
type = SOCKS_ADDRESS_FQDN; |
||||||
|
length = remoteName.length + 1; |
||||||
|
} else { |
||||||
|
length = rawAddress.length; |
||||||
|
type = length == 4 ? SOCKS_ADDRESS_IPv4 : SOCKS_ADDRESS_IPv6; |
||||||
|
} |
||||||
|
Buffer buffer = new ByteArrayBuffer(4 + length + 2, false); |
||||||
|
buffer.putByte(SOCKS_VERSION_5); |
||||||
|
buffer.putByte(SOCKS_CMD_CONNECT); |
||||||
|
buffer.putByte((byte) 0); // Reserved
|
||||||
|
buffer.putByte(type); |
||||||
|
if (remoteName != null) { |
||||||
|
buffer.putByte((byte) remoteName.length); |
||||||
|
buffer.putRawBytes(remoteName); |
||||||
|
} else { |
||||||
|
buffer.putRawBytes(rawAddress); |
||||||
|
} |
||||||
|
int port = remoteAddress.getPort(); |
||||||
|
if (port <= 0) { |
||||||
|
port = SshConstants.SSH_DEFAULT_PORT; |
||||||
|
} |
||||||
|
buffer.putByte((byte) ((port >> 8) & 0xFF)); |
||||||
|
buffer.putByte((byte) (port & 0xFF)); |
||||||
|
state = ProtocolState.CONNECTING; |
||||||
|
session.writePacket(buffer).verify(getTimeout()); |
||||||
|
} |
||||||
|
|
||||||
|
private void doPasswordAuth(IoSession session) throws Exception { |
||||||
|
GssApiMechanisms.closeContextSilently(context); |
||||||
|
authenticator = new SocksBasicAuthentication(); |
||||||
|
session.addCloseFutureListener(f -> close()); |
||||||
|
startAuth(session); |
||||||
|
} |
||||||
|
|
||||||
|
private void doGssApiAuth(IoSession session) throws Exception { |
||||||
|
authenticator = new SocksGssApiAuthentication(); |
||||||
|
session.addCloseFutureListener(f -> close()); |
||||||
|
startAuth(session); |
||||||
|
} |
||||||
|
|
||||||
|
private void close() { |
||||||
|
AuthenticationHandler<?, ?> handler = authenticator; |
||||||
|
authenticator = null; |
||||||
|
if (handler != null) { |
||||||
|
handler.close(); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void startAuth(IoSession session) throws Exception { |
||||||
|
Buffer buffer = null; |
||||||
|
try { |
||||||
|
authenticator.setParams(null); |
||||||
|
authenticator.start(); |
||||||
|
buffer = authenticator.getToken(); |
||||||
|
state = ProtocolState.AUTHENTICATING; |
||||||
|
if (buffer == null) { |
||||||
|
// Internal error; no translation
|
||||||
|
throw new IOException( |
||||||
|
"No data for proxy authentication with " //$NON-NLS-1$
|
||||||
|
+ proxyAddress); |
||||||
|
} |
||||||
|
session.writePacket(buffer).verify(getTimeout()); |
||||||
|
} finally { |
||||||
|
if (buffer != null) { |
||||||
|
buffer.clear(true); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void authStep(IoSession session, Buffer input) throws Exception { |
||||||
|
Buffer buffer = null; |
||||||
|
try { |
||||||
|
authenticator.setParams(input); |
||||||
|
authenticator.process(); |
||||||
|
buffer = authenticator.getToken(); |
||||||
|
if (buffer != null) { |
||||||
|
session.writePacket(buffer).verify(getTimeout()); |
||||||
|
} |
||||||
|
} finally { |
||||||
|
if (buffer != null) { |
||||||
|
buffer.clear(true); |
||||||
|
} |
||||||
|
} |
||||||
|
if (authenticator.isDone()) { |
||||||
|
sendConnectInfo(session); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void establishConnection(Buffer data) throws Exception { |
||||||
|
byte reply = data.getByte(); |
||||||
|
switch (reply) { |
||||||
|
case SOCKS_REPLY_SUCCESS: |
||||||
|
state = ProtocolState.CONNECTED; |
||||||
|
setDone(true); |
||||||
|
return; |
||||||
|
case SOCKS_REPLY_FAILURE: |
||||||
|
throw new IOException(format( |
||||||
|
SshdText.get().proxySocksFailureGeneral, proxyAddress)); |
||||||
|
case SOCKS_REPLY_FORBIDDEN: |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksFailureForbidden, |
||||||
|
proxyAddress, remoteAddress)); |
||||||
|
case SOCKS_REPLY_NETWORK_UNREACHABLE: |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksFailureNetworkUnreachable, |
||||||
|
proxyAddress, remoteAddress)); |
||||||
|
case SOCKS_REPLY_HOST_UNREACHABLE: |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksFailureHostUnreachable, |
||||||
|
proxyAddress, remoteAddress)); |
||||||
|
case SOCKS_REPLY_CONNECTION_REFUSED: |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksFailureRefused, |
||||||
|
proxyAddress, remoteAddress)); |
||||||
|
case SOCKS_REPLY_TTL_EXPIRED: |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksFailureTTL, proxyAddress)); |
||||||
|
case SOCKS_REPLY_COMMAND_UNSUPPORTED: |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksFailureUnsupportedCommand, |
||||||
|
proxyAddress)); |
||||||
|
case SOCKS_REPLY_ADDRESS_UNSUPPORTED: |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksFailureUnsupportedAddress, |
||||||
|
proxyAddress)); |
||||||
|
default: |
||||||
|
throw new IOException(format( |
||||||
|
SshdText.get().proxySocksFailureUnspecified, proxyAddress)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void messageReceived(IoSession session, Readable buffer) |
||||||
|
throws Exception { |
||||||
|
try { |
||||||
|
// Dispatch according to protocol state
|
||||||
|
ByteArrayBuffer data = new ByteArrayBuffer(buffer.available(), |
||||||
|
false); |
||||||
|
data.putBuffer(buffer); |
||||||
|
data.compact(); |
||||||
|
state.handleMessage(this, session, data); |
||||||
|
} catch (Exception e) { |
||||||
|
state = ProtocolState.FAILED; |
||||||
|
if (authenticator != null) { |
||||||
|
authenticator.close(); |
||||||
|
authenticator = null; |
||||||
|
} |
||||||
|
try { |
||||||
|
setDone(false); |
||||||
|
} catch (Exception inner) { |
||||||
|
e.addSuppressed(inner); |
||||||
|
} |
||||||
|
throw e; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void versionCheck(byte version) throws Exception { |
||||||
|
if (version != SOCKS_VERSION_5) { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksUnexpectedVersion, |
||||||
|
Integer.toString(version & 0xFF))); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private SocksAuthenticationMethod getAuthMethod(byte value) { |
||||||
|
if (value != SocksAuthenticationMethod.NONE_ACCEPTABLE.getValue()) { |
||||||
|
for (byte proposed : authenticationProposals) { |
||||||
|
if (proposed == value) { |
||||||
|
for (SocksAuthenticationMethod method : SocksAuthenticationMethod |
||||||
|
.values()) { |
||||||
|
if (method.getValue() == value) { |
||||||
|
return method; |
||||||
|
} |
||||||
|
} |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return SocksAuthenticationMethod.NONE_ACCEPTABLE; |
||||||
|
} |
||||||
|
|
||||||
|
private static byte[] getRawAddress(@NonNull InetSocketAddress address) { |
||||||
|
InetAddress ipAddress = GssApiMechanisms.resolve(address); |
||||||
|
return ipAddress == null ? null : ipAddress.getAddress(); |
||||||
|
} |
||||||
|
|
||||||
|
private static GSSContext getGSSContext( |
||||||
|
@NonNull InetSocketAddress address) { |
||||||
|
if (!GssApiMechanisms.getSupportedMechanisms() |
||||||
|
.contains(GssApiMechanisms.KERBEROS_5)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return GssApiMechanisms.createContext(GssApiMechanisms.KERBEROS_5, |
||||||
|
GssApiMechanisms.getCanonicalName(address)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc1929">RFC 1929</a> |
||||||
|
*/ |
||||||
|
private class SocksBasicAuthentication |
||||||
|
extends BasicAuthentication<Buffer, Buffer> { |
||||||
|
|
||||||
|
private static final byte SOCKS_BASIC_PROTOCOL_VERSION = 1; |
||||||
|
|
||||||
|
private static final byte SOCKS_BASIC_AUTH_SUCCESS = 0; |
||||||
|
|
||||||
|
public SocksBasicAuthentication() { |
||||||
|
super(proxyAddress, proxyUser, proxyPassword); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void process() throws Exception { |
||||||
|
// Retries impossible. RFC 1929 specifies that the server MUST
|
||||||
|
// close the connection if authentication is unsuccessful.
|
||||||
|
done = true; |
||||||
|
if (params.getByte() != SOCKS_BASIC_PROTOCOL_VERSION |
||||||
|
|| params.getByte() != SOCKS_BASIC_AUTH_SUCCESS) { |
||||||
|
throw new IOException(format( |
||||||
|
SshdText.get().proxySocksAuthenticationFailed, proxy)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void askCredentials() { |
||||||
|
super.askCredentials(); |
||||||
|
adjustTimeout(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Buffer getToken() throws IOException { |
||||||
|
if (done) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
try { |
||||||
|
byte[] rawUser = user.getBytes(StandardCharsets.UTF_8); |
||||||
|
if (rawUser.length > 255) { |
||||||
|
throw new IOException(format( |
||||||
|
SshdText.get().proxySocksUsernameTooLong, proxy, |
||||||
|
Integer.toString(rawUser.length), user)); |
||||||
|
} |
||||||
|
|
||||||
|
if (password.length > 255) { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksPasswordTooLong, |
||||||
|
proxy, Integer.toString(password.length))); |
||||||
|
} |
||||||
|
ByteArrayBuffer buffer = new ByteArrayBuffer( |
||||||
|
3 + rawUser.length + password.length, false); |
||||||
|
buffer.putByte(SOCKS_BASIC_PROTOCOL_VERSION); |
||||||
|
buffer.putByte((byte) rawUser.length); |
||||||
|
buffer.putRawBytes(rawUser); |
||||||
|
buffer.putByte((byte) password.length); |
||||||
|
buffer.putRawBytes(password); |
||||||
|
return buffer; |
||||||
|
} finally { |
||||||
|
clearPassword(); |
||||||
|
done = true; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @see <a href="https://tools.ietf.org/html/rfc1961">RFC 1961</a> |
||||||
|
*/ |
||||||
|
private class SocksGssApiAuthentication |
||||||
|
extends GssApiAuthentication<Buffer, Buffer> { |
||||||
|
|
||||||
|
private static final byte SOCKS5_GSSAPI_VERSION = 1; |
||||||
|
|
||||||
|
private static final byte SOCKS5_GSSAPI_TOKEN = 1; |
||||||
|
|
||||||
|
private static final int SOCKS5_GSSAPI_FAILURE = 0xFF; |
||||||
|
|
||||||
|
public SocksGssApiAuthentication() { |
||||||
|
super(proxyAddress); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected GSSContext createContext() throws Exception { |
||||||
|
return context; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Buffer getToken() throws Exception { |
||||||
|
if (token == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
Buffer buffer = new ByteArrayBuffer(4 + token.length, false); |
||||||
|
buffer.putByte(SOCKS5_GSSAPI_VERSION); |
||||||
|
buffer.putByte(SOCKS5_GSSAPI_TOKEN); |
||||||
|
buffer.putByte((byte) ((token.length >> 8) & 0xFF)); |
||||||
|
buffer.putByte((byte) (token.length & 0xFF)); |
||||||
|
buffer.putRawBytes(token); |
||||||
|
return buffer; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected byte[] extractToken(Buffer input) throws Exception { |
||||||
|
if (context == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
int version = input.getUByte(); |
||||||
|
if (version != SOCKS5_GSSAPI_VERSION) { |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksGssApiVersionMismatch, |
||||||
|
remoteAddress, Integer.toString(version))); |
||||||
|
} |
||||||
|
int msgType = input.getUByte(); |
||||||
|
if (msgType == SOCKS5_GSSAPI_FAILURE) { |
||||||
|
throw new IOException(format( |
||||||
|
SshdText.get().proxySocksGssApiFailure, remoteAddress)); |
||||||
|
} else if (msgType != SOCKS5_GSSAPI_TOKEN) { |
||||||
|
throw new IOException(format( |
||||||
|
SshdText.get().proxySocksGssApiUnknownMessage, |
||||||
|
remoteAddress, Integer.toHexString(msgType & 0xFF))); |
||||||
|
} |
||||||
|
if (input.available() >= 2) { |
||||||
|
int length = (input.getUByte() << 8) + input.getUByte(); |
||||||
|
if (input.available() >= length) { |
||||||
|
byte[] value = new byte[length]; |
||||||
|
if (length > 0) { |
||||||
|
input.getRawBytes(value); |
||||||
|
} |
||||||
|
return value; |
||||||
|
} |
||||||
|
} |
||||||
|
throw new IOException( |
||||||
|
format(SshdText.get().proxySocksGssApiMessageTooShort, |
||||||
|
remoteAddress)); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,89 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.proxy; |
||||||
|
|
||||||
|
import java.util.concurrent.Callable; |
||||||
|
|
||||||
|
import org.apache.sshd.client.session.ClientProxyConnector; |
||||||
|
import org.apache.sshd.common.io.IoSession; |
||||||
|
import org.apache.sshd.common.util.Readable; |
||||||
|
|
||||||
|
/** |
||||||
|
* Some proxy connections are stateful and require the exchange of multiple |
||||||
|
* request-reply messages. The default {@link ClientProxyConnector} has only |
||||||
|
* support for sending a message; replies get routed through the Ssh session, |
||||||
|
* and don't get back to this proxy connector. Augment the interface so that the |
||||||
|
* session can know when to route messages received to the proxy connector, and |
||||||
|
* when to start handling them itself. |
||||||
|
*/ |
||||||
|
public interface StatefulProxyConnector extends ClientProxyConnector { |
||||||
|
|
||||||
|
/** |
||||||
|
* A property key for a session property defining the timeout for setting up |
||||||
|
* the proxy connection. |
||||||
|
*/ |
||||||
|
static final String TIMEOUT_PROPERTY = StatefulProxyConnector.class |
||||||
|
.getName() + "-timeout"; //$NON-NLS-1$
|
||||||
|
|
||||||
|
/** |
||||||
|
* Handle a received message. |
||||||
|
* |
||||||
|
* @param session |
||||||
|
* to use for writing data |
||||||
|
* @param buffer |
||||||
|
* received data |
||||||
|
* @throws Exception |
||||||
|
* if data cannot be read, or the connection attempt fails |
||||||
|
*/ |
||||||
|
void messageReceived(IoSession session, Readable buffer) throws Exception; |
||||||
|
|
||||||
|
/** |
||||||
|
* Runs {@code startSsh} once the proxy connection is established. |
||||||
|
* |
||||||
|
* @param startSsh |
||||||
|
* operation to run |
||||||
|
* @throws Exception |
||||||
|
* if the operation is run synchronously and throws an exception |
||||||
|
*/ |
||||||
|
void runWhenDone(Callable<Void> startSsh) throws Exception; |
||||||
|
} |
@ -0,0 +1,99 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.internal.transport.sshd.proxy; |
||||||
|
|
||||||
|
/** |
||||||
|
* A very simple representation of a HTTP status line. |
||||||
|
*/ |
||||||
|
public class StatusLine { |
||||||
|
|
||||||
|
private final String version; |
||||||
|
|
||||||
|
private final int resultCode; |
||||||
|
|
||||||
|
private final String reason; |
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new {@link StatusLine} with the given response code and reason |
||||||
|
* string. |
||||||
|
* |
||||||
|
* @param version |
||||||
|
* the version string (normally "HTTP/1.1" or "HTTP/1.0") |
||||||
|
* @param resultCode |
||||||
|
* the HTTP response code (200, 401, etc.) |
||||||
|
* @param reason |
||||||
|
* the reason phrase for the code |
||||||
|
*/ |
||||||
|
public StatusLine(String version, int resultCode, String reason) { |
||||||
|
this.version = version; |
||||||
|
this.resultCode = resultCode; |
||||||
|
this.reason = reason; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieves the version string. |
||||||
|
* |
||||||
|
* @return the version string |
||||||
|
*/ |
||||||
|
public String getVersion() { |
||||||
|
return version; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieves the HTTP response code. |
||||||
|
* |
||||||
|
* @return the code |
||||||
|
*/ |
||||||
|
public int getResultCode() { |
||||||
|
return resultCode; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Retrieves the HTTP reason phrase. |
||||||
|
* |
||||||
|
* @return the reason |
||||||
|
*/ |
||||||
|
public String getReason() { |
||||||
|
return reason; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,103 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.transport.sshd; |
||||||
|
|
||||||
|
import java.net.InetSocketAddress; |
||||||
|
import java.net.Proxy; |
||||||
|
import java.net.ProxySelector; |
||||||
|
import java.net.SocketAddress; |
||||||
|
import java.net.URI; |
||||||
|
import java.net.URISyntaxException; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.apache.sshd.client.config.hosts.HostConfigEntry; |
||||||
|
|
||||||
|
/** |
||||||
|
* A default implementation of a {@link ProxyDataFactory} based on the standard |
||||||
|
* {@link java.net.ProxySelector}. |
||||||
|
* |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public class DefaultProxyDataFactory implements ProxyDataFactory { |
||||||
|
|
||||||
|
@Override |
||||||
|
public ProxyData get(HostConfigEntry hostConfig, |
||||||
|
InetSocketAddress remoteAddress) { |
||||||
|
try { |
||||||
|
List<Proxy> proxies = ProxySelector.getDefault() |
||||||
|
.select(new URI(Proxy.Type.SOCKS.name(), |
||||||
|
"//" + remoteAddress.getHostString(), null)); //$NON-NLS-1$
|
||||||
|
ProxyData data = getData(proxies, Proxy.Type.SOCKS); |
||||||
|
if (data == null) { |
||||||
|
proxies = ProxySelector.getDefault() |
||||||
|
.select(new URI(Proxy.Type.HTTP.name(), |
||||||
|
"//" + remoteAddress.getHostString(), //$NON-NLS-1$
|
||||||
|
null)); |
||||||
|
data = getData(proxies, Proxy.Type.HTTP); |
||||||
|
} |
||||||
|
return data; |
||||||
|
} catch (URISyntaxException e) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private ProxyData getData(List<Proxy> proxies, Proxy.Type type) { |
||||||
|
Proxy proxy = proxies.stream().filter(p -> type == p.type()).findFirst() |
||||||
|
.orElse(null); |
||||||
|
if (proxy == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
SocketAddress address = proxy.address(); |
||||||
|
if (!(address instanceof InetSocketAddress)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
switch (type) { |
||||||
|
case HTTP: |
||||||
|
return new ProxyData(proxy); |
||||||
|
case SOCKS: |
||||||
|
return new ProxyData(proxy); |
||||||
|
default: |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,136 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.transport.sshd; |
||||||
|
|
||||||
|
import java.net.InetSocketAddress; |
||||||
|
import java.net.Proxy; |
||||||
|
import java.util.Arrays; |
||||||
|
|
||||||
|
import org.eclipse.jgit.annotations.NonNull; |
||||||
|
|
||||||
|
/** |
||||||
|
* A DTO encapsulating the data needed to connect through a proxy server. |
||||||
|
* |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public class ProxyData { |
||||||
|
|
||||||
|
private final @NonNull Proxy proxy; |
||||||
|
|
||||||
|
private final String proxyUser; |
||||||
|
|
||||||
|
private final char[] proxyPassword; |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link ProxyData} instance without user name or password. |
||||||
|
* |
||||||
|
* @param proxy |
||||||
|
* to connect to; must not be {@link java.net.Proxy.Type#DIRECT} |
||||||
|
* and must have an {@link InetSocketAddress}. |
||||||
|
*/ |
||||||
|
public ProxyData(@NonNull Proxy proxy) { |
||||||
|
this(proxy, null, null); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Creates a new {@link ProxyData} instance. |
||||||
|
* |
||||||
|
* @param proxy |
||||||
|
* to connect to; must not be {@link java.net.Proxy.Type#DIRECT} |
||||||
|
* and must have an {@link InetSocketAddress}. |
||||||
|
* @param proxyUser |
||||||
|
* to use for log-in to the proxy, may be {@code null} |
||||||
|
* @param proxyPassword |
||||||
|
* to use for log-in to the proxy, may be {@code null} |
||||||
|
*/ |
||||||
|
public ProxyData(@NonNull Proxy proxy, String proxyUser, |
||||||
|
char[] proxyPassword) { |
||||||
|
this.proxy = proxy; |
||||||
|
if (!(proxy.address() instanceof InetSocketAddress)) { |
||||||
|
// Internal error not translated
|
||||||
|
throw new IllegalArgumentException( |
||||||
|
"Proxy does not have an InetSocketAddress"); //$NON-NLS-1$
|
||||||
|
} |
||||||
|
this.proxyUser = proxyUser; |
||||||
|
this.proxyPassword = proxyPassword == null ? null |
||||||
|
: proxyPassword.clone(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Obtains the remote {@link InetSocketAddress} of the proxy to connect to. |
||||||
|
* |
||||||
|
* @return the remote address of the proxy |
||||||
|
*/ |
||||||
|
@NonNull |
||||||
|
public Proxy getProxy() { |
||||||
|
return proxy; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Obtains the user to log in at the proxy with. |
||||||
|
* |
||||||
|
* @return the user name, or {@code null} if none |
||||||
|
*/ |
||||||
|
public String getUser() { |
||||||
|
return proxyUser; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Obtains a copy of the internally stored password. |
||||||
|
* |
||||||
|
* @return the password or {@code null} if none |
||||||
|
*/ |
||||||
|
public char[] getPassword() { |
||||||
|
return proxyPassword == null ? null : proxyPassword.clone(); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Clears the stored password, if any. |
||||||
|
*/ |
||||||
|
public void clearPassword() { |
||||||
|
if (proxyPassword != null) { |
||||||
|
Arrays.fill(proxyPassword, '\000'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
/* |
||||||
|
* Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> |
||||||
|
* and other copyright owners as documented in the project's IP log. |
||||||
|
* |
||||||
|
* This program and the accompanying materials are made available |
||||||
|
* under the terms of the Eclipse Distribution License v1.0 which |
||||||
|
* accompanies this distribution, is reproduced below, and is |
||||||
|
* available at http://www.eclipse.org/org/documents/edl-v10.php
|
||||||
|
* |
||||||
|
* All rights reserved. |
||||||
|
* |
||||||
|
* Redistribution and use in source and binary forms, with or |
||||||
|
* without modification, are permitted provided that the following |
||||||
|
* conditions are met: |
||||||
|
* |
||||||
|
* - Redistributions of source code must retain the above copyright |
||||||
|
* notice, this list of conditions and the following disclaimer. |
||||||
|
* |
||||||
|
* - Redistributions in binary form must reproduce the above |
||||||
|
* copyright notice, this list of conditions and the following |
||||||
|
* disclaimer in the documentation and/or other materials provided |
||||||
|
* with the distribution. |
||||||
|
* |
||||||
|
* - Neither the name of the Eclipse Foundation, Inc. nor the |
||||||
|
* names of its contributors may be used to endorse or promote |
||||||
|
* products derived from this software without specific prior |
||||||
|
* written permission. |
||||||
|
* |
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND |
||||||
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, |
||||||
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES |
||||||
|
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||||
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT |
||||||
|
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
||||||
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
||||||
|
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
||||||
|
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
||||||
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
||||||
|
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
|
*/ |
||||||
|
package org.eclipse.jgit.transport.sshd; |
||||||
|
|
||||||
|
import java.net.InetSocketAddress; |
||||||
|
|
||||||
|
import org.apache.sshd.client.config.hosts.HostConfigEntry; |
||||||
|
|
||||||
|
/** |
||||||
|
* Interface for obtaining {@link ProxyData} to connect through some proxy. |
||||||
|
* |
||||||
|
* @since 5.2 |
||||||
|
*/ |
||||||
|
public interface ProxyDataFactory { |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the {@link ProxyData} to connect to a proxy. It should return a |
||||||
|
* <em>new</em> {@link ProxyData} instance every time; if the returned |
||||||
|
* {@link ProxyData} contains a password, the {@link SshdSession} will clear |
||||||
|
* it once it is no longer needed. |
||||||
|
* |
||||||
|
* @param hostConfig |
||||||
|
* from the ssh config that we're going to connect for |
||||||
|
* @param remoteAddress |
||||||
|
* to connect to |
||||||
|
* @return the {@link ProxyData} or {@code null} if a direct connection is |
||||||
|
* to be made |
||||||
|
*/ |
||||||
|
ProxyData get(HostConfigEntry hostConfig, InetSocketAddress remoteAddress); |
||||||
|
} |
Loading…
Reference in new issue