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