Proxyee代码阅读

简介: 本篇文章主要讲述了对开源项目proxyee的代码阅读

背景信息

主要功能

Proxyee 是一个 JAVA 编写的 HTTP 代理服务器类库,支持 HTTP、HTTPS、Websocket 协议,并且支持 MITM(中间人攻击),可以对 HTTP、HTTPS 协议的报文进行捕获和篡改。

获取地址

https://github.com/monkeyWie/proxyee

使用方式

在pom中加入如下依赖

<dependency>
    <groupId>com.github.monkeywie</groupId>
    <artifactId>proxyee</artifactId>
    <version>1.5.4</version>
</dependency>

源码阅读

代码结构

java模块

image.png
一个很清晰简单的单模块maven项目

test模块

image.png
这里面包含了十余种示例来示范如何使用proxyee

pom依赖

<dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-buffer</artifactId>
            <version>${netty.version}</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-codec</artifactId>
            <version>${netty.version}</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-codec-http</artifactId>
            <version>${netty.version}</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-handler</artifactId>
            <version>${netty.version}</version>
        </dependency>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-handler-proxy</artifactId>
            <version>${netty.version}</version>
        </dependency>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk15on</artifactId>
            <version>1.58</version>
        </dependency>
    </dependencies>

依赖内容相当简洁,只有netty和bouncycastle
引入netty模块主要是用于代理服务器的实现
bouncycastle是一种用于 Java 平台的开放源码的轻量级密码术包;它支持大量的密码术算法,并提供JCE 1.2.1的实现。

启动

    public static void main(String[] args) throws Exception {
        HttpProxyServerConfig config = new HttpProxyServerConfig();
        config.setHandleSsl(true);
        new HttpProxyServer()
                .serverConfig(config)
               .proxyInterceptInitializer(new HttpProxyInterceptInitializer() {
                    @Override
                    public void init(HttpProxyInterceptPipeline pipeline) {
                        pipeline.addLast(new CertDownIntercept());  //处理证书下载
                        pipeline.addLast(new HttpProxyIntercept() {
                            @Override
                            public void beforeRequest(Channel clientChannel, HttpRequest httpRequest,
                                                      HttpProxyInterceptPipeline pipeline) throws Exception {
                                //替换UA,伪装成手机浏览器
                /*httpRequest.headers().set(HttpHeaderNames.USER_AGENT,
                    "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1");*/
                                //转到下一个拦截器处理
                                pipeline.beforeRequest(clientChannel, httpRequest);
                            }

                            @Override
                            public void afterResponse(Channel clientChannel, Channel proxyChannel,
                                                      HttpResponse httpResponse, HttpProxyInterceptPipeline pipeline) throws Exception {

                                //拦截响应,添加一个响应头
                                httpResponse.headers().add("intercept", "test");
                                //转到下一个拦截器处理
                                pipeline.afterResponse(clientChannel, proxyChannel, httpResponse);
                            }
                        });
                    }
                })
                .httpProxyExceptionHandle(new HttpProxyExceptionHandle() {
                    @Override
                    public void beforeCatch(Channel clientChannel, Throwable cause) throws Exception {
                        cause.printStackTrace();
                    }

                    @Override
                    public void afterCatch(Channel clientChannel, Channel proxyChannel, Throwable cause)
                            throws Exception {
                        cause.printStackTrace();
                    }
                })
                .start(9999);
    }

这是代码中Test模块的InterceptHttpProxyServer类,这个类提供了调用的示例,我们以此来研究服务启动的过程。
首先我们来看start方法

  public void start(int port) {
        start(null, port);
    }

    public void start(String ip, int port) {
        try {
            ChannelFuture channelFuture = doBind(ip, port);
            CountDownLatch latch = new CountDownLatch(1);
            channelFuture.addListener(future -> {
                if (future.cause() != null) {
                    httpProxyExceptionHandle.startCatch(future.cause());
                }
                latch.countDown();
            });
            latch.await();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            httpProxyExceptionHandle.startCatch(e);
        } finally {
            close();
        }
    }

