/* * Copyright 2017 ZhangJiupeng * * Licensed 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 cc.agentx.client.net.nio; import cc.agentx.client.Configuration; import cc.agentx.protocol.request.XRequestWrapper; import cc.agentx.wrapper.Wrapper; import cc.agentx.wrapper.WrapperFactory; import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.socks.SocksCmdRequest; import io.netty.handler.codec.socks.SocksCmdResponse; import io.netty.handler.codec.socks.SocksCmdStatus; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.Promise; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; @ChannelHandler.Sharable public final class XConnectHandler extends SimpleChannelInboundHandler<SocksCmdRequest> { private static final InternalLogger log; private static final Wrapper rawWrapper; static { log = InternalLoggerFactory.getInstance(XConnectHandler.class); rawWrapper = WrapperFactory.newRawWrapperInstance(); } private final Bootstrap bootstrap = new Bootstrap(); private final Configuration config; private final XRequestWrapper requestWrapper; private final boolean exposeRequest; private final Wrapper wrapper; public XConnectHandler() { this.config = Configuration.INSTANCE; this.requestWrapper = config.getXRequestWrapper(); this.exposeRequest = requestWrapper.exposeRequest(); this.wrapper = config.getWrapper(); } @Override public void channelRead0(final ChannelHandlerContext ctx, final SocksCmdRequest request) throws Exception { boolean proxyMode = isAgentXNeeded(request.host()); log.info("\tClient -> Proxy \tTarget {}:{} [{}]", request.host(), request.port(), proxyMode ? "AGENTX" : "DIRECT"); Promise<Channel> promise = ctx.executor().newPromise(); promise.addListener( new FutureListener<Channel>() { @Override public void operationComplete(final Future<Channel> future) throws Exception { final Channel outboundChannel = future.getNow(); if (future.isSuccess()) { ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.SUCCESS, request.addressType())) .addListener(channelFuture -> { ByteBuf byteBuf = Unpooled.buffer(); request.encodeAsByteBuf(byteBuf); if (byteBuf.hasArray()) { byte[] xRequestBytes = new byte[byteBuf.readableBytes()]; byteBuf.getBytes(0, xRequestBytes); if (proxyMode) { // handshaking to remote proxy xRequestBytes = requestWrapper.wrap(xRequestBytes); outboundChannel.writeAndFlush(Unpooled.wrappedBuffer( exposeRequest ? xRequestBytes : wrapper.wrap(xRequestBytes) )); } // task handover ReferenceCountUtil.retain(request); // auto-release? a trap? ctx.pipeline().remove(XConnectHandler.this); outboundChannel.pipeline().addLast(new XRelayHandler(ctx.channel(), proxyMode ? wrapper : rawWrapper, false)); ctx.pipeline().addLast(new XRelayHandler(outboundChannel, proxyMode ? wrapper : rawWrapper, true)); } }); } else { ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.FAILURE, request.addressType())); if (ctx.channel().isActive()) { ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } } } } ); String host = request.host(); int port = request.port(); if (host.equals(config.getConsoleDomain())) { host = "localhost"; port = config.getConsolePort(); } else if (proxyMode) { host = config.getServerHost(); port = config.getServerPort(); } // ping target bootstrap.group(ctx.channel().eventLoop()) .channel(NioSocketChannel.class) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) .option(ChannelOption.SO_KEEPALIVE, true) .handler(new XPingHandler(promise, System.currentTimeMillis())) .connect(host, port) .addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.FAILURE, request.addressType())); if (ctx.channel().isActive()) { ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } } } }); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (ctx.channel().isActive()) { ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); } log.warn("\tBad Connection! ({})", cause.getMessage()); } private boolean isAgentXNeeded(String domain) { // this method is reserved for pac querying return !domain.equals(config.getConsoleDomain()) && !(domain.equals("localhost") || domain.equals("127.0.0.1")) && !config.getMode().equals("socks5"); } }