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