我们来看doBind方法中做了些什么

private ChannelFuture doBind(String ip, int port) {
        init();
        bossGroup = new NioEventLoopGroup(serverConfig.getBossGroupThreads());
        workerGroup = new NioEventLoopGroup(serverConfig.getWorkerGroupThreads());
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
//                .option(ChannelOption.SO_BACKLOG, 100)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .childHandler(new ChannelInitializer<Channel>() {

                    @Override
                    protected void initChannel(Channel ch) throws Exception {
                        ch.pipeline().addLast("httpCodec", new HttpServerCodec());
                        ch.pipeline().addLast("serverHandle",
                                new HttpProxyServerHandler(serverConfig, proxyInterceptInitializer, proxyConfig,
                                        httpProxyExceptionHandle));
                    }
                });

        return ip == null ? bootstrap.bind(port) : bootstrap.bind(ip, port);
    }

首先调用了init()方法,

private void init() {
        if (serverConfig == null) {
            serverConfig = new HttpProxyServerConfig();
        }
        serverConfig.setProxyLoopGroup(new NioEventLoopGroup(serverConfig.getProxyGroupThreads()));

        if (serverConfig.isHandleSsl()) {
            try {
                serverConfig.setClientSslCtx(
                        SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE)
                                .build());
                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                X509Certificate caCert;
                PrivateKey caPriKey;
                if (caCertFactory == null) {
                    caCert = CertUtil.loadCert(classLoader.getResourceAsStream("ca.crt"));
                    caPriKey = CertUtil.loadPriKey(classLoader.getResourceAsStream("ca_private.der"));
                } else {
                    caCert = caCertFactory.getCACert();
                    caPriKey = caCertFactory.getCAPriKey();
                }
                //读取CA证书使用者信息
                serverConfig.setIssuer(CertUtil.getSubject(caCert));
                //读取CA证书有效时段(server证书有效期超出CA证书的,在手机上会提示证书不安全)
                serverConfig.setCaNotBefore(caCert.getNotBefore());
                serverConfig.setCaNotAfter(caCert.getNotAfter());
                //CA私钥用于给动态生成的网站SSL证书签证
                serverConfig.setCaPriKey(caPriKey);
                //生产一对随机公私钥用于网站SSL证书动态创建
                KeyPair keyPair = CertUtil.genKeyPair();
                serverConfig.setServerPriKey(keyPair.getPrivate());
                serverConfig.setServerPubKey(keyPair.getPublic());
            } catch (Exception e) {
                serverConfig.setHandleSsl(false);
                log.warn("SSL init fail,cause:" + e.getMessage());
            }
        }
        if (proxyInterceptInitializer == null) {
            proxyInterceptInitializer = new HttpProxyInterceptInitializer();
        }
        if (httpProxyExceptionHandle == null) {
            httpProxyExceptionHandle = new HttpProxyExceptionHandle();
        }
    }

在这里主要是进行了SSL证书相关的操作和初始化,对proxyInterceptInitializer和httpProxyExceptionHandle进行了判空处理,这里在此示例的main方法中已经进行了初始化。

    public HttpProxyServer proxyInterceptInitializer(
            HttpProxyInterceptInitializer proxyInterceptInitializer) {
        this.proxyInterceptInitializer = proxyInterceptInitializer;
        return this;
    }

    public HttpProxyServer httpProxyExceptionHandle(
            HttpProxyExceptionHandle httpProxyExceptionHandle) {
        this.httpProxyExceptionHandle = httpProxyExceptionHandle;
        return this;
    }

接着我们来看一下对于netty的操作

bossGroup = new NioEventLoopGroup(serverConfig.getBossGroupThreads());
workerGroup = new NioEventLoopGroup(serverConfig.getWorkerGroupThreads());

