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