001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.proxy.handlers.socks;
021
022import java.io.UnsupportedEncodingException;
023import java.net.Inet4Address;
024import java.net.Inet6Address;
025import java.net.InetSocketAddress;
026
027import org.apache.mina.core.buffer.IoBuffer;
028import org.apache.mina.core.filterchain.IoFilter.NextFilter;
029import org.apache.mina.proxy.session.ProxyIoSession;
030import org.apache.mina.proxy.utils.ByteUtilities;
031import org.ietf.jgss.GSSContext;
032import org.ietf.jgss.GSSException;
033import org.ietf.jgss.GSSManager;
034import org.ietf.jgss.GSSName;
035import org.ietf.jgss.Oid;
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039/**
040 * Socks5LogicHandler.java - SOCKS5 authentication mechanisms logic handler.
041 * 
042 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
043 * @since MINA 2.0.0-M3
044 */
045public class Socks5LogicHandler extends AbstractSocksLogicHandler {
046
047    private final static Logger LOGGER = LoggerFactory.getLogger(Socks5LogicHandler.class);
048
049    /**
050     * The selected authentication method attribute key.
051     */
052    private final static String SELECTED_AUTH_METHOD = Socks5LogicHandler.class.getName() + ".SelectedAuthMethod";
053
054    /**
055     * The current step in the handshake attribute key.
056     */
057    private final static String HANDSHAKE_STEP = Socks5LogicHandler.class.getName() + ".HandshakeStep";
058
059    /**
060     * The Java GSS-API context attribute key.
061     */
062    private final static String GSS_CONTEXT = Socks5LogicHandler.class.getName() + ".GSSContext";
063
064    /**
065     * Last GSS token received attribute key.
066     */
067    private final static String GSS_TOKEN = Socks5LogicHandler.class.getName() + ".GSSToken";
068
069    /**
070     * @see AbstractSocksLogicHandler#AbstractSocksLogicHandler(ProxyIoSession)
071     * 
072     * @param proxyIoSession The original session
073     */
074    public Socks5LogicHandler(final ProxyIoSession proxyIoSession) {
075        super(proxyIoSession);
076        getSession().setAttribute(HANDSHAKE_STEP, SocksProxyConstants.SOCKS5_GREETING_STEP);
077    }
078
079    /**
080     * Performs the handshake process.
081     * 
082     * @param nextFilter the next filter
083     */
084    public synchronized void doHandshake(final NextFilter nextFilter) {
085        LOGGER.debug(" doHandshake()");
086
087        // Send request
088        writeRequest(nextFilter, request, ((Integer) getSession().getAttribute(HANDSHAKE_STEP)).intValue());
089    }
090
091    /**
092     * Encodes the initial greeting packet.
093     * 
094     * @param request the socks proxy request data
095     * @return the encoded buffer
096     */
097    private IoBuffer encodeInitialGreetingPacket(final SocksProxyRequest request) {
098        byte nbMethods = (byte) SocksProxyConstants.SUPPORTED_AUTH_METHODS.length;
099        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}