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