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 Socks4Parser parser = new Socks4Parser();
83 private final ClientConnectionFactory connectionFactory;
84 private final Map<String, Object> context;
85
86 public Socks4ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map<String, Object> context)
87 {
88 super(endPoint, executor);
89 this.connectionFactory = connectionFactory;
90 this.context = context;
91 }
92
93 @Override
94 public void onOpen()
95 {
96 super.onOpen();
97 writeSocks4Connect();
98 }
99
100
101
102
103
104 private void writeSocks4Connect()
105 {
106 HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
107 String host = destination.getHost();
108 short port = (short)destination.getPort();
109 Matcher matcher = IPv4_PATTERN.matcher(host);
110 if (matcher.matches())
111 {
112
113 ByteBuffer buffer = ByteBuffer.allocate(9);
114 buffer.put((byte)4).put((byte)1).putShort(port);
115 for (int i = 1; i <= 4; ++i)
116 buffer.put((byte)Integer.parseInt(matcher.group(i)));
117 buffer.put((byte)0);
118 buffer.flip();
119 getEndPoint().write(this, buffer);
120 }
121 else
122 {
123
124 byte[] hostBytes = host.getBytes(StandardCharsets.UTF_8);
125 ByteBuffer buffer = ByteBuffer.allocate(9 + hostBytes.length + 1);
126 buffer.put((byte)4).put((byte)1).putShort(port);
127 buffer.put((byte)0).put((byte)0).put((byte)0).put((byte)1).put((byte)0);
128 buffer.put(hostBytes).put((byte)0);
129 buffer.flip();
130 getEndPoint().write(this, buffer);
131 }
132 }
133
134 @Override
135 public void succeeded()
136 {
137 if (LOG.isDebugEnabled())
138 LOG.debug("Written SOCKS4 connect request");
139 fillInterested();
140 }
141
142 @Override
143 public void failed(Throwable x)
144 {
145 close();
146 @SuppressWarnings("unchecked")
147 Promise<Connection> promise = (Promise<Connection>)context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
148 promise.failed(x);
149 }
150
151 @Override
152 public void onFillable()
153 {
154 try
155 {
156 while (true)
157 {
158
159
160 ByteBuffer buffer = BufferUtil.allocate(parser.expected());
161 int filled = getEndPoint().fill(buffer);
162 if (LOG.isDebugEnabled())
163 LOG.debug("Read SOCKS4 connect response, {} bytes", filled);
164
165 if (filled < 0)
166 throw new IOException("SOCKS4 tunnel failed, connection closed");
167
168 if (filled == 0)
169 {
170 fillInterested();
171 return;
172 }
173
174 if (parser.parse(buffer))
175 return;
176 }
177 }
178 catch (Throwable x)
179 {
180 failed(x);
181 }
182 }
183
184 private void onSocks4Response(int responseCode) throws IOException
185 {
186 if (responseCode == 0x5A)
187 tunnel();
188 else
189 throw new IOException("SOCKS4 tunnel failed with code " + responseCode);
190 }
191
192 private void tunnel()
193 {
194 try
195 {
196 HttpDestination destination = (HttpDestination)context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
197 HttpClient client = destination.getHttpClient();
198 ClientConnectionFactory connectionFactory = this.connectionFactory;
199 if (HttpScheme.HTTPS.is(destination.getScheme()))
200 connectionFactory = new SslClientConnectionFactory(client.getSslContextFactory(), client.getByteBufferPool(), client.getExecutor(), connectionFactory);
201 org.eclipse.jetty.io.Connection newConnection = connectionFactory.newConnection(getEndPoint(), context);
202 getEndPoint().upgrade(newConnection);
203 if (LOG.isDebugEnabled())
204 LOG.debug("SOCKS4 tunnel established: {} over {}", this, newConnection);
205 }
206 catch (Throwable x)
207 {
208 failed(x);
209 }
210 }
211
212 private class Socks4Parser
213 {
214 private static final int EXPECTED_LENGTH = 8;
215 private int cursor;
216 private int response;
217
218 private boolean parse(ByteBuffer buffer) throws IOException
219 {
220 while (buffer.hasRemaining())
221 {
222 byte current = buffer.get();
223 if (cursor == 1)
224 response = current & 0xFF;
225 ++cursor;
226 if (cursor == EXPECTED_LENGTH)
227 {
228 onSocks4Response(response);
229 return true;
230 }
231 }
232 return false;
233 }
234
235 private int expected()
236 {
237 return EXPECTED_LENGTH - cursor;
238 }
239 }
240 }
241 }