/*
* Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package java.net;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedOutputStream;
import java.security.AccessController;
import jdk.internal.util.StaticProperty;
import sun.net.SocksProxy;
import sun.net.spi.DefaultProxySelector;
import sun.net.www.ParseUtil;
/**
* SOCKS (V4 & V5) TCP socket implementation (RFC 1928).
*/
class SocksSocketImpl extends DelegatingSocketImpl implements SocksConsts {
private String server = null;
private int serverPort = DEFAULT_PORT;
private InetSocketAddress external_address;
private boolean useV4 = false;
private Socket cmdsock = null;
private InputStream cmdIn = null;
private OutputStream cmdOut = null;
SocksSocketImpl(SocketImpl delegate) {
super(delegate);
}
SocksSocketImpl(Proxy proxy, SocketImpl delegate) {
super(delegate);
SocketAddress a = proxy.address();
if (a instanceof InetSocketAddress) {
InetSocketAddress ad = (InetSocketAddress) a;
// Use getHostString() to avoid reverse lookups
server = ad.getHostString();
serverPort = ad.getPort();
}
useV4 = useV4(proxy);
}
private static boolean useV4(Proxy proxy) {
if (proxy instanceof SocksProxy
&& ((SocksProxy)proxy).protocolVersion() == 4) {
return true;
}
return DefaultProxySelector.socksProxyVersion() == 4;
}
private synchronized void privilegedConnect(final String host,
final int port,
final int timeout)
throws IOException
{
try {
AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<>() {
public Void run() throws IOException {
superConnectServer(host, port, timeout);
cmdIn = getInputStream();
cmdOut = getOutputStream();
return null;
}
});
} catch (java.security.PrivilegedActionException pae) {
throw (IOException) pae.getException();
}
}
private void superConnectServer(String host, int port,
int timeout) throws IOException {
delegate.connect(new InetSocketAddress(host, port), timeout);
}
private static int remainingMillis(long deadlineMillis) throws IOException {
if (deadlineMillis == 0L)
return 0;
final long remaining = deadlineMillis - System.currentTimeMillis();
if (remaining > 0)
return (int) remaining;
throw new SocketTimeoutException();
}
private int readSocksReply(InputStream in, byte[] data, long deadlineMillis) throws IOException {
int len = data.length;
int received = 0;
int originalTimeout = (int) getOption(SocketOptions.SO_TIMEOUT);
try {
while (received < len) {
int count;
int remaining = remainingMillis(deadlineMillis);
setOption(SocketOptions.SO_TIMEOUT, remaining);
try {
count = in.read(data, received, len - received);
} catch (SocketTimeoutException e) {
throw new SocketTimeoutException("Connect timed out");
}
if (count < 0)
throw new SocketException("Malformed reply from SOCKS server");
received += count;
}
} finally {
setOption(SocketOptions.SO_TIMEOUT, originalTimeout);
}
return received;
}
private boolean authenticate(byte method, InputStream in,
BufferedOutputStream out,
long deadlineMillis) throws IOException {
// No Authentication required. We're done then!
if (method == NO_AUTH)
return true;
/*
* User/Password authentication. Try, in that order :
* - The application provided Authenticator, if any
* - the user.name & no password (backward compatibility behavior).
*/
if (method == USER_PASSW) {
String userName;
String password = null;
final InetAddress addr = InetAddress.getByName(server);
PasswordAuthentication pw =
java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<>() {
public PasswordAuthentication run() {
return Authenticator.requestPasswordAuthentication(
server, addr, serverPort, "SOCKS5", "SOCKS authentication", null);
}
});
if (pw != null) {
userName = pw.getUserName();
password = new String(pw.getPassword());
} else {
userName = StaticProperty.userName();
}
if (userName == null)
return false;
out.write(1);
out.write(userName.length());
try {
out.write(userName.getBytes("ISO-8859-1"));
} catch (java.io.UnsupportedEncodingException uee) {
assert false;
}
if (password != null) {
out.write(password.length());
try {
out.write(password.getBytes("ISO-8859-1"));
} catch (java.io.UnsupportedEncodingException uee) {
assert false;
}
} else
out.write(0);
out.flush();
byte[] data = new byte[2];
int i = readSocksReply(in, data, deadlineMillis);
if (i != 2 || data[1] != 0) {
/* RFC 1929 specifies that the connection MUST be closed if
authentication fails */
out.close();
in.close();
return false;
}
/* Authentication succeeded */
return true;
}
return false;
}
private void connectV4(InputStream in, OutputStream out,
InetSocketAddress endpoint,
long deadlineMillis) throws IOException {
if (!(endpoint.getAddress() instanceof Inet4Address)) {
throw new SocketException("SOCKS V4 requires IPv4 only addresses");
}
out.write(PROTO_VERS4);
out.write(CONNECT);
out.write((endpoint.getPort() >> 8) & 0xff);
out.write((endpoint.getPort() >> 0) & 0xff);
out.write(endpoint.getAddress().getAddress());
String userName = getUserName();
try {
out.write(userName.getBytes("ISO-8859-1"));
} catch (java.io.UnsupportedEncodingException uee) {
assert false;
}
out.write(0);
out.flush();
byte[] data = new byte[8];
int n = readSocksReply(in, data, deadlineMillis);
if (n != 8)
throw new SocketException("Reply from SOCKS server has bad length: " + n);
if (data[0] != 0 && data[0] != 4)
throw new SocketException("Reply from SOCKS server has bad version");
SocketException ex = null;
switch (data[1]) {
case 90:
// Success!
external_address = endpoint;
break;
case 91:
ex = new SocketException("SOCKS request rejected");
break;
case 92:
ex = new SocketException("SOCKS server couldn't reach destination");
break;
case 93:
ex = new SocketException("SOCKS authentication failed");
break;
default:
ex = new SocketException("Reply from SOCKS server contains bad status");
break;
}
if (ex != null) {
in.close();
out.close();
throw ex;
}
}
@Override
protected void connect(String host, int port) throws IOException {
connect(new InetSocketAddress(host, port), 0);
}
@Override
protected void connect(InetAddress address, int port) throws IOException {
connect(new InetSocketAddress(address, port), 0);
}
@Override
void setSocket(Socket soc) {
delegate.socket = soc;
super.setSocket(soc);
}
@Override
void setServerSocket(ServerSocket soc) {
throw new InternalError("should not get here");
}
/**
* Connects the Socks Socket to the specified endpoint. It will first
* connect to the SOCKS proxy and negotiate the access. If the proxy
* grants the connections, then the connect is successful and all
* further traffic will go to the "real" endpoint.
*
* @param endpoint the {@code SocketAddress} to connect to.
* @param timeout the timeout value in milliseconds
* @throws IOException if the connection can't be established.
* @throws SecurityException if there is a security manager and it
* doesn't allow the connection
* @throws IllegalArgumentException if endpoint is null or a
* SocketAddress subclass not supported by this socket
*/
@Override
protected void connect(SocketAddress endpoint, int timeout) throws IOException {
final long deadlineMillis;
if (timeout == 0) {
deadlineMillis = 0L;
} else {
long finish = System.currentTimeMillis() + timeout;
deadlineMillis = finish < 0 ? Long.MAX_VALUE : finish;
}
SecurityManager security = System.getSecurityManager();
if (endpoint == null || !(endpoint instanceof InetSocketAddress))
throw new IllegalArgumentException("Unsupported address type");
InetSocketAddress epoint = (InetSocketAddress) endpoint;
if (security != null) {
if (epoint.isUnresolved())
security.checkConnect(epoint.getHostName(),
epoint.getPort());
else
security.checkConnect(epoint.getAddress().getHostAddress(),
epoint.getPort());
}
if (server == null) {
// This is the general case
// server is not null only when the socket was created with a
// specified proxy in which case it does bypass the ProxySelector
ProxySelector sel = java.security.AccessController.doPrivileged(
new java.security.PrivilegedAction<>() {
public ProxySelector run() {
return ProxySelector.getDefault();
}
});
if (sel == null) {
/*
* No default proxySelector --> direct connection
*/
delegate.connect(epoint, remainingMillis(deadlineMillis));
return;
}
URI uri;
// Use getHostString() to avoid reverse lookups
String host = epoint.getHostString();
// IPv6 literal?
if (epoint.getAddress() instanceof Inet6Address &&
(!host.startsWith("[")) && (host.indexOf(':') >= 0)) {
host = "[" + host + "]";
}
try {
uri = new URI("socket://" + ParseUtil.encodePath(host) + ":"+ epoint.getPort());
} catch (URISyntaxException e) {
// This shouldn't happen
assert false : e;
uri = null;
}
Proxy p = null;
IOException savedExc = null;
java.util.Iterator<Proxy> iProxy = null;
iProxy = sel.select(uri).iterator();
if (iProxy == null || !(iProxy.hasNext())) {
delegate.connect(epoint, remainingMillis(deadlineMillis));
return;
}
while (iProxy.hasNext()) {
p = iProxy.next();
if (p == null || p.type() != Proxy.Type.SOCKS) {
delegate.connect(epoint, remainingMillis(deadlineMillis));
return;
}
if (!(p.address() instanceof InetSocketAddress))
throw new SocketException("Unknown address type for proxy: " + p);
// Use getHostString() to avoid reverse lookups
server = ((InetSocketAddress) p.address()).getHostString();
serverPort = ((InetSocketAddress) p.address()).getPort();
useV4 = useV4(p);
// Connects to the SOCKS server
try {
privilegedConnect(server, serverPort, remainingMillis(deadlineMillis));
// Worked, let's get outta here
break;
} catch (IOException e) {
// Ooops, let's notify the ProxySelector
sel.connectFailed(uri,p.address(),e);
server = null;
serverPort = -1;
savedExc = e;
// Will continue the while loop and try the next proxy
}
}
/*
* If server is still null at this point, none of the proxy
* worked
*/
if (server == null) {
throw new SocketException("Can't connect to SOCKS proxy:"
+ savedExc.getMessage());
}
} else {
// Connects to the SOCKS server
try {
privilegedConnect(server, serverPort, remainingMillis(deadlineMillis));
} catch (IOException e) {
throw new SocketException(e.getMessage());
}
}
// cmdIn & cmdOut were initialized during the privilegedConnect() call
BufferedOutputStream out = new BufferedOutputStream(cmdOut, 512);
InputStream in = cmdIn;
if (useV4) {
// SOCKS Protocol version 4 doesn't know how to deal with
// DOMAIN type of addresses (unresolved addresses here)
if (epoint.isUnresolved())
throw new UnknownHostException(epoint.toString());
connectV4(in, out, epoint, deadlineMillis);
return;
}
// This is SOCKS V5
out.write(PROTO_VERS);
out.write(2);
out.write(NO_AUTH);
out.write(USER_PASSW);
out.flush();
byte[] data = new byte[2];
int i = readSocksReply(in, data, deadlineMillis);
if (i != 2 || ((int)data[0]) != PROTO_VERS) {
// Maybe it's not a V5 sever after all
// Let's try V4 before we give up
// SOCKS Protocol version 4 doesn't know how to deal with
// DOMAIN type of addresses (unresolved addresses here)
if (epoint.isUnresolved())
throw new UnknownHostException(epoint.toString());
connectV4(in, out, epoint, deadlineMillis);
return;
}
if (((int)data[1]) == NO_METHODS)
throw new SocketException("SOCKS : No acceptable methods");
if (!authenticate(data[1], in, out, deadlineMillis)) {
throw new SocketException("SOCKS : authentication failed");
}
out.write(PROTO_VERS);
out.write(CONNECT);
out.write(0);
/* Test for IPV4/IPV6/Unresolved */
if (epoint.isUnresolved()) {
out.write(DOMAIN_NAME);
out.write(epoint.getHostName().length());
try {
out.write(epoint.getHostName().getBytes("ISO-8859-1"));
} catch (java.io.UnsupportedEncodingException uee) {
assert false;
}
out.write((epoint.getPort() >> 8) & 0xff);
out.write((epoint.getPort() >> 0) & 0xff);
} else if (epoint.getAddress() instanceof Inet6Address) {
out.write(IPV6);
out.write(epoint.getAddress().getAddress());
out.write((epoint.getPort() >> 8) & 0xff);
out.write((epoint.getPort() >> 0) & 0xff);
} else {
out.write(IPV4);
out.write(epoint.getAddress().getAddress());
out.write((epoint.getPort() >> 8) & 0xff);
out.write((epoint.getPort() >> 0) & 0xff);
}
out.flush();
data = new byte[4];
i = readSocksReply(in, data, deadlineMillis);
if (i != 4)
throw new SocketException("Reply from SOCKS server has bad length");
SocketException ex = null;
int len;
byte[] addr;
switch (data[1]) {
case REQUEST_OK:
// success!
switch(data[3]) {
case IPV4:
addr = new byte[4];
i = readSocksReply(in, addr, deadlineMillis);
if (i != 4)
throw new SocketException("Reply from SOCKS server badly formatted");
data = new byte[2];
i = readSocksReply(in, data, deadlineMillis);
if (i != 2)
throw new SocketException("Reply from SOCKS server badly formatted");
break;
case DOMAIN_NAME:
byte[] lenByte = new byte[1];
i = readSocksReply(in, lenByte, deadlineMillis);
if (i != 1)
throw new SocketException("Reply from SOCKS server badly formatted");
len = lenByte[0] & 0xFF;
byte[] host = new byte[len];
i = readSocksReply(in, host, deadlineMillis);
if (i != len)
throw new SocketException("Reply from SOCKS server badly formatted");
data = new byte[2];
i = readSocksReply(in, data, deadlineMillis);
if (i != 2)
throw new SocketException("Reply from SOCKS server badly formatted");
break;
case IPV6:
len = 16;
addr = new byte[len];
i = readSocksReply(in, addr, deadlineMillis);
if (i != len)
throw new SocketException("Reply from SOCKS server badly formatted");
data = new byte[2];
i = readSocksReply(in, data, deadlineMillis);
if (i != 2)
throw new SocketException("Reply from SOCKS server badly formatted");
break;
default:
ex = new SocketException("Reply from SOCKS server contains wrong code");
break;
}
break;
case GENERAL_FAILURE:
ex = new SocketException("SOCKS server general failure");
break;
case NOT_ALLOWED:
ex = new SocketException("SOCKS: Connection not allowed by ruleset");
break;
case NET_UNREACHABLE:
ex = new SocketException("SOCKS: Network unreachable");
break;
case HOST_UNREACHABLE:
ex = new SocketException("SOCKS: Host unreachable");
break;
case CONN_REFUSED:
ex = new SocketException("SOCKS: Connection refused");
break;
case TTL_EXPIRED:
ex = new SocketException("SOCKS: TTL expired");
break;
case CMD_NOT_SUPPORTED:
ex = new SocketException("SOCKS: Command not supported");
break;
case ADDR_TYPE_NOT_SUP:
ex = new SocketException("SOCKS: address type not supported");
break;
}
if (ex != null) {
in.close();
out.close();
throw ex;
}
external_address = epoint;
}
@Override
protected void listen(int backlog) {
throw new InternalError("should not get here");
}
@Override
protected void accept(SocketImpl s) {
throw new InternalError("should not get here");
}
/**
* Returns the value of this socket's {@code address} field.
*
* @return the value of this socket's {@code address} field.
* @see java.net.SocketImpl#address
*/
@Override
protected InetAddress getInetAddress() {
if (external_address != null)
return external_address.getAddress();
else
return delegate.getInetAddress();
}
/**
* Returns the value of this socket's {@code port} field.
*
* @return the value of this socket's {@code port} field.
* @see java.net.SocketImpl#port
*/
@Override
protected int getPort() {
if (external_address != null)
return external_address.getPort();
else
return delegate.getPort();
}
@Override
protected void close() throws IOException {
if (cmdsock != null)
cmdsock.close();
cmdsock = null;
delegate.close();
}
private String getUserName() {
return StaticProperty.userName();
}
@Override
void reset() {
throw new InternalError("should not get here");
}
}