那么NioEventLoopGroup是何方神圣呢?
image.png
从这个类的名字来看,NioEventLoopGroup,它是一个无限循环(Loop),在循环中不断处理接收到的事件(Event),从设计上来看,EventLoop采用了一种协同设计,它建立在两个基本的API之上:Concurrent和Channel,也就是并发和网络。并发是因为它采用了线程池来管理大量的任务,并且这些任务可以并发的执行。其继承了EventExecutor接口,而EventExecutor就是一个事件的执行器。另外为了与Channel的事件进行交互,EventLoop继承了EventLoopGroup接口。
一个Netty服务端启动时,通常会有两个NioEventLoopGroup:一个是监听线程组,主要是监听客户端请求,另一个是工作线程组,主要是处理与客户端的数据
在这里,bossGroup专门用于监听客户端的连接事件,连接成功以后,workerGroup则会负责连接中发生的IO事件
最后用netty中的bootStrap启动了netty的服务端,并且建立了一个channel。

                                         @Override
                    protected void initChannel(Channel ch) throws Exception {
                        ch.pipeline().addLast("httpCodec", new HttpServerCodec());
                        ch.pipeline().addLast("serverHandle",
                                new HttpProxyServerHandler(serverConfig, proxyInterceptInitializer, proxyConfig,
                                        httpProxyExceptionHandle));
                    }

在这里addLast中添加的是作者自定义的Netty IO handler。
第一步,添加了一个httpCodec的handler。这是Netty自带的提供http服务的handler。它是一个“全双工”handler,也就是既是一个InHandler,也是一个outHandler,负责把接收到的二进制请求反序列化成Http Request对象,并序列化Http Response。

然后,添加了一个“serverHandler”,类型是HttpProxyServerHandler,它也继承了Netty的ChannelInboundHandlerAdapter,所以它是一个处理IO请求的“拦截器”。

代理

我们主要来看HttpProxyServerHandler 这个类
他继承了ChannelInboundHandlerAdapter
我们来看作者override的方法

    @Override
    public void channelRead(final ChannelHandlerContext ctx, final Object msg) throws Exception {
        if (msg instanceof HttpRequest) {
            HttpRequest request = (HttpRequest) msg;
            // 第一次建立连接取host和端口号和处理代理握手
            if (status == 0) {
                requestProto = ProtoUtil.getRequestProto(request);
                if (requestProto == null) { // bad request
                    ctx.channel().close();
                    return;
                }
                // 首次连接处理
                if (serverConfig.getHttpProxyAcceptHandler() != null
                        && !serverConfig.getHttpProxyAcceptHandler().onAccept(request, ctx.channel())) {
                    status = 2;
                    ctx.channel().close();
                    return;
                }
                // 代理身份验证
                if (!authenticate(ctx, request)) {
                    status = 2;
                    ctx.channel().close();
                    return;
                }
                status = 1;
                if ("CONNECT".equalsIgnoreCase(request.method().name())) {// 建立代理握手
                    status = 2;
                    HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpProxyServer.SUCCESS);
                    ctx.writeAndFlush(response);
                    ctx.channel().pipeline().remove("httpCodec");
                    // fix issue #42
                    ReferenceCountUtil.release(msg);
                    return;
                }
            }
            interceptPipeline = buildPipeline();
            interceptPipeline.setRequestProto(requestProto.copy());
            // fix issue #27
            if (request.uri().indexOf("/") != 0) {
                URL url = new URL(request.uri());
                request.setUri(url.getFile());
            }
            interceptPipeline.beforeRequest(ctx.channel(), request);
        } else if (msg instanceof HttpContent) {
            if (status != 2) {
                interceptPipeline.beforeRequest(ctx.channel(), (HttpContent) msg);
            } else {
                ReferenceCountUtil.release(msg);
                status = 1;
            }
        } else { // ssl和websocket的握手处理
            if (serverConfig.isHandleSsl()) {
                ByteBuf byteBuf = (ByteBuf) msg;
                if (byteBuf.getByte(0) == 22) {// ssl握手
                    requestProto.setSsl(true);
                    int port = ((InetSocketAddress) ctx.channel().localAddress()).getPort();
                    SslContext sslCtx = SslContextBuilder
                            .forServer(serverConfig.getServerPriKey(), CertPool.getCert(port, requestProto.getHost(), serverConfig)).build();
                    ctx.pipeline().addFirst("httpCodec", new HttpServerCodec());
                    ctx.pipeline().addFirst("sslHandle", sslCtx.newHandler(ctx.alloc()));
                    // 重新过一遍pipeline,拿到解密后的的http报文
                    ctx.pipeline().fireChannelRead(msg);
                    return;
                }
            }
            handleProxyData(ctx.channel(), msg, false);
        }
    }

