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