/* * Copyright 2014 The Netty Project * * The Netty Project 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 io.netty.handler.proxy; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.handler.codec.socksx.v5.DefaultSocks5InitialRequest; import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandRequest; import io.netty.handler.codec.socksx.v5.DefaultSocks5PasswordAuthRequest; import io.netty.handler.codec.socksx.v5.Socks5AddressType; import io.netty.handler.codec.socksx.v5.Socks5AuthMethod; import io.netty.handler.codec.socksx.v5.Socks5InitialRequest; import io.netty.handler.codec.socksx.v5.Socks5InitialResponse; import io.netty.handler.codec.socksx.v5.Socks5InitialResponseDecoder; import io.netty.handler.codec.socksx.v5.Socks5ClientEncoder; import io.netty.handler.codec.socksx.v5.Socks5CommandResponse; import io.netty.handler.codec.socksx.v5.Socks5CommandResponseDecoder; import io.netty.handler.codec.socksx.v5.Socks5CommandStatus; import io.netty.handler.codec.socksx.v5.Socks5CommandType; import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthResponse; import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthResponseDecoder; import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthStatus; import io.netty.util.NetUtil; import io.netty.util.internal.StringUtil; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Arrays; import java.util.Collections; public final class Socks5ProxyHandler extends ProxyHandler { private static final String PROTOCOL = "socks5"; private static final String AUTH_PASSWORD = "password"; private static final Socks5InitialRequest INIT_REQUEST_NO_AUTH = new DefaultSocks5InitialRequest(Collections.singletonList(Socks5AuthMethod.NO_AUTH)); private static final Socks5InitialRequest INIT_REQUEST_PASSWORD = new DefaultSocks5InitialRequest(Arrays.asList(Socks5AuthMethod.NO_AUTH, Socks5AuthMethod.PASSWORD)); private final String username; private final String password; private String decoderName; private String encoderName; public Socks5ProxyHandler(SocketAddress proxyAddress) { this(proxyAddress, null, null); } public Socks5ProxyHandler(SocketAddress proxyAddress, String username, String password) { super(proxyAddress); if (username != null && username.isEmpty()) { username = null; } if (password != null && password.isEmpty()) { password = null; } this.username = username; this.password = password; } @Override public String protocol() { return PROTOCOL; } @Override public String authScheme() { return socksAuthMethod() == Socks5AuthMethod.PASSWORD? AUTH_PASSWORD : AUTH_NONE; } public String username() { return username; } public String password() { return password; } @Override protected void addCodec(ChannelHandlerContext ctx) throws Exception { ChannelPipeline p = ctx.pipeline(); String name = ctx.name(); Socks5InitialResponseDecoder decoder = new Socks5InitialResponseDecoder(); p.addBefore(name, null, decoder); decoderName = p.context(decoder).name(); encoderName = decoderName + ".encoder"; p.addBefore(name, encoderName, Socks5ClientEncoder.DEFAULT); } @Override protected void removeEncoder(ChannelHandlerContext ctx) throws Exception { ctx.pipeline().remove(encoderName); } @Override protected void removeDecoder(ChannelHandlerContext ctx) throws Exception { ChannelPipeline p = ctx.pipeline(); if (p.context(decoderName) != null) { p.remove(decoderName); } } @Override protected Object newInitialMessage(ChannelHandlerContext ctx) throws Exception { return socksAuthMethod() == Socks5AuthMethod.PASSWORD? INIT_REQUEST_PASSWORD : INIT_REQUEST_NO_AUTH; } @Override protected boolean handleResponse(ChannelHandlerContext ctx, Object response) throws Exception { if (response instanceof Socks5InitialResponse) { Socks5InitialResponse res = (Socks5InitialResponse) response; Socks5AuthMethod authMethod = socksAuthMethod(); if (res.authMethod() != Socks5AuthMethod.NO_AUTH && res.authMethod() != authMethod) { // Server did not allow unauthenticated access nor accept the requested authentication scheme. throw new ProxyConnectException(exceptionMessage("unexpected authMethod: " + res.authMethod())); } if (authMethod == Socks5AuthMethod.NO_AUTH) { sendConnectCommand(ctx); } else if (authMethod == Socks5AuthMethod.PASSWORD) { // In case of password authentication, send an authentication request. ctx.pipeline().replace(decoderName, decoderName, new Socks5PasswordAuthResponseDecoder()); sendToProxyServer(new DefaultSocks5PasswordAuthRequest( username != null? username : "", password != null? password : "")); } else { // Should never reach here. throw new Error(); } return false; } if (response instanceof Socks5PasswordAuthResponse) { // Received an authentication response from the server. Socks5PasswordAuthResponse res = (Socks5PasswordAuthResponse) response; if (res.status() != Socks5PasswordAuthStatus.SUCCESS) { throw new ProxyConnectException(exceptionMessage("authStatus: " + res.status())); } sendConnectCommand(ctx); return false; } // This should be the last message from the server. Socks5CommandResponse res = (Socks5CommandResponse) response; if (res.status() != Socks5CommandStatus.SUCCESS) { throw new ProxyConnectException(exceptionMessage("status: " + res.status())); } return true; } private Socks5AuthMethod socksAuthMethod() { Socks5AuthMethod authMethod; if (username == null && password == null) { authMethod = Socks5AuthMethod.NO_AUTH; } else { authMethod = Socks5AuthMethod.PASSWORD; } return authMethod; } private void sendConnectCommand(ChannelHandlerContext ctx) throws Exception { InetSocketAddress raddr = destinationAddress(); Socks5AddressType addrType; String rhost; if (raddr.isUnresolved()) { addrType = Socks5AddressType.DOMAIN; rhost = raddr.getHostString(); } else { rhost = raddr.getAddress().getHostAddress(); if (NetUtil.isValidIpV4Address(rhost)) { addrType = Socks5AddressType.IPv4; } else if (NetUtil.isValidIpV6Address(rhost)) { addrType = Socks5AddressType.IPv6; } else { throw new ProxyConnectException( exceptionMessage("unknown address type: " + StringUtil.simpleClassName(rhost))); } } ctx.pipeline().replace(decoderName, decoderName, new Socks5CommandResponseDecoder()); sendToProxyServer(new DefaultSocks5CommandRequest(Socks5CommandType.CONNECT, addrType, rhost, raddr.getPort())); } }