在这里进行了一个判断msg是HttpRequest还是HttpContent或者都不是的情况。
HttpRequest时:
首先进行了是否是首次链接的判断,并进行链接和握手的处理包括身份的认证。然后初始化proxyee自定义的pipeline,最后调用beforeRequest(HttpRequest  msg)方法
我们来看看这个方法

public void beforeRequest(Channel clientChannel, HttpRequest httpRequest) throws Exception {
        this.httpRequest = httpRequest;
        if (this.posBeforeHead < intercepts.size()) {
            HttpProxyIntercept intercept = intercepts.get(this.posBeforeHead++);
            intercept.beforeRequest(clientChannel, this.httpRequest, this);
        }
        this.posBeforeHead = 0;
    }

这里采用递归的方法去把intercepts中存储的所有intercept拿出来并执行

HttpContent时:
调用了beforeRequest(HttpContent msg)方法

 public void beforeRequest(Channel clientChannel, HttpContent httpContent) throws Exception {
        if (this.posBeforeContent < intercepts.size()) {
            HttpProxyIntercept intercept = intercepts.get(this.posBeforeContent++);
            intercept.beforeRequest(clientChannel, httpContent, this);
        }
        this.posBeforeContent = 0;
    }

这里与上面beforeRequest(HttpRequest msg)没有太多区别

两者都不是时:
首先进行了ssl和websocket的握手处理,然后进行了proxyData的处理

我们再来看下HttpProxyInterceptPipeline这个作者自定义的pipeline,实现了Iterable接口,用于遍历HttpProxyIntercept。

    @Override
    public Iterator<HttpProxyIntercept> iterator() {
        return intercepts.iterator();
    }

我们再来看一下如何建立pipeline的

 private HttpProxyInterceptPipeline buildPipeline() {
        HttpProxyInterceptPipeline interceptPipeline = new HttpProxyInterceptPipeline(new HttpProxyIntercept() {
            @Override
            public void beforeRequest(Channel clientChannel, HttpRequest httpRequest, HttpProxyInterceptPipeline pipeline)
                    throws Exception {
                handleProxyData(clientChannel, httpRequest, true);
            }

            @Override
            public void beforeRequest(Channel clientChannel, HttpContent httpContent, HttpProxyInterceptPipeline pipeline)
                    throws Exception {
                handleProxyData(clientChannel, httpContent, true);
            }

            @Override
            public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpResponse httpResponse,
                                      HttpProxyInterceptPipeline pipeline) throws Exception {
                clientChannel.writeAndFlush(httpResponse);
                if (HttpHeaderValues.WEBSOCKET.toString().equals(httpResponse.headers().get(HttpHeaderNames.UPGRADE))) {
                    // websocket转发原始报文
                    proxyChannel.pipeline().remove("httpCodec");
                    clientChannel.pipeline().remove("httpCodec");
                }
            }

            @Override
            public void afterResponse(Channel clientChannel, Channel proxyChannel, HttpContent httpContent,
                                      HttpProxyInterceptPipeline pipeline) throws Exception {
                clientChannel.writeAndFlush(httpContent);
            }
        });
        interceptInitializer.init(interceptPipeline);
        return interceptPipeline;
    }

这里加入了一个默认的HttpProxyIntercept,置于pipeline的队尾
用户在main方法中自定义的intercept实际上都在这个pipeline中,从第一个开始执行到默认的这个Intercept结束

handleProxyData中又干了写什么呢

