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}