View Javadoc

1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.mina.proxy.handlers.socks;
21  
22  import java.io.UnsupportedEncodingException;
23  import java.net.Inet4Address;
24  import java.net.Inet6Address;
25  import java.net.InetSocketAddress;
26  
27  import org.apache.mina.core.buffer.IoBuffer;
28  import org.apache.mina.core.filterchain.IoFilter.NextFilter;
29  import org.apache.mina.proxy.session.ProxyIoSession;
30  import org.apache.mina.proxy.utils.ByteUtilities;
31  import org.ietf.jgss.GSSContext;
32  import org.ietf.jgss.GSSException;
33  import org.ietf.jgss.GSSManager;
34  import org.ietf.jgss.GSSName;
35  import org.ietf.jgss.Oid;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  /**
40   * Socks5LogicHandler.java - SOCKS5 authentication mechanisms logic handler.
41   * 
42   * @author The Apache MINA Project (dev@mina.apache.org)
43   * @since MINA 2.0.0-M3
44   */
45  public class Socks5LogicHandler extends AbstractSocksLogicHandler {
46  
47      private final static Logger LOGGER = LoggerFactory
48              .getLogger(Socks5LogicHandler.class);
49  
50      /**
51       * The selected authentication method attribute key.
52       */
53      private final static String SELECTED_AUTH_METHOD = Socks5LogicHandler.class
54              .getName()
55              + ".SelectedAuthMethod";
56  
57      /**
58       * The current step in the handshake attribute key.
59       */
60      private final static String HANDSHAKE_STEP = Socks5LogicHandler.class
61              .getName()
62              + ".HandshakeStep";
63  
64      /**
65       * The Java GSS-API context attribute key.
66       */
67      private final static String GSS_CONTEXT = Socks5LogicHandler.class
68              .getName()
69              + ".GSSContext";
70  
71      /**
72       * Last GSS token received attribute key.
73       */
74      private final static String GSS_TOKEN = Socks5LogicHandler.class.getName()
75              + ".GSSToken";
76  
77      /**
78       * {@inheritDoc}
79       */
80      public Socks5LogicHandler(final ProxyIoSession proxyIoSession) {
81          super(proxyIoSession);
82          getSession().setAttribute(HANDSHAKE_STEP,
83                  SocksProxyConstants.SOCKS5_GREETING_STEP);
84      }
85  
86      /**
87       * Performs the handshake process.
88       * 
89       * @param nextFilter the next filter
90       */
91      public synchronized void doHandshake(final NextFilter nextFilter) {
92          LOGGER.debug(" doHandshake()");
93  
94          // Send request
95          writeRequest(nextFilter, request, ((Integer) getSession().getAttribute(
96                  HANDSHAKE_STEP)).intValue());
97      }
98  
99      /**
100      * Encodes the initial greeting packet.
101      * 
102      * @param request the socks proxy request data
103      * @return the encoded buffer
104      */
105     private IoBuffer encodeInitialGreetingPacket(final SocksProxyRequest request) {
106         byte nbMethods = (byte) SocksProxyConstants.SUPPORTED_AUTH_METHODS.length;
107         IoBuffer buf = IoBuffer.allocate(2 + nbMethods);
108 
109         buf.put(request.getProtocolVersion());
110         buf.put(nbMethods);
111         buf.put(SocksProxyConstants.SUPPORTED_AUTH_METHODS);
112 
113         return buf;
114     }
115 
116     /**
117      * Encodes the proxy authorization request packet.
118      * 
119      * @param request the socks proxy request data
120      * @return the encoded buffer
121      * @throws UnsupportedEncodingException if request's hostname charset 
122      * can't be converted to ASCII. 
123      */
124     private IoBuffer encodeProxyRequestPacket(final SocksProxyRequest request)
125             throws UnsupportedEncodingException {
126         int len = 6;
127         InetSocketAddress adr = request.getEndpointAddress();
128         byte addressType = 0;
129         byte[] host = null;
130         
131         if (adr != null && !adr.isUnresolved()) {
132             if (adr.getAddress() instanceof Inet6Address) {
133                 len += 16;
134                 addressType = SocksProxyConstants.IPV6_ADDRESS_TYPE;
135             } else if (adr.getAddress() instanceof Inet4Address) {
136                 len += 4;
137                 addressType = SocksProxyConstants.IPV4_ADDRESS_TYPE;
138             }
139         } else {
140             host = request.getHost() != null ? 
141                     request.getHost().getBytes("ASCII") : null;
142 
143             if (host != null) {
144                 len += 1 + host.length;
145                 addressType = SocksProxyConstants.DOMAIN_NAME_ADDRESS_TYPE;
146             } else {
147                 throw new IllegalArgumentException("SocksProxyRequest object " +
148                         "has no suitable endpoint information");
149             }
150         }
151         
152         IoBuffer buf = IoBuffer.allocate(len);
153 
154         buf.put(request.getProtocolVersion());
155         buf.put(request.getCommandCode());
156         buf.put((byte) 0x00); // Reserved
157         buf.put(addressType);
158 
159         if (host == null) {
160             buf.put(request.getIpAddress());
161         } else {
162             buf.put((byte) host.length);
163             buf.put(host);            
164         }
165 
166         buf.put(request.getPort());
167 
168         return buf;
169     }
170 
171     /**
172      * Encodes the authentication packet for supported authentication methods.
173      * 
174      * @param request the socks proxy request data
175      * @return the encoded buffer, if null then authentication step is over 
176      * and handshake process can jump immediately to the next step without waiting
177      * for a server reply.
178      * @throws UnsupportedEncodingException if some string charset convertion fails
179      * @throws GSSException when something fails while using GSSAPI
180      */
181     private IoBuffer encodeAuthenticationPacket(final SocksProxyRequest request)
182             throws UnsupportedEncodingException, GSSException {
183         byte method = ((Byte) getSession().getAttribute(
184                 Socks5LogicHandler.SELECTED_AUTH_METHOD)).byteValue();
185 
186         switch (method) {
187             case SocksProxyConstants.NO_AUTH:
188                 // In this case authentication is immediately considered as successfull
189                 // Next writeRequest() call will send the proxy request
190                 getSession().setAttribute(HANDSHAKE_STEP,
191                         SocksProxyConstants.SOCKS5_REQUEST_STEP);
192                 break;
193     
194             case SocksProxyConstants.GSSAPI_AUTH:
195                 return encodeGSSAPIAuthenticationPacket(request);
196     
197             case SocksProxyConstants.BASIC_AUTH:
198                 // The basic auth scheme packet is sent
199                 byte[] user = request.getUserName().getBytes("ASCII");
200                 byte[] pwd = request.getPassword().getBytes("ASCII");
201                 IoBuffer buf = IoBuffer.allocate(3 + user.length + pwd.length);
202     
203                 buf.put(SocksProxyConstants.BASIC_AUTH_SUBNEGOTIATION_VERSION);
204                 buf.put((byte) user.length);
205                 buf.put(user);
206                 buf.put((byte) pwd.length);
207                 buf.put(pwd);
208     
209                 return buf;
210         }
211 
212         return null;
213     }
214 
215     /**
216      * Encodes the authentication packet for supported authentication methods.
217      * 
218      * @param request the socks proxy request data
219      * @return the encoded buffer
220      * @throws GSSException when something fails while using GSSAPI
221      */
222     private IoBuffer encodeGSSAPIAuthenticationPacket(
223             final SocksProxyRequest request) throws GSSException {
224         GSSContext ctx = (GSSContext) getSession().getAttribute(GSS_CONTEXT);
225         if (ctx == null) {
226             // first step in the authentication process
227             GSSManager manager = GSSManager.getInstance();
228             GSSName serverName = manager.createName(request
229                     .getServiceKerberosName(), null);
230             Oid krb5OID = new Oid(SocksProxyConstants.KERBEROS_V5_OID);
231 
232             if (LOGGER.isDebugEnabled()) {
233                 LOGGER.debug("Available mechs:");
234                 for (Oid o : manager.getMechs()) {
235                     if (o.equals(krb5OID)) {
236                         LOGGER.debug("Found Kerberos V OID available");
237                     }
238                     LOGGER.debug("{} with oid = {}",
239                             manager.getNamesForMech(o), o);
240                 }
241             }
242 
243             ctx = manager.createContext(serverName, krb5OID, null,
244                     GSSContext.DEFAULT_LIFETIME);
245 
246             ctx.requestMutualAuth(true); // Mutual authentication
247             ctx.requestConf(false);
248             ctx.requestInteg(false);
249 
250             getSession().setAttribute(GSS_CONTEXT, ctx);
251         }
252 
253         byte[] token = (byte[]) getSession().getAttribute(GSS_TOKEN);
254         if (token != null) {
255             LOGGER.debug("  Received Token[{}] = {}", token.length,
256                     ByteUtilities.asHex(token));
257         }
258         IoBuffer buf = null;
259 
260         if (!ctx.isEstablished()) {
261             // token is ignored on the first call
262             if (token == null) {
263                 token = new byte[32];
264             }
265 
266             token = ctx.initSecContext(token, 0, token.length);
267 
268             // Send a token to the server if one was generated by
269             // initSecContext
270             if (token != null) {
271                 LOGGER.debug("  Sending Token[{}] = {}", token.length,
272                         ByteUtilities.asHex(token));
273 
274                 getSession().setAttribute(GSS_TOKEN, token);
275                 buf = IoBuffer.allocate(4 + token.length);
276                 buf.put(new byte[] {
277                         SocksProxyConstants.GSSAPI_AUTH_SUBNEGOTIATION_VERSION,
278                         SocksProxyConstants.GSSAPI_MSG_TYPE });
279 
280                 buf.put(ByteUtilities.intToNetworkByteOrder(token.length, 2));
281                 buf.put(token);
282             }
283         }
284 
285         return buf;
286     }
287 
288     /**
289      * Encodes a SOCKS5 request and writes it to the next filter
290      * so it can be sent to the proxy server.
291      * 
292      * @param nextFilter the next filter
293      * @param request the request to send.
294      * @param step the current step in the handshake process
295      */
296     private void writeRequest(final NextFilter nextFilter,
297             final SocksProxyRequest request, int step) {
298         try {
299             IoBuffer buf = null;
300 
301             if (step == SocksProxyConstants.SOCKS5_GREETING_STEP) {
302                 buf = encodeInitialGreetingPacket(request);
303             } else if (step == SocksProxyConstants.SOCKS5_AUTH_STEP) {
304                 // This step can happen multiple times like in GSSAPI auth for instance
305                 buf = encodeAuthenticationPacket(request);
306                 // If buf is null then go to the next step
307                 if (buf == null) {
308                     step = SocksProxyConstants.SOCKS5_REQUEST_STEP;
309                 }
310             }
311 
312             if (step == SocksProxyConstants.SOCKS5_REQUEST_STEP) {
313                 buf = encodeProxyRequestPacket(request);
314             }
315 
316             buf.flip();
317             writeData(nextFilter, buf);
318 
319         } catch (Exception ex) {
320             closeSession("Unable to send Socks request: ", ex);
321         }
322     }
323 
324     /**
325      * Handles incoming data during the handshake process. Should consume only the
326      * handshake data from the buffer, leaving any extra data in place.
327      * 
328      * @param nextFilter the next filter
329      * @param buf the buffered data received 
330      */
331     public synchronized void messageReceived(final NextFilter nextFilter,
332             final IoBuffer buf) {
333         try {
334             int step = ((Integer) getSession().getAttribute(HANDSHAKE_STEP))
335                     .intValue();
336 
337             if (step == SocksProxyConstants.SOCKS5_GREETING_STEP
338                     && buf.get(0) != SocksProxyConstants.SOCKS_VERSION_5) {
339                 throw new IllegalStateException(
340                         "Wrong socks version running on server");
341             }
342 
343             if ((step == SocksProxyConstants.SOCKS5_GREETING_STEP || 
344                     step == SocksProxyConstants.SOCKS5_AUTH_STEP)
345                     && buf.remaining() >= 2) {
346                 handleResponse(nextFilter, buf, step);
347             } else if (step == SocksProxyConstants.SOCKS5_REQUEST_STEP
348                     && buf.remaining() >= 5) {
349                 handleResponse(nextFilter, buf, step);
350             }
351         } catch (Exception ex) {
352             closeSession("Proxy handshake failed: ", ex);
353         }
354     }
355 
356     /**
357      * Handle a SOCKS v5 response from the proxy server.
358      * 
359      * @param nextFilter the next filter
360      * @param buf the buffered data received 
361      * @param step the current step in the authentication process     
362      */
363     protected void handleResponse(final NextFilter nextFilter,
364             final IoBuffer buf, int step) throws Exception {
365         int len = 2;
366         if (step == SocksProxyConstants.SOCKS5_GREETING_STEP) {
367             // Send greeting message
368             byte method = buf.get(1);
369 
370             if (method == SocksProxyConstants.NO_ACCEPTABLE_AUTH_METHOD) {
371                 throw new IllegalStateException(
372                         "No acceptable authentication method to use with " +
373                         "the socks proxy server");
374             }
375 
376             getSession().setAttribute(SELECTED_AUTH_METHOD, new Byte(method));
377 
378         } else if (step == SocksProxyConstants.SOCKS5_AUTH_STEP) {
379             // Authentication to the SOCKS server 
380             byte method = ((Byte) getSession().getAttribute(
381                     Socks5LogicHandler.SELECTED_AUTH_METHOD)).byteValue();
382 
383             if (method == SocksProxyConstants.GSSAPI_AUTH) {
384                 int oldPos = buf.position();
385 
386                 if (buf.get(0) != 0x01) {
387                     throw new IllegalStateException("Authentication failed");
388                 }
389                 if (buf.get(1) == 0xFF) {
390                     throw new IllegalStateException(
391                             "Authentication failed: GSS API Security Context Failure");
392                 }
393 
394                 if (buf.remaining() >= 2) {
395                     byte[] size = new byte[2];
396                     buf.get(size);
397                     int s = ByteUtilities.makeIntFromByte2(size);
398                     if (buf.remaining() >= s) {
399                         byte[] token = new byte[s];
400                         buf.get(token);
401                         getSession().setAttribute(GSS_TOKEN, token);
402                         len = 0;
403                     } else {
404                         //buf.position(oldPos);
405                         return;
406                     }
407                 } else {
408                     buf.position(oldPos);
409                     return;
410                 }
411             } else if (buf.get(1) != SocksProxyConstants.V5_REPLY_SUCCEEDED) {
412                 throw new IllegalStateException("Authentication failed");
413             }
414 
415         } else if (step == SocksProxyConstants.SOCKS5_REQUEST_STEP) {
416             // Send the request
417             byte addressType = buf.get(3);
418             len = 6;
419             if (addressType == SocksProxyConstants.IPV6_ADDRESS_TYPE) {
420                 len += 16;
421             } else if (addressType == SocksProxyConstants.IPV4_ADDRESS_TYPE) {
422                 len += 4;
423             } else if (addressType == SocksProxyConstants.DOMAIN_NAME_ADDRESS_TYPE) {
424                 len += 1 + (buf.get(4));
425             } else {
426                 throw new IllegalStateException("Unknwon address type");
427             }
428 
429             if (buf.remaining() >= len) {
430                 // handle response
431                 byte status = buf.get(1);
432                 LOGGER.debug("  response status: {}", SocksProxyConstants
433                         .getReplyCodeAsString(status));
434 
435                 if (status == SocksProxyConstants.V5_REPLY_SUCCEEDED) {
436                     buf.position(buf.position() + len);
437                     setHandshakeComplete();
438                     return;
439                 }
440 
441                 throw new Exception("Proxy handshake failed - Code: 0x"
442                             + ByteUtilities.asHex(new byte[] { status }));
443             }
444 
445             return;
446         }
447 
448         if (len > 0) {
449             buf.position(buf.position() + len);
450         }
451 
452         // Move to the handshaking next step if not in the middle of
453         // the authentication process
454         boolean isAuthenticating = false;
455         if (step == SocksProxyConstants.SOCKS5_AUTH_STEP) {
456             byte method = ((Byte) getSession().getAttribute(
457                     Socks5LogicHandler.SELECTED_AUTH_METHOD)).byteValue();
458             if (method == SocksProxyConstants.GSSAPI_AUTH) {
459                 GSSContext ctx = (GSSContext) getSession().getAttribute(
460                         GSS_CONTEXT);
461                 if (ctx == null || !ctx.isEstablished()) {
462                     isAuthenticating = true;
463                 }
464             }
465         }
466 
467         if (!isAuthenticating) {
468             getSession().setAttribute(HANDSHAKE_STEP, ++step);
469         }
470 
471         doHandshake(nextFilter);
472     }
473 
474     /**
475      * Closes the session. If any {@link GSSContext} is present in the session 
476      * then it is closed.
477      * 
478      * @param message the error message
479      */    
480     @Override
481     protected void closeSession(String message) {
482         GSSContext ctx = (GSSContext) getSession().getAttribute(GSS_CONTEXT);
483         if (ctx != null) {
484             try {
485                 ctx.dispose();
486             } catch (GSSException e) {
487                 e.printStackTrace();
488                 super.closeSession(message, e);
489                 return;
490             }
491         }
492         super.closeSession(message);
493     }
494 }