private void handleProxyData(Channel channel, Object msg, boolean isHttp) throws Exception {
        if (cf == null) {
            // connection异常 还有HttpContent进来,不转发
            if (isHttp && !(msg instanceof HttpRequest)) {
                return;
            }
            if (interceptPipeline == null) {
                interceptPipeline = buildOnlyConnectPipeline();
                interceptPipeline.setRequestProto(requestProto.copy());
            }
            interceptPipeline.beforeConnect(channel);

            // by default we use the proxy config set in the pipeline
            ProxyHandler proxyHandler = ProxyHandleFactory.build(interceptPipeline.getProxyConfig() == null ?
                    proxyConfig : interceptPipeline.getProxyConfig());

            RequestProto requestProto = interceptPipeline.getRequestProto();
            if (isHttp) {
                HttpRequest httpRequest = (HttpRequest) msg;
                // 检查requestProto是否有修改
                RequestProto newRP = ProtoUtil.getRequestProto(httpRequest);
                if (!newRP.equals(requestProto)) {
                    // 更新Host请求头
                    if ((requestProto.getSsl() && requestProto.getPort() == 443)
                            || (!requestProto.getSsl() && requestProto.getPort() == 80)) {
                        httpRequest.headers().set(HttpHeaderNames.HOST, requestProto.getHost());
                    } else {
                        httpRequest.headers().set(HttpHeaderNames.HOST, requestProto.getHost() + ":" + requestProto.getPort());
                    }
                }
            }

            /*
             * 添加SSL client hello的Server Name Indication extension(SNI扩展) 有些服务器对于client
             * hello不带SNI扩展时会直接返回Received fatal alert: handshake_failure(握手错误)
             * 例如:https://cdn.mdn.mozilla.net/static/img/favicon32.7f3da72dcea1.png
             */
            ChannelInitializer channelInitializer = isHttp ? new HttpProxyInitializer(channel, requestProto, proxyHandler)
                    : new TunnelProxyInitializer(channel, proxyHandler);
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(serverConfig.getProxyLoopGroup()) // 注册线程池
                    .channel(NioSocketChannel.class) // 使用NioSocketChannel来作为连接用的channel类
                    .handler(channelInitializer);
            if (proxyHandler != null) {
                // 代理服务器解析DNS和连接
                bootstrap.resolver(NoopAddressResolverGroup.INSTANCE);
            } else {
                bootstrap.resolver(serverConfig.resolver());
            }
            requestList = new LinkedList();
            cf = bootstrap.connect(requestProto.getHost(), requestProto.getPort());
            cf.addListener((ChannelFutureListener) future -> {
                if (future.isSuccess()) {
                    future.channel().writeAndFlush(msg);
                    synchronized (requestList) {
                        requestList.forEach(obj -> future.channel().writeAndFlush(obj));
                        requestList.clear();
                        isConnect = true;
                    }
                } else {
                    requestList.forEach(obj -> ReferenceCountUtil.release(obj));
                    requestList.clear();
                    getExceptionHandle().beforeCatch(channel, future.cause());
                    future.channel().close();
                    channel.close();
                }
            });
        } else {
            synchronized (requestList) {
                if (isConnect) {
                    cf.channel().writeAndFlush(msg);
                } else {
                    requestList.add(msg);
                }
            }
        }
    }

实际上是对数据的转发
再来看一下ProxyHandleFactory中的build方法

public static ProxyHandler build(ProxyConfig config) {
        ProxyHandler proxyHandler = null;
        if (config != null) {
            boolean isAuth = config.getUser() != null && config.getPwd() != null;
            InetSocketAddress inetSocketAddress = new InetSocketAddress(config.getHost(),
                    config.getPort());
            switch (config.getProxyType()) {
                case HTTP:
                    if (isAuth) {
                        proxyHandler = new HttpProxyHandler(inetSocketAddress,
                                config.getUser(), config.getPwd());
                    } else {
                        proxyHandler = new HttpProxyHandler(inetSocketAddress);
                    }
                    break;
                case SOCKS4:
                    proxyHandler = new Socks4ProxyHandler(inetSocketAddress);
                    break;
                case SOCKS5:
                    if (isAuth) {
                        proxyHandler = new Socks5ProxyHandler(inetSocketAddress,
                                config.getUser(), config.getPwd());
                    } else {
                        proxyHandler = new Socks5ProxyHandler(inetSocketAddress);
                    }
                    break;
            }
        }
        return proxyHandler;

    }

