/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ package org.apache.mina.proxy.handlers.socks; import java.io.UnsupportedEncodingException; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetSocketAddress; import org.apache.mina.core.buffer.IoBuffer; import org.apache.mina.core.filterchain.IoFilter.NextFilter; import org.apache.mina.proxy.session.ProxyIoSession; import org.apache.mina.proxy.utils.ByteUtilities; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; import org.ietf.jgss.Oid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Socks5LogicHandler.java - SOCKS5 authentication mechanisms logic handler. * * @author <a href="http://mina.apache.org">Apache MINA Project</a> * @since MINA 2.0.0-M3 */ public class Socks5LogicHandler extends AbstractSocksLogicHandler { private final static Logger LOGGER = LoggerFactory.getLogger(Socks5LogicHandler.class); /** * The selected authentication method attribute key. */ private final static String SELECTED_AUTH_METHOD = Socks5LogicHandler.class.getName() + ".SelectedAuthMethod"; /** * The current step in the handshake attribute key. */ private final static String HANDSHAKE_STEP = Socks5LogicHandler.class.getName() + ".HandshakeStep"; /** * The Java GSS-API context attribute key. */ private final static String GSS_CONTEXT = Socks5LogicHandler.class.getName() + ".GSSContext"; /** * Last GSS token received attribute key. */ private final static String GSS_TOKEN = Socks5LogicHandler.class.getName() + ".GSSToken"; /** * @see AbstractSocksLogicHandler#AbstractSocksLogicHandler(ProxyIoSession) */ public Socks5LogicHandler(final ProxyIoSession proxyIoSession) { super(proxyIoSession); getSession().setAttribute(HANDSHAKE_STEP, SocksProxyConstants.SOCKS5_GREETING_STEP); } /** * Performs the handshake process. * * @param nextFilter the next filter */ public synchronized void doHandshake(final NextFilter nextFilter) { LOGGER.debug(" doHandshake()"); // Send request writeRequest(nextFilter, request, ((Integer) getSession().getAttribute(HANDSHAKE_STEP)).intValue()); } /** * Encodes the initial greeting packet. * * @param request the socks proxy request data * @return the encoded buffer */ private IoBuffer encodeInitialGreetingPacket(final SocksProxyRequest request) { byte nbMethods = (byte) SocksProxyConstants.SUPPORTED_AUTH_METHODS.length; IoBuffer buf = IoBuffer.allocate(2 + nbMethods); buf.put(request.getProtocolVersion()); buf.put(nbMethods); buf.put(SocksProxyConstants.SUPPORTED_AUTH_METHODS); return buf; } /** * Encodes the proxy authorization request packet. * * @param request the socks proxy request data * @return the encoded buffer * @throws UnsupportedEncodingException if request's hostname charset * can't be converted to ASCII. */ private IoBuffer encodeProxyRequestPacket(final SocksProxyRequest request) throws UnsupportedEncodingException { int len = 6; InetSocketAddress adr = request.getEndpointAddress(); byte addressType = 0; byte[] host = null; if (adr != null && !adr.isUnresolved()) { if (adr.getAddress() instanceof Inet6Address) { len += 16; addressType = SocksProxyConstants.IPV6_ADDRESS_TYPE; } else if (adr.getAddress() instanceof Inet4Address) { len += 4; addressType = SocksProxyConstants.IPV4_ADDRESS_TYPE; } } else { host = request.getHost() != null ? request.getHost().getBytes("ASCII") : null; if (host != null) { len += 1 + host.length; addressType = SocksProxyConstants.DOMAIN_NAME_ADDRESS_TYPE; } else { throw new IllegalArgumentException("SocksProxyRequest object " + "has no suitable endpoint information"); } } IoBuffer buf = IoBuffer.allocate(len); buf.put(request.getProtocolVersion()); buf.put(request.getCommandCode()); buf.put((byte) 0x00); // Reserved buf.put(addressType); if (host == null) { buf.put(request.getIpAddress()); } else { buf.put((byte) host.length); buf.put(host); } buf.put(request.getPort()); return buf; } /** * Encodes the authentication packet for supported authentication methods. * * @param request the socks proxy request data * @return the encoded buffer, if null then authentication step is over * and handshake process can jump immediately to the next step without waiting * for a server reply. * @throws UnsupportedEncodingException if some string charset convertion fails * @throws GSSException when something fails while using GSSAPI */ private IoBuffer encodeAuthenticationPacket(final SocksProxyRequest request) throws UnsupportedEncodingException, GSSException { byte method = ((Byte) getSession().getAttribute(Socks5LogicHandler.SELECTED_AUTH_METHOD)).byteValue(); switch (method) { case SocksProxyConstants.NO_AUTH: // In this case authentication is immediately considered as successfull // Next writeRequest() call will send the proxy request getSession().setAttribute(HANDSHAKE_STEP, SocksProxyConstants.SOCKS5_REQUEST_STEP); break; case SocksProxyConstants.GSSAPI_AUTH: return encodeGSSAPIAuthenticationPacket(request); case SocksProxyConstants.BASIC_AUTH: // The basic auth scheme packet is sent byte[] user = request.getUserName().getBytes("ASCII"); byte[] pwd = request.getPassword().getBytes("ASCII"); IoBuffer buf = IoBuffer.allocate(3 + user.length + pwd.length); buf.put(SocksProxyConstants.BASIC_AUTH_SUBNEGOTIATION_VERSION); buf.put((byte) user.length); buf.put(user); buf.put((byte) pwd.length); buf.put(pwd); return buf; } return null; } /** * Encodes the authentication packet for supported authentication methods. * * @param request the socks proxy request data * @return the encoded buffer * @throws GSSException when something fails while using GSSAPI */ private IoBuffer encodeGSSAPIAuthenticationPacket(final SocksProxyRequest request) throws GSSException { GSSContext ctx = (GSSContext) getSession().getAttribute(GSS_CONTEXT); if (ctx == null) { // first step in the authentication process GSSManager manager = GSSManager.getInstance(); GSSName serverName = manager.createName(request.getServiceKerberosName(), null); Oid krb5OID = new Oid(SocksProxyConstants.KERBEROS_V5_OID); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Available mechs:"); for (Oid o : manager.getMechs()) { if (o.equals(krb5OID)) { LOGGER.debug("Found Kerberos V OID available"); } LOGGER.debug("{} with oid = {}", manager.getNamesForMech(o), o); } } ctx = manager.createContext(serverName, krb5OID, null, GSSContext.DEFAULT_LIFETIME); ctx.requestMutualAuth(true); // Mutual authentication ctx.requestConf(false); ctx.requestInteg(false); getSession().setAttribute(GSS_CONTEXT, ctx); } byte[] token = (byte[]) getSession().getAttribute(GSS_TOKEN); if (token != null) { LOGGER.debug(" Received Token[{}] = {}", token.length, ByteUtilities.asHex(token)); } IoBuffer buf = null; if (!ctx.isEstablished()) { // token is ignored on the first call if (token == null) { token = new byte[32]; } token = ctx.initSecContext(token, 0, token.length); // Send a token to the server if one was generated by // initSecContext if (token != null) { LOGGER.debug(" Sending Token[{}] = {}", token.length, ByteUtilities.asHex(token)); getSession().setAttribute(GSS_TOKEN, token); buf = IoBuffer.allocate(4 + token.length); buf.put(new byte[] { SocksProxyConstants.GSSAPI_AUTH_SUBNEGOTIATION_VERSION, SocksProxyConstants.GSSAPI_MSG_TYPE }); buf.put(ByteUtilities.intToNetworkByteOrder(token.length, 2)); buf.put(token); } } return buf; } /** * Encodes a SOCKS5 request and writes it to the next filter * so it can be sent to the proxy server. * * @param nextFilter the next filter * @param request the request to send. * @param step the current step in the handshake process */ private void writeRequest(final NextFilter nextFilter, final SocksProxyRequest request, int step) { try { IoBuffer buf = null; if (step == SocksProxyConstants.SOCKS5_GREETING_STEP) { buf = encodeInitialGreetingPacket(request); } else if (step == SocksProxyConstants.SOCKS5_AUTH_STEP) { // This step can happen multiple times like in GSSAPI auth for instance buf = encodeAuthenticationPacket(request); // If buf is null then go to the next step if (buf == null) { step = SocksProxyConstants.SOCKS5_REQUEST_STEP; } } if (step == SocksProxyConstants.SOCKS5_REQUEST_STEP) { buf = encodeProxyRequestPacket(request); } buf.flip(); writeData(nextFilter, buf); } catch (Exception ex) { closeSession("Unable to send Socks request: ", ex); } } /** * Handles incoming data during the handshake process. Should consume only the * handshake data from the buffer, leaving any extra data in place. * * @param nextFilter the next filter * @param buf the buffered data received */ public synchronized void messageReceived(final NextFilter nextFilter, final IoBuffer buf) { try { int step = ((Integer) getSession().getAttribute(HANDSHAKE_STEP)).intValue(); if (step == SocksProxyConstants.SOCKS5_GREETING_STEP && buf.get(0) != SocksProxyConstants.SOCKS_VERSION_5) { throw new IllegalStateException("Wrong socks version running on server"); } if ((step == SocksProxyConstants.SOCKS5_GREETING_STEP || step == SocksProxyConstants.SOCKS5_AUTH_STEP) && buf.remaining() >= 2) { handleResponse(nextFilter, buf, step); } else if (step == SocksProxyConstants.SOCKS5_REQUEST_STEP && buf.remaining() >= 5) { handleResponse(nextFilter, buf, step); } } catch (Exception ex) { closeSession("Proxy handshake failed: ", ex); } } /** * Handle a SOCKS v5 response from the proxy server. * * @param nextFilter the next filter * @param buf the buffered data received * @param step the current step in the authentication process */ protected void handleResponse(final NextFilter nextFilter, final IoBuffer buf, int step) throws Exception { int len = 2; if (step == SocksProxyConstants.SOCKS5_GREETING_STEP) { // Send greeting message byte method = buf.get(1); if (method == SocksProxyConstants.NO_ACCEPTABLE_AUTH_METHOD) { throw new IllegalStateException("No acceptable authentication method to use with " + "the socks proxy server"); } getSession().setAttribute(SELECTED_AUTH_METHOD, Byte.valueOf(method)); } else if (step == SocksProxyConstants.SOCKS5_AUTH_STEP) { // Authentication to the SOCKS server byte method = ((Byte) getSession().getAttribute(Socks5LogicHandler.SELECTED_AUTH_METHOD)).byteValue(); if (method == SocksProxyConstants.GSSAPI_AUTH) { int oldPos = buf.position(); if (buf.get(0) != 0x01) { throw new IllegalStateException("Authentication failed"); } if (buf.get(1) == 0xFF) { throw new IllegalStateException("Authentication failed: GSS API Security Context Failure"); } if (buf.remaining() >= 2) { byte[] size = new byte[2]; buf.get(size); int s = ByteUtilities.makeIntFromByte2(size); if (buf.remaining() >= s) { byte[] token = new byte[s]; buf.get(token); getSession().setAttribute(GSS_TOKEN, token); len = 0; } else { return; } } else { buf.position(oldPos); return; } } else if (buf.get(1) != SocksProxyConstants.V5_REPLY_SUCCEEDED) { throw new IllegalStateException("Authentication failed"); } } else if (step == SocksProxyConstants.SOCKS5_REQUEST_STEP) { // Send the request byte addressType = buf.get(3); len = 6; if (addressType == SocksProxyConstants.IPV6_ADDRESS_TYPE) { len += 16; } else if (addressType == SocksProxyConstants.IPV4_ADDRESS_TYPE) { len += 4; } else if (addressType == SocksProxyConstants.DOMAIN_NAME_ADDRESS_TYPE) { len += 1 + (buf.get(4)); } else { throw new IllegalStateException("Unknwon address type"); } if (buf.remaining() >= len) { // handle response byte status = buf.get(1); LOGGER.debug(" response status: {}", SocksProxyConstants.getReplyCodeAsString(status)); if (status == SocksProxyConstants.V5_REPLY_SUCCEEDED) { buf.position(buf.position() + len); setHandshakeComplete(); return; } throw new Exception("Proxy handshake failed - Code: 0x" + ByteUtilities.asHex(new byte[] { status })); } return; } if (len > 0) { buf.position(buf.position() + len); } // Move to the handshaking next step if not in the middle of // the authentication process boolean isAuthenticating = false; if (step == SocksProxyConstants.SOCKS5_AUTH_STEP) { byte method = ((Byte) getSession().getAttribute(Socks5LogicHandler.SELECTED_AUTH_METHOD)).byteValue(); if (method == SocksProxyConstants.GSSAPI_AUTH) { GSSContext ctx = (GSSContext) getSession().getAttribute(GSS_CONTEXT); if (ctx == null || !ctx.isEstablished()) { isAuthenticating = true; } } } if (!isAuthenticating) { getSession().setAttribute(HANDSHAKE_STEP, ++step); } doHandshake(nextFilter); } /** * Closes the session. If any {@link GSSContext} is present in the session * then it is closed. * * @param message the error message */ @Override protected void closeSession(String message) { GSSContext ctx = (GSSContext) getSession().getAttribute(GSS_CONTEXT); if (ctx != null) { try { ctx.dispose(); } catch (GSSException e) { e.printStackTrace(); super.closeSession(message, e); return; } } super.closeSession(message); } }