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