| /* | |
| * Copyright (c) 2000, 2025, 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.nio.charset.StandardCharsets; | |
| import java.util.Iterator; | |
| import jdk.internal.util.StaticProperty; | |
| import sun.net.SocksProxy; | |
| import sun.net.spi.DefaultProxySelector; | |
| import sun.net.www.ParseUtil; | |
| import static sun.net.util.IPAddressUtil.isIPv6LiteralAddress; | |
| import static jdk.internal.util.Exceptions.filterSocketInfo; | |
| import static jdk.internal.util.Exceptions.formatMsg; | |
| /** | |
| * 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 ad) { | |
| // 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 doConnect(final String host, final int port, final int timeout) throws IOException { | |
| delegate.connect(new InetSocketAddress(host, port), timeout); | |
| cmdIn = getInputStream(); | |
| cmdOut = getOutputStream(); | |
| } | |
| 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 = 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()); | |
| out.write(userName.getBytes(StandardCharsets.ISO_8859_1)); | |
| if (password != null) { | |
| out.write(password.length()); | |
| out.write(password.getBytes(StandardCharsets.ISO_8859_1)); | |
| } 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(); | |
| out.write(userName.getBytes(StandardCharsets.ISO_8859_1)); | |
| 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 = switch (data[1]) { | |
| case 90 -> { | |
| // Success! | |
| external_address = endpoint; | |
| yield null; | |
| } | |
| case 91 -> new SocketException("SOCKS request rejected"); | |
| case 92 -> new SocketException("SOCKS server couldn't reach destination"); | |
| case 93 -> new SocketException("SOCKS authentication failed"); | |
| default -> new SocketException("Reply from SOCKS server contains bad status"); | |
| }; | |
| 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); | |
| } | |
| /** | |
| * 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 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; | |
| } | |
| if (!(endpoint instanceof InetSocketAddress epoint)) | |
| throw new IllegalArgumentException("Unsupported address type"); | |
| 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 = 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(); | |
| if (isIPv6LiteralAddress(host)) { | |
| host = "[" + host + "]"; | |
| } else { | |
| host = ParseUtil.encodePath(host); | |
| } | |
| try { | |
| uri = new URI("socket://" + host + ":"+ epoint.getPort()); | |
| } catch (URISyntaxException e) { | |
| throw new IOException("Failed to select a proxy", e); | |
| } | |
| Proxy p = null; | |
| IOException savedExc = null; | |
| final Iterator<Proxy> iProxy; | |
| try { | |
| iProxy = sel.select(uri).iterator(); | |
| } catch (IllegalArgumentException iae) { | |
| throw new IOException("Failed to select a proxy", iae); | |
| } | |
| 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 { | |
| doConnect(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 { | |
| doConnect(server, serverPort, remainingMillis(deadlineMillis)); | |
| } catch (IOException e) { | |
| throw new SocketException(e.getMessage(), e); | |
| } | |
| } | |
| // `cmdIn` & `cmdOut` were initialized during the `doConnect()` 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(formatMsg("%s", filterSocketInfo(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(formatMsg("%s", filterSocketInfo(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()); | |
| out.write(epoint.getHostName().getBytes(StandardCharsets.ISO_8859_1)); | |
| 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"); | |
| } | |
| } |