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.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.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_COMMAND_CONNECT_LENGTH = 22;
54  
55      private static final byte CLIENT_VERSION = 5;
56  
57      private static final byte NO_AUTHENTICATION_REQUIRED = 0;
58  
59      private static final byte USERNAME_PASSWORD = 2;
60  
61      private static final byte USERNAME_PASSWORD_VERSION = 1;
62  
63      private static final byte SUCCESS = 0;
64  
65      private static final byte COMMAND_CONNECT = 1;
66  
67      private static final byte ATYP_IPV4 = 1;
68  
69      private static final byte ATYP_DOMAINNAME = 3;
70  
71      private static final byte ATYP_IPV6 = 4;
72  
73      private enum State {
74          SEND_AUTH, RECEIVE_AUTH_METHOD, SEND_USERNAME_PASSWORD, RECEIVE_AUTH, SEND_CONNECT, RECEIVE_RESPONSE_CODE, RECEIVE_ADDRESS_TYPE, RECEIVE_ADDRESS, COMPLETE
75      }
76  
77      private final ProtocolIOSession ioSession;
78      private final Object attachment;
79      private final InetSocketAddress targetAddress;
80      private final String username;
81      private final String password;
82      private final IOEventHandlerFactory eventHandlerFactory;
83  
84      // a 32 byte buffer is enough for all usual SOCKS negotiations, we expand it if necessary during the processing
85      private ByteBuffer buffer = ByteBuffer.allocate(32);
86      private State state = State.SEND_AUTH;
87      private int remainingResponseSize = -1;
88  
89      SocksProxyProtocolHandler(final ProtocolIOSession ioSession, final Object attachment, final InetSocketAddress targetAddress,
90              final String username, final String password, final IOEventHandlerFactory eventHandlerFactory) {
91          this.ioSession = ioSession;
92          this.attachment = attachment;
93          this.targetAddress = targetAddress;
94          this.username = username;
95          this.password = password;
96          this.eventHandlerFactory = eventHandlerFactory;
97      }
98  
99      @Override
100     public void connected(final IOSession session) throws IOException {
101         this.buffer.put(CLIENT_VERSION);
102         this.buffer.put((byte) 1);
103         this.buffer.put(NO_AUTHENTICATION_REQUIRED);
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     @Override
142     public void inputReady(final IOSession session, final ByteBuffer src) throws IOException {
143         if (src != null) {
144             try {
145                 this.buffer.put(src);
146             } catch (final BufferOverflowException ex) {
147                 throw new IOException("Unexpected input data");
148             }
149         }
150         switch (this.state) {
151             case RECEIVE_AUTH_METHOD:
152                 if (fillBuffer(session)) {
153                     this.buffer.flip();
154                     final byte serverVersion = this.buffer.get();
155                     final byte serverMethod = this.buffer.get();
156                     if (serverVersion != CLIENT_VERSION) {
157                         throw new IOException("SOCKS server returned unsupported version: " + serverVersion);
158                     }
159                     if (serverMethod == USERNAME_PASSWORD) {
160                         this.buffer.clear();
161                         setBufferLimit(this.username.length() + this.password.length() + 3);
162                         this.buffer.put(USERNAME_PASSWORD_VERSION);
163                         this.buffer.put((byte) this.username.length());
164                         this.buffer.put(this.username.getBytes(StandardCharsets.ISO_8859_1));
165                         this.buffer.put((byte) this.password.length());
166                         this.buffer.put(this.password.getBytes(StandardCharsets.ISO_8859_1));
167                         session.setEventMask(SelectionKey.OP_WRITE);
168                         this.state = State.SEND_USERNAME_PASSWORD;
169                     } else if (serverMethod == NO_AUTHENTICATION_REQUIRED) {
170                         prepareConnectCommand();
171                         session.setEventMask(SelectionKey.OP_WRITE);
172                         this.state = State.SEND_CONNECT;
173                     } else {
174                         throw new IOException("SOCKS server return unsupported authentication method: " + serverMethod);
175                     }
176                 }
177                 break;
178             case RECEIVE_AUTH:
179                 if (fillBuffer(session)) {
180                     this.buffer.flip();
181                     this.buffer.get(); // skip server auth version
182                     final byte status = this.buffer.get();
183                     if (status != SUCCESS) {
184                         throw new IOException("Authentication failed for external SOCKS proxy");
185                     }
186                     prepareConnectCommand();
187                     session.setEventMask(SelectionKey.OP_WRITE);
188                     this.state = State.SEND_CONNECT;
189                 }
190                 break;
191             case RECEIVE_RESPONSE_CODE:
192                 if (fillBuffer(session)) {
193                     this.buffer.flip();
194                     final byte serverVersion = this.buffer.get();
195                     final byte responseCode = this.buffer.get();
196                     if (serverVersion != CLIENT_VERSION) {
197                         throw new IOException("SOCKS server returned unsupported version: " + serverVersion);
198                     }
199                     if (responseCode != SUCCESS) {
200                         throw new IOException("SOCKS server was unable to establish connection returned error code: " + responseCode);
201                     }
202                     this.buffer.compact();
203                     this.buffer.limit(3);
204                     this.state = State.RECEIVE_ADDRESS_TYPE;
205                     // deliberate fall-through
206                 } else {
207                     break;
208                 }
209             case RECEIVE_ADDRESS_TYPE:
210                 if (fillBuffer(session)) {
211                     this.buffer.flip();
212                     this.buffer.get(); // reserved byte that has no purpose
213                     final byte aType = this.buffer.get();
214                     final int addressSize;
215                     if (aType == ATYP_IPV4) {
216                         addressSize = 4;
217                     } else if (aType == ATYP_IPV6) {
218                         addressSize = 16;
219                     } else if (aType == ATYP_DOMAINNAME) {
220                         // mask with 0xFF to convert to unsigned byte value
221                         addressSize = this.buffer.get() & 0xFF;
222                     } else {
223                         throw new IOException("SOCKS server returned unsupported address type: " + aType);
224                     }
225                     this.remainingResponseSize = addressSize + 2;
226                     this.buffer.compact();
227                     // make sure we only read what we need to, don't read too much
228                     this.buffer.limit(this.remainingResponseSize);
229                     this.state = State.RECEIVE_ADDRESS;
230                     // deliberate fall-through
231                 } else {
232                     break;
233                 }
234             case RECEIVE_ADDRESS:
235                 if (fillBuffer(session)) {
236                     this.buffer.clear();
237                     this.state = State.COMPLETE;
238                     final IOEventHandler newHandler = this.eventHandlerFactory.createHandler(this.ioSession, this.attachment);
239                     this.ioSession.upgrade(newHandler);
240                     newHandler.connected(this.ioSession);
241                 }
242                 break;
243             case SEND_AUTH:
244             case SEND_USERNAME_PASSWORD:
245             case SEND_CONNECT:
246                 session.setEventMask(SelectionKey.OP_WRITE);
247                 break;
248             case COMPLETE:
249                 break;
250         }
251     }
252 
253     private void prepareConnectCommand() throws IOException {
254         final InetAddress address = this.targetAddress.getAddress();
255         final int port = this.targetAddress.getPort();
256         if (address == null || port == 0) {
257             throw new UnresolvedAddressException();
258         }
259 
260         this.buffer.clear();
261         setBufferLimit(MAX_COMMAND_CONNECT_LENGTH);
262         this.buffer.put(CLIENT_VERSION);
263         this.buffer.put(COMMAND_CONNECT);
264         this.buffer.put((byte) 0); // reserved
265         if (address instanceof Inet4Address) {
266             this.buffer.put(ATYP_IPV4);
267             this.buffer.put(address.getAddress());
268         } else if (address instanceof Inet6Address) {
269             this.buffer.put(ATYP_IPV6);
270             this.buffer.put(address.getAddress());
271         } else {
272             throw new IOException("Unsupported remote address class: " + address.getClass().getName());
273         }
274         this.buffer.putShort((short) port);
275         this.buffer.flip();
276     }
277 
278     private void setBufferLimit(final int newLimit) {
279         if (this.buffer.capacity() < newLimit) {
280             final ByteBuffer newBuffer = ByteBuffer.allocate(newLimit);
281             this.buffer.flip();
282             newBuffer.put(this.buffer);
283             this.buffer = newBuffer;
284         } else {
285             this.buffer.limit(newLimit);
286         }
287     }
288 
289     private boolean writeAndPrepareRead(final ByteChannel channel, final int readSize) throws IOException {
290         if (writeBuffer(channel)) {
291             this.buffer.clear();
292             setBufferLimit(readSize);
293             return true;
294         }
295         return false;
296     }
297 
298     private boolean writeBuffer(final ByteChannel channel) throws IOException {
299         if (this.buffer.hasRemaining()) {
300             channel.write(this.buffer);
301         }
302         return !this.buffer.hasRemaining();
303     }
304 
305     private boolean fillBuffer(final ByteChannel channel) throws IOException {
306         if (this.buffer.hasRemaining()) {
307             channel.read(this.buffer);
308         }
309         return !this.buffer.hasRemaining();
310     }
311 
312     @Override
313     public void timeout(final IOSession session, final Timeout timeout) throws IOException {
314         exception(session, SocketTimeoutExceptionFactory.create(timeout));
315     }
316 
317     @Override
318     public void exception(final IOSession session, final Exception cause) {
319         session.close(CloseMode.IMMEDIATE);
320         CommandSupport.failCommands(session, cause);
321     }
322 
323     @Override
324     public void disconnected(final IOSession session) {
325         CommandSupport.cancelCommands(session);
326     }
327 
328 }