1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 package org.apache.hc.core5.reactor;
29
30 import java.io.IOException;
31 import java.net.Inet4Address;
32 import java.net.Inet6Address;
33 import java.net.InetAddress;
34 import java.net.InetSocketAddress;
35 import java.nio.BufferOverflowException;
36 import java.nio.ByteBuffer;
37 import java.nio.channels.ByteChannel;
38 import java.nio.channels.SelectionKey;
39 import java.nio.channels.UnresolvedAddressException;
40 import java.nio.charset.StandardCharsets;
41
42 import org.apache.hc.core5.http.nio.command.CommandSupport;
43 import org.apache.hc.core5.io.CloseMode;
44 import org.apache.hc.core5.io.SocketTimeoutExceptionFactory;
45 import org.apache.hc.core5.net.InetAddressUtils;
46 import org.apache.hc.core5.util.Timeout;
47
48
49
50
51
52 final class SocksProxyProtocolHandler implements IOEventHandler {
53
54 private static final int MAX_COMMAND_CONNECT_LENGTH = 22;
55
56 private static final byte CLIENT_VERSION = 5;
57
58 private static final byte NO_AUTHENTICATION_REQUIRED = 0;
59
60 private static final byte USERNAME_PASSWORD = 2;
61
62 private static final byte USERNAME_PASSWORD_VERSION = 1;
63
64 private static final byte SUCCESS = 0;
65
66 private static final byte COMMAND_CONNECT = 1;
67
68 private static final byte ATYP_DOMAINNAME = 3;
69
70
71 private enum State {
72 SEND_AUTH, RECEIVE_AUTH_METHOD, SEND_USERNAME_PASSWORD, RECEIVE_AUTH, SEND_CONNECT, RECEIVE_RESPONSE_CODE, RECEIVE_ADDRESS_TYPE, RECEIVE_ADDRESS, COMPLETE
73 }
74
75 private final ProtocolIOSession ioSession;
76 private final Object attachment;
77 private final InetSocketAddress targetAddress;
78 private final String username;
79 private final String password;
80 private final IOEventHandlerFactory eventHandlerFactory;
81
82
83 private ByteBuffer buffer = ByteBuffer.allocate(32);
84 private State state = State.SEND_AUTH;
85 SocksProxyProtocolHandler(final ProtocolIOSession ioSession, final Object attachment, final InetSocketAddress targetAddress,
86 final String username, final String password, final IOEventHandlerFactory eventHandlerFactory) {
87 this.ioSession = ioSession;
88 this.attachment = attachment;
89 this.targetAddress = targetAddress;
90 this.username = username;
91 this.password = password;
92 this.eventHandlerFactory = eventHandlerFactory;
93 }
94
95 @Override
96 public void connected(final IOSession session) throws IOException {
97 this.buffer.put(CLIENT_VERSION);
98 this.buffer.put((byte) 1);
99 this.buffer.put(NO_AUTHENTICATION_REQUIRED);
100 this.buffer.flip();
101 session.setEventMask(SelectionKey.OP_WRITE);
102 }
103
104 @Override
105 public void outputReady(final IOSession session) throws IOException {
106 switch (this.state) {
107 case SEND_AUTH:
108 if (writeAndPrepareRead(session, 2)) {
109 session.setEventMask(SelectionKey.OP_READ);
110 this.state = State.RECEIVE_AUTH_METHOD;
111 }
112 break;
113 case SEND_USERNAME_PASSWORD:
114 if (writeAndPrepareRead(session, 2)) {
115 session.setEventMask(SelectionKey.OP_READ);
116 this.state = State.RECEIVE_AUTH;
117 }
118 break;
119 case SEND_CONNECT:
120 if (writeAndPrepareRead(session, 2)) {
121 session.setEventMask(SelectionKey.OP_READ);
122 this.state = State.RECEIVE_RESPONSE_CODE;
123 }
124 break;
125 case RECEIVE_AUTH_METHOD:
126 case RECEIVE_AUTH:
127 case RECEIVE_ADDRESS:
128 case RECEIVE_ADDRESS_TYPE:
129 case RECEIVE_RESPONSE_CODE:
130 session.setEventMask(SelectionKey.OP_READ);
131 break;
132 case COMPLETE:
133 break;
134 }
135 }
136
137 @Override
138 public void inputReady(final IOSession session, final ByteBuffer src) throws IOException {
139 if (src != null) {
140 try {
141 this.buffer.put(src);
142 } catch (final BufferOverflowException ex) {
143 throw new IOException("Unexpected input data");
144 }
145 }
146 switch (this.state) {
147 case RECEIVE_AUTH_METHOD:
148 if (fillBuffer(session)) {
149 this.buffer.flip();
150 final byte serverVersion = this.buffer.get();
151 final byte serverMethod = this.buffer.get();
152 if (serverVersion != CLIENT_VERSION) {
153 throw new IOException("SOCKS server returned unsupported version: " + serverVersion);
154 }
155 if (serverMethod == USERNAME_PASSWORD) {
156 this.buffer.clear();
157 setBufferLimit(this.username.length() + this.password.length() + 3);
158 this.buffer.put(USERNAME_PASSWORD_VERSION);
159 this.buffer.put((byte) this.username.length());
160 this.buffer.put(this.username.getBytes(StandardCharsets.ISO_8859_1));
161 this.buffer.put((byte) this.password.length());
162 this.buffer.put(this.password.getBytes(StandardCharsets.ISO_8859_1));
163 session.setEventMask(SelectionKey.OP_WRITE);
164 this.state = State.SEND_USERNAME_PASSWORD;
165 } else if (serverMethod == NO_AUTHENTICATION_REQUIRED) {
166 prepareConnectCommand();
167 session.setEventMask(SelectionKey.OP_WRITE);
168 this.state = State.SEND_CONNECT;
169 } else {
170 throw new IOException("SOCKS server return unsupported authentication method: " + serverMethod);
171 }
172 }
173 break;
174 case RECEIVE_AUTH:
175 if (fillBuffer(session)) {
176 this.buffer.flip();
177 this.buffer.get();
178 final byte status = this.buffer.get();
179 if (status != SUCCESS) {
180 throw new IOException("Authentication failed for external SOCKS proxy");
181 }
182 prepareConnectCommand();
183 session.setEventMask(SelectionKey.OP_WRITE);
184 this.state = State.SEND_CONNECT;
185 }
186 break;
187 case RECEIVE_RESPONSE_CODE:
188 if (fillBuffer(session)) {
189 this.buffer.flip();
190 final byte serverVersion = this.buffer.get();
191 final byte responseCode = this.buffer.get();
192 if (serverVersion != CLIENT_VERSION) {
193 throw new IOException("SOCKS server returned unsupported version: " + serverVersion);
194 }
195 if (responseCode != SUCCESS) {
196 throw new IOException("SOCKS server was unable to establish connection returned error code: " + responseCode);
197 }
198 this.buffer.compact();
199 this.buffer.limit(3);
200 this.state = State.RECEIVE_ADDRESS_TYPE;
201
202 } else {
203 break;
204 }
205 case RECEIVE_ADDRESS_TYPE:
206 if (fillBuffer(session)) {
207 this.buffer.flip();
208 this.buffer.get();
209 final byte aType = this.buffer.get();
210 final int addressSize;
211 if (aType == InetAddressUtils.IPV4) {
212 addressSize = 4;
213 } else if (aType == InetAddressUtils.IPV6) {
214 addressSize = 16;
215 } else if (aType == ATYP_DOMAINNAME) {
216
217 addressSize = this.buffer.get() & 0xFF;
218 } else {
219 throw new IOException("SOCKS server returned unsupported address type: " + aType);
220 }
221 final int remainingResponseSize = addressSize + 2;
222 this.buffer.compact();
223
224 this.buffer.limit(remainingResponseSize);
225 this.state = State.RECEIVE_ADDRESS;
226
227 } else {
228 break;
229 }
230 case RECEIVE_ADDRESS:
231 if (fillBuffer(session)) {
232 this.buffer.clear();
233 this.state = State.COMPLETE;
234 final IOEventHandler newHandler = this.eventHandlerFactory.createHandler(this.ioSession, this.attachment);
235 this.ioSession.upgrade(newHandler);
236 newHandler.connected(this.ioSession);
237 }
238 break;
239 case SEND_AUTH:
240 case SEND_USERNAME_PASSWORD:
241 case SEND_CONNECT:
242 session.setEventMask(SelectionKey.OP_WRITE);
243 break;
244 case COMPLETE:
245 break;
246 }
247 }
248
249 private void prepareConnectCommand() throws IOException {
250 final InetAddress address = this.targetAddress.getAddress();
251 final int port = this.targetAddress.getPort();
252 if (address == null || port == 0) {
253 throw new UnresolvedAddressException();
254 }
255
256 this.buffer.clear();
257 setBufferLimit(MAX_COMMAND_CONNECT_LENGTH);
258 this.buffer.put(CLIENT_VERSION);
259 this.buffer.put(COMMAND_CONNECT);
260 this.buffer.put((byte) 0);
261 if (address instanceof Inet4Address) {
262 this.buffer.put(InetAddressUtils.IPV4);
263 } else if (address instanceof Inet6Address) {
264 this.buffer.put(InetAddressUtils.IPV6);
265 } else {
266 throw new IOException("Unsupported remote address class: " + address.getClass().getName());
267 }
268 this.buffer.put(address.getAddress());
269 this.buffer.putShort((short) port);
270 this.buffer.flip();
271 }
272
273 private void setBufferLimit(final int newLimit) {
274 if (this.buffer.capacity() < newLimit) {
275 final ByteBuffer newBuffer = ByteBuffer.allocate(newLimit);
276 this.buffer.flip();
277 newBuffer.put(this.buffer);
278 this.buffer = newBuffer;
279 } else {
280 this.buffer.limit(newLimit);
281 }
282 }
283
284 private boolean writeAndPrepareRead(final ByteChannel channel, final int readSize) throws IOException {
285 if (writeBuffer(channel)) {
286 this.buffer.clear();
287 setBufferLimit(readSize);
288 return true;
289 }
290 return false;
291 }
292
293 private boolean writeBuffer(final ByteChannel channel) throws IOException {
294 if (this.buffer.hasRemaining()) {
295 channel.write(this.buffer);
296 }
297 return !this.buffer.hasRemaining();
298 }
299
300 private boolean fillBuffer(final ByteChannel channel) throws IOException {
301 if (this.buffer.hasRemaining()) {
302 channel.read(this.buffer);
303 }
304 return !this.buffer.hasRemaining();
305 }
306
307 @Override
308 public void timeout(final IOSession session, final Timeout timeout) throws IOException {
309 exception(session, SocketTimeoutExceptionFactory.create(timeout));
310 }
311
312 @Override
313 public void exception(final IOSession session, final Exception cause) {
314 session.close(CloseMode.IMMEDIATE);
315 CommandSupport.failCommands(session, cause);
316 }
317
318 @Override
319 public void disconnected(final IOSession session) {
320 CommandSupport.cancelCommands(session);
321 }
322
323 }