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