1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.client;
20
21 import java.io.IOException;
22 import java.nio.ByteBuffer;
23 import java.nio.charset.StandardCharsets;
24 import java.util.Map;
25 import java.util.concurrent.Executor;
26 import java.util.regex.Matcher;
27 import java.util.regex.Pattern;
28
29 import org.eclipse.jetty.client.api.Connection;
30 import org.eclipse.jetty.http.HttpScheme;
31 import org.eclipse.jetty.io.AbstractConnection;
32 import org.eclipse.jetty.io.ClientConnectionFactory;
33 import org.eclipse.jetty.io.EndPoint;
34 import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
35 import org.eclipse.jetty.util.BufferUtil;
36 import org.eclipse.jetty.util.Callback;
37 import org.eclipse.jetty.util.Promise;
38 import org.eclipse.jetty.util.log.Log;
39 import org.eclipse.jetty.util.log.Logger;
40
41 public class Socks4Proxy extends ProxyConfiguration.Proxy
42 {
43 public Socks4Proxy(String host, int port)
44 {
45 this(new Origin.Address(host, port), false);
46 }
47
48 public Socks4Proxy(Origin.Address address, boolean secure)
49 {
50 super(address, secure);
51 }
52
53 @Override
54 public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory)
55 {
56 return new Socks4ProxyClientConnectionFactory(connectionFactory);
57 }
58
59 public static class Socks4ProxyClientConnectionFactory implements ClientConnectionFactory
60 {
61 private final ClientConnectionFactory connectionFactory;
62
63 public Socks4ProxyClientConnectionFactory(ClientConnectionFactory connectionFactory)
64 {
65 this.connectionFactory = connectionFactory;
66 }
67
68 @Override
69 public org.eclipse.jetty.io.Connection newConnection(EndPoint endPoint, Map<String, Object> context) throws IOException
70 {
71 HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
72 Executor executor = destination.getHttpClient().getExecutor();
73 return new Socks4ProxyConnection(endPoint, executor, connectionFactory, context);
74 }
75 }
76
77 private static class Socks4ProxyConnection extends AbstractConnection implements Callback
78 {
79 private static final Pattern IPv4_PATTERN = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");
80 private static final Logger LOG = Log.getLogger(Socks4ProxyConnection.class);
81
82 private final ClientConnectionFactory connectionFactory;
83 private final Map<String, Object> context;
84
85 public Socks4ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map<String, Object> context)
86 {
87 super(endPoint, executor);
88 this.connectionFactory = connectionFactory;
89 this.context = context;
90 }
91
92 @Override
93 public void onOpen()
94 {
95 super.onOpen();
96 writeSocks4Connect();
97 }
98
99
100
101
102
103 private void writeSocks4Connect()
104 {
105 HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
106 String host = destination.getHost();
107 short port = (short)destination.getPort();
108 Matcher matcher = IPv4_PATTERN.matcher(host);
109 if (matcher.matches())
110 {
111
112 ByteBuffer buffer = ByteBuffer.allocate(9);
113 buffer.put((byte)4).put((byte)1).putShort(port);
114 for (int i = 1; i <= 4; ++i)
115 buffer.put((byte)Integer.parseInt(matcher.group(i)));
116 buffer.put((byte)0);
117 buffer.flip();
118 getEndPoint().write(this, buffer);
119 }
120 else
121 {
122
123 byte[] hostBytes = host.getBytes(StandardCharsets.UTF_8);
124 ByteBuffer buffer = ByteBuffer.allocate(9 + hostBytes.length + 1);
125 buffer.put((byte)4).put((byte)1).putShort(port);
126 buffer.put((byte)0).put((byte)0).put((byte)0).put((byte)1).put((byte)0);
127 buffer.put(hostBytes).put((byte)0);
128 buffer.flip();
129 getEndPoint().write(this, buffer);
130 }
131 }
132
133 @Override
134 public void succeeded()
135 {
136 LOG.debug("Written SOCKS4 connect request");
137 fillInterested();
138 }
139
140 @Override
141 public void failed(Throwable x)
142 {
143 close();
144 @SuppressWarnings("unchecked")
145 Promise<Connection> promise = (Promise<Connection>)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
146 promise.failed(x);
147 }
148
149 @Override
150 public void onFillable()
151 {
152 try
153 {
154 ByteBuffer buffer = BufferUtil.allocate(8);
155 int filled = getEndPoint().fill(buffer);
156 LOG.debug("Read SOCKS4 connect response, {} bytes", filled);
157 if (filled != 8)
158 throw new IOException("Invalid response from SOCKS4 proxy");
159 int result = buffer.get(1);
160 if (result == 0x5A)
161 tunnel();
162 else
163 throw new IOException("SOCKS4 tunnel failed with code " + result);
164 }
165 catch (Throwable x)
166 {
167 failed(x);
168 }
169 }
170
171 private void tunnel()
172 {
173 try
174 {
175 HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
176 HttpClient client = destination.getHttpClient();
177 ClientConnectionFactory connectionFactory = this.connectionFactory;
178 if (HttpScheme.HTTPS.is(destination.getScheme()))
179 connectionFactory = new SslClientConnectionFactory(client.getSslContextFactory(), client.getByteBufferPool(), client.getExecutor(), connectionFactory);
180 org.eclipse.jetty.io.Connection connection = connectionFactory.newConnection(getEndPoint(), context);
181 ClientConnectionFactory.Helper.replaceConnection(this, connection);
182 LOG.debug("SOCKS4 tunnel established: {} over {}", this, connection);
183 }
184 catch (Throwable x)
185 {
186 failed(x);
187 }
188 }
189 }
190 }