976db9269d
While implementing netty-handler-proxy, I realized various issues in our current socksx package. Here's the list of the modifications and their background: - Split message types into interfaces and default implementations - so that a user can implement an alternative message implementations - Use classes instead of enums when a user might want to define a new constant - so that a user can extend SOCKS5 protocol, such as: - defining a new error code - defining a new address type - Rename the message classes - to avoid abbreviated class names. e.g: - Cmd -> Command - Init -> Initial - so that the class names align better with the protocol specifications. e.g: - AuthRequest -> PasswordAuthRequest - AuthScheme -> AuthMethod - Rename the property names of the messages - so that the property names align better when the field names in the protocol specifications - Improve the decoder implementations - Give a user more control over when a decoder has to be removed - Use DecoderResult and DecoderResultProvider to handle decode failure gracefully. i.e. no more Unknown* message classes - Add SocksPortUnifinicationServerHandler since it's useful to the users who write a SOCKS server - Cleaned up and moved from the socksproxy example
179 lines
6.8 KiB
Java
179 lines
6.8 KiB
Java
/*
|
|
* 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.buffer.Unpooled;
|
|
import io.netty.channel.ChannelHandlerContext;
|
|
import io.netty.channel.ChannelPipeline;
|
|
import io.netty.channel.socket.SocketChannel;
|
|
import io.netty.handler.codec.LineBasedFrameDecoder;
|
|
import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandResponse;
|
|
import io.netty.handler.codec.socksx.v5.DefaultSocks5InitialResponse;
|
|
import io.netty.handler.codec.socksx.v5.DefaultSocks5PasswordAuthResponse;
|
|
import io.netty.handler.codec.socksx.v5.Socks5AddressType;
|
|
import io.netty.handler.codec.socksx.v5.Socks5AuthMethod;
|
|
import io.netty.handler.codec.socksx.v5.Socks5CommandRequest;
|
|
import io.netty.handler.codec.socksx.v5.Socks5CommandRequestDecoder;
|
|
import io.netty.handler.codec.socksx.v5.Socks5CommandResponse;
|
|
import io.netty.handler.codec.socksx.v5.Socks5CommandStatus;
|
|
import io.netty.handler.codec.socksx.v5.Socks5CommandType;
|
|
import io.netty.handler.codec.socksx.v5.Socks5InitialRequest;
|
|
import io.netty.handler.codec.socksx.v5.Socks5InitialRequestDecoder;
|
|
import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthRequest;
|
|
import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthRequestDecoder;
|
|
import io.netty.handler.codec.socksx.v5.Socks5PasswordAuthStatus;
|
|
import io.netty.handler.codec.socksx.v5.Socks5ServerEncoder;
|
|
import io.netty.util.CharsetUtil;
|
|
|
|
import java.net.InetSocketAddress;
|
|
import java.net.SocketAddress;
|
|
|
|
import static org.hamcrest.Matchers.*;
|
|
import static org.junit.Assert.*;
|
|
|
|
final class Socks5ProxyServer extends ProxyServer {
|
|
|
|
private static final String ENCODER = "encoder";
|
|
private static final String DECODER = "decoder";
|
|
|
|
Socks5ProxyServer(boolean useSsl, TestMode testMode, InetSocketAddress destination) {
|
|
super(useSsl, testMode, destination);
|
|
}
|
|
|
|
Socks5ProxyServer(
|
|
boolean useSsl, TestMode testMode, InetSocketAddress destination, String username, String password) {
|
|
super(useSsl, testMode, destination, username, password);
|
|
}
|
|
|
|
@Override
|
|
protected void configure(SocketChannel ch) throws Exception {
|
|
ChannelPipeline p = ch.pipeline();
|
|
switch (testMode) {
|
|
case INTERMEDIARY:
|
|
p.addLast(DECODER, new Socks5InitialRequestDecoder());
|
|
p.addLast(ENCODER, Socks5ServerEncoder.DEFAULT);
|
|
p.addLast(new Socks5IntermediaryHandler());
|
|
break;
|
|
case TERMINAL:
|
|
p.addLast(DECODER, new Socks5InitialRequestDecoder());
|
|
p.addLast(ENCODER, Socks5ServerEncoder.DEFAULT);
|
|
p.addLast(new Socks5TerminalHandler());
|
|
break;
|
|
case UNRESPONSIVE:
|
|
p.addLast(UnresponsiveHandler.INSTANCE);
|
|
break;
|
|
}
|
|
}
|
|
|
|
boolean authenticate(ChannelHandlerContext ctx, Object msg) {
|
|
if (username == null) {
|
|
ctx.pipeline().replace(DECODER, DECODER, new Socks5CommandRequestDecoder());
|
|
ctx.write(new DefaultSocks5InitialResponse(Socks5AuthMethod.NO_AUTH));
|
|
return true;
|
|
}
|
|
|
|
if (msg instanceof Socks5InitialRequest) {
|
|
ctx.pipeline().replace(DECODER, DECODER, new Socks5PasswordAuthRequestDecoder());
|
|
ctx.write(new DefaultSocks5InitialResponse(Socks5AuthMethod.PASSWORD));
|
|
return false;
|
|
}
|
|
|
|
Socks5PasswordAuthRequest req = (Socks5PasswordAuthRequest) msg;
|
|
if (req.username().equals(username) && req.password().equals(password)) {
|
|
ctx.pipeline().replace(DECODER, DECODER, new Socks5CommandRequestDecoder());
|
|
ctx.write(new DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus.SUCCESS));
|
|
return true;
|
|
}
|
|
|
|
ctx.pipeline().replace(DECODER, DECODER, new Socks5PasswordAuthRequestDecoder());
|
|
ctx.write(new DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus.FAILURE));
|
|
return false;
|
|
}
|
|
|
|
private final class Socks5IntermediaryHandler extends IntermediaryHandler {
|
|
|
|
private boolean authenticated;
|
|
private SocketAddress intermediaryDestination;
|
|
|
|
@Override
|
|
protected boolean handleProxyProtocol(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
if (!authenticated) {
|
|
authenticated = authenticate(ctx, msg);
|
|
return false;
|
|
}
|
|
|
|
Socks5CommandRequest req = (Socks5CommandRequest) msg;
|
|
assertThat(req.type(), is(Socks5CommandType.CONNECT));
|
|
|
|
Socks5CommandResponse res =
|
|
new DefaultSocks5CommandResponse(Socks5CommandStatus.SUCCESS, Socks5AddressType.IPv4);
|
|
intermediaryDestination = new InetSocketAddress(req.dstAddr(), req.dstPort());
|
|
|
|
ctx.write(res);
|
|
|
|
ctx.pipeline().remove(ENCODER);
|
|
ctx.pipeline().remove(DECODER);
|
|
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
protected SocketAddress intermediaryDestination() {
|
|
return intermediaryDestination;
|
|
}
|
|
}
|
|
|
|
private final class Socks5TerminalHandler extends TerminalHandler {
|
|
|
|
private boolean authenticated;
|
|
|
|
@Override
|
|
protected boolean handleProxyProtocol(ChannelHandlerContext ctx, Object msg) throws Exception {
|
|
if (!authenticated) {
|
|
authenticated = authenticate(ctx, msg);
|
|
return false;
|
|
}
|
|
|
|
Socks5CommandRequest req = (Socks5CommandRequest) msg;
|
|
assertThat(req.type(), is(Socks5CommandType.CONNECT));
|
|
|
|
ctx.pipeline().addBefore(ctx.name(), "lineDecoder", new LineBasedFrameDecoder(64, false, true));
|
|
|
|
Socks5CommandResponse res;
|
|
boolean sendGreeting = false;
|
|
if (!req.dstAddr().equals(destination.getHostString()) ||
|
|
req.dstPort() != destination.getPort()) {
|
|
res = new DefaultSocks5CommandResponse(Socks5CommandStatus.FORBIDDEN, Socks5AddressType.IPv4);
|
|
} else {
|
|
res = new DefaultSocks5CommandResponse(Socks5CommandStatus.SUCCESS, Socks5AddressType.IPv4);
|
|
sendGreeting = true;
|
|
}
|
|
|
|
ctx.write(res);
|
|
|
|
ctx.pipeline().remove(ENCODER);
|
|
ctx.pipeline().remove(DECODER);
|
|
|
|
if (sendGreeting) {
|
|
ctx.write(Unpooled.copiedBuffer("0\n", CharsetUtil.US_ASCII));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|