View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
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.charset.StandardCharsets;
40  
41  import org.apache.hc.core5.http.nio.command.CommandSupport;
42  import org.apache.hc.core5.io.CloseMode;
43  import org.apache.hc.core5.io.SocketTimeoutExceptionFactory;
44  import org.apache.hc.core5.net.InetAddressUtils;
45  import org.apache.hc.core5.util.Timeout;
46  
47  /**
48   * Implements the client side of SOCKS protocol version 5 as per https://tools.ietf.org/html/rfc1928. Supports SOCKS username/password
49   * authentication as per https://tools.ietf.org/html/rfc1929.
50   */
51  final class SocksProxyProtocolHandler implements IOEventHandler {
52  
53      private static final int MAX_DNS_NAME_LENGTH = 255;
54  
55      private static final int MAX_COMMAND_CONNECT_LENGTH = 6 + MAX_DNS_NAME_LENGTH + 1;
56  
57      private static final byte CLIENT_VERSION = 5;
58  
59      private static final byte NO_AUTHENTICATION_REQUIRED = 0;
60  
61      private static final byte USERNAME_PASSWORD = 2;
62  
63      private static final byte USERNAME_PASSWORD_VERSION = 1;
64  
65      private static final byte SUCCESS = 0;
66  
67      private static final byte COMMAND_CONNECT = 1;
68  
69      private static final byte ATYP_DOMAINNAME = 3;
70  
71  
72      private enum State {
73          SEND_AUTH, RECEIVE_AUTH_METHOD, SEND_USERNAME_PASSWORD, RECEIVE_AUTH, SEND_CONNECT, RECEIVE_RESPONSE_CODE, RECEIVE_ADDRESS_TYPE, RECEIVE_ADDRESS, COMPLETE
74      }
75  
76      private final InternalDataChannel dataChannel;
77      private final IOSessionRequest sessionRequest;
78      private final IOEventHandlerFactory eventHandlerFactory;
79      private final IOReactorConfig reactorConfig;
80  
81      private ByteBuffer buffer = ByteBuffer.allocate(512);
82      private State state = State.SEND_AUTH;
83      SocksProxyProtocolHandler(final InternalDataChannel dataChannel,
84                                final IOSessionRequest sessionRequest,
85                                final IOEventHandlerFactory eventHandlerFactory,
86                                final IOReactorConfig reactorConfig) {
87          this.dataChannel = dataChannel;
88          this.sessionRequest = sessionRequest;
89          this.eventHandlerFactory = eventHandlerFactory;
90          this.reactorConfig = reactorConfig;
91      }
92  
93      @Override
94      public void connected(final IOSession session) throws IOException {
95          this.buffer.put(CLIENT_VERSION);
96          if (this.reactorConfig.getSocksProxyUsername() != null && this.reactorConfig.getSocksProxyPassword() != null) {
97              this.buffer.put((byte) 2);
98              this.buffer.put(NO_AUTHENTICATION_REQUIRED);
99              this.buffer.put(USERNAME_PASSWORD);
100         } else {
101             this.buffer.put((byte) 1);
102             this.buffer.put(NO_AUTHENTICATION_REQUIRED);
103         }
104         this.buffer.flip();
105         session.setEventMask(SelectionKey.OP_WRITE);
106     }
107 
108     @Override
109     public void outputReady(final IOSession session) throws IOException {
110         switch (this.state) {
111             case SEND_AUTH:
112                 if (writeAndPrepareRead(session, 2)) {
113                     session.setEventMask(SelectionKey.OP_READ);
114                     this.state = State.RECEIVE_AUTH_METHOD;
115                 }
116                 break;
117             case SEND_USERNAME_PASSWORD:
118                 if (writeAndPrepareRead(session, 2)) {
119                     session.setEventMask(SelectionKey.OP_READ);
120                     this.state = State.RECEIVE_AUTH;
121                 }
122                 break;
123             case SEND_CONNECT:
124                 if (writeAndPrepareRead(session, 2)) {
125                     session.setEventMask(SelectionKey.OP_READ);
126                     this.state = State.RECEIVE_RESPONSE_CODE;
127                 }
128                 break;
129             case RECEIVE_AUTH_METHOD:
130             case RECEIVE_AUTH:
131             case RECEIVE_ADDRESS:
132             case RECEIVE_ADDRESS_TYPE:
133             case RECEIVE_RESPONSE_CODE:
134                 session.setEventMask(SelectionKey.OP_READ);
135                 break;
136             case COMPLETE:
137                 break;
138         }
139     }
140 
141     private byte[] cred(final String cred) throws IOException {
142         if (cred == null) {
143             return new byte[] {};
144         }
145         // These will remain with ISO-8859-1 since the RFC does not mention any string
146         // to octet encoding. So neither one is wrong or right.
147         final byte[] bytes = cred.getBytes(StandardCharsets.ISO_8859_1);
148         if (bytes.length >= 255) {
149             throw new IOException("SOCKS username / password are too long");
150         }
151         return bytes;
152     }
153 
154     @Override
155     public void inputReady(final IOSession session, final ByteBuffer src) throws IOException {
156         if (src != null) {
157             try {
158                 this.buffer.put(src);
159             } catch (final BufferOverflowException ex) {
160                 throw new IOException("Unexpected input data");
161             }
162         }
163         switch (this.state) {
164             case RECEIVE_AUTH_METHOD:
165                 if (fillBuffer(session)) {
166                     this.buffer.flip();
167                     final byte serverVersion = this.buffer.get();
168                     final byte serverMethod = this.buffer.get();
169                     if (serverVersion != CLIENT_VERSION) {
170                         throw new IOException("SOCKS server returned unsupported version: " + serverVersion);
171                     }
172                     if (serverMethod == USERNAME_PASSWORD) {
173                         this.buffer.clear();
174                         final byte[] username = cred(reactorConfig.getSocksProxyUsername());
175                         final byte[] password = cred(reactorConfig.getSocksProxyPassword());
176                         setBufferLimit(username.length + password.length + 3);
177                         this.buffer.put(USERNAME_PASSWORD_VERSION);
178                         this.buffer.put((byte) username.length);
179                         this.buffer.put(username);
180                         this.buffer.put((byte) password.length);
181                         this.buffer.put(password);
182                         this.buffer.flip();
183                         session.setEventMask(SelectionKey.OP_WRITE);
184                         this.state = State.SEND_USERNAME_PASSWORD;
185                     } else if (serverMethod == NO_AUTHENTICATION_REQUIRED) {
186                         prepareConnectCommand();
187                         session.setEventMask(SelectionKey.OP_WRITE);
188                         this.state = State.SEND_CONNECT;
189                     } else {
190                         throw new IOException("SOCKS server return unsupported authentication method: " + serverMethod);
191                     }
192                 }
193                 break;
194             case RECEIVE_AUTH:
195                 if (fillBuffer(session)) {
196                     this.buffer.flip();
197                     this.buffer.get(); // skip server auth version
198                     final byte status = this.buffer.get();
199                     if (status != SUCCESS) {
200                         throw new IOException("Authentication failed for external SOCKS proxy");
201                     }
202                     prepareConnectCommand();
203                     session.setEventMask(SelectionKey.OP_WRITE);
204                     this.state = State.SEND_CONNECT;
205                 }
206                 break;
207             case RECEIVE_RESPONSE_CODE:
208                 if (fillBuffer(session)) {
209                     this.buffer.flip();
210                     final byte serverVersion = this.buffer.get();
211                     final byte responseCode = this.buffer.get();
212                     if (serverVersion != CLIENT_VERSION) {
213                         throw new IOException("SOCKS server returned unsupported version: " + serverVersion);
214                     }
215                     switch (responseCode) {
216                         case SUCCESS:
217                             break;
218                         case 1:
219                             throw new IOException("SOCKS: General SOCKS server failure");
220                         case 2:
221                             throw new IOException("SOCKS5: Connection not allowed by ruleset");
222                         case 3:
223                             throw new IOException("SOCKS5: Network unreachable");
224                         case 4:
225                             throw new IOException("SOCKS5: Host unreachable");
226                         case 5:
227                             throw new IOException("SOCKS5: Connection refused");
228                         case 6:
229                             throw new IOException("SOCKS5: TTL expired");
230                         case 7:
231                             throw new IOException("SOCKS5: Command not supported");
232                         case 8:
233                             throw new IOException("SOCKS5: Address type not supported");
234                         default:
235                             throw new IOException("SOCKS5: Unexpected SOCKS response code " + responseCode);
236                     }
237                     this.buffer.compact();
238                     this.buffer.limit(3);
239                     this.state = State.RECEIVE_ADDRESS_TYPE;
240                     // deliberate fall-through
241                 } else {
242                     break;
243                 }
244             case RECEIVE_ADDRESS_TYPE:
245                 if (fillBuffer(session)) {
246                     this.buffer.flip();
247                     this.buffer.get(); // reserved byte that has no purpose
248                     final byte aType = this.buffer.get();
249                     final int addressSize;
250                     if (aType == InetAddressUtils.IPV4) {
251                         addressSize = 4;
252                     } else if (aType == InetAddressUtils.IPV6) {
253                         addressSize = 16;
254                     } else if (aType == ATYP_DOMAINNAME) {
255                         // mask with 0xFF to convert to unsigned byte value
256                         addressSize = this.buffer.get() & 0xFF;
257                     } else {
258                         throw new IOException("SOCKS server returned unsupported address type: " + aType);
259                     }
260                     final int remainingResponseSize = addressSize + 2;
261                     this.buffer.compact();
262                     // make sure we only read what we need to, don't read too much
263                     this.buffer.limit(remainingResponseSize);
264                     this.state = State.RECEIVE_ADDRESS;
265                     // deliberate fall-through
266                 } else {
267                     break;
268                 }
269             case RECEIVE_ADDRESS:
270                 if (fillBuffer(session)) {
271                     this.buffer.clear();
272                     this.state = State.COMPLETE;
273                     final IOEventHandler newHandler = this.eventHandlerFactory.createHandler(dataChannel, sessionRequest.attachment);
274                     dataChannel.upgrade(newHandler);
275                     sessionRequest.completed(dataChannel);
276                     dataChannel.handleIOEvent(SelectionKey.OP_CONNECT);
277                 }
278                 break;
279             case SEND_AUTH:
280             case SEND_USERNAME_PASSWORD:
281             case SEND_CONNECT:
282                 session.setEventMask(SelectionKey.OP_WRITE);
283                 break;
284             case COMPLETE:
285                 break;
286         }
287     }
288 
289     private void prepareConnectCommand() throws IOException {
290         this.buffer.clear();
291         setBufferLimit(MAX_COMMAND_CONNECT_LENGTH);
292         this.buffer.put(CLIENT_VERSION);
293         this.buffer.put(COMMAND_CONNECT);
294         this.buffer.put((byte) 0); // reserved
295         if (!(sessionRequest.remoteAddress instanceof InetSocketAddress)) {
296             throw new IOException("Unsupported address class: " + sessionRequest.remoteAddress.getClass());
297         }
298         final InetSocketAddress targetAddress = ((InetSocketAddress) sessionRequest.remoteAddress);
299         if (targetAddress.isUnresolved()) {
300             this.buffer.put(ATYP_DOMAINNAME);
301             final String hostName = targetAddress.getHostName();
302             final byte[] hostnameBytes = hostName.getBytes(StandardCharsets.US_ASCII);
303             if (hostnameBytes.length > MAX_DNS_NAME_LENGTH) {
304                 throw new IOException("Host name exceeds " + MAX_DNS_NAME_LENGTH + " bytes");
305             }
306             this.buffer.put((byte) hostnameBytes.length);
307             this.buffer.put(hostnameBytes);
308         } else {
309             final InetAddress address = targetAddress.getAddress();
310             if (address instanceof Inet4Address) {
311                 this.buffer.put(InetAddressUtils.IPV4);
312             } else if (address instanceof Inet6Address) {
313                 this.buffer.put(InetAddressUtils.IPV6);
314             } else {
315                 throw new IOException("Unsupported remote address class: " + address.getClass().getName());
316             }
317             this.buffer.put(address.getAddress());
318         }
319         final int port = targetAddress.getPort();
320         this.buffer.putShort((short) port);
321         this.buffer.flip();
322     }
323 
324     private void setBufferLimit(final int newLimit) {
325         if (this.buffer.capacity() < newLimit) {
326             final ByteBuffer newBuffer = ByteBuffer.allocate(newLimit);
327             this.buffer.flip();
328             newBuffer.put(this.buffer);
329             this.buffer = newBuffer;
330         } else {
331             this.buffer.limit(newLimit);
332         }
333     }
334 
335     private boolean writeAndPrepareRead(final ByteChannel channel, final int readSize) throws IOException {
336         if (writeBuffer(channel)) {
337             this.buffer.clear();
338             setBufferLimit(readSize);
339             return true;
340         }
341         return false;
342     }
343 
344     private boolean writeBuffer(final ByteChannel channel) throws IOException {
345         if (this.buffer.hasRemaining()) {
346             channel.write(this.buffer);
347         }
348         return !this.buffer.hasRemaining();
349     }
350 
351     private boolean fillBuffer(final ByteChannel channel) throws IOException {
352         if (this.buffer.hasRemaining()) {
353             channel.read(this.buffer);
354         }
355         return !this.buffer.hasRemaining();
356     }
357 
358     @Override
359     public void timeout(final IOSession session, final Timeout timeout) throws IOException {
360         exception(session, SocketTimeoutExceptionFactory.create(timeout));
361     }
362 
363     @Override
364     public void exception(final IOSession session, final Exception cause) {
365         try {
366             sessionRequest.failed(cause);
367         } finally {
368             session.close(CloseMode.IMMEDIATE);
369             CommandSupport.failCommands(session, cause);
370         }
371     }
372 
373     @Override
374     public void disconnected(final IOSession session) {
375         sessionRequest.cancel();
376         CommandSupport.cancelCommands(session);
377     }
378 
379 }