├── .gitignore ├── README.md ├── http-proxy ├── pom.xml └── src │ ├── main │ └── java │ │ └── cc │ │ └── leevi │ │ └── common │ │ └── httpproxy │ │ ├── DirectClientHandler.java │ │ ├── HttpProxyRequestHead.java │ │ ├── HttpProxyServerInitializer.java │ │ ├── HttpServer.java │ │ ├── HttpServerConnectHandler.java │ │ ├── HttpServerHeadDecoder.java │ │ ├── HttpServerUtils.java │ │ └── RelayHandler.java │ └── test │ ├── java │ └── cc │ │ └── leevi │ │ └── common │ │ └── httpproxy │ │ └── HttpProxyClientTest.java │ └── resources │ └── log4j2.xml ├── mixin-proxy ├── pom.xml └── src │ ├── main │ └── java │ │ └── cc │ │ └── leevi │ │ └── common │ │ └── socks5proxy │ │ ├── DirectClientHandler.java │ │ ├── HttpProxyRequestHead.java │ │ ├── HttpServerConnectHandler.java │ │ ├── HttpServerHeadDecoder.java │ │ ├── MixinProxyServer.java │ │ ├── MixinSelectHandler.java │ │ ├── MixinServerInitializer.java │ │ ├── MixinServerUtils.java │ │ ├── RelayHandler.java │ │ ├── SocksServerConnectHandler.java │ │ └── SocksServerHandler.java │ └── test │ ├── java │ └── cc │ │ └── leevi │ │ └── common │ │ └── socks5proxy │ │ └── MixinProxyServerTest.java │ └── resources │ └── log4j2.xml ├── pom.xml └── socks5-proxy ├── pom.xml └── src ├── main └── java │ └── cc │ └── leevi │ └── common │ └── socks5proxy │ ├── DirectClientHandler.java │ ├── RelayHandler.java │ ├── Socks5ProxyServer.java │ ├── SocksServerConnectHandler.java │ ├── SocksServerHandler.java │ ├── SocksServerInitializer.java │ └── SocksServerUtils.java └── test ├── java └── cc │ └── leevi │ └── common │ └── socks5proxy │ └── Socks5ProxyServerTest.java └── resources └── log4j2.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .idea/* 3 | *.iml 4 | target/ 5 | *.class 6 | .project 7 | .settings/ 8 | .settings/* 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netty-proxy-server 2 | 基于Netty实现的代理服务器,Web Proxy Server(普通Web代理和SSL隧道代理),Socks5 Proxy Server和混合模式(同时支持以上两种,自动选择) 3 | 4 | 5 | 虽然是个玩具,但麻雀虽小五脏俱全,基本的都有。不管是用来做 netty 的学习,还是代理协议的学习都是不错的参考资料 6 | -------------------------------------------------------------------------------- /http-proxy/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | netty-proxy-server 7 | cc.leevi.common 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | http-proxy 13 | 14 | 15 | 16 | org.apache.maven.plugins 17 | maven-compiler-plugin 18 | 19 | 7 20 | 7 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | io.netty 29 | netty-all 30 | 31 | 32 | org.apache.commons 33 | commons-lang3 34 | 35 | 36 | org.slf4j 37 | slf4j-api 38 | 39 | 40 | junit 41 | junit 42 | 4.13.1 43 | test 44 | 45 | 46 | org.apache.logging.log4j 47 | log4j-core 48 | 2.14.0 49 | test 50 | 51 | 52 | org.apache.logging.log4j 53 | log4j-slf4j-impl 54 | 2.14.0 55 | test 56 | 57 | 58 | com.google.guava 59 | guava 60 | 30.0-jre 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /http-proxy/src/main/java/cc/leevi/common/httpproxy/DirectClientHandler.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.httpproxy; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelInboundHandlerAdapter; 6 | import io.netty.util.concurrent.Promise; 7 | 8 | public final class DirectClientHandler extends ChannelInboundHandlerAdapter { 9 | 10 | private final Promise promise; 11 | 12 | public DirectClientHandler(Promise promise) { 13 | this.promise = promise; 14 | } 15 | 16 | @Override 17 | public void channelActive(ChannelHandlerContext ctx) { 18 | ctx.pipeline().remove(this); 19 | promise.setSuccess(ctx.channel()); 20 | } 21 | 22 | @Override 23 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable throwable) { 24 | promise.setFailure(throwable); 25 | } 26 | } -------------------------------------------------------------------------------- /http-proxy/src/main/java/cc/leevi/common/httpproxy/HttpProxyRequestHead.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.httpproxy; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | 5 | public class HttpProxyRequestHead { 6 | private String host; 7 | private int port; 8 | private String proxyType;//tunnel or web 9 | private String protocolVersion; 10 | 11 | private ByteBuf byteBuf; 12 | 13 | public HttpProxyRequestHead(String host, int port, String proxyType, String protocolVersion, ByteBuf byteBuf) { 14 | this.host = host; 15 | this.port = port; 16 | this.proxyType = proxyType; 17 | this.protocolVersion = protocolVersion; 18 | this.byteBuf = byteBuf; 19 | } 20 | 21 | public String getHost() { 22 | return host; 23 | } 24 | 25 | public void setHost(String host) { 26 | this.host = host; 27 | } 28 | 29 | public int getPort() { 30 | return port; 31 | } 32 | 33 | public void setPort(int port) { 34 | this.port = port; 35 | } 36 | 37 | public String getProxyType() { 38 | return proxyType; 39 | } 40 | 41 | public void setProxyType(String proxyType) { 42 | this.proxyType = proxyType; 43 | } 44 | 45 | public ByteBuf getByteBuf() { 46 | return byteBuf; 47 | } 48 | 49 | public void setByteBuf(ByteBuf byteBuf) { 50 | this.byteBuf = byteBuf; 51 | } 52 | 53 | public String getProtocolVersion() { 54 | return protocolVersion; 55 | } 56 | 57 | public void setProtocolVersion(String protocolVersion) { 58 | this.protocolVersion = protocolVersion; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /http-proxy/src/main/java/cc/leevi/common/httpproxy/HttpProxyServerInitializer.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.httpproxy; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelInitializer; 5 | import io.netty.channel.ChannelPipeline; 6 | import io.netty.handler.logging.LoggingHandler; 7 | 8 | public class HttpProxyServerInitializer extends ChannelInitializer { 9 | 10 | protected void initChannel(Channel channel) throws Exception { 11 | ChannelPipeline p = channel.pipeline(); 12 | p.addLast(new LoggingHandler()); 13 | p.addLast(new HttpServerHeadDecoder()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /http-proxy/src/main/java/cc/leevi/common/httpproxy/HttpServer.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.httpproxy; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.channel.*; 5 | import io.netty.channel.nio.NioEventLoopGroup; 6 | import io.netty.channel.socket.nio.NioServerSocketChannel; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | public class HttpServer { 11 | 12 | private Logger logger = LoggerFactory.getLogger(HttpServer.class); 13 | 14 | private ServerBootstrap serverBootstrap; 15 | 16 | private EventLoopGroup serverEventLoopGroup; 17 | 18 | private Channel acceptorChannel; 19 | 20 | public void startServer(){ 21 | logger.info("Proxy Server starting..."); 22 | 23 | serverEventLoopGroup = new NioEventLoopGroup(4); 24 | 25 | serverBootstrap = new ServerBootstrap() 26 | .channel(NioServerSocketChannel.class) 27 | .childHandler(new HttpProxyServerInitializer()) 28 | .group(serverEventLoopGroup); 29 | acceptorChannel = serverBootstrap.bind(17891).syncUninterruptibly().channel(); 30 | } 31 | 32 | public void shutdown(){ 33 | logger.info("Proxy Server shutting down..."); 34 | acceptorChannel.close().syncUninterruptibly(); 35 | serverEventLoopGroup.shutdownGracefully().syncUninterruptibly(); 36 | logger.info("shutdown completed!"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /http-proxy/src/main/java/cc/leevi/common/httpproxy/HttpServerConnectHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package cc.leevi.common.httpproxy; 17 | 18 | import io.netty.bootstrap.Bootstrap; 19 | import io.netty.buffer.Unpooled; 20 | import io.netty.channel.Channel; 21 | import io.netty.channel.ChannelFuture; 22 | import io.netty.channel.ChannelFutureListener; 23 | import io.netty.channel.ChannelHandler; 24 | import io.netty.channel.ChannelHandlerContext; 25 | import io.netty.channel.ChannelOption; 26 | import io.netty.channel.SimpleChannelInboundHandler; 27 | import io.netty.channel.socket.nio.NioSocketChannel; 28 | import io.netty.util.concurrent.Future; 29 | import io.netty.util.concurrent.FutureListener; 30 | import io.netty.util.concurrent.Promise; 31 | 32 | @ChannelHandler.Sharable 33 | public final class HttpServerConnectHandler extends SimpleChannelInboundHandler { 34 | 35 | private final Bootstrap b = new Bootstrap(); 36 | 37 | @Override 38 | public void channelRead0(final ChannelHandlerContext ctx, final HttpProxyRequestHead requestHead) throws Exception { 39 | 40 | Promise promise = ctx.executor().newPromise(); 41 | final Channel inboundChannel = ctx.channel(); 42 | promise.addListener( 43 | new FutureListener() { 44 | @Override 45 | public void operationComplete(final Future future) throws Exception { 46 | final Channel outboundChannel = future.getNow(); 47 | if (future.isSuccess()) { 48 | ChannelFuture responseFuture; 49 | if("TUNNEL".equals(requestHead.getProxyType())){ 50 | responseFuture = inboundChannel.writeAndFlush(Unpooled.wrappedBuffer((requestHead.getProtocolVersion() + " 200 Connection Established\r\n\r\n").getBytes())); 51 | }else if("WEB".equals(requestHead.getProxyType())){ 52 | responseFuture = outboundChannel.writeAndFlush(requestHead.getByteBuf()); 53 | }else{ 54 | HttpServerUtils.closeOnFlush(inboundChannel); 55 | return; 56 | } 57 | responseFuture.addListener(new ChannelFutureListener() { 58 | @Override 59 | public void operationComplete(ChannelFuture channelFuture) { 60 | ctx.pipeline().remove(HttpServerConnectHandler.this); 61 | outboundChannel.pipeline().addLast(new RelayHandler(inboundChannel)); 62 | ctx.pipeline().addLast(new RelayHandler(outboundChannel)); 63 | } 64 | }); 65 | } else { 66 | HttpServerUtils.closeOnFlush(inboundChannel); 67 | } 68 | } 69 | }); 70 | 71 | b.group(inboundChannel.eventLoop()) 72 | .channel(NioSocketChannel.class) 73 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) 74 | .option(ChannelOption.SO_KEEPALIVE, true) 75 | .handler(new DirectClientHandler(promise)); 76 | 77 | b.connect(requestHead.getHost(), requestHead.getPort()).addListener(new ChannelFutureListener() { 78 | @Override 79 | public void operationComplete(ChannelFuture future) throws Exception { 80 | if (future.isSuccess()) { 81 | // Connection established use handler provided results 82 | } else { 83 | // Close the connection if the connection attempt has failed. 84 | HttpServerUtils.closeOnFlush(inboundChannel); 85 | } 86 | } 87 | }); 88 | } 89 | 90 | @Override 91 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 92 | HttpServerUtils.closeOnFlush(ctx.channel()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /http-proxy/src/main/java/cc/leevi/common/httpproxy/HttpServerHeadDecoder.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.httpproxy; 2 | 3 | import com.google.common.net.HostAndPort; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.SimpleChannelInboundHandler; 7 | import io.netty.handler.codec.http.HttpConstants; 8 | import io.netty.handler.codec.http.HttpMethod; 9 | import io.netty.util.ByteProcessor; 10 | import io.netty.util.internal.AppendableCharSequence; 11 | 12 | import java.net.URL; 13 | 14 | public class HttpServerHeadDecoder extends SimpleChannelInboundHandler { 15 | 16 | private HttpServerHeadDecoder.HeadLineByteProcessor headLineByteProcessor = new HttpServerHeadDecoder.HeadLineByteProcessor(); 17 | 18 | private 19 | 20 | class HeadLineByteProcessor implements ByteProcessor{ 21 | private AppendableCharSequence seq; 22 | 23 | public HeadLineByteProcessor() { 24 | this.seq = new AppendableCharSequence(4096); 25 | } 26 | 27 | public AppendableCharSequence parse(ByteBuf buffer) { 28 | seq.reset(); 29 | int i = buffer.forEachByte(this); 30 | if (i == -1) { 31 | return null; 32 | } 33 | buffer.readerIndex(i + 1); 34 | return seq; 35 | } 36 | 37 | @Override 38 | public boolean process(byte value) throws Exception { 39 | char nextByte = (char) (value & 0xFF); 40 | if (nextByte == HttpConstants.LF) { 41 | int len = seq.length(); 42 | if (len >= 1 && seq.charAtUnsafe(len - 1) == HttpConstants.CR) { 43 | seq.append(nextByte); 44 | } 45 | return false; 46 | } 47 | //continue loop byte 48 | seq.append(nextByte); 49 | return true; 50 | } 51 | } 52 | 53 | @Override 54 | protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception { 55 | AppendableCharSequence seq = headLineByteProcessor.parse(in); 56 | if(seq.charAt(seq.length()-1) == HttpConstants.LF){ 57 | HttpProxyRequestHead httpProxyRequestHead; 58 | String[] splitInitialLine = splitInitialLine(seq); 59 | String method = splitInitialLine[0]; 60 | String uri = splitInitialLine[1]; 61 | String protocolVersion = splitInitialLine[2]; 62 | String host; 63 | int port; 64 | if(HttpMethod.CONNECT.name().equals(method)){ 65 | //https tunnel proxy 66 | HostAndPort hostAndPort = HostAndPort.fromString(uri); 67 | host = hostAndPort.getHost(); 68 | port = hostAndPort.getPort(); 69 | 70 | httpProxyRequestHead = new HttpProxyRequestHead(host, port, "TUNNEL",protocolVersion,null); 71 | }else{ 72 | //http proxy 73 | URL url = new URL(uri); 74 | host = url.getHost(); 75 | port = url.getPort(); 76 | if(port == -1){ 77 | port = 80; 78 | } 79 | 80 | httpProxyRequestHead = new HttpProxyRequestHead(host, port,"WEB",protocolVersion,in.retain().resetReaderIndex()); 81 | } 82 | ctx.pipeline().addLast(new HttpServerConnectHandler()).remove(this); 83 | ctx.fireChannelRead(httpProxyRequestHead); 84 | } 85 | } 86 | 87 | private static String[] splitInitialLine(AppendableCharSequence sb) { 88 | int aStart; 89 | int aEnd; 90 | int bStart; 91 | int bEnd; 92 | int cStart; 93 | int cEnd; 94 | 95 | aStart = findNonSPLenient(sb, 0); 96 | aEnd = findSPLenient(sb, aStart); 97 | 98 | bStart = findNonSPLenient(sb, aEnd); 99 | bEnd = findSPLenient(sb, bStart); 100 | 101 | cStart = findNonSPLenient(sb, bEnd); 102 | cEnd = findEndOfString(sb); 103 | 104 | return new String[] { 105 | sb.subStringUnsafe(aStart, aEnd), 106 | sb.subStringUnsafe(bStart, bEnd), 107 | cStart < cEnd? sb.subStringUnsafe(cStart, cEnd) : "" }; 108 | } 109 | 110 | private static int findNonSPLenient(AppendableCharSequence sb, int offset) { 111 | for (int result = offset; result < sb.length(); ++result) { 112 | char c = sb.charAtUnsafe(result); 113 | // See https://tools.ietf.org/html/rfc7230#section-3.5 114 | if (isSPLenient(c)) { 115 | continue; 116 | } 117 | if (Character.isWhitespace(c)) { 118 | // Any other whitespace delimiter is invalid 119 | throw new IllegalArgumentException("Invalid separator"); 120 | } 121 | return result; 122 | } 123 | return sb.length(); 124 | } 125 | 126 | private static int findSPLenient(AppendableCharSequence sb, int offset) { 127 | for (int result = offset; result < sb.length(); ++result) { 128 | if (isSPLenient(sb.charAtUnsafe(result))) { 129 | return result; 130 | } 131 | } 132 | return sb.length(); 133 | } 134 | 135 | private static boolean isSPLenient(char c) { 136 | // See https://tools.ietf.org/html/rfc7230#section-3.5 137 | return c == ' ' || c == (char) 0x09 || c == (char) 0x0B || c == (char) 0x0C || c == (char) 0x0D; 138 | } 139 | 140 | private static int findNonWhitespace(AppendableCharSequence sb, int offset, boolean validateOWS) { 141 | for (int result = offset; result < sb.length(); ++result) { 142 | char c = sb.charAtUnsafe(result); 143 | if (!Character.isWhitespace(c)) { 144 | return result; 145 | } else if (validateOWS && !isOWS(c)) { 146 | // Only OWS is supported for whitespace 147 | throw new IllegalArgumentException("Invalid separator, only a single space or horizontal tab allowed," + 148 | " but received a '" + c + "'"); 149 | } 150 | } 151 | return sb.length(); 152 | } 153 | 154 | private static int findEndOfString(AppendableCharSequence sb) { 155 | for (int result = sb.length() - 1; result > 0; --result) { 156 | if (!Character.isWhitespace(sb.charAtUnsafe(result))) { 157 | return result + 1; 158 | } 159 | } 160 | return 0; 161 | } 162 | 163 | private static boolean isOWS(char ch) { 164 | return ch == ' ' || ch == (char) 0x09; 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /http-proxy/src/main/java/cc/leevi/common/httpproxy/HttpServerUtils.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.httpproxy; 2 | 3 | 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.ChannelFutureListener; 7 | 8 | public final class HttpServerUtils { 9 | 10 | /** 11 | * Closes the specified channel after all queued write requests are flushed. 12 | */ 13 | public static void closeOnFlush(Channel ch) { 14 | if (ch.isActive()) { 15 | ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 16 | } 17 | } 18 | 19 | private HttpServerUtils() { } 20 | } -------------------------------------------------------------------------------- /http-proxy/src/main/java/cc/leevi/common/httpproxy/RelayHandler.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.httpproxy; 2 | 3 | import io.netty.buffer.Unpooled; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelFutureListener; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.channel.ChannelInboundHandlerAdapter; 8 | import io.netty.util.ReferenceCountUtil; 9 | 10 | public class RelayHandler extends ChannelInboundHandlerAdapter { 11 | private final Channel relayChannel; 12 | 13 | public RelayHandler(Channel relayChannel) { 14 | this.relayChannel = relayChannel; 15 | } 16 | 17 | @Override 18 | public void channelActive(ChannelHandlerContext ctx) { 19 | ctx.writeAndFlush(Unpooled.EMPTY_BUFFER); 20 | } 21 | 22 | @Override 23 | public void channelRead(ChannelHandlerContext ctx, Object msg) { 24 | if (relayChannel.isActive()) { 25 | relayChannel.writeAndFlush(msg); 26 | } else { 27 | ReferenceCountUtil.release(msg); 28 | } 29 | } 30 | 31 | @Override 32 | public void channelInactive(ChannelHandlerContext ctx) { 33 | HttpServerUtils.closeOnFlush(ctx.channel()); 34 | } 35 | 36 | @Override 37 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 38 | ctx.close(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /http-proxy/src/test/java/cc/leevi/common/httpproxy/HttpProxyClientTest.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.httpproxy; 2 | 3 | import com.google.common.net.HostAndPort; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.io.IOException; 8 | 9 | public class HttpProxyClientTest { 10 | 11 | @Before 12 | public void setUp() throws Exception { 13 | } 14 | 15 | @Test 16 | public void startServer() throws IOException { 17 | HttpServer httpServer = new HttpServer(); 18 | httpServer.startServer(); 19 | System.in.read(); 20 | } 21 | 22 | @Test 23 | public void parseURI(){ 24 | HostAndPort hostAndPort = HostAndPort.fromString("cdn.segmentfault.com:443"); 25 | System.out.println(hostAndPort.getHost()); 26 | System.out.println(hostAndPort.getPort()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /http-proxy/src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /mixin-proxy/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | 9 | netty-proxy-server 10 | cc.leevi.common 11 | 1.0-SNAPSHOT 12 | 13 | 14 | mixin-proxy 15 | 16 | socks5-proxy 17 | 18 | 19 | UTF-8 20 | 1.7 21 | 1.7 22 | 23 | 24 | 25 | 26 | io.netty 27 | netty-all 28 | 29 | 30 | org.apache.commons 31 | commons-lang3 32 | 33 | 34 | org.slf4j 35 | slf4j-api 36 | 37 | 38 | junit 39 | junit 40 | 4.13.1 41 | test 42 | 43 | 44 | org.apache.logging.log4j 45 | log4j-core 46 | 2.14.0 47 | test 48 | 49 | 50 | org.apache.logging.log4j 51 | log4j-slf4j-impl 52 | 2.14.0 53 | test 54 | 55 | 56 | com.google.guava 57 | guava 58 | 30.0-jre 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/DirectClientHandler.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelInboundHandlerAdapter; 6 | import io.netty.util.concurrent.Promise; 7 | 8 | public final class DirectClientHandler extends ChannelInboundHandlerAdapter { 9 | 10 | private final Promise promise; 11 | 12 | public DirectClientHandler(Promise promise) { 13 | this.promise = promise; 14 | } 15 | 16 | @Override 17 | public void channelActive(ChannelHandlerContext ctx) { 18 | ctx.pipeline().remove(this); 19 | promise.setSuccess(ctx.channel()); 20 | } 21 | 22 | @Override 23 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable throwable) { 24 | promise.setFailure(throwable); 25 | } 26 | } -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/HttpProxyRequestHead.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | 5 | public class HttpProxyRequestHead { 6 | private String host; 7 | private int port; 8 | private String proxyType;//tunnel or web 9 | private String protocolVersion; 10 | 11 | private ByteBuf byteBuf; 12 | 13 | public HttpProxyRequestHead(String host, int port, String proxyType, String protocolVersion, ByteBuf byteBuf) { 14 | this.host = host; 15 | this.port = port; 16 | this.proxyType = proxyType; 17 | this.protocolVersion = protocolVersion; 18 | this.byteBuf = byteBuf; 19 | } 20 | 21 | public String getHost() { 22 | return host; 23 | } 24 | 25 | public void setHost(String host) { 26 | this.host = host; 27 | } 28 | 29 | public int getPort() { 30 | return port; 31 | } 32 | 33 | public void setPort(int port) { 34 | this.port = port; 35 | } 36 | 37 | public String getProxyType() { 38 | return proxyType; 39 | } 40 | 41 | public void setProxyType(String proxyType) { 42 | this.proxyType = proxyType; 43 | } 44 | 45 | public ByteBuf getByteBuf() { 46 | return byteBuf; 47 | } 48 | 49 | public void setByteBuf(ByteBuf byteBuf) { 50 | this.byteBuf = byteBuf; 51 | } 52 | 53 | public String getProtocolVersion() { 54 | return protocolVersion; 55 | } 56 | 57 | public void setProtocolVersion(String protocolVersion) { 58 | this.protocolVersion = protocolVersion; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/HttpServerConnectHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package cc.leevi.common.socks5proxy; 17 | 18 | import io.netty.bootstrap.Bootstrap; 19 | import io.netty.buffer.Unpooled; 20 | import io.netty.channel.*; 21 | import io.netty.channel.socket.nio.NioSocketChannel; 22 | import io.netty.util.concurrent.Future; 23 | import io.netty.util.concurrent.FutureListener; 24 | import io.netty.util.concurrent.Promise; 25 | 26 | @ChannelHandler.Sharable 27 | public final class HttpServerConnectHandler extends SimpleChannelInboundHandler { 28 | 29 | private final Bootstrap b = new Bootstrap(); 30 | 31 | @Override 32 | public void channelRead0(final ChannelHandlerContext ctx, final HttpProxyRequestHead requestHead) throws Exception { 33 | 34 | 35 | Promise promise = ctx.executor().newPromise(); 36 | final Channel inboundChannel = ctx.channel(); 37 | promise.addListener( 38 | new FutureListener() { 39 | @Override 40 | public void operationComplete(final Future future) throws Exception { 41 | final Channel outboundChannel = future.getNow(); 42 | if (future.isSuccess()) { 43 | ChannelFuture responseFuture; 44 | if("TUNNEL".equals(requestHead.getProxyType())){ 45 | responseFuture = inboundChannel.writeAndFlush(Unpooled.wrappedBuffer((requestHead.getProtocolVersion() + " 200 Connection Established\r\n\r\n").getBytes())); 46 | }else if("WEB".equals(requestHead.getProxyType())){ 47 | responseFuture = outboundChannel.writeAndFlush(requestHead.getByteBuf()); 48 | }else{ 49 | MixinServerUtils.closeOnFlush(inboundChannel); 50 | return; 51 | } 52 | responseFuture.addListener(new ChannelFutureListener() { 53 | @Override 54 | public void operationComplete(ChannelFuture channelFuture) { 55 | ctx.pipeline().remove(HttpServerConnectHandler.this); 56 | outboundChannel.pipeline().addLast(new RelayHandler(inboundChannel)); 57 | ctx.pipeline().addLast(new RelayHandler(outboundChannel)); 58 | } 59 | }); 60 | } else { 61 | MixinServerUtils.closeOnFlush(inboundChannel); 62 | } 63 | } 64 | }); 65 | 66 | b.group(inboundChannel.eventLoop()) 67 | .channel(NioSocketChannel.class) 68 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) 69 | .option(ChannelOption.SO_KEEPALIVE, true) 70 | .handler(new DirectClientHandler(promise)); 71 | 72 | b.connect(requestHead.getHost(), requestHead.getPort()).addListener(new ChannelFutureListener() { 73 | @Override 74 | public void operationComplete(ChannelFuture future) throws Exception { 75 | if (future.isSuccess()) { 76 | // Connection established use handler provided results 77 | } else { 78 | // Close the connection if the connection attempt has failed. 79 | MixinServerUtils.closeOnFlush(inboundChannel); 80 | } 81 | } 82 | }); 83 | } 84 | 85 | @Override 86 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 87 | MixinServerUtils.closeOnFlush(ctx.channel()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/HttpServerHeadDecoder.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import com.google.common.net.HostAndPort; 4 | import io.netty.buffer.ByteBuf; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.SimpleChannelInboundHandler; 7 | import io.netty.handler.codec.http.HttpConstants; 8 | import io.netty.handler.codec.http.HttpMethod; 9 | import io.netty.util.ByteProcessor; 10 | import io.netty.util.internal.AppendableCharSequence; 11 | 12 | import java.net.URL; 13 | 14 | public class HttpServerHeadDecoder extends SimpleChannelInboundHandler { 15 | 16 | private HeadLineByteProcessor headLineByteProcessor = new HeadLineByteProcessor(); 17 | 18 | private 19 | 20 | class HeadLineByteProcessor implements ByteProcessor{ 21 | private AppendableCharSequence seq; 22 | 23 | public HeadLineByteProcessor() { 24 | this.seq = new AppendableCharSequence(4096); 25 | } 26 | 27 | public AppendableCharSequence parse(ByteBuf buffer) { 28 | seq.reset(); 29 | int i = buffer.forEachByte(this); 30 | if (i == -1) { 31 | return null; 32 | } 33 | buffer.readerIndex(i + 1); 34 | return seq; 35 | } 36 | 37 | @Override 38 | public boolean process(byte value) throws Exception { 39 | char nextByte = (char) (value & 0xFF); 40 | if (nextByte == HttpConstants.LF) { 41 | int len = seq.length(); 42 | if (len >= 1 && seq.charAtUnsafe(len - 1) == HttpConstants.CR) { 43 | seq.append(nextByte); 44 | } 45 | return false; 46 | } 47 | //continue loop byte 48 | seq.append(nextByte); 49 | return true; 50 | } 51 | } 52 | 53 | @Override 54 | protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) throws Exception { 55 | AppendableCharSequence seq = headLineByteProcessor.parse(in); 56 | if(seq.charAt(seq.length()-1) == HttpConstants.LF){ 57 | HttpProxyRequestHead httpProxyRequestHead; 58 | String[] splitInitialLine = splitInitialLine(seq); 59 | String method = splitInitialLine[0]; 60 | String uri = splitInitialLine[1]; 61 | String protocolVersion = splitInitialLine[2]; 62 | String host; 63 | int port; 64 | if(HttpMethod.CONNECT.name().equals(method)){ 65 | //https tunnel proxy 66 | HostAndPort hostAndPort = HostAndPort.fromString(uri); 67 | host = hostAndPort.getHost(); 68 | port = hostAndPort.getPort(); 69 | 70 | httpProxyRequestHead = new HttpProxyRequestHead(host, port, "TUNNEL",protocolVersion,null); 71 | }else{ 72 | //http proxy 73 | URL url = new URL(uri); 74 | host = url.getHost(); 75 | port = url.getPort(); 76 | if(port == -1){ 77 | port = 80; 78 | } 79 | 80 | httpProxyRequestHead = new HttpProxyRequestHead(host, port, protocolVersion,"WEB",in.resetReaderIndex()); 81 | } 82 | ctx.pipeline().addLast(new HttpServerConnectHandler()).remove(this); 83 | ctx.fireChannelRead(httpProxyRequestHead); 84 | } 85 | } 86 | 87 | private static String[] splitInitialLine(AppendableCharSequence sb) { 88 | int aStart; 89 | int aEnd; 90 | int bStart; 91 | int bEnd; 92 | int cStart; 93 | int cEnd; 94 | 95 | aStart = findNonSPLenient(sb, 0); 96 | aEnd = findSPLenient(sb, aStart); 97 | 98 | bStart = findNonSPLenient(sb, aEnd); 99 | bEnd = findSPLenient(sb, bStart); 100 | 101 | cStart = findNonSPLenient(sb, bEnd); 102 | cEnd = findEndOfString(sb); 103 | 104 | return new String[] { 105 | sb.subStringUnsafe(aStart, aEnd), 106 | sb.subStringUnsafe(bStart, bEnd), 107 | cStart < cEnd? sb.subStringUnsafe(cStart, cEnd) : "" }; 108 | } 109 | 110 | private static int findNonSPLenient(AppendableCharSequence sb, int offset) { 111 | for (int result = offset; result < sb.length(); ++result) { 112 | char c = sb.charAtUnsafe(result); 113 | // See https://tools.ietf.org/html/rfc7230#section-3.5 114 | if (isSPLenient(c)) { 115 | continue; 116 | } 117 | if (Character.isWhitespace(c)) { 118 | // Any other whitespace delimiter is invalid 119 | throw new IllegalArgumentException("Invalid separator"); 120 | } 121 | return result; 122 | } 123 | return sb.length(); 124 | } 125 | 126 | private static int findSPLenient(AppendableCharSequence sb, int offset) { 127 | for (int result = offset; result < sb.length(); ++result) { 128 | if (isSPLenient(sb.charAtUnsafe(result))) { 129 | return result; 130 | } 131 | } 132 | return sb.length(); 133 | } 134 | 135 | private static boolean isSPLenient(char c) { 136 | // See https://tools.ietf.org/html/rfc7230#section-3.5 137 | return c == ' ' || c == (char) 0x09 || c == (char) 0x0B || c == (char) 0x0C || c == (char) 0x0D; 138 | } 139 | 140 | private static int findNonWhitespace(AppendableCharSequence sb, int offset, boolean validateOWS) { 141 | for (int result = offset; result < sb.length(); ++result) { 142 | char c = sb.charAtUnsafe(result); 143 | if (!Character.isWhitespace(c)) { 144 | return result; 145 | } else if (validateOWS && !isOWS(c)) { 146 | // Only OWS is supported for whitespace 147 | throw new IllegalArgumentException("Invalid separator, only a single space or horizontal tab allowed," + 148 | " but received a '" + c + "'"); 149 | } 150 | } 151 | return sb.length(); 152 | } 153 | 154 | private static int findEndOfString(AppendableCharSequence sb) { 155 | for (int result = sb.length() - 1; result > 0; --result) { 156 | if (!Character.isWhitespace(sb.charAtUnsafe(result))) { 157 | return result + 1; 158 | } 159 | } 160 | return 0; 161 | } 162 | 163 | private static boolean isOWS(char ch) { 164 | return ch == ' ' || ch == (char) 0x09; 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/MixinProxyServer.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.EventLoopGroup; 6 | import io.netty.channel.nio.NioEventLoopGroup; 7 | import io.netty.channel.socket.nio.NioServerSocketChannel; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class MixinProxyServer { 12 | private Logger logger = LoggerFactory.getLogger(MixinProxyServer.class); 13 | 14 | private ServerBootstrap serverBootstrap; 15 | 16 | private EventLoopGroup serverEventLoopGroup; 17 | 18 | private Channel acceptorChannel; 19 | 20 | public void startServer(){ 21 | logger.info("Proxy Server starting..."); 22 | 23 | serverEventLoopGroup = new NioEventLoopGroup(4); 24 | 25 | serverBootstrap = new ServerBootstrap() 26 | .channel(NioServerSocketChannel.class) 27 | .childHandler(new MixinServerInitializer()) 28 | .group(serverEventLoopGroup); 29 | acceptorChannel = serverBootstrap.bind(8065).syncUninterruptibly().channel(); 30 | } 31 | 32 | public void shutdown(){ 33 | logger.info("Proxy Server shutting down..."); 34 | acceptorChannel.close().syncUninterruptibly(); 35 | serverEventLoopGroup.shutdownGracefully().syncUninterruptibly(); 36 | logger.info("shutdown completed!"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/MixinSelectHandler.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelPipeline; 6 | import io.netty.channel.SimpleChannelInboundHandler; 7 | import io.netty.handler.codec.socksx.SocksPortUnificationServerHandler; 8 | import io.netty.handler.codec.socksx.SocksVersion; 9 | 10 | public class MixinSelectHandler extends SimpleChannelInboundHandler { 11 | 12 | @Override 13 | protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { 14 | final int readerIndex = msg.readerIndex(); 15 | if (msg.writerIndex() == readerIndex) { 16 | return; 17 | } 18 | 19 | ChannelPipeline p = ctx.pipeline(); 20 | final byte versionVal = msg.getByte(readerIndex); 21 | 22 | SocksVersion version = SocksVersion.valueOf(versionVal); 23 | if(version.equals(SocksVersion.SOCKS4a) || version.equals(SocksVersion.SOCKS5)){ 24 | //socks proxy 25 | p.addLast(new SocksPortUnificationServerHandler(), 26 | SocksServerHandler.INSTANCE).remove(this); 27 | }else{ 28 | //http/tunnel proxy 29 | p.addLast(new HttpServerHeadDecoder()).remove(this); 30 | } 31 | msg.retain(); 32 | ctx.fireChannelRead(msg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/MixinServerInitializer.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.channel.ChannelInitializer; 4 | import io.netty.channel.socket.SocketChannel; 5 | import io.netty.handler.codec.socksx.SocksPortUnificationServerHandler; 6 | import io.netty.handler.logging.LogLevel; 7 | import io.netty.handler.logging.LoggingHandler; 8 | 9 | public final class MixinServerInitializer extends ChannelInitializer { 10 | @Override 11 | public void initChannel(SocketChannel ch) throws Exception { 12 | ch.pipeline().addLast( 13 | new LoggingHandler(LogLevel.DEBUG), 14 | new MixinSelectHandler()); 15 | } 16 | } -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/MixinServerUtils.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.ChannelFutureListener; 7 | 8 | public final class MixinServerUtils { 9 | 10 | /** 11 | * Closes the specified channel after all queued write requests are flushed. 12 | */ 13 | public static void closeOnFlush(Channel ch) { 14 | if (ch.isActive()) { 15 | ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 16 | } 17 | } 18 | 19 | private MixinServerUtils() { } 20 | } -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/RelayHandler.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.buffer.Unpooled; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelFutureListener; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.channel.ChannelInboundHandlerAdapter; 8 | import io.netty.util.ReferenceCountUtil; 9 | 10 | public class RelayHandler extends ChannelInboundHandlerAdapter { 11 | private final Channel relayChannel; 12 | 13 | public RelayHandler(Channel relayChannel) { 14 | this.relayChannel = relayChannel; 15 | } 16 | 17 | @Override 18 | public void channelActive(ChannelHandlerContext ctx) { 19 | ctx.writeAndFlush(Unpooled.EMPTY_BUFFER); 20 | } 21 | 22 | @Override 23 | public void channelRead(ChannelHandlerContext ctx, Object msg) { 24 | if (relayChannel.isActive()) { 25 | relayChannel.writeAndFlush(msg); 26 | } else { 27 | ReferenceCountUtil.release(msg); 28 | } 29 | } 30 | 31 | @Override 32 | public void channelInactive(ChannelHandlerContext ctx) { 33 | if (relayChannel.isActive()) { 34 | relayChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 35 | } 36 | } 37 | 38 | @Override 39 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 40 | cause.printStackTrace(); 41 | ctx.close(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/SocksServerConnectHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package cc.leevi.common.socks5proxy; 17 | 18 | import io.netty.bootstrap.Bootstrap; 19 | import io.netty.channel.*; 20 | import io.netty.channel.socket.nio.NioSocketChannel; 21 | import io.netty.handler.codec.socksx.SocksMessage; 22 | import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandResponse; 23 | import io.netty.handler.codec.socksx.v5.Socks5CommandRequest; 24 | import io.netty.handler.codec.socksx.v5.Socks5CommandStatus; 25 | import io.netty.util.concurrent.Future; 26 | import io.netty.util.concurrent.FutureListener; 27 | import io.netty.util.concurrent.Promise; 28 | 29 | @ChannelHandler.Sharable 30 | public final class SocksServerConnectHandler extends SimpleChannelInboundHandler { 31 | 32 | private final Bootstrap b = new Bootstrap(); 33 | 34 | @Override 35 | public void channelRead0(final ChannelHandlerContext ctx, final SocksMessage message) throws Exception { 36 | final Socks5CommandRequest request = (Socks5CommandRequest) message; 37 | 38 | Promise promise = ctx.executor().newPromise(); 39 | promise.addListener( 40 | new FutureListener() { 41 | @Override 42 | public void operationComplete(final Future future) throws Exception { 43 | final Channel outboundChannel = future.getNow(); 44 | if (future.isSuccess()) { 45 | ChannelFuture responseFuture = 46 | ctx.channel().writeAndFlush(new DefaultSocks5CommandResponse( 47 | Socks5CommandStatus.SUCCESS, 48 | request.dstAddrType(), 49 | request.dstAddr(), 50 | request.dstPort())); 51 | 52 | responseFuture.addListener(new ChannelFutureListener() { 53 | @Override 54 | public void operationComplete(ChannelFuture channelFuture) { 55 | ctx.pipeline().remove(SocksServerConnectHandler.this); 56 | outboundChannel.pipeline().addLast(new RelayHandler(ctx.channel())); 57 | ctx.pipeline().addLast(new RelayHandler(outboundChannel)); 58 | } 59 | }); 60 | } else { 61 | ctx.channel().writeAndFlush(new DefaultSocks5CommandResponse( 62 | Socks5CommandStatus.FAILURE, request.dstAddrType())); 63 | MixinServerUtils.closeOnFlush(ctx.channel()); 64 | } 65 | } 66 | }); 67 | 68 | final Channel inboundChannel = ctx.channel(); 69 | b.group(inboundChannel.eventLoop()) 70 | .channel(NioSocketChannel.class) 71 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) 72 | .option(ChannelOption.SO_KEEPALIVE, true) 73 | .handler(new DirectClientHandler(promise)); 74 | 75 | b.connect(request.dstAddr(), request.dstPort()).addListener(new ChannelFutureListener() { 76 | @Override 77 | public void operationComplete(ChannelFuture future) throws Exception { 78 | if (future.isSuccess()) { 79 | // Connection established use handler provided results 80 | } else { 81 | // Close the connection if the connection attempt has failed. 82 | ctx.channel().writeAndFlush( 83 | new DefaultSocks5CommandResponse(Socks5CommandStatus.FAILURE, request.dstAddrType())); 84 | MixinServerUtils.closeOnFlush(ctx.channel()); 85 | } 86 | } 87 | }); 88 | } 89 | 90 | @Override 91 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 92 | MixinServerUtils.closeOnFlush(ctx.channel()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /mixin-proxy/src/main/java/cc/leevi/common/socks5proxy/SocksServerHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package cc.leevi.common.socks5proxy; 17 | 18 | import io.netty.buffer.Unpooled; 19 | import io.netty.channel.ChannelHandler; 20 | import io.netty.channel.ChannelHandlerContext; 21 | import io.netty.channel.SimpleChannelInboundHandler; 22 | import io.netty.handler.codec.socksx.SocksMessage; 23 | import io.netty.handler.codec.socksx.SocksVersion; 24 | import io.netty.handler.codec.socksx.v5.*; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | @ChannelHandler.Sharable 29 | public final class SocksServerHandler extends SimpleChannelInboundHandler { 30 | 31 | private Logger logger = LoggerFactory.getLogger(SocksServerHandler.class); 32 | 33 | public static final SocksServerHandler INSTANCE = new SocksServerHandler(); 34 | 35 | private SocksServerHandler() { } 36 | 37 | @Override 38 | public void channelRead0(ChannelHandlerContext ctx, SocksMessage socksRequest) throws Exception { 39 | if(!socksRequest.version().equals(SocksVersion.SOCKS5)){ 40 | logger.error("only supports socks5 protocol!"); 41 | ctx.writeAndFlush(Unpooled.wrappedBuffer("protocol version illegal!".getBytes())); 42 | return ; 43 | } 44 | if (socksRequest instanceof Socks5InitialRequest) { 45 | ctx.pipeline().addFirst(new Socks5CommandRequestDecoder()); 46 | ctx.write(new DefaultSocks5InitialResponse(Socks5AuthMethod.NO_AUTH)); 47 | //如果需要密码,这里可以换成 48 | // ctx.write(new DefaultSocks5InitialResponse(Socks5AuthMethod.PASSWORD)); 49 | } else if (socksRequest instanceof Socks5PasswordAuthRequest) { 50 | //如果需要密码,这里需要验证密码 51 | ctx.pipeline().addFirst(new Socks5CommandRequestDecoder()); 52 | ctx.write(new DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus.SUCCESS)); 53 | } else if (socksRequest instanceof Socks5CommandRequest) { 54 | Socks5CommandRequest socks5CmdRequest = (Socks5CommandRequest) socksRequest; 55 | if (socks5CmdRequest.type() == Socks5CommandType.CONNECT) { 56 | ctx.pipeline().addLast(new SocksServerConnectHandler()); 57 | ctx.pipeline().remove(this); 58 | ctx.fireChannelRead(socksRequest); 59 | } else { 60 | ctx.close(); 61 | } 62 | } else { 63 | ctx.close(); 64 | } 65 | } 66 | 67 | @Override 68 | public void channelReadComplete(ChannelHandlerContext ctx) { 69 | ctx.flush(); 70 | } 71 | 72 | @Override 73 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable throwable) { 74 | logger.error("exceptionCaught",throwable); 75 | MixinServerUtils.closeOnFlush(ctx.channel()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /mixin-proxy/src/test/java/cc/leevi/common/socks5proxy/MixinProxyServerTest.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | 6 | import java.io.IOException; 7 | 8 | public class MixinProxyServerTest { 9 | 10 | @Before 11 | public void setUp() throws Exception { 12 | } 13 | 14 | @Test 15 | public void startServer() throws IOException { 16 | MixinProxyServer mixinProxyServer = new MixinProxyServer(); 17 | mixinProxyServer.startServer(); 18 | System.in.read(); 19 | } 20 | } -------------------------------------------------------------------------------- /mixin-proxy/src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cc.leevi.common 8 | netty-proxy-server 9 | pom 10 | 1.0-SNAPSHOT 11 | 12 | http-proxy 13 | socks5-proxy 14 | mixin-proxy 15 | 16 | 17 | 18 | 19 | 20 | io.netty 21 | netty-all 22 | 4.1.54.Final 23 | 24 | 25 | org.apache.commons 26 | commons-lang3 27 | 3.11 28 | 29 | 30 | org.slf4j 31 | slf4j-api 32 | 1.7.30 33 | 34 | 35 | org.apache.logging.log4j 36 | log4j-core 37 | 2.14.0 38 | 39 | 40 | org.apache.logging.log4j 41 | log4j-slf4j-impl 42 | 2.14.0 43 | test 44 | 45 | 46 | junit 47 | junit 48 | 4.13.1 49 | test 50 | 51 | 52 | com.google.guava 53 | guava 54 | 30.0-jre 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /socks5-proxy/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | 9 | netty-proxy-server 10 | cc.leevi.common 11 | 1.0-SNAPSHOT 12 | 13 | 14 | socks5-proxy 15 | 16 | socks5-proxy 17 | 18 | 19 | UTF-8 20 | 1.7 21 | 1.7 22 | 23 | 24 | 25 | 26 | io.netty 27 | netty-all 28 | 29 | 30 | org.apache.commons 31 | commons-lang3 32 | 33 | 34 | org.slf4j 35 | slf4j-api 36 | 37 | 38 | junit 39 | junit 40 | 4.13.1 41 | test 42 | 43 | 44 | org.apache.logging.log4j 45 | log4j-core 46 | 2.14.0 47 | test 48 | 49 | 50 | org.apache.logging.log4j 51 | log4j-slf4j-impl 52 | 2.14.0 53 | test 54 | 55 | 56 | com.google.guava 57 | guava 58 | 30.0-jre 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /socks5-proxy/src/main/java/cc/leevi/common/socks5proxy/DirectClientHandler.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.channel.Channel; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.ChannelInboundHandlerAdapter; 6 | import io.netty.util.concurrent.Promise; 7 | 8 | public final class DirectClientHandler extends ChannelInboundHandlerAdapter { 9 | 10 | private final Promise promise; 11 | 12 | public DirectClientHandler(Promise promise) { 13 | this.promise = promise; 14 | } 15 | 16 | @Override 17 | public void channelActive(ChannelHandlerContext ctx) { 18 | ctx.pipeline().remove(this); 19 | promise.setSuccess(ctx.channel()); 20 | } 21 | 22 | @Override 23 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable throwable) { 24 | promise.setFailure(throwable); 25 | } 26 | } -------------------------------------------------------------------------------- /socks5-proxy/src/main/java/cc/leevi/common/socks5proxy/RelayHandler.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.buffer.Unpooled; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelFutureListener; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.channel.ChannelInboundHandlerAdapter; 8 | import io.netty.util.ReferenceCountUtil; 9 | 10 | public class RelayHandler extends ChannelInboundHandlerAdapter { 11 | private final Channel relayChannel; 12 | 13 | public RelayHandler(Channel relayChannel) { 14 | this.relayChannel = relayChannel; 15 | } 16 | 17 | @Override 18 | public void channelActive(ChannelHandlerContext ctx) { 19 | ctx.writeAndFlush(Unpooled.EMPTY_BUFFER); 20 | } 21 | 22 | @Override 23 | public void channelRead(ChannelHandlerContext ctx, Object msg) { 24 | if (relayChannel.isActive()) { 25 | relayChannel.writeAndFlush(msg); 26 | } else { 27 | ReferenceCountUtil.release(msg); 28 | } 29 | } 30 | 31 | @Override 32 | public void channelInactive(ChannelHandlerContext ctx) { 33 | if (relayChannel.isActive()) { 34 | relayChannel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 35 | } 36 | } 37 | 38 | @Override 39 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 40 | cause.printStackTrace(); 41 | ctx.close(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /socks5-proxy/src/main/java/cc/leevi/common/socks5proxy/Socks5ProxyServer.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.bootstrap.ServerBootstrap; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.EventLoopGroup; 6 | import io.netty.channel.nio.NioEventLoopGroup; 7 | import io.netty.channel.socket.nio.NioServerSocketChannel; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class Socks5ProxyServer { 12 | private Logger logger = LoggerFactory.getLogger(Socks5ProxyServer.class); 13 | 14 | private ServerBootstrap serverBootstrap; 15 | 16 | private EventLoopGroup serverEventLoopGroup; 17 | 18 | private Channel acceptorChannel; 19 | 20 | public void startServer(){ 21 | logger.info("Proxy Server starting..."); 22 | 23 | serverEventLoopGroup = new NioEventLoopGroup(4); 24 | 25 | serverBootstrap = new ServerBootstrap() 26 | .channel(NioServerSocketChannel.class) 27 | .childHandler(new SocksServerInitializer()) 28 | .group(serverEventLoopGroup); 29 | acceptorChannel = serverBootstrap.bind(1080).syncUninterruptibly().channel(); 30 | } 31 | 32 | public void shutdown(){ 33 | logger.info("Proxy Server shutting down..."); 34 | acceptorChannel.close().syncUninterruptibly(); 35 | serverEventLoopGroup.shutdownGracefully().syncUninterruptibly(); 36 | logger.info("shutdown completed!"); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /socks5-proxy/src/main/java/cc/leevi/common/socks5proxy/SocksServerConnectHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package cc.leevi.common.socks5proxy; 17 | 18 | import io.netty.bootstrap.Bootstrap; 19 | import io.netty.buffer.Unpooled; 20 | import io.netty.channel.Channel; 21 | import io.netty.channel.ChannelFuture; 22 | import io.netty.channel.ChannelFutureListener; 23 | import io.netty.channel.ChannelHandler; 24 | import io.netty.channel.ChannelHandlerContext; 25 | import io.netty.channel.ChannelOption; 26 | import io.netty.channel.SimpleChannelInboundHandler; 27 | import io.netty.channel.socket.nio.NioSocketChannel; 28 | import io.netty.handler.codec.socksx.SocksMessage; 29 | import io.netty.handler.codec.socksx.v4.DefaultSocks4CommandResponse; 30 | import io.netty.handler.codec.socksx.v4.Socks4CommandRequest; 31 | import io.netty.handler.codec.socksx.v4.Socks4CommandStatus; 32 | import io.netty.handler.codec.socksx.v5.DefaultSocks5CommandResponse; 33 | import io.netty.handler.codec.socksx.v5.Socks5CommandRequest; 34 | import io.netty.handler.codec.socksx.v5.Socks5CommandStatus; 35 | import io.netty.util.concurrent.Future; 36 | import io.netty.util.concurrent.FutureListener; 37 | import io.netty.util.concurrent.Promise; 38 | 39 | @ChannelHandler.Sharable 40 | public final class SocksServerConnectHandler extends SimpleChannelInboundHandler { 41 | 42 | private final Bootstrap b = new Bootstrap(); 43 | 44 | @Override 45 | public void channelRead0(final ChannelHandlerContext ctx, final SocksMessage message) throws Exception { 46 | final Socks5CommandRequest request = (Socks5CommandRequest) message; 47 | 48 | Promise promise = ctx.executor().newPromise(); 49 | promise.addListener( 50 | new FutureListener() { 51 | @Override 52 | public void operationComplete(final Future future) throws Exception { 53 | final Channel outboundChannel = future.getNow(); 54 | if (future.isSuccess()) { 55 | ChannelFuture responseFuture = 56 | ctx.channel().writeAndFlush(new DefaultSocks5CommandResponse( 57 | Socks5CommandStatus.SUCCESS, 58 | request.dstAddrType(), 59 | request.dstAddr(), 60 | request.dstPort())); 61 | 62 | responseFuture.addListener(new ChannelFutureListener() { 63 | @Override 64 | public void operationComplete(ChannelFuture channelFuture) { 65 | ctx.pipeline().remove(SocksServerConnectHandler.this); 66 | outboundChannel.pipeline().addLast(new RelayHandler(ctx.channel())); 67 | ctx.pipeline().addLast(new RelayHandler(outboundChannel)); 68 | } 69 | }); 70 | } else { 71 | ctx.channel().writeAndFlush(new DefaultSocks5CommandResponse( 72 | Socks5CommandStatus.FAILURE, request.dstAddrType())); 73 | SocksServerUtils.closeOnFlush(ctx.channel()); 74 | } 75 | } 76 | }); 77 | 78 | final Channel inboundChannel = ctx.channel(); 79 | b.group(inboundChannel.eventLoop()) 80 | .channel(NioSocketChannel.class) 81 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) 82 | .option(ChannelOption.SO_KEEPALIVE, true) 83 | .handler(new DirectClientHandler(promise)); 84 | 85 | b.connect(request.dstAddr(), request.dstPort()).addListener(new ChannelFutureListener() { 86 | @Override 87 | public void operationComplete(ChannelFuture future) throws Exception { 88 | if (future.isSuccess()) { 89 | // Connection established use handler provided results 90 | } else { 91 | // Close the connection if the connection attempt has failed. 92 | ctx.channel().writeAndFlush( 93 | new DefaultSocks5CommandResponse(Socks5CommandStatus.FAILURE, request.dstAddrType())); 94 | SocksServerUtils.closeOnFlush(ctx.channel()); 95 | } 96 | } 97 | }); 98 | } 99 | 100 | @Override 101 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 102 | SocksServerUtils.closeOnFlush(ctx.channel()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /socks5-proxy/src/main/java/cc/leevi/common/socks5proxy/SocksServerHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012 The Netty Project 3 | * 4 | * The Netty Project licenses this file to you under the Apache License, 5 | * version 2.0 (the "License"); you may not use this file except in compliance 6 | * with the License. You may obtain a copy of the License at: 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | package cc.leevi.common.socks5proxy; 17 | 18 | import io.netty.buffer.Unpooled; 19 | import io.netty.channel.ChannelHandler; 20 | import io.netty.channel.ChannelHandlerContext; 21 | import io.netty.channel.SimpleChannelInboundHandler; 22 | import io.netty.handler.codec.socksx.SocksMessage; 23 | import io.netty.handler.codec.socksx.SocksVersion; 24 | import io.netty.handler.codec.socksx.v4.Socks4CommandRequest; 25 | import io.netty.handler.codec.socksx.v4.Socks4CommandType; 26 | import io.netty.handler.codec.socksx.v5.*; 27 | import org.slf4j.Logger; 28 | import org.slf4j.LoggerFactory; 29 | 30 | @ChannelHandler.Sharable 31 | public final class SocksServerHandler extends SimpleChannelInboundHandler { 32 | 33 | private Logger logger = LoggerFactory.getLogger(SocksServerHandler.class); 34 | 35 | public static final SocksServerHandler INSTANCE = new SocksServerHandler(); 36 | 37 | private SocksServerHandler() { } 38 | 39 | @Override 40 | public void channelRead0(ChannelHandlerContext ctx, SocksMessage socksRequest) throws Exception { 41 | if(!socksRequest.version().equals(SocksVersion.SOCKS5)){ 42 | logger.error("only supports socks5 protocol!"); 43 | ctx.writeAndFlush(Unpooled.wrappedBuffer("protocol version illegal!".getBytes())); 44 | return ; 45 | } 46 | if (socksRequest instanceof Socks5InitialRequest) { 47 | ctx.pipeline().addFirst(new Socks5CommandRequestDecoder()); 48 | ctx.write(new DefaultSocks5InitialResponse(Socks5AuthMethod.NO_AUTH)); 49 | //如果需要密码,这里可以换成 50 | // ctx.write(new DefaultSocks5InitialResponse(Socks5AuthMethod.PASSWORD)); 51 | } else if (socksRequest instanceof Socks5PasswordAuthRequest) { 52 | //如果需要密码,这里需要验证密码 53 | ctx.pipeline().addFirst(new Socks5CommandRequestDecoder()); 54 | ctx.write(new DefaultSocks5PasswordAuthResponse(Socks5PasswordAuthStatus.SUCCESS)); 55 | } else if (socksRequest instanceof Socks5CommandRequest) { 56 | Socks5CommandRequest socks5CmdRequest = (Socks5CommandRequest) socksRequest; 57 | if (socks5CmdRequest.type() == Socks5CommandType.CONNECT) { 58 | ctx.pipeline().addLast(new SocksServerConnectHandler()); 59 | ctx.pipeline().remove(this); 60 | ctx.fireChannelRead(socksRequest); 61 | } else { 62 | ctx.close(); 63 | } 64 | } else { 65 | ctx.close(); 66 | } 67 | } 68 | 69 | @Override 70 | public void channelReadComplete(ChannelHandlerContext ctx) { 71 | ctx.flush(); 72 | } 73 | 74 | @Override 75 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable throwable) { 76 | logger.error("exceptionCaught",throwable); 77 | SocksServerUtils.closeOnFlush(ctx.channel()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /socks5-proxy/src/main/java/cc/leevi/common/socks5proxy/SocksServerInitializer.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import io.netty.channel.ChannelInitializer; 4 | import io.netty.channel.socket.SocketChannel; 5 | import io.netty.handler.codec.socksx.SocksPortUnificationServerHandler; 6 | import io.netty.handler.logging.LogLevel; 7 | import io.netty.handler.logging.LoggingHandler; 8 | 9 | public final class SocksServerInitializer extends ChannelInitializer { 10 | @Override 11 | public void initChannel(SocketChannel ch) throws Exception { 12 | ch.pipeline().addLast( 13 | new LoggingHandler(LogLevel.DEBUG), 14 | new SocksPortUnificationServerHandler(), 15 | SocksServerHandler.INSTANCE); 16 | } 17 | } -------------------------------------------------------------------------------- /socks5-proxy/src/main/java/cc/leevi/common/socks5proxy/SocksServerUtils.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.ChannelFutureListener; 7 | 8 | public final class SocksServerUtils { 9 | 10 | /** 11 | * Closes the specified channel after all queued write requests are flushed. 12 | */ 13 | public static void closeOnFlush(Channel ch) { 14 | if (ch.isActive()) { 15 | ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 16 | } 17 | } 18 | 19 | private SocksServerUtils() { } 20 | } -------------------------------------------------------------------------------- /socks5-proxy/src/test/java/cc/leevi/common/socks5proxy/Socks5ProxyServerTest.java: -------------------------------------------------------------------------------- 1 | package cc.leevi.common.socks5proxy; 2 | 3 | import com.google.common.net.HostAndPort; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | 7 | import java.io.IOException; 8 | 9 | import static org.junit.Assert.*; 10 | 11 | public class Socks5ProxyServerTest { 12 | 13 | @Before 14 | public void setUp() throws Exception { 15 | } 16 | 17 | @Test 18 | public void startServer() throws IOException { 19 | Socks5ProxyServer socks5ProxyServer = new Socks5ProxyServer(); 20 | socks5ProxyServer.startServer(); 21 | System.in.read(); 22 | } 23 | } -------------------------------------------------------------------------------- /socks5-proxy/src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | --------------------------------------------------------------------------------