在这里会根据类型的不同会调用不同的初始化方法来得到proxyHandler

调用自定义intercept并且转发数据的操作流程就是这样,更深层次的关于netty的内容,nio的相关操作再次就不做深述。

总结

总的来说,proxyee是以netty为核心实现的一个较为轻量化的代理服务器,整体代码量和依赖都很少,在源码阅读的过程中能够不断加深对于netty的理解,从这种角度来说是一个很好的学习netty的项目。同时因为他可以对 HTTP、HTTPS 协议的报文进行捕获和篡改,我们可以以此来封装一层实现接口调用时入参、返回的统一记录,白名单的校验。

相关文章
|
7月前
|
算法 程序员 Python
如何写出优美整洁的代码
【4月更文挑战第5天】 编写优美整洁的代码能提升可读性、可维护性和开发效率。遵循命名规范,如使用小写字母和下划线命名变量,驼峰命名法命名函数和类。适当注释代码,但避免过度注释。避免冗余代码,通过函数封装重复逻辑。使用空格和缩进增强代码可读性,遵循PEP 8编码规范。利用异常处理机制处理错误,保持代码简洁。
51 0
|
4月前
|
设计模式 程序员
故意把代码写得很烂,这样的 “防御性编程“ 可取吗?
故意把代码写得很烂,这样的 “防御性编程“ 可取吗?
|
6月前
|
算法 安全 编译器
【简洁的代码永远不会掩盖设计者的意图】如何写出规范整洁的代码
【简洁的代码永远不会掩盖设计者的意图】如何写出规范整洁的代码
54 1
|
6月前
|
JSON 自然语言处理 前端开发
学会这个插件,职业生涯少写 1w 行代码。
学会这个插件,职业生涯少写 1w 行代码。
42 0
|
7月前
|
前端开发 测试技术
代码注释怎么写:让你的代码更易维护
在编程中,有一种无声的艺术,那就是代码注释。这可能看起来微不足道,但其实非常关键。它不仅有助于他人理解你的代码,也是自我表达的一种方式。
|
7月前
|
设计模式 算法 程序员
如何写出好的代码注释?
作为程序员,想必大家在日常开发中必写注释,而且在软件开发过程中,给代码写注释是一项至关重要的工作,也是一名合格的程序员该具备的编程素养。恰当的注释可以提高代码的可读性和可维护性,方便其他人理解熟悉和修改代码,但是不恰当或过度的注释可能会导致混乱和误导,会起到适得其反的作用。那么本文就来分享一些关于如何正确地给代码写注释的方法和指导原则,并提供一些减少注释但仍能让他人理解代码的方法。
161 3
如何写出好的代码注释?
|
消息中间件 设计模式 JavaScript
如何写出整洁的代码 上
如何写出整洁的代码 上
|
敏捷开发 测试技术 数据安全/隐私保护
如何写出整洁的代码 下
如何写出整洁的代码 下
|
IDE NoSQL Java
我来告诉你代码重构有什么好处
根据两本关于重构的书籍的作者 Martin Fowler的说法 “重构是改变软件系统的过程,它不会改变代码的外部行为,但会改善其内部结构。这是一种清理代码的严格方法,可以最大限度地减少引入错误的机会。本质上,当你重构时,你是在改进编写代码后的设计。”
250 0
|
前端开发 数据安全/隐私保护 Windows
如何做到每天比别人少写200行代码?
大家好,我是冰河~~ 这次我把工作中总结的经常使用的正则表达式共享出来了,正是掌握了这些正则表达式,冰河平均每天比别人少写200行代码,极大的提高了研发效率,建议小伙伴们收藏,平时尝试着使用到自己的项目中!!
113 0