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, new Byte(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                         //buf.position(oldPos);
    372                         return;
    373                     }
    374                 } else {
    375                     buf.position(oldPos);
    376                     return;
    377                 }
    378             } else if (buf.get(1) != SocksProxyConstants.V5_REPLY_SUCCEEDED) {
    379                 throw new IllegalStateException("Authentication failed");
    380             }
    381
    382         } else if (step == SocksProxyConstants.SOCKS5_REQUEST_STEP) {
    383             // Send the request
    384             byte addressType = buf.get(3);
    385             len = 6;
    386             if (addressType == SocksProxyConstants.IPV6_ADDRESS_TYPE) {
    387                 len += 16;
    388             } else if (addressType == SocksProxyConstants.IPV4_ADDRESS_TYPE) {
    389                 len += 4;
    390             } else if (addressType == SocksProxyConstants.DOMAIN_NAME_ADDRESS_TYPE) {
    391                 len += 1 + (buf.get(4));
    392             } else {
    393                 throw new IllegalStateException("Unknwon address type");
    394             }
    395
    396             if (buf.remaining() >= len) {
    397                 // handle response
    398                 byte status = buf.get(1);
    399                 LOGGER.debug("  response status: {}", SocksProxyConstants.getReplyCodeAsString(status));
    400
    401                 if (status == SocksProxyConstants.V5_REPLY_SUCCEEDED) {
    402                     buf.position(buf.position() + len);
    403                     setHandshakeComplete();
    404                     return;
    405                 }
    406
    407                 throw new Exception("Proxy handshake failed - Code: 0x" + ByteUtilities.asHex(new byte[] { status }));
    408             }
    409
    410             return;
    411         }
    412
    413         if (len > 0) {
    414             buf.position(buf.position() + len);
    415         }
    416
    417         // Move to the handshaking next step if not in the middle of
    418         // the authentication process
    419         boolean isAuthenticating = false;
    420         if (step == SocksProxyConstants.SOCKS5_AUTH_STEP) {
    421             byte method = ((Byte) getSession().getAttribute(Socks5LogicHandler.SELECTED_AUTH_METHOD)).byteValue();
    422             if (method == SocksProxyConstants.GSSAPI_AUTH) {
    423                 GSSContext ctx = (GSSContext) getSession().getAttribute(GSS_CONTEXT);
    424                 if (ctx == null || !ctx.isEstablished()) {
    425                     isAuthenticating = true;
    426                 }
    427             }
    428         }
    429
    430         if (!isAuthenticating) {
    431             getSession().setAttribute(HANDSHAKE_STEP, ++step);
    432         }
    433
    434         doHandshake(nextFilter);
    435     }
    436
    437     /**
    438      * Closes the session. If any {@link GSSContext} is present in the session
    439      * then it is closed.
    440      *
    441      * @param message the error message
    442      */
    443     @Override
    444     protected void closeSession(String message) {
    445         GSSContext ctx = (GSSContext) getSession().getAttribute(GSS_CONTEXT);
    446         if (ctx != null) {
    447             try {
    448                 ctx.dispose();
    449             } catch (GSSException e) {
    450                 e.printStackTrace();
    451                 super.closeSession(message, e);
    452                 return;
    453             }
    454         }
    455         super.closeSession(message);
    456     }
    457 }