├── .github └── workflows │ └── maven.yml ├── .gitignore ├── LICENSE ├── README.md ├── cert.pem ├── deploy.sh ├── directByteBufferConstructor.png ├── index.html ├── jdk9以上设置netty直接内存监控的说明.md ├── openssl.md ├── pom.xml ├── privkey.pem ├── proxy.service ├── src ├── assembly │ └── release.xml ├── main │ ├── java │ │ └── com │ │ │ └── arloor │ │ │ └── forwardproxy │ │ │ ├── HttpProxyServer.java │ │ │ ├── handler │ │ │ ├── HttpProxyServerInitializer.java │ │ │ ├── HttpsProxyServerInitializer.java │ │ │ ├── RelayHandler.java │ │ │ └── SessionHandShakeHandler.java │ │ │ ├── monitor │ │ │ ├── ChannelTrafficMonitor.java │ │ │ ├── GlobalTrafficMonitor.java │ │ │ ├── MonitorService.java │ │ │ ├── NetStats.java │ │ │ └── PromMonitorImpl.java │ │ │ ├── session │ │ │ ├── Session.java │ │ │ └── Status.java │ │ │ ├── ssl │ │ │ └── SslContextFactory.java │ │ │ ├── trace │ │ │ ├── LogSpanExporter.java │ │ │ ├── OtelContextDemo.java │ │ │ ├── TraceConstant.java │ │ │ └── Tracer.java │ │ │ ├── util │ │ │ ├── JsonUtil.java │ │ │ ├── OsUtils.java │ │ │ ├── RenderUtil.java │ │ │ └── SocksServerUtils.java │ │ │ ├── vo │ │ │ ├── Config.java │ │ │ ├── HttpConfig.java │ │ │ ├── RenderParam.java │ │ │ └── SslConfig.java │ │ │ └── web │ │ │ ├── Dispatcher.java │ │ │ └── ResourceReader.java │ └── resources │ │ ├── META-INF │ │ └── services │ │ │ └── com.arloor.forwardproxy.monitor.MonitorService │ │ ├── echarts.min.js │ │ ├── favicon.ico │ │ ├── log4j2-test.xml │ │ ├── log4j2.xml │ │ ├── logback.xml │ │ └── proxy.properties └── test │ └── java │ └── MemoryMonitorTest.java ├── 实时网速.png └── 性能测试.md /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI 5 | 6 | on: [ push ] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up JDK 17 15 | uses: actions/setup-java@v2 16 | with: 17 | java-version: '17' 18 | distribution: 'adopt' 19 | - name: Build with Maven 20 | run: mvn -B package --file pom.xml -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | *.iml 4 | log/ 5 | .DS_Store 6 | */.DS_Store 7 | arloor.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 admin@arloor.com 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **声明:本项目仅以学习为目的,请在当地法律允许的范围内使用本程序。任何因错误用途导致的法律责任,与本项目无关!** 2 | 3 | ## 基于netty的http代理 4 | 5 | - 支持普通GET/POST和CONNECT隧道代理 6 | - 代理支持over TLS(也就是surge、小火箭等软件说的https proxy) 7 | - 防止主动嗅探是否为http代理 8 | - 使用openssl、epoll等技术,支持TLS v1.3。 9 | 10 | ## 支持的客户端 11 | 12 | | 平台 | 支持的客户端 | 13 | |---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| 14 | | Linux、Windows | [clash_for_windows](https://github.com/Fndroid/clash_for_windows_pkg)、[go语言客户端](https://github.com/arloor/forward)、[Java语言客户端](https://github.com/arloor/connect) | 15 | | MacOS | ClashX、ClashX Pro| | | | 16 | | IOS | [Surge](https://apps.apple.com/us/app/surge-4/id1442620678)、[shawdowrocket](https://apps.apple.com/us/app/shadowrocket/id932747118) | 17 | | Android | [ClashForAndroid](https://github.com/Kr328/ClashForAndroid) | 18 | | chrome | [SwitchyOmega](https://chrome.google.com/webstore/detail/proxy-switchyomega/padekgcemlokbadohgkifijomclgjgif)插件(不推荐,会存在被嗅探的风险) | 19 | 20 | ## 网速监控 21 | 22 | 1. `http(s)://host:port/net`提供了基于echarts.js的网速监控,展示最近500秒的网速,如下图所示 23 | ![](/实时网速.png) 24 | 2 `http(s)://host:port/metrics`提供了prometheus的exporter,可以方便地接入prometheus监控,提供网速、内存等监控指标,如下所示 25 | 26 | ```shell 27 | # HELP proxy_out 上行流量 28 | # TYPE proxy_out counter 29 | proxy_out{host="localhost",} 65205 30 | # HELP proxy_in 下行流量 31 | # TYPE proxy_in counter 32 | proxy_in{host="localhost",} 21205 33 | # HELP proxy_out_rate 上行网速 34 | # TYPE proxy_out_rate gauge 35 | proxy_out_rate{host="localhost",} 23967 36 | # HELP proxy_in_rate 下行网速 37 | # TYPE proxy_in_rate gauge 38 | proxy_in_rate{host="localhost",} 5758 39 | # HELP direct_memory_total netty直接内存 对于jdk9+,请增加-Dio.netty.tryReflectionSetAccessible=true,对于jdk16+,请增加--add-opens java.base/java.nio=ALL-UNNAMED 40 | # TYPE direct_memory_total gauge 41 | direct_memory_total{host="localhost",} 33554439 42 | ``` 43 | 44 | [jdk9以上设置netty直接内存监控的说明](/jdk9以上设置netty直接内存监控的说明.md) 45 | 46 | ## 运行指南 47 | 48 | ```shell 49 | # 需要使用jdk17 50 | maven clean package 51 | java -jar target/httpproxy-1.0-SNAPSHOT-all.jar -c proxy.properties 52 | ``` 53 | 54 | ### 配置文件说明 55 | 56 | `proxy.properties`内容如下 57 | 58 | ```shell script 59 | # true:主动索要Proxy-Authorization,可能会被探测到是代理服务器。除非通过Chrom插件SwitchyOmega使用该代理,否则不建议设置为true 60 | # false: 不索要Proxy-Authorization 61 | ask4Authcate=false 62 | 63 | # http代理部分设置 64 | # true:开启,false:不开启 65 | http.enable=false 66 | # http代理的端口 67 | http.port=6789 68 | # http代理的用户名和密码,逗号分割多个用户。不设置则不启用鉴权 69 | #http.auth=user1:passwd,user2:passwd 70 | 71 | # https代理部分设置 72 | # true:开启,false:不开启 73 | https.enable=true 74 | # https代理的端口 75 | https.port=443 76 | # http代理的用户名和密码,逗号分割多个用户。不设置则不启用鉴权 77 | #https.auth=user1:passwd,user2:passwd 78 | # TLS证书的fullchain(从CA证书到域名证书) 79 | https.fullchain.pem=/path/to/fullchain.cer 80 | # TLS证书的私钥 81 | https.privkey.pem=/path/to/private.key 82 | ``` 83 | 84 | ### TLS证书的更多说明 85 | 86 | 以腾讯云的免费ssl证书为例,nginx文件夹中的`1_xxx.com_bundle.crt`是fullchain,`2_xxx.com.key`是private.key。相信代码从业者能够从这里举一反三,从而知道从其他途径签发的证书应该如何配置。 87 | 88 | 测试时,可以使用项目内的`cert.pem`和`privkey.pem`(他们由openssl生成),同时需要设置设置chrome不验证localhost的证书 89 | 90 | ```shell 91 | ## 证书生成脚本 92 | openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout privkey.pem -out cert.pem -days 3650 93 | 94 | ## chrome不验证本地证书 95 | 打开 chrome://flags/#allow-insecure-localhost 96 | ``` 97 | 98 | ## 性能测试 99 | 100 | [性能测试](性能测试.md) 101 | 102 | ## 推荐查看Rust语言版本 103 | 104 | [rust_http_proxy](https://github.com/arloor/rust_http_proxy) 105 | 106 | 轻量、高性能、内存占用低 107 | -------------------------------------------------------------------------------- /cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFPjCCAyYCCQDaUQT3cUjntDANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJj 3 | bjELMAkGA1UECAwCY24xCzAJBgNVBAcMAmNuMQswCQYDVQQKDAJjbjELMAkGA1UE 4 | CwwCY24xCzAJBgNVBAMMAmNuMREwDwYJKoZIhvcNAQkBFgJjbjAeFw0yMTA4MjMx 5 | MTA0NDZaFw0zMTA4MjExMTA0NDZaMGExCzAJBgNVBAYTAmNuMQswCQYDVQQIDAJj 6 | bjELMAkGA1UEBwwCY24xCzAJBgNVBAoMAmNuMQswCQYDVQQLDAJjbjELMAkGA1UE 7 | AwwCY24xETAPBgkqhkiG9w0BCQEWAmNuMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A 8 | MIICCgKCAgEAt/ctw4u7Edl+O/21BnyGa5kXZLrIsHb0Rq75TF/ltFnY6yZnZFRm 9 | kmsXp3z+YjPXNsTDSmGw1rOIxVTkmV9gCsHVrzKX8eLFuBFkdys6WpD83uYfMqxi 10 | GxCVXtZWo0JBrk2r9YsDw1qg0QnyIetWdq0b0dRkNJC5n1IkU8v66uldM9z5g2Zg 11 | geEvM+NoIYhStES26liG79rVXD2x5B5LY1gQnCsF6VKv2TXZ4o4fOrqh1SEp64nM 12 | EzEtDEakusYBWfAKzFGEn1QcZc4Vc5aGR5WhkAV5HT0rZDPuykACbtC+WdkHxwP4 13 | cKgcER9HyYSoJ/zR+r6IjPYftU203d1gBF8K68mNcP8DosaWh24w7ySQqFkP/GJ1 14 | QEDja0F7baTolF4DMYaP/XGcg6+41XIY0emg41FZOW8G5w37BsPe4Z54/hB8ads0 15 | dahADUoLJTIMG3uBEAjd9LoD7Bs5qsexg7BHFDx+A71TBwnFQ3rm+p5ct+z/Sno4 16 | 9I0gO0ddVZFZXvIr6AAffRQC2AeLEO+BAPOrsr+bmobMbZv/5XYxTL93Wk0+zqcM 17 | sbCrgKeWUOBxgFK6kQjbfUvcd0VwFOe2z/EHr/lxuxiuAOsY6JayAdO3DrZwrIIO 18 | urkbgf36WXcATooGTYQevz59T2nqiClxOqyaCWeemiYFz4zBU5QdjvUCAwEAATAN 19 | BgkqhkiG9w0BAQsFAAOCAgEAb/G1qz/md1f/TPL92VNUXdZVGo8BstUe/g1sCREN 20 | mI/SPKGOb4332UhmpJOvlenenVCFDxjrN2z1FDRKFMaBJrFmstoRY4f0KOibCAVD 21 | ZMj5XzuX1HrljeydJgA1v2/wsGuu+ylsNi5uw4qXrxpKyH2IV6Kk4k+2Y9B+gVwh 22 | NkGYUsdqU+lsew2vK5eBWK3T9pxmI58HDApPPDD9PScPhs+QiVdrNoQCbFbX/EWJ 23 | fqapVr0sS93wxXIPeBMZSoAUPXGLitKmbtud+wixRTSBW/22Gf8ahMvlhmNKj/s5 24 | MwmYkqVh9TtUtWgrw41RpzLfZ8AoGemfTf+bhtNZQKk7YALVjH7CTqfQs7KsysYY 25 | D+mUYfSP7zee+coPDR1qXvBeQL4vzs5yBVyQ0cjjQDfhQspmKgBjDaMcFwNrr285 26 | f5FPBFWtpTn5VrPs55E3xj1RG6hQUMrQ5+FGqfP5Sh+LvqM0Sck8wPYGy1Zh/GJJ 27 | pYP48urTVcwL7oL7lUPKWCfe5nNVxOk2tDHMrW48/BeRBc1kZxK3WyDVVQ0FBQr3 28 | 2+1Y8H8LNJsda/9B/SrfWzGGjaBgqpn0jvB81aS5oQaqYySyGzbpYqtifoIIjAgk 29 | q409WWyxQN0CPo7n6GKthOCMUd99owsebBlnbEup2gHZUjvjd5KHr+1433KVQzf+ 30 | l4Y= 31 | -----END CERTIFICATE----- 32 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | if [ "$*" = "" ]; then 2 | echo "输入hostname(以空格分割)" 3 | read hosts 4 | else 5 | echo 目标为"$*" 6 | hosts=$* 7 | fi 8 | 9 | for host in $hosts; do 10 | echo "部署到${host}" 11 | scp target/httpproxy-1.0-SNAPSHOT-all.jar root@${host}:/opt/proxy/forwardproxy-1.0-jar-with-dependencies.jar 12 | ssh root@${host} ' 13 | systemctl stop proxy 14 | systemctl start proxy 15 | systemctl status proxy --no-page 16 | ' 17 | done 18 | -------------------------------------------------------------------------------- /directByteBufferConstructor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arloor/HttpProxy/44b171bc591d562266cd47e145197d38d57a2e8b/directByteBufferConstructor.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SG161 实时网速 6 | 7 | 8 | 9 | 10 |
11 | 211 | 212 | 213 | -------------------------------------------------------------------------------- /jdk9以上设置netty直接内存监控的说明.md: -------------------------------------------------------------------------------- 1 | ## 堆外内存代码监控 2 | 3 | JDK 默认采用 Cleaner 回收释放 DirectByteBuffer,Cleaner 继承于 PhantomReference,因为依赖 GC 进行处理,所以回收的时间是不可控的。对于 hasCleaner 的 DirectByteBuffer,Java 提供了一系列不同类型的 MXBean 用于获取 JVM 进程线程、内存等监控指标,代码实现如下:来自[Netty 在项目开发中的一些最佳实践](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Netty%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90%E4%B8%8E%20RPC%20%E5%AE%9E%E8%B7%B5-%E5%AE%8C/30%20%20%E5%AE%9E%E8%B7%B5%E6%80%BB%E7%BB%93%EF%BC%9ANetty%20%E5%9C%A8%E9%A1%B9%E7%9B%AE%E5%BC%80%E5%8F%91%E4%B8%AD%E7%9A%84%E4%B8%80%E4%BA%9B%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.md) 4 | 5 | ```java 6 | BufferPoolMXBean directBufferPoolMXBean = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class).get(0); 7 | 8 | LOGGER.info("DirectBuffer count: {}, MemoryUsed: {} K", directBufferPoolMXBean.getCount(), directBufferPoolMXBean.getMemoryUsed()/1024); 9 | ``` 10 | 11 | 对于 Netty 中 noCleaner 的 DirectByteBuffer,直接通过 PlatformDependent.usedDirectMemory() 读取即可。但是随着JDK的发展,在新JDK中要加些jvm参数才能获取到。 12 | 13 | ## jdk9以上设置 `-Dio.netty.tryReflectionSetAccessible=true` 的说明 14 | 15 | 要统计netty直接内存使用量,实际使用的是netty中PlatformDependent类的`DIRECT_MEMORY_COUNTER`变量。 16 | 17 | netty在初始化这个变量前,会检查时候能反射拿到DirectByteBuffer的构造方法。 18 | 19 | 在jdk9以上,拿构造方法被认为是`illegal reflective access`会看到这样的警告信息: 20 | 21 | ```shell 22 | WARNING: An illegal reflective access operation has occurred 23 | WARNING: Illegal reflective access by io.netty.util.internal.ReflectionUtil (file:/C:/Users/arloor/.m2/repository/io/netty/netty-all/4.1.53.Final/netty-all-4.1.53.Final.jar) to constructor java.nio.DirectByteBuffer(long,int) 24 | WARNING: Please consider reporting this to the maintainers of io.netty.util.internal.ReflectionUtil 25 | WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations 26 | WARNING: All illegal access operations will be denied in a future release 27 | ``` 28 | 29 | 所以有人就在github上提issue了:[https://github.com/netty/netty/pull/7650](https://github.com/netty/netty/pull/7650) 30 | 31 | netty的解决方案是:默认关闭这个反射拿构造方法的操作,相关代码: 32 | 33 | ```java 34 | // PlatformDependent0.java 35 | private static boolean explicitTryReflectionSetAccessible0(){ 36 | // we disable reflective access 37 | return SystemPropertyUtil.getBoolean("io.netty.tryReflectionSetAccessible",javaVersion()< 9); 38 | } 39 | ``` 40 | 41 | 我们为了统计直接内存使用量,所以需要把这个打开 42 | 43 | ## jdk16以上设置 `--add-opens java.base/java.nio=ALL-UNNAMED` 的说明 44 | 45 | 如果不设置,会在反射获取 `DirectByteBuffer` 的构造函数 `private java.nio.DirectByteBuffer(long,int)` 的时候抛出下面的异常,导致无法获取: 46 | 47 | ```shell 48 | java.lang.reflect.InaccessibleObjectException: 49 | Unable to make private java.nio.DirectByteBuffer(long,int) accessible: 50 | module java.base does not "opens java.nio" to unnamed module @5a4aa2f2 51 | ``` 52 | 53 | ![](/directByteBufferConstructor.png) 54 | 55 | 相关的一些issue:[renaissance-benchmarks的issue](https://github.com/renaissance-benchmarks/renaissance/issues/241) 56 | -------------------------------------------------------------------------------- /openssl.md: -------------------------------------------------------------------------------- 1 | ```shell 2 | openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout privkey.pem -out cert.pem -days 3650 3 | 4 | ## chrome不验证本地证书 5 | chrome://flags/#allow-insecure-localhost 6 | ``` -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | HttpProxy 8 | A httpproxy based on netty 9 | https://github.com/arloor/HttpProxy 10 | 11 | 12 | The Apache Software License, Version 2.0 13 | http://www.apache.org/licenses/LICENSE-2.0.txt 14 | 15 | 16 | 17 | 18 | arloor 19 | admin@arloor.com 20 | 21 | 22 | 23 | scm:git:git@github.com:arloor/HttpProxy.git 24 | scm:git:git@github.com:arloor/HttpProxy.git 25 | git@github.com:arloor/HttpProxy.git 26 | 27 | 28 | 29 | com.arloor 30 | httpproxy 31 | 1.0-SNAPSHOT 32 | 33 | 17 34 | 4.1.77.Final 35 | com.arloor.forwardproxy.HttpProxyServer 36 | UTF-8 37 | 38 | 39 | 40 | 41 | 42 | io.opentelemetry 43 | opentelemetry-bom 44 | 1.7.1 45 | pom 46 | import 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | io.opentelemetry 55 | opentelemetry-api 56 | 57 | 58 | io.opentelemetry 59 | opentelemetry-sdk 60 | 61 | 62 | io.opentelemetry 63 | opentelemetry-sdk-trace 64 | 65 | 66 | io.opentelemetry 67 | opentelemetry-exporter-logging 68 | 69 | 70 | io.opentelemetry 71 | opentelemetry-exporter-jaeger 72 | 73 | 74 | 75 | io.grpc 76 | grpc-netty 77 | 1.42.1 78 | 79 | 80 | 81 | 82 | io.netty 83 | netty-all 84 | ${netty.version} 85 | 86 | 87 | com.fasterxml.jackson.core 88 | jackson-core 89 | 2.13.0 90 | 91 | 92 | com.fasterxml.jackson.core 93 | jackson-databind 94 | 2.13.4.2 95 | 96 | 97 | com.fasterxml.jackson.core 98 | jackson-annotations 99 | 2.13.0 100 | 101 | 102 | org.thymeleaf 103 | thymeleaf 104 | 3.0.12.RELEASE 105 | 106 | 107 | io.netty 108 | netty-tcnative-boringssl-static 109 | 2.0.56.Final 110 | runtime 111 | 112 | 113 | 114 | com.google.guava 115 | guava 116 | 32.0.0-jre 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | org.apache.logging.log4j 133 | log4j-api 134 | 2.17.1 135 | 136 | 137 | org.apache.logging.log4j 138 | log4j-core 139 | 2.17.1 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | com.lmax 150 | disruptor 151 | 3.3.4 152 | 153 | 154 | 155 | org.apache.logging.log4j 156 | log4j-slf4j-impl 157 | 2.14.0 158 | 159 | 160 | org.slf4j 161 | slf4j-api 162 | 1.7.32 163 | 164 | 165 | org.bouncycastle 166 | bcprov-jdk15on 167 | 1.69 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | org.apache.maven.plugins 176 | maven-jar-plugin 177 | 3.2.2 178 | 179 | 180 | 181 | ${mainClass} 182 | 183 | 184 | 185 | log4j2-test.xml 186 | 187 | 188 | 189 | 190 | org.apache.maven.plugins 191 | maven-surefire-plugin 192 | 2.5 193 | 194 | true 195 | 196 | 197 | 198 | maven-assembly-plugin 199 | 200 | 201 | 202 | ${mainClass} 203 | 204 | 205 | 206 | 207 | src/assembly/release.xml 208 | 209 | 210 | 211 | 212 | 213 | make-assembly 214 | package 215 | 216 | single 217 | 218 | 219 | 220 | 221 | 222 | org.apache.maven.plugins 223 | maven-compiler-plugin 224 | 225 | 17 226 | 17 227 | UTF-8 228 | 229 | 230 | 231 | 232 | org.apache.maven.plugins 233 | maven-gpg-plugin 234 | 1.6 235 | 236 | 237 | verify 238 | 239 | sign 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | central 250 | 251 | true 252 | 253 | 254 | 255 | oss-snapshot 256 | https://oss.sonatype.org/content/repositories/snapshots/ 257 | 258 | 259 | oss 260 | https://oss.sonatype.org/service/local/staging/deploy/maven2 261 | 262 | 263 | 264 | 265 | github 266 | 267 | 268 | github 269 | GitHub OWNER Apache Maven Packages 270 | https://maven.pkg.github.com/arloor/HttpProxy 271 | 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQC39y3Di7sR2X47 3 | /bUGfIZrmRdkusiwdvRGrvlMX+W0WdjrJmdkVGaSaxenfP5iM9c2xMNKYbDWs4jF 4 | VOSZX2AKwdWvMpfx4sW4EWR3KzpakPze5h8yrGIbEJVe1lajQkGuTav1iwPDWqDR 5 | CfIh61Z2rRvR1GQ0kLmfUiRTy/rq6V0z3PmDZmCB4S8z42ghiFK0RLbqWIbv2tVc 6 | PbHkHktjWBCcKwXpUq/ZNdnijh86uqHVISnricwTMS0MRqS6xgFZ8ArMUYSfVBxl 7 | zhVzloZHlaGQBXkdPStkM+7KQAJu0L5Z2QfHA/hwqBwRH0fJhKgn/NH6voiM9h+1 8 | TbTd3WAEXwrryY1w/wOixpaHbjDvJJCoWQ/8YnVAQONrQXttpOiUXgMxho/9cZyD 9 | r7jVchjR6aDjUVk5bwbnDfsGw97hnnj+EHxp2zR1qEANSgslMgwbe4EQCN30ugPs 10 | Gzmqx7GDsEcUPH4DvVMHCcVDeub6nly37P9Kejj0jSA7R11VkVle8ivoAB99FALY 11 | B4sQ74EA86uyv5uahsxtm//ldjFMv3daTT7OpwyxsKuAp5ZQ4HGAUrqRCNt9S9x3 12 | RXAU57bP8Qev+XG7GK4A6xjolrIB07cOtnCsgg66uRuB/fpZdwBOigZNhB6/Pn1P 13 | aeqIKXE6rJoJZ56aJgXPjMFTlB2O9QIDAQABAoICAHVU8lxA15xn0kpi4z1y2Kzx 14 | pGj7dJqQE/atDJW4qCHiGCbKhLEJ363NHfYWE40AsWfeEQx0yDv3n+jz3yUvHmtd 15 | +3LVWvHMh/yBI0maSYqP5Kgpr4Y7SgsnBY3KONHBpXCNW3qtIkZLnDwXEtataaUh 16 | fZfT1eFs2OcTldXYSH3hPy+f5Yf4GN9dhySaCsUwVEdjeCJ5Of7q3KALPVVYOQ7w 17 | kQ95pTiOI1fvL7t+l852UFdF4p22cnFfMPMeb1wsg5ogLlLoPyT7mW0ZBreFLrmj 18 | ZrsbsoaCiBVNxiQ6tRKGkpFtibdWAfOgVN5azAdsU2/FoQtC0IbPR6P0HsJOujia 19 | KRzuvGUvQxMZS7cP4V1/clY7iV0kDtNutJ8P0NKINf6RUt6wtYK5n3IqCSrV7+ww 20 | NNl/i6L1sl6eRwJhjTHopTUXzrWm6v1c53572JZpZZ59mO3jgbTpa0Zc1Ydx+wDp 21 | 4cu1AKzya/3Cvo8JIZAH1bjUOJdpniJ910Z6pH5V20uC6mJ+YZIa5QH42XzVwLaz 22 | lKTN9vB8UP41oyT17qNAZH8QYZOiEcHJ10pLpIn8H/xxRSf+zRvRS0YbBukOjtjh 23 | 1SCSbePUhJoIHKLGwSaaiOu+lNFgZIjUIirpSr49x/md+WuN6BEV3AEZRTwRx95D 24 | sbSp79VReelBQKMLz7HVAoIBAQDmwRMDBbxBn5eetwQVA7hptkXvLD8xQlq17Gmm 25 | yHbZc23xEkv13CMRzkfutNwYFfTN4/bYsQZY4Fu7vA+J/lJB0o+cvVakU99h1jca 26 | ychmJVX1ScYuIWf5WgbRqI+f0Tb8SJEMe9DTE9njSD982IUQVrn2SdNOlPg3etlu 27 | X/HJZhA0sK1mZm63qmyAYdSk/wcHkt4AMSYfaIwYqYHIeBvE323eHtnvFW5o4LhV 28 | IqCrHQ4/Jtto6ULDkBjt97p/7jxUuiphDQt8KS1A4RkOUVkuKoq9GJoXbBELYsgc 29 | LDIof+LM/N8URb5JVb1X/yuh8fWqTjFtlUHyuf9rX8KThtX3AoIBAQDMF6fRGVxK 30 | iqs8p6L6XbGChs+Q7H/FyvXGl9guaWGvsRrLB55k+mlKLcPy35njO7FPmCQ4fUNs 31 | vlho+ff52P5Njvtu2qUcCRyGmw6pwIGsKGDg1Qw6AA7i384l7cGsSUG0QNG/k/Hw 32 | aJCgezQ+VgkqjM0xvBN9/ngWWzr4MDxkVj9V7AjSU1e+AbpVWnqTg92vsD7zJTUy 33 | bBDzLooVznLRTaRqkHIHb4Tp3ybkQpJb/Po60DTFSwZRc9IGxFC+ftWXn6S5E8K/ 34 | hJvtWemwMyhFnet2qs8IcOloFFGF5wWmJ6ShMcVDO49dt+uaZAHASlv8HeUljGJB 35 | Ru7yuAZbr9dzAoIBAEj0Yf5iodJdkqIrWq8KLhO9/P5RDumUAPnjdMO2PV0ikW/W 36 | SQ3CO5Q1k6h0peE3j95IZJK95TPUOyxvmLoYHgmReLYlZm8t1UIpZ+KgSSuCQlr6 37 | qB2NkkHTpLREhqQQkUHR74ny22Lgs9KX1Pqzud4gjUrArH37uz5PKM8splT2X8lI 38 | 0om9eOO4jfVI+OHxf1d1p5qH73WeqCxj6xRmZOEpgqLnPh/Its+RvLWt7P4sgYSV 39 | 1Gim1uTdegRCfkRUHd7rvNpSNX9bxWLtx+4u6fMeoB6I2K7vC7R91qaRkoobZKlK 40 | iXzvT+n/oY1gr3rT9MeTUS1EM0V6aLaZ3/qkdqECggEBAKDQovMoQIib64HHtawB 41 | 17U59KQFNkjsO+1YCIfJkaeWrb39Ktn2VrCUjyn49pEOoBBPmXrJeS9ebNuK29KL 42 | IRaL66LVtfP5WfjWhR9NCOBWkL+YICIzmYc7Qnywc0MGFjeqO5vxP1pnik5pxii4 43 | MlDM0YK258UHlihHipe1qmCFdTKG4gyYjzKwudo3iQ5jgXLGNVPxpZEthc2YhkXY 44 | frBdtYgieTdSsDB4HXUO37SMCb/9/xbWnwvfb0bVuang1vy6VllfML0oCJVlm7Wz 45 | GbCBVuAShz/neIMCP5p7BwB9jENXrE/lxnnaSlNFKfwSm5h1FfIYQ/ObqPyn9Jqa 46 | W/ECggEBAMLRquStM8zZiTLodX0MfGJ6qSeZ7DY5bql33HBcXc1PZzXJeTazvaj7 47 | C8F1r0NMzYLFCvB5SRRJA2dLj3xYsjWpQyGqCxXEzS0wJ12IonHKBN1pdFGnOTBZ 48 | sEKo4wrYECZkbpfNJIrc42Gh1TUx9bEv+wIz1s4ZevQrek05WfV0JcXmv42u6/Qj 49 | BQz8a9/vwZQwiZZqKo5w/6QI+Fo+G8nKY2kq48souxBuhfQS/snNoVl/E/sJ0JwX 50 | JlBVsrKufEEAX/NrKH1bK0ODDA1YC9YWgp7H3vuI617TD3g3q7itpPvBswyZb2y3 51 | D5lJsZYip9QciUvxYux+BBnpacBQTUQ= 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /proxy.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=forwardproxy-Http代理 3 | After=network-online.target 4 | Wants=network-online.target 5 | 6 | [Service] 7 | WorkingDirectory=/opt/proxy 8 | ExecStart=/usr/bin/java -jar forwardproxy-1.0-jar-with-dependencies.jar -c /opt/proxy/proxy.properties 9 | LimitNOFILE=100000 10 | Restart=always 11 | RestartSec=30 12 | 13 | [Install] 14 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /src/assembly/release.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | all 6 | 7 | jar 8 | 9 | false 10 | 11 | 12 | / 13 | true 14 | true 15 | runtime 16 | 17 | 18 | **/log4j2-test.xml 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/HttpProxyServer.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy; 2 | 3 | import com.arloor.forwardproxy.handler.HttpProxyServerInitializer; 4 | import com.arloor.forwardproxy.handler.HttpsProxyServerInitializer; 5 | import com.arloor.forwardproxy.util.OsUtils; 6 | import com.arloor.forwardproxy.vo.Config; 7 | import com.arloor.forwardproxy.vo.HttpConfig; 8 | import com.arloor.forwardproxy.vo.SslConfig; 9 | import io.netty.bootstrap.ServerBootstrap; 10 | import io.netty.channel.Channel; 11 | import io.netty.channel.ChannelOption; 12 | import io.netty.channel.EventLoopGroup; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.io.File; 17 | import java.io.FileReader; 18 | import java.util.Properties; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | public final class HttpProxyServer { 22 | 23 | private static final Logger log = LoggerFactory.getLogger(HttpProxyServer.class); 24 | 25 | public static void main(String[] args) { 26 | String propertiesPath = null; 27 | if (args.length == 2 && args[0].equals("-c")) { 28 | propertiesPath = args[1]; 29 | } 30 | Properties properties = parseProperties(propertiesPath); 31 | 32 | Config config = Config.parse(properties); 33 | log.info("主动要求验证:" + Config.ask4Authcate); 34 | SslConfig sslConfig = config.ssl(); 35 | HttpConfig httpConfig = config.http(); 36 | 37 | EventLoopGroup bossGroup = OsUtils.buildEventLoopGroup(1); 38 | EventLoopGroup workerGroup = OsUtils.buildEventLoopGroup(0); 39 | try { 40 | if (sslConfig != null && httpConfig != null) { 41 | Channel sslChannel = startSSl(bossGroup, workerGroup, sslConfig); 42 | Channel httpChannel = startHttp(bossGroup, workerGroup, httpConfig); 43 | if (httpChannel != null) { 44 | httpChannel.closeFuture().sync(); 45 | } 46 | if (sslChannel != null) { 47 | sslChannel.closeFuture().sync(); 48 | } 49 | } else if (sslConfig != null) { 50 | Channel httpChannel = startSSl(bossGroup, workerGroup, sslConfig); 51 | if (httpChannel != null) { 52 | httpChannel.closeFuture().sync(); 53 | } 54 | } else if (httpConfig != null) { 55 | Channel sslChannel = startHttp(bossGroup, workerGroup, httpConfig); 56 | if (sslChannel != null) { 57 | sslChannel.closeFuture().sync(); 58 | } 59 | } 60 | } catch (InterruptedException e) { 61 | log.error("interrupt!", e); 62 | } finally { 63 | bossGroup.shutdownGracefully(); 64 | workerGroup.shutdownGracefully(); 65 | } 66 | } 67 | 68 | private static Properties parseProperties(String propertiesPath) { 69 | Properties properties = new Properties(); 70 | try { 71 | if (propertiesPath != null) { 72 | properties.load(new FileReader(new File(propertiesPath))); 73 | } else { 74 | properties.load(HttpProxyServer.class.getClassLoader().getResourceAsStream("proxy.properties")); 75 | } 76 | } catch (Exception e) { 77 | log.error("loadProperties Error!", e); 78 | } 79 | return properties; 80 | } 81 | 82 | 83 | public static Channel startHttp(EventLoopGroup bossGroup, EventLoopGroup workerGroup, HttpConfig httpConfig) { 84 | try { 85 | // Configure the server. 86 | ServerBootstrap b = new ServerBootstrap(); 87 | b.option(ChannelOption.SO_BACKLOG, 10240); 88 | b.group(bossGroup, workerGroup) 89 | .channel(OsUtils.serverSocketChannelClazz()) 90 | .childHandler(new HttpProxyServerInitializer(httpConfig)); 91 | 92 | Channel httpChannel = b.bind(httpConfig.getPort()).sync().channel(); 93 | log.info("http proxy@ port={} auth={} url=http://localhost:{}",httpConfig.getPort(), httpConfig.needAuth(),httpConfig.getPort()); 94 | return httpChannel; 95 | } catch (Exception e) { 96 | log.error("无法启动Http Proxy", e); 97 | } 98 | return null; 99 | } 100 | 101 | public static Channel startSSl(EventLoopGroup bossGroup, EventLoopGroup workerGroup, SslConfig sslConfig) { 102 | try { 103 | // Configure the server. 104 | ServerBootstrap b = new ServerBootstrap(); 105 | b.option(ChannelOption.SO_BACKLOG, 10240); 106 | HttpsProxyServerInitializer initializer = new HttpsProxyServerInitializer(sslConfig); 107 | b.group(bossGroup, workerGroup) 108 | .channel(OsUtils.serverSocketChannelClazz()) 109 | .childHandler(initializer); 110 | 111 | Channel sslChannel = b.bind(sslConfig.getPort()).sync().channel(); 112 | // 每天更新一次ssl证书 113 | sslChannel.eventLoop().scheduleAtFixedRate(() -> { 114 | log.info("定时重加载ssl证书!"); 115 | initializer.loadSslContext(); 116 | }, 1, 1, TimeUnit.DAYS); 117 | log.info("https proxy@ port={} auth={} url:https://localhost:{}" ,sslConfig.getPort(), sslConfig.needAuth(),sslConfig.getPort()); 118 | return sslChannel; 119 | } catch (Exception e) { 120 | log.error("无法启动Https Proxy", e); 121 | } 122 | return null; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/handler/HttpProxyServerInitializer.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.handler; 2 | 3 | import com.arloor.forwardproxy.monitor.ChannelTrafficMonitor; 4 | import com.arloor.forwardproxy.monitor.GlobalTrafficMonitor; 5 | import com.arloor.forwardproxy.trace.TraceConstant; 6 | import com.arloor.forwardproxy.trace.Tracer; 7 | import com.arloor.forwardproxy.vo.HttpConfig; 8 | import io.netty.channel.ChannelInitializer; 9 | import io.netty.channel.ChannelPipeline; 10 | import io.netty.channel.socket.SocketChannel; 11 | import io.netty.handler.codec.http.HttpRequestDecoder; 12 | import io.netty.handler.codec.http.HttpResponseEncoder; 13 | import io.netty.handler.codec.http.HttpServerExpectContinueHandler; 14 | import io.netty.handler.timeout.IdleStateHandler; 15 | import io.opentelemetry.api.trace.Span; 16 | import io.opentelemetry.api.trace.SpanKind; 17 | 18 | import java.io.IOException; 19 | import java.security.GeneralSecurityException; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | public class HttpProxyServerInitializer extends ChannelInitializer { 23 | 24 | private final HttpConfig httpConfig; 25 | 26 | public HttpProxyServerInitializer(HttpConfig httpConfig) throws IOException, GeneralSecurityException { 27 | this.httpConfig = httpConfig; 28 | } 29 | 30 | @Override 31 | public void initChannel(SocketChannel ch) { 32 | ChannelPipeline p = ch.pipeline(); 33 | p.addLast(GlobalTrafficMonitor.getInstance()); 34 | Span streamSpan = Tracer.spanBuilder(TraceConstant.stream.name()) 35 | .setSpanKind(SpanKind.SERVER) 36 | .setAttribute(TraceConstant.client.name(), ch.remoteAddress().getHostName()) 37 | .startSpan(); 38 | p.addLast(new IdleStateHandler(0, 0, 15, TimeUnit.SECONDS)); 39 | p.addLast(new ChannelTrafficMonitor(1000, streamSpan)); 40 | p.addLast(new HttpRequestDecoder()); 41 | p.addLast(new HttpResponseEncoder()); 42 | p.addLast(new HttpServerExpectContinueHandler()); 43 | p.addLast(SessionHandShakeHandler.NAME, new SessionHandShakeHandler(httpConfig.getAuthMap(), streamSpan, httpConfig.getDomainWhiteList())); 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/handler/HttpsProxyServerInitializer.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.handler; 2 | 3 | import com.arloor.forwardproxy.monitor.ChannelTrafficMonitor; 4 | import com.arloor.forwardproxy.monitor.GlobalTrafficMonitor; 5 | import com.arloor.forwardproxy.ssl.SslContextFactory; 6 | import com.arloor.forwardproxy.trace.TraceConstant; 7 | import com.arloor.forwardproxy.trace.Tracer; 8 | import com.arloor.forwardproxy.vo.SslConfig; 9 | import io.netty.channel.ChannelInitializer; 10 | import io.netty.channel.ChannelPipeline; 11 | import io.netty.channel.socket.SocketChannel; 12 | import io.netty.handler.codec.http.HttpRequestDecoder; 13 | import io.netty.handler.codec.http.HttpResponseEncoder; 14 | import io.netty.handler.codec.http.HttpServerExpectContinueHandler; 15 | import io.netty.handler.ssl.SslContext; 16 | import io.netty.handler.timeout.IdleStateHandler; 17 | import io.opentelemetry.api.trace.Span; 18 | import io.opentelemetry.api.trace.SpanKind; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import java.util.HashSet; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | public class HttpsProxyServerInitializer extends ChannelInitializer { 26 | private static final Logger log = LoggerFactory.getLogger(HttpsProxyServerInitializer.class); 27 | 28 | private final SslConfig sslConfig; 29 | 30 | private SslContext sslCtx; 31 | 32 | public HttpsProxyServerInitializer(SslConfig sslConfig) { 33 | this.sslConfig = sslConfig; 34 | loadSslContext(); 35 | } 36 | 37 | @Override 38 | public void initChannel(SocketChannel ch) { 39 | ChannelPipeline p = ch.pipeline(); 40 | p.addLast(GlobalTrafficMonitor.getInstance()); 41 | p.addLast(new IdleStateHandler(0, 0, 15, TimeUnit.SECONDS)); 42 | Span streamSpan = Tracer.spanBuilder(TraceConstant.stream.name()) 43 | .setSpanKind(SpanKind.SERVER) 44 | .setAttribute(TraceConstant.client.name(), ch.remoteAddress().getHostName()) 45 | .startSpan(); 46 | p.addLast(new ChannelTrafficMonitor(1000, streamSpan)); 47 | if (sslCtx != null) { 48 | p.addLast(sslCtx.newHandler(ch.alloc())); 49 | } 50 | p.addLast(new HttpRequestDecoder(1000,8192,8192)); 51 | p.addLast(new HttpResponseEncoder()); 52 | p.addLast(new HttpServerExpectContinueHandler()); 53 | p.addLast(SessionHandShakeHandler.NAME, new SessionHandShakeHandler(sslConfig.getAuthMap(), streamSpan, new HashSet<>())); 54 | 55 | } 56 | 57 | public void loadSslContext() { 58 | try { 59 | this.sslCtx = SslContextFactory.getSSLContext(sslConfig.getFullchain(), sslConfig.getPrivkey()); 60 | } catch (Throwable e) { 61 | log.error("init ssl context error!", e); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/handler/RelayHandler.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.handler; 2 | 3 | import com.arloor.forwardproxy.util.SocksServerUtils; 4 | import io.netty.buffer.Unpooled; 5 | import io.netty.channel.Channel; 6 | import io.netty.channel.ChannelHandlerContext; 7 | import io.netty.channel.ChannelInboundHandlerAdapter; 8 | import io.netty.handler.codec.http.HttpRequest; 9 | import io.netty.util.ReferenceCountUtil; 10 | import io.opentelemetry.api.trace.Span; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.net.InetSocketAddress; 15 | import java.util.Objects; 16 | 17 | public final class RelayHandler extends ChannelInboundHandlerAdapter { 18 | private static final Logger log = LoggerFactory.getLogger(RelayHandler.class); 19 | 20 | private final Channel relayChannel; 21 | private Span span; 22 | private String host; 23 | 24 | public RelayHandler(Channel relayChannel, String host) { 25 | this.relayChannel = relayChannel; 26 | this.host = host; 27 | } 28 | 29 | public RelayHandler(Channel relayChannel, String host, Span connectSpan) { 30 | this(relayChannel, host); 31 | this.span = connectSpan; 32 | } 33 | 34 | @Override 35 | public void channelActive(ChannelHandlerContext ctx) { 36 | ctx.writeAndFlush(Unpooled.EMPTY_BUFFER); 37 | } 38 | 39 | @Override 40 | public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { 41 | boolean canWrite = ctx.channel().isWritable(); 42 | relayChannel.config().setAutoRead(canWrite); 43 | super.channelWritabilityChanged(ctx); 44 | } 45 | 46 | @Override 47 | public void channelRead(ChannelHandlerContext ctx, Object msg) { 48 | if (msg instanceof HttpRequest request) {//删除代理特有的请求头 49 | request.headers().remove("Proxy-Authorization"); 50 | String proxyConnection = request.headers().get("Proxy-Connection"); 51 | if (Objects.nonNull(proxyConnection)) { 52 | request.headers().set("Connection", proxyConnection); 53 | request.headers().remove("Proxy-Connection"); 54 | } 55 | 56 | //获取Host和port 57 | String hostAndPortStr = request.headers().get("Host"); 58 | String[] hostPortArray = hostAndPortStr.split(":"); 59 | String host = hostPortArray[0]; 60 | String portStr = hostPortArray.length == 2 ? hostPortArray[1] : "80"; 61 | int port = Integer.parseInt(portStr); 62 | 63 | try { 64 | String url = request.uri(); 65 | int index = url.indexOf(host) + host.length(); 66 | url = url.substring(index); 67 | if (url.startsWith(":")) { 68 | url = url.substring(1 + String.valueOf(port).length()); 69 | } 70 | request.setUri(url); 71 | } catch (Exception e) { 72 | log.error("无法获取url:{} {} ", request.uri(), host); 73 | } 74 | } 75 | 76 | if (relayChannel.isActive()) { 77 | relayChannel.writeAndFlush(msg).addListener(future -> { 78 | if (!future.isSuccess()) { 79 | log.warn("relay error for [{}]! {}: {}", host, future.cause().getClass().getName(), future.cause().getMessage()); 80 | } 81 | }); 82 | } else { 83 | ReferenceCountUtil.release(msg); 84 | } 85 | } 86 | 87 | @Override 88 | public void channelInactive(ChannelHandlerContext ctx) { 89 | if (relayChannel.isActive()) { 90 | SocksServerUtils.closeOnFlush(relayChannel); 91 | } 92 | if (span != null) { 93 | span.end(); 94 | } 95 | } 96 | 97 | @Override 98 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 99 | String clientHostname = ((InetSocketAddress) ctx.channel().remoteAddress()).getHostString(); 100 | log.info("[EXCEPTION][" + clientHostname + "] " + cause.getMessage()); 101 | ctx.close(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/handler/SessionHandShakeHandler.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.handler; 2 | 3 | import com.arloor.forwardproxy.session.Session; 4 | import com.arloor.forwardproxy.util.SocksServerUtils; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.channel.SimpleChannelInboundHandler; 7 | import io.netty.handler.codec.http.HttpObject; 8 | import io.netty.handler.timeout.IdleStateEvent; 9 | import io.opentelemetry.api.trace.Span; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.net.InetSocketAddress; 14 | import java.util.Map; 15 | import java.util.Set; 16 | 17 | 18 | public class SessionHandShakeHandler extends SimpleChannelInboundHandler { 19 | public static final String NAME = "session"; 20 | private static final Logger log = LoggerFactory.getLogger(SessionHandShakeHandler.class); 21 | private final Session session; 22 | 23 | public SessionHandShakeHandler(Map auths, Span streamSpan, Set whiteDomains) { 24 | this.session = new Session(auths, streamSpan, whiteDomains); 25 | } 26 | 27 | @Override 28 | public void channelRead0(final ChannelHandlerContext ctx, HttpObject msg) { 29 | session.handle(ctx, msg); 30 | } 31 | 32 | @Override 33 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 34 | if (evt instanceof IdleStateEvent) { 35 | IdleStateEvent event = (IdleStateEvent) evt; 36 | log.debug("close channel {} because of {}", ctx.channel().remoteAddress(), event.state()); 37 | SocksServerUtils.closeOnFlush(ctx.channel()); 38 | } 39 | } 40 | 41 | @Override 42 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 43 | String clientHostname = ((InetSocketAddress) ctx.channel().remoteAddress()).getHostString(); 44 | log.info("[EXCEPTION][" + clientHostname + "] " + cause.getClass().getSimpleName() + " " + cause.getMessage()); 45 | ctx.close(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/monitor/ChannelTrafficMonitor.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.monitor; 2 | 3 | import io.netty.channel.ChannelHandlerContext; 4 | import io.netty.handler.traffic.ChannelTrafficShapingHandler; 5 | import io.opentelemetry.api.trace.Span; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.math.BigDecimal; 10 | import java.math.RoundingMode; 11 | 12 | public class ChannelTrafficMonitor extends ChannelTrafficShapingHandler { 13 | private static final Logger log = LoggerFactory.getLogger(ChannelTrafficMonitor.class); 14 | private static String[] array = {"B", "KB", "MB", "GB"}; 15 | private final Span streamSpan; 16 | 17 | public ChannelTrafficMonitor(int checkInterval, Span streamSpan) { 18 | super(checkInterval); 19 | this.streamSpan = streamSpan; 20 | } 21 | 22 | public long getReadBytes() { 23 | return this.trafficCounter().cumulativeReadBytes(); 24 | } 25 | 26 | public long getWriteBytes() { 27 | return this.trafficCounter().cumulativeWrittenBytes(); 28 | } 29 | 30 | 31 | @Override 32 | public void channelInactive(ChannelHandlerContext ctx) throws Exception { 33 | super.channelInactive(ctx); 34 | streamSpan.setAttribute("in", format(getReadBytes())); 35 | streamSpan.setAttribute("out", format(getWriteBytes())); 36 | streamSpan.end(); 37 | } 38 | 39 | private static String format(long bytes) { 40 | double value = bytes; 41 | 42 | int index = 0; 43 | for (double i = value; i >= 1024 && index < array.length - 1; i /= 1024, index++, value = i) { 44 | } 45 | BigDecimal bigDecimal = new BigDecimal(value); 46 | if (index == array.length - 1) { 47 | bigDecimal = bigDecimal.setScale(2, RoundingMode.HALF_UP); 48 | } else { 49 | bigDecimal = bigDecimal.setScale(0, RoundingMode.HALF_UP); 50 | } 51 | String string = bigDecimal.toString(); 52 | if (string.endsWith(".00")) { 53 | string = string.substring(0, string.length() - 3); 54 | } 55 | return string + array[index]; 56 | } 57 | 58 | public static void main(String[] args) { 59 | System.out.println(format(1023)); 60 | System.out.println(format(1024)); 61 | System.out.println(format(1024 * 1024)); 62 | System.out.println(format(1064 * 1024)); 63 | System.out.println(format(1064 * 1024 * 1024)); 64 | System.out.println(format(1064L * 1024 * 1024 * 1024)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/monitor/GlobalTrafficMonitor.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.monitor; 2 | 3 | import com.arloor.forwardproxy.util.JsonUtil; 4 | import com.arloor.forwardproxy.util.RenderUtil; 5 | import com.arloor.forwardproxy.vo.RenderParam; 6 | import com.google.common.collect.Lists; 7 | import io.netty.channel.ChannelHandler; 8 | import io.netty.handler.traffic.GlobalTrafficShapingHandler; 9 | import io.netty.handler.traffic.TrafficCounter; 10 | import io.netty.util.concurrent.EventExecutor; 11 | import io.netty.util.internal.PlatformDependent; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.net.InetAddress; 16 | import java.net.UnknownHostException; 17 | import java.util.ArrayList; 18 | import java.util.LinkedList; 19 | import java.util.List; 20 | import java.util.concurrent.ScheduledExecutorService; 21 | 22 | @ChannelHandler.Sharable 23 | /** 24 | * 该应用的网速监控 25 | */ 26 | public class GlobalTrafficMonitor extends GlobalTrafficShapingHandler { 27 | private static final Logger log = LoggerFactory.getLogger(GlobalTrafficMonitor.class); 28 | private static GlobalTrafficMonitor instance = new GlobalTrafficMonitor(MonitorService.EXECUTOR_SERVICE, 1000); 29 | 30 | public static GlobalTrafficMonitor getInstance() { 31 | return instance; 32 | } 33 | 34 | private static String hostname; 35 | private static final int seconds = 500; 36 | private static List xScales = new ArrayList<>(); 37 | private static List yScalesUp = new LinkedList<>(); 38 | private static List yScalesDown = new LinkedList<>(); 39 | volatile long outTotal = 0L; 40 | volatile long inTotal = 0L; 41 | volatile long outRate = 0L; 42 | volatile long inRate = 0L; 43 | 44 | static { 45 | try { 46 | hostname = InetAddress.getLocalHost().getHostName(); 47 | } catch (UnknownHostException e) { 48 | e.printStackTrace(); 49 | } 50 | for (int i = 1; i <= seconds; i++) { 51 | xScales.add(String.valueOf(i)); 52 | } 53 | } 54 | 55 | 56 | private GlobalTrafficMonitor(ScheduledExecutorService executor, long writeLimit, long readLimit, long checkInterval, long maxTime) { 57 | super(executor, writeLimit, readLimit, checkInterval, maxTime); 58 | } 59 | 60 | private GlobalTrafficMonitor(ScheduledExecutorService executor, long writeLimit, long readLimit, long checkInterval) { 61 | super(executor, writeLimit, readLimit, checkInterval); 62 | } 63 | 64 | private GlobalTrafficMonitor(ScheduledExecutorService executor, long writeLimit, long readLimit) { 65 | super(executor, writeLimit, readLimit); 66 | } 67 | 68 | private GlobalTrafficMonitor(ScheduledExecutorService executor, long checkInterval) { 69 | super(executor, checkInterval); 70 | } 71 | 72 | private GlobalTrafficMonitor(EventExecutor executor) { 73 | super(executor); 74 | } 75 | 76 | @Override 77 | protected void doAccounting(TrafficCounter counter) { 78 | synchronized (this) { 79 | long lastWriteThroughput = counter.lastWriteThroughput(); 80 | outRate = lastWriteThroughput; 81 | yScalesUp.add((double) lastWriteThroughput); 82 | if (yScalesUp.size() > seconds) { 83 | yScalesUp.remove(0); 84 | } 85 | long lastReadThroughput = counter.lastReadThroughput(); 86 | inRate = lastReadThroughput; 87 | yScalesDown.add((double) lastReadThroughput); 88 | if (yScalesDown.size() > seconds) { 89 | yScalesDown.remove(0); 90 | } 91 | outTotal = counter.cumulativeWrittenBytes(); 92 | inTotal = counter.cumulativeReadBytes(); 93 | } 94 | super.doAccounting(counter); 95 | } 96 | 97 | private static long getDirectMemoryCounter() { 98 | return PlatformDependent.usedDirectMemory(); 99 | } 100 | 101 | public static final String html(boolean localEcharts) { 102 | try { 103 | String legends = JsonUtil.toJson(Lists.newArrayList("上行网速", "下行网速")); 104 | String scales = JsonUtil.toJson(xScales); 105 | String seriesUp = JsonUtil.toJson(yScalesUp); 106 | String seriesDown = JsonUtil.toJson(yScalesDown); 107 | 108 | long interval = 1024 * 1024; 109 | Double upMax = yScalesUp.stream().max(Double::compareTo).orElse(0D); 110 | Double downMax = yScalesDown.stream().max(Double::compareTo).orElse(0D); 111 | Double max = Math.max(upMax, downMax); 112 | if (max / (interval) > 10) { 113 | interval = (long) Math.ceil(max / interval / 10) * interval; 114 | } 115 | 116 | RenderParam param = new RenderParam(); 117 | param.add("legends", legends); 118 | param.add("scales", scales); 119 | param.add("seriesUp", seriesUp); 120 | param.add("seriesDown", seriesDown); 121 | param.add("interval", interval); 122 | param.add("title", hostname + "实时网速"); 123 | if (localEcharts) { 124 | param.add("echarts_url", "/echarts.min.js"); 125 | } else { 126 | param.add("echarts_url", "https://www.arloor.com/echarts.min.js"); 127 | } 128 | return RenderUtil.text(TEMPLATE, param); 129 | } catch (Throwable e) { 130 | log.error("", e); 131 | } 132 | return ""; 133 | } 134 | 135 | 136 | private static final String TEMPLATE = """ 137 | 138 | 139 | 140 | 141 | [(${title})] 142 | 143 | 144 | 145 | 146 |
147 | 347 | 348 | 349 | """; 350 | } 351 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/monitor/MonitorService.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.monitor; 2 | 3 | import java.util.ServiceLoader; 4 | import java.util.concurrent.Executors; 5 | import java.util.concurrent.ScheduledExecutorService; 6 | 7 | public interface MonitorService { 8 | ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(1); 9 | 10 | String metrics(); 11 | 12 | static MonitorService getInstance() { 13 | ServiceLoader MonitorServices = ServiceLoader.load(MonitorService.class); 14 | if (MonitorServices.iterator().hasNext()) { 15 | return MonitorServices.iterator().next(); 16 | } 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/monitor/NetStats.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.monitor; 2 | 3 | import com.arloor.forwardproxy.util.JsonUtil; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.google.common.collect.Lists; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.nio.file.Files; 12 | import java.nio.file.Paths; 13 | import java.util.*; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * 网卡网速监控 18 | */ 19 | public class NetStats { 20 | private static final Logger log = LoggerFactory.getLogger(NetStats.class); 21 | private static final String filename = "/proc/net/dev"; 22 | private static List interfaces = new ArrayList<>(); 23 | private static List xScales = new ArrayList<>(); 24 | private static final int seconds = 600; 25 | 26 | static { 27 | for (int i = 1; i <= seconds; i++) { 28 | xScales.add(String.valueOf(i)); 29 | } 30 | } 31 | 32 | private static class YValue { 33 | String name; 34 | List data; 35 | String type = "line"; 36 | boolean smooth = false; 37 | List color = Lists.newArrayList("#b111f6");//#90EC7D 38 | Map> markPoint = ImmutableMap.of("data", Lists.newArrayList(ImmutableMap.of("type", "max", "name", "最大值"))); 39 | Map> markLine = ImmutableMap.of("data", Lists.newArrayList(ImmutableMap.of("type", "average", "name", "平均值"))); 40 | 41 | public Map> getMarkLine() { 42 | return markLine; 43 | } 44 | 45 | public YValue(String name, List data) { 46 | this.name = name; 47 | this.data = data; 48 | } 49 | 50 | public Map> getMarkPoint() { 51 | return markPoint; 52 | } 53 | 54 | public String getName() { 55 | return name; 56 | } 57 | 58 | public List getData() { 59 | return data; 60 | } 61 | 62 | public String getType() { 63 | return type; 64 | } 65 | 66 | public boolean isSmooth() { 67 | return smooth; 68 | } 69 | } 70 | 71 | private static final Map> inSpeedMap = new HashMap<>(); 72 | private static final Map> outSpeedMap = new HashMap<>(); 73 | private static final Map interIn = new HashMap<>(); 74 | private static final Map interOut = new HashMap<>(); 75 | 76 | public static Runnable task = () -> { 77 | File file = new File(filename); 78 | if (file.exists()) { 79 | try { 80 | List lines = Files.readAllLines(Paths.get(file.toURI())); 81 | interfaces = lines.stream().skip(2).map(line -> line.replaceAll("(\\s)+", ",").split(",")[1]) 82 | .filter(eth -> !eth.startsWith("lo:")) 83 | .flatMap(eth -> { 84 | ArrayList objects = new ArrayList<>(); 85 | objects.add(eth + "入"); 86 | objects.add(eth + "出"); 87 | return objects.stream(); 88 | }).collect(Collectors.toList()); 89 | // interfaces.stream().forEach(System.out::println); 90 | } catch (IOException e) { 91 | e.printStackTrace(); 92 | } 93 | while (true) { 94 | try { 95 | List lines = Files.readAllLines(Paths.get(file.toURI())); 96 | lines.stream().skip(2).forEach((line) -> { 97 | line = line.replaceAll("(\\s)+", ","); 98 | String[] split = line.split(","); 99 | String eth = split[1]; 100 | if (eth.startsWith("lo:")) { 101 | return; 102 | } 103 | long in = Long.parseLong(split[2]); 104 | long out = Long.parseLong(split[10]); 105 | long oldOut = interOut.getOrDefault(eth, (long) 0); 106 | long oldIn = interIn.getOrDefault(eth, (long) 0); 107 | long outChange = out - oldOut; 108 | long inChange = in - oldIn; 109 | if (oldIn != 0) { 110 | inSpeedMap.computeIfAbsent(eth, s -> new LinkedList<>()); 111 | inSpeedMap.get(eth).add((double) (inChange / 1024)); 112 | if (inSpeedMap.get(eth).size() > seconds) { 113 | inSpeedMap.get(eth).remove(0); 114 | } 115 | } 116 | if (oldOut != 0) { 117 | outSpeedMap.computeIfAbsent(eth, s -> new LinkedList()); 118 | outSpeedMap.get(eth).add((double) (outChange / 1024)); 119 | if (outSpeedMap.get(eth).size() > seconds) { 120 | outSpeedMap.get(eth).remove(0); 121 | } 122 | } 123 | interIn.put(eth, in); 124 | interOut.put(eth, out); 125 | }); 126 | Thread.sleep(1000); 127 | } catch (IOException e) { 128 | e.printStackTrace(); 129 | } catch (InterruptedException e) { 130 | e.printStackTrace(); 131 | } 132 | } 133 | } 134 | 135 | }; 136 | 137 | private static final List buildYvalues() { 138 | List YValues = new ArrayList<>(); 139 | // inSpeedMap.entrySet().forEach(entry->{ 140 | // String eth =entry.getKey(); 141 | // List speeds=entry.getValue(); 142 | // YValue yValue =new YValue(eth+"入",speeds); 143 | // YValues.add(yValue); 144 | // }); 145 | outSpeedMap.entrySet().forEach(entry -> { 146 | String eth = entry.getKey(); 147 | List speeds = entry.getValue(); 148 | YValue yValue = new YValue(eth + "出", speeds); 149 | YValues.add(yValue); 150 | }); 151 | return YValues; 152 | } 153 | 154 | public static final void start() { 155 | new Thread(NetStats.task).start(); 156 | } 157 | 158 | public static final String html() { 159 | try { 160 | List yValues = buildYvalues(); 161 | String legends = JsonUtil.toJson(interfaces); 162 | String scales = JsonUtil.toJson(xScales); 163 | String series = JsonUtil.toJson(yValues); 164 | 165 | String template = "\n" + 166 | "\n" + 167 | "\n" + 168 | " \n" + 169 | " 谁在跑流量\n" + 170 | " \n" + 171 | "\n" + 172 | "\n" + 173 | "
\n" + 174 | "\n" + 210 | "\n" + 211 | ""; 212 | return template; 213 | } catch (Throwable e) { 214 | log.error("", e); 215 | } 216 | return ""; 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/monitor/PromMonitorImpl.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.monitor; 2 | 3 | import com.google.common.collect.Lists; 4 | import io.netty.buffer.PooledByteBufAllocator; 5 | import io.netty.util.internal.PlatformDependent; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.File; 10 | import java.lang.management.BufferPoolMXBean; 11 | import java.lang.management.ManagementFactory; 12 | import java.lang.management.MemoryMXBean; 13 | import java.lang.management.MemoryUsage; 14 | import java.net.InetAddress; 15 | import java.net.UnknownHostException; 16 | import java.nio.file.Files; 17 | import java.nio.file.Paths; 18 | import java.util.*; 19 | import java.util.stream.Collectors; 20 | 21 | /** 22 | * prometheus exporter实现类 23 | */ 24 | public class PromMonitorImpl implements MonitorService { 25 | private static final Logger logger = LoggerFactory.getLogger(PromMonitorImpl.class); 26 | private static String hostname; 27 | static MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); 28 | List bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class); 29 | 30 | static { 31 | try { 32 | hostname = InetAddress.getLocalHost().getHostName(); 33 | } catch (UnknownHostException e) { 34 | e.printStackTrace(); 35 | } 36 | } 37 | 38 | private static final class Metric { 39 | private MetricType type; 40 | private String help; 41 | private String name; 42 | private Map tags = new HashMap<>(); 43 | private T value; 44 | 45 | 46 | public Metric(MetricType type, String help, String name, T value) { 47 | this.type = type; 48 | this.help = help; 49 | this.name = name; 50 | this.value = value; 51 | } 52 | 53 | public Metric tag(String key, String value) { 54 | tags.put(key, value); 55 | return this; 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "# HELP " + name + " " + help + "\n" + 61 | "# TYPE " + name + " " + type + "\n" + 62 | nameTags() + " " + getValue() + "\n"; 63 | } 64 | 65 | private String getValue() { 66 | if (value == null) { 67 | return "0"; 68 | } else { 69 | return String.valueOf(value); 70 | } 71 | } 72 | 73 | private String nameTags() { 74 | StringBuilder sb = new StringBuilder(); 75 | sb.append(name); 76 | sb.append("{"); 77 | for (Map.Entry tagvalue : tags.entrySet()) { 78 | sb.append(tagvalue.getKey()); 79 | sb.append("=\""); 80 | sb.append(tagvalue.getValue()); 81 | sb.append("\","); 82 | } 83 | sb.append("}"); 84 | return sb.toString(); 85 | } 86 | } 87 | 88 | private enum MetricType { 89 | gauge, counter 90 | } 91 | 92 | @Override 93 | public String metrics() { 94 | MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage(); 95 | MemoryUsage nonHeapMemoryUsage = memoryMXBean.getNonHeapMemoryUsage(); 96 | List> metrics = new ArrayList<>(); 97 | metrics.add(new Metric<>(MetricType.counter, "上行流量", "proxy_out", GlobalTrafficMonitor.getInstance().outTotal).tag("host", hostname)); 98 | metrics.add(new Metric<>(MetricType.counter, "下行流量", "proxy_in", GlobalTrafficMonitor.getInstance().inTotal).tag("host", hostname)); 99 | metrics.add(new Metric<>(MetricType.gauge, "上行网速", "proxy_out_rate", GlobalTrafficMonitor.getInstance().outRate).tag("host", hostname)); 100 | metrics.add(new Metric<>(MetricType.gauge, "下行网速", "proxy_in_rate", GlobalTrafficMonitor.getInstance().inRate).tag("host", hostname)); 101 | metrics.add(new Metric<>(MetricType.gauge, "netty直接内存 对于jdk9+,请增加-Dio.netty.tryReflectionSetAccessible=true,对于jdk16+,请增加--add-opens java.base/java.nio=ALL-UNNAMED", "direct_memory_total", nettyUsedDirectMemory()).tag("host", hostname)); 102 | metrics.add(new Metric<>(MetricType.gauge, "堆内存使用量", "heap_memory_usage", heapMemoryUsage.getUsed()).tag("host", hostname)); 103 | metrics.add(new Metric<>(MetricType.gauge, "堆内存容量", "heap_memory_committed", heapMemoryUsage.getCommitted()).tag("host", hostname)); 104 | metrics.add(new Metric<>(MetricType.gauge, "非堆内存使用量", "nonheap_memory_usage", nonHeapMemoryUsage.getUsed()).tag("host", hostname)); 105 | metrics.add(new Metric<>(MetricType.gauge, "非堆内存容量", "nonheap_memory_committed", nonHeapMemoryUsage.getCommitted()).tag("host", hostname)); 106 | for (BufferPoolMXBean bufferPool : bufferPoolMXBeans) { 107 | metrics.add(new Metric(MetricType.gauge, "bufferPool使用量" + bufferPool.getName(), "bufferpool_used_" + fixName(bufferPool.getName()), bufferPool.getMemoryUsed()).tag("host", hostname)); 108 | metrics.add(new Metric(MetricType.gauge, "bufferPool容量" + bufferPool.getName(), "bufferpool_capacity_" + fixName(bufferPool.getName()), bufferPool.getTotalCapacity()).tag("host", hostname)); 109 | } 110 | metrics.addAll(procNetDevMetric()); 111 | return metrics.stream().map(Metric::toString).collect(Collectors.joining()); 112 | } 113 | 114 | private String fixName(String name) { 115 | String s = name.replaceAll(" ", "_").replaceAll("'", "").replaceAll("-", "_"); 116 | return s; 117 | } 118 | 119 | private long nettyUsedDirectMemory() { 120 | return PlatformDependent.usedDirectMemory(); 121 | // return PooledByteBufAllocator.DEFAULT.metric().usedDirectMemory(); 122 | } 123 | 124 | private static List> procNetDevMetric() { 125 | String filename = "/proc/net/dev"; 126 | File file = new File(filename); 127 | if (file.exists()) { 128 | try { 129 | List lines = Files.readAllLines(Paths.get(file.toURI())); 130 | List> metrics = lines.stream() 131 | .skip(2) 132 | .map(line -> line.trim().replaceAll("(\\s)+", " ").split(" ")) 133 | .flatMap(splits -> { 134 | if (splits.length == 17) { 135 | String interfaceName = splits[0].substring(0, splits[0].length() - 1).replaceAll("-", "_"); 136 | return Lists.newArrayList( 137 | new Metric(MetricType.counter, "网卡流量", interfaceName + "_in_total", Long.parseLong(splits[1])).tag("host", hostname), 138 | new Metric(MetricType.counter, "网卡流量", interfaceName + "_out_total", Long.parseLong(splits[9])).tag("host", hostname) 139 | ).stream(); 140 | } 141 | return null; 142 | }) 143 | .filter(Objects::nonNull) 144 | .collect(Collectors.toList()); 145 | return metrics; 146 | } catch (Exception e) { 147 | logger.error("", e); 148 | } 149 | } 150 | return new ArrayList<>(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/session/Session.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.session; 2 | 3 | import com.arloor.forwardproxy.handler.SessionHandShakeHandler; 4 | import io.netty.bootstrap.Bootstrap; 5 | import io.netty.channel.ChannelHandlerContext; 6 | import io.netty.handler.codec.http.HttpContent; 7 | import io.netty.handler.codec.http.HttpObject; 8 | import io.netty.handler.codec.http.HttpRequest; 9 | import io.opentelemetry.api.trace.Span; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Map; 15 | import java.util.Set; 16 | 17 | public class Session { 18 | private static final int MAX_REQUEST_NUM = 100; 19 | private static final Logger log = LoggerFactory.getLogger(SessionHandShakeHandler.class); 20 | private final Map auths; 21 | private Span streamSpan; 22 | private Set whiteDomains; 23 | private Status status = Status.HTTP_REQUEST; 24 | private final Bootstrap bootstrap = new Bootstrap(); 25 | 26 | private String host; 27 | private int port; 28 | private int requestCount = 0; 29 | private HttpRequest request; 30 | private ArrayList contents = new ArrayList<>(); 31 | 32 | public Session(Map auths, Span streamSpan, Set whiteDomains) { 33 | this.auths = auths; 34 | this.streamSpan = streamSpan; 35 | this.whiteDomains = whiteDomains; 36 | } 37 | 38 | public boolean incrementCountAndIfNeedClose() { 39 | requestCount++; 40 | return requestCount >= MAX_REQUEST_NUM; 41 | } 42 | 43 | public void handle(ChannelHandlerContext channelHandlerContext, HttpObject msg) { 44 | this.status.handle(this, channelHandlerContext, msg); 45 | } 46 | 47 | public Bootstrap getBootStrap() { 48 | return bootstrap; 49 | } 50 | 51 | public void addContent(HttpContent httpContent) { 52 | this.contents.add(httpContent); 53 | } 54 | 55 | public void setAttribute(String key, String value) { 56 | this.streamSpan.setAttribute(key, value); 57 | } 58 | 59 | public Span getStreamSpan() { 60 | return streamSpan; 61 | } 62 | 63 | public Map getAuths() { 64 | return auths; 65 | } 66 | 67 | public ArrayList getContents() { 68 | return contents; 69 | } 70 | 71 | public Status getStatus() { 72 | return status; 73 | } 74 | 75 | public void setStatus(Status status) { 76 | this.status = status; 77 | } 78 | 79 | public String getHost() { 80 | return host; 81 | } 82 | 83 | public void setHost(String host) { 84 | this.host = host; 85 | } 86 | 87 | public int getPort() { 88 | return port; 89 | } 90 | 91 | public void setPort(int port) { 92 | this.port = port; 93 | } 94 | 95 | public HttpRequest getRequest() { 96 | return request; 97 | } 98 | 99 | public void setRequest(HttpRequest request) { 100 | this.request = request; 101 | } 102 | 103 | public boolean checkAuth(String basicAuth) { 104 | if (isCheckAuthByWhiteDomain()) { 105 | if (host != null) { 106 | for (String whiteDomain : whiteDomains) { 107 | if (host.endsWith(whiteDomain)) { 108 | return true; 109 | } 110 | } 111 | } 112 | } else { 113 | if (auths != null && auths.size() != 0) { 114 | if (basicAuth != null && auths.containsKey(basicAuth)) { 115 | return true; 116 | } 117 | } else { 118 | return true; 119 | } 120 | } 121 | return false; 122 | } 123 | 124 | public boolean isCheckAuthByWhiteDomain() { 125 | return !whiteDomains.isEmpty(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/session/Status.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.session; 2 | 3 | import com.arloor.forwardproxy.handler.RelayHandler; 4 | import com.arloor.forwardproxy.handler.SessionHandShakeHandler; 5 | import com.arloor.forwardproxy.trace.TraceConstant; 6 | import com.arloor.forwardproxy.trace.Tracer; 7 | import com.arloor.forwardproxy.util.OsUtils; 8 | import com.arloor.forwardproxy.util.SocksServerUtils; 9 | import com.arloor.forwardproxy.vo.Config; 10 | import com.arloor.forwardproxy.web.Dispatcher; 11 | import io.netty.bootstrap.Bootstrap; 12 | import io.netty.channel.*; 13 | import io.netty.channel.socket.SocketChannel; 14 | import io.netty.handler.codec.http.*; 15 | import io.netty.handler.timeout.IdleStateHandler; 16 | import io.netty.util.ReferenceCountUtil; 17 | import io.opentelemetry.api.trace.Span; 18 | import io.opentelemetry.context.Scope; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import java.net.InetSocketAddress; 23 | import java.util.Map; 24 | import java.util.concurrent.ThreadLocalRandom; 25 | 26 | import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR; 27 | import static io.netty.handler.codec.http.HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED; 28 | 29 | public enum Status { 30 | HTTP_REQUEST { 31 | @Override 32 | public void handle(Session session, ChannelHandlerContext channelContext, HttpObject msg) { 33 | if (msg instanceof HttpRequest request) { 34 | session.setRequest(request); 35 | String hostAndPortStr = HttpMethod.CONNECT.equals(request.method()) ? request.uri() : request.headers().get("Host"); 36 | String[] hostPortArray = hostAndPortStr.split(":"); 37 | String host = hostPortArray[0]; 38 | session.setHost(host); 39 | String portStr = hostPortArray.length == 2 ? hostPortArray[1] : !HttpMethod.CONNECT.equals(request.method()) ? "80" : "443"; 40 | session.setPort(Integer.parseInt(portStr)); 41 | session.setAttribute(TraceConstant.host.name(), host); 42 | session.setStatus(LAST_HTTP_CONTENT); 43 | } 44 | } 45 | }, LAST_HTTP_CONTENT { 46 | @Override 47 | public void handle(Session session, ChannelHandlerContext channelContext, HttpObject msg) { 48 | //SimpleChannelInboundHandler会将HttpContent中的bytebuf Release,但是这个还会转给relayHandler,所以需要在这里预先retain 49 | ((HttpContent) msg).content().retain(); 50 | session.addContent((HttpContent) msg); //暂存transfer-encode: chuncked中的chunk或者普通模式下的body。这里会占用内存,大文件上传下可能会OOM,尚未遇到 51 | if (msg instanceof LastHttpContent) { 52 | // 1. 如果url以 / 开头,则认为是直接请求,而不是代理请求 53 | if (session.getRequest().uri().startsWith("/")) { 54 | session.setStatus(WEB); 55 | session.handle(channelContext, msg); 56 | } else { 57 | session.setStatus(CheckAuth); 58 | session.handle(channelContext, msg); 59 | } 60 | } 61 | } 62 | }, WEB { 63 | @Override 64 | public void handle(Session session, ChannelHandlerContext channelContext, HttpObject msg) { 65 | session.setAttribute(TraceConstant.host.name(), "localhost"); 66 | Span dispatch = Tracer.spanBuilder(TraceConstant.web.name()) 67 | .setAttribute(TraceConstant.url.name(), String.valueOf(session.getRequest().uri())) 68 | .setParent(io.opentelemetry.context.Context.current().with(session.getStreamSpan())) 69 | .startSpan(); 70 | try (Scope scope = dispatch.makeCurrent()) { 71 | boolean ifNeedClose = session.incrementCountAndIfNeedClose(); 72 | Dispatcher.handle(session.getRequest(), channelContext, ifNeedClose); 73 | // 这里需要将content全部release 74 | session.getContents().forEach(ReferenceCountUtil::release); 75 | } finally { 76 | dispatch.end(); 77 | } 78 | session.setStatus(HTTP_REQUEST); 79 | } 80 | }, CheckAuth { 81 | @Override 82 | public void handle(Session session, ChannelHandlerContext channelContext, HttpObject msg) { 83 | String clientHostname = ((InetSocketAddress) channelContext.channel().remoteAddress()).getAddress().getHostAddress(); 84 | //2. 检验auth 85 | HttpRequest request = session.getRequest(); 86 | String basicAuth = request.headers().get("Proxy-Authorization"); 87 | String userName = "nouser"; 88 | Map auths = session.getAuths(); 89 | if (basicAuth != null && basicAuth.length() != 0) { 90 | String raw = auths.get(basicAuth); 91 | if (raw != null && raw.length() != 0) { 92 | userName = raw.split(":")[0]; 93 | } 94 | } 95 | 96 | if (!session.checkAuth(basicAuth)) { 97 | log.warn(clientHostname + " " + request.method() + " " + request.uri() + " {" + session.getHost() + "} wrong_auth:{" + basicAuth + "}"); 98 | // 这里需要将content全部release 99 | session.getContents().forEach(ReferenceCountUtil::release); 100 | DefaultHttpResponse responseAuthRequired; 101 | if (Config.ask4Authcate && !session.isCheckAuthByWhiteDomain() && !request.method().equals(HttpMethod.OPTIONS) && !request.method().equals(HttpMethod.HEAD)) { 102 | responseAuthRequired = new DefaultHttpResponse(request.protocolVersion(), PROXY_AUTHENTICATION_REQUIRED); 103 | responseAuthRequired.headers().add("Proxy-Authenticate", "Basic realm=\"netty forwardproxy\""); 104 | } else { 105 | responseAuthRequired = new DefaultHttpResponse(request.protocolVersion(), INTERNAL_SERVER_ERROR); 106 | } 107 | channelContext.channel().writeAndFlush(responseAuthRequired); 108 | SocksServerUtils.closeOnFlush(channelContext.channel()); 109 | Tracer.spanBuilder(TraceConstant.wrong_auth.name()) 110 | .setAttribute(TraceConstant.auth.name(), String.valueOf(basicAuth)) 111 | .setParent(io.opentelemetry.context.Context.current().with(session.getStreamSpan())) 112 | .startSpan() 113 | .end(); 114 | session.setStatus(HTTP_REQUEST); 115 | return; 116 | } 117 | 118 | //3. 这里进入代理请求处理,分为两种:CONNECT方法和其他HTTP方法 119 | log.info("{}@{} ==> {} {} {}", userName, clientHostname, request.method(), request.uri(), !request.uri().equals(request.headers().get("Host")) ? "Host=" + request.headers().get("Host") : ""); 120 | if (request.method().equals(HttpMethod.CONNECT)) { 121 | session.setStatus(TUNNEL); 122 | } else { 123 | session.setStatus(GETPOST); 124 | } 125 | session.handle(channelContext, msg); 126 | } 127 | }, TUNNEL { 128 | @Override 129 | public void handle(Session session, ChannelHandlerContext channelContext, HttpObject msg) { 130 | HttpRequest request = session.getRequest(); 131 | final Channel inboundChannel = channelContext.channel(); 132 | Bootstrap b = session.getBootStrap(); 133 | b.group(inboundChannel.eventLoop()) 134 | .channel(OsUtils.socketChannelClazz()) 135 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) 136 | .option(ChannelOption.SO_KEEPALIVE, true) 137 | .handler(new RelayHandler(channelContext.channel(), session.getHost())); 138 | b.connect(session.getHost(), session.getPort()).addListener(new ChannelFutureListener() { 139 | @Override 140 | public void operationComplete(ChannelFuture future) throws Exception { 141 | if (future.isSuccess()) { 142 | final Channel outboundChannel = future.channel(); 143 | String targetAddr = ((InetSocketAddress) outboundChannel.remoteAddress()).getAddress().getHostAddress(); 144 | session.setAttribute(TraceConstant.target.name(), targetAddr); 145 | // Connection established use handler provided results 146 | DefaultHttpResponse response = new DefaultHttpResponse(request.protocolVersion(), new HttpResponseStatus(200, "Connection Established")); 147 | int size = ThreadLocalRandom.current().nextInt(150); 148 | for (int i = 0; i < size; i++) { 149 | response.headers().add("Server", "JavaHttpProxy"); 150 | } 151 | ChannelFuture responseFuture = channelContext.channel().writeAndFlush(response); 152 | responseFuture.addListener(new ChannelFutureListener() { 153 | @Override 154 | public void operationComplete(ChannelFuture channelFuture) { 155 | if (channelFuture.isSuccess()) { 156 | channelContext.pipeline().remove(IdleStateHandler.class); 157 | channelContext.pipeline().remove(HttpRequestDecoder.class); 158 | channelContext.pipeline().remove(HttpResponseEncoder.class); 159 | channelContext.pipeline().remove(HttpServerExpectContinueHandler.class); 160 | channelContext.pipeline().remove(SessionHandShakeHandler.class); 161 | channelContext.pipeline().addLast(new RelayHandler(outboundChannel, session.getHost())); 162 | } else { 163 | log.info("reply tunnel established Failed: " + channelContext.channel().remoteAddress() + " " + request.method() + " " + request.uri()); 164 | SocksServerUtils.closeOnFlush(channelContext.channel()); 165 | SocksServerUtils.closeOnFlush(outboundChannel); 166 | } 167 | } 168 | }); 169 | } else { 170 | // Close the connection if the connection attempt has failed. 171 | channelContext.channel().writeAndFlush( 172 | new DefaultHttpResponse(request.protocolVersion(), INTERNAL_SERVER_ERROR) 173 | ); 174 | SocksServerUtils.closeOnFlush(channelContext.channel()); 175 | } 176 | } 177 | }); 178 | session.setStatus(WAIT_ESTABLISH); 179 | } 180 | }, GETPOST { 181 | @Override 182 | public void handle(Session session, ChannelHandlerContext channelContext, HttpObject msg) { 183 | HttpRequest request = session.getRequest(); 184 | final Channel inboundChannel = channelContext.channel(); 185 | Bootstrap b = session.getBootStrap(); 186 | b.group(inboundChannel.eventLoop()) 187 | .channel(OsUtils.socketChannelClazz()) 188 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) 189 | .option(ChannelOption.SO_KEEPALIVE, true) 190 | .handler(new ChannelInitializer() { 191 | @Override 192 | protected void initChannel(SocketChannel outboundChannel) throws Exception { 193 | outboundChannel.pipeline().addLast(new HttpRequestEncoder()); 194 | outboundChannel.pipeline().addLast(new RelayHandler(channelContext.channel(), session.getHost())); 195 | } 196 | }); 197 | b.connect(session.getHost(), session.getPort()).addListener(new ChannelFutureListener() { 198 | @Override 199 | public void operationComplete(ChannelFuture future) throws Exception { 200 | if (future.isSuccess()) { 201 | final Channel outboundChannel = future.channel(); 202 | String targetAddr = ((InetSocketAddress) outboundChannel.remoteAddress()).getAddress().getHostAddress(); 203 | session.setAttribute(TraceConstant.target.name(), targetAddr); 204 | // Connection established use handler provided results 205 | // 这里有几率抛出NoSuchElementException,原因是连接target host完成时,客户端已经关闭连接。 206 | // 考虑到是比较小的几率,不catch。注:该异常没有啥影响。 207 | channelContext.pipeline().remove(IdleStateHandler.class); 208 | channelContext.pipeline().remove(SessionHandShakeHandler.class); 209 | channelContext.pipeline().remove(HttpResponseEncoder.class); 210 | RelayHandler clientEndtoRemoteHandler = new RelayHandler(outboundChannel, session.getHost()); 211 | channelContext.pipeline().addLast(clientEndtoRemoteHandler); 212 | // ctx.channel().config().setAutoRead(true); 213 | 214 | //出于未知的原因,不知道为什么fireChannelread不行 215 | clientEndtoRemoteHandler.channelRead(channelContext, request); 216 | session.getContents().forEach(content -> { 217 | try { 218 | clientEndtoRemoteHandler.channelRead(channelContext, content); 219 | } catch (Exception e) { 220 | log.error("处理非CONNECT方法的代理请求失败!", e); 221 | } 222 | }); 223 | 224 | } else { 225 | // Close the connection if the connection attempt has failed. 226 | channelContext.channel().writeAndFlush( 227 | new DefaultHttpResponse(request.protocolVersion(), INTERNAL_SERVER_ERROR) 228 | ); 229 | SocksServerUtils.closeOnFlush(channelContext.channel()); 230 | } 231 | } 232 | }); 233 | session.setStatus(WAIT_ESTABLISH); 234 | } 235 | }, WAIT_ESTABLISH { // 等待到target的连接建立前不应该有新请求进入 236 | 237 | @Override 238 | public void handle(Session session, ChannelHandlerContext channelContext, HttpObject msg) { 239 | log.error("receive new message before tunnel is established, msg: {}", msg); 240 | } 241 | }; 242 | 243 | private static final Logger log = LoggerFactory.getLogger(Status.class); 244 | 245 | public abstract void handle(Session session, ChannelHandlerContext channelContext, HttpObject msg); 246 | } 247 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/ssl/SslContextFactory.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.ssl; 2 | 3 | import io.netty.handler.ssl.ClientAuth; 4 | import io.netty.handler.ssl.SslContext; 5 | import io.netty.handler.ssl.SslContextBuilder; 6 | import io.netty.handler.ssl.SslProvider; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.*; 10 | import java.security.GeneralSecurityException; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | 14 | public class SslContextFactory { 15 | private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(SslContextFactory.class); 16 | 17 | static { 18 | // 解决algid parse error, not a sequence 19 | // https://blog.csdn.net/ls0111/article/details/77533768 20 | java.security.Security.addProvider( 21 | new org.bouncycastle.jce.provider.BouncyCastleProvider() 22 | ); 23 | } 24 | 25 | 26 | public static SslContext getSSLContext(String fullchainFile, String privkeyFile) throws IOException, GeneralSecurityException { 27 | try { 28 | //jdk8删除gcm加密 29 | List ciphers = Arrays.asList("ECDHE-RSA-AES128-SHA", "ECDHE-RSA-AES256-SHA", "AES128-SHA", "AES256-SHA", "DES-CBC3-SHA"); 30 | 31 | return SslContextBuilder.forServer(new File(fullchainFile),new File(privkeyFile)) 32 | .protocols("TLSv1.3", "TLSv1.2") 33 | .sslProvider(SslProvider.OPENSSL) 34 | .clientAuth(ClientAuth.NONE) 35 | .trustManager(new File(fullchainFile)) 36 | // .ciphers(ciphers) 37 | .build(); 38 | 39 | } catch (IOException e) { 40 | LOGGER.warn("Failed to establish SSL Context"); 41 | LOGGER.debug("Failed to establish SSL Context", e); 42 | throw e; 43 | } 44 | } 45 | 46 | 47 | 48 | private static void closeSilent(final InputStream is) { 49 | if (is == null) 50 | return; 51 | try { 52 | is.close(); 53 | } catch (Exception ignored) { 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/trace/LogSpanExporter.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.trace; 2 | 3 | import io.opentelemetry.api.trace.SpanKind; 4 | import io.opentelemetry.sdk.common.CompletableResultCode; 5 | import io.opentelemetry.sdk.trace.data.SpanData; 6 | import io.opentelemetry.sdk.trace.export.SpanExporter; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.util.Collection; 11 | import java.util.Comparator; 12 | import java.util.stream.Collectors; 13 | 14 | public class LogSpanExporter implements SpanExporter { 15 | private static final Logger logger = LoggerFactory.getLogger(LogSpanExporter.class); 16 | 17 | @Override 18 | public CompletableResultCode export(Collection collection) { 19 | for (SpanData spanData : collection) { 20 | long durationInSeconds = (spanData.getEndEpochNanos() - spanData.getStartEpochNanos()) / 1000000000; 21 | long durationInMills = (spanData.getEndEpochNanos() - spanData.getStartEpochNanos()) / 1000000; 22 | String time = (durationInSeconds <= 0) ? durationInMills + "ms" : durationInSeconds + "s"; 23 | if (SpanKind.SERVER.equals(spanData.getKind())) { 24 | String attrs = spanData.getAttributes().asMap().entrySet().stream() 25 | .sorted(Comparator.comparing(o -> o.getKey().getKey())) 26 | .map(entry -> String.format("%s=%s", entry.getKey().getKey(), entry.getValue())) 27 | .collect(Collectors.joining(", ")); 28 | logger.info("{} for {}", String.format("%8s", time), attrs); 29 | } 30 | } 31 | return CompletableResultCode.ofSuccess(); 32 | } 33 | 34 | @Override 35 | public CompletableResultCode flush() { 36 | return CompletableResultCode.ofSuccess(); 37 | } 38 | 39 | @Override 40 | public CompletableResultCode shutdown() { 41 | return CompletableResultCode.ofSuccess(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/trace/OtelContextDemo.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.trace; 2 | 3 | import io.opentelemetry.context.Context; 4 | import io.opentelemetry.context.ContextKey; 5 | import io.opentelemetry.context.ImplicitContextKeyed; 6 | import io.opentelemetry.context.Scope; 7 | 8 | import java.util.Optional; 9 | import java.util.concurrent.ExecutorService; 10 | import java.util.concurrent.Executors; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | public class OtelContextDemo { 14 | 15 | /** 16 | * @see io.opentelemetry.api.trace.SpanContextKey 17 | * @see io.opentelemetry.api.baggage.BaggageContextKey 18 | */ 19 | public static class XrayContextKey { 20 | static final ContextKey KEY = ContextKey.named("xray-context-key"); 21 | 22 | private XrayContextKey() { 23 | } 24 | } 25 | 26 | /** 27 | * implements ImplicitContextKeyed 28 | * @see io.opentelemetry.api.baggage.Baggage#storeInContext(Context) 29 | */ 30 | public static class XrayContext implements ImplicitContextKeyed { 31 | private String payload; 32 | 33 | public String getPayload() { 34 | return payload; 35 | } 36 | 37 | public void setPayload(String payload) { 38 | this.payload = payload; 39 | } 40 | 41 | public XrayContext(String payload) { 42 | this.payload = payload; 43 | } 44 | 45 | @Override 46 | public Context storeInContext(Context context) { 47 | return context.with(XrayContextKey.KEY, this); 48 | } 49 | } 50 | 51 | private static final ExecutorService poolWrapped = Context.taskWrapping(Executors.newCachedThreadPool()); // OpenTelemetry增强的线程池 52 | private static final ExecutorService poolUnwrapped = Executors.newCachedThreadPool(); 53 | 54 | public static void main(String[] args) { 55 | Context root = Context.current().with(new XrayContext("some value")); // 设置Context 56 | XrayContext contextOutSideOfScope = Context.current().get(XrayContextKey.KEY); 57 | System.out.println("outside context is " + Optional.ofNullable(contextOutSideOfScope).map(XrayContext::getPayload).orElse(null)); 58 | try (Scope scope = root.makeCurrent()) { // 放置到threadlocal 59 | XrayContext contextInScope = Context.current().get(XrayContextKey.KEY); 60 | System.out.println("inner context is " + Optional.ofNullable(contextInScope).map(XrayContext::getPayload).orElse(null)); 61 | poolWrapped.execute(() -> { 62 | XrayContext xrayContext = Context.current().get(XrayContextKey.KEY); 63 | System.out.println("pool wrapped context is " + Optional.ofNullable(xrayContext).map(XrayContext::getPayload).orElse(null)); 64 | }); 65 | poolUnwrapped.execute(() -> { 66 | XrayContext xrayContext = Context.current().get(XrayContextKey.KEY); 67 | System.out.println("pool unwrapped context is " + Optional.ofNullable(xrayContext).map(XrayContext::getPayload).orElse(null)); 68 | }); 69 | } 70 | try { 71 | poolUnwrapped.shutdown(); 72 | poolWrapped.shutdown(); 73 | poolUnwrapped.awaitTermination(1, TimeUnit.SECONDS); 74 | poolWrapped.awaitTermination(1, TimeUnit.SECONDS); 75 | } catch (Throwable e) { 76 | poolUnwrapped.shutdownNow(); 77 | poolWrapped.shutdownNow(); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/trace/TraceConstant.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.trace; 2 | 3 | public enum TraceConstant { 4 | stream, web, proxy, wrong_auth, host, port, url, auth, method, client, target 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/trace/Tracer.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.trace; 2 | 3 | import io.opentelemetry.api.OpenTelemetry; 4 | import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; 5 | import io.opentelemetry.api.trace.Span; 6 | import io.opentelemetry.api.trace.SpanBuilder; 7 | import io.opentelemetry.api.trace.SpanKind; 8 | import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; 9 | import io.opentelemetry.context.Scope; 10 | import io.opentelemetry.context.propagation.ContextPropagators; 11 | import io.opentelemetry.context.propagation.TextMapPropagator; 12 | import io.opentelemetry.sdk.OpenTelemetrySdk; 13 | import io.opentelemetry.sdk.resources.Resource; 14 | import io.opentelemetry.sdk.trace.SdkTracerProvider; 15 | import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; 16 | import io.opentelemetry.sdk.trace.samplers.Sampler; 17 | 18 | import java.net.InetAddress; 19 | import java.net.UnknownHostException; 20 | 21 | public enum Tracer { 22 | 23 | INSTANCE; 24 | 25 | private io.opentelemetry.api.trace.Tracer delegate; 26 | 27 | Tracer() { 28 | // 创建TracerProvider,可以自定义TraceId,spanId生成规则;采样规则;后端(jaeger,otlp,logging) 29 | SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder() 30 | .setSampler(Sampler.alwaysOn()) 31 | .setResource(Resource.getDefault().toBuilder().put("service.name", serviceName()).build()) 32 | .addSpanProcessor(SimpleSpanProcessor.create(new LogSpanExporter())) 33 | // .addSpanProcessor(BatchSpanProcessor.builder(JaegerGrpcSpanExporter.builder().setEndpoint("http://hk.gcall.me:14250").build()).build()) 34 | .build(); 35 | 36 | OpenTelemetry openTelemetry = OpenTelemetrySdk.builder() 37 | .setTracerProvider(sdkTracerProvider) 38 | // 跨进程传播规则 39 | .setPropagators(ContextPropagators.create(TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance()))) 40 | .buildAndRegisterGlobal(); 41 | this.delegate = openTelemetry.getTracer("main"); 42 | } 43 | 44 | private String serviceName() { 45 | String hostName = null; 46 | try { 47 | hostName = InetAddress.getLocalHost().getHostName(); 48 | } catch (UnknownHostException e) { 49 | hostName = "unknown"; 50 | } 51 | return hostName; 52 | } 53 | 54 | public static SpanBuilder spanBuilder(String s) { 55 | return INSTANCE.delegate.spanBuilder(s); 56 | } 57 | 58 | public static void main(String[] args) throws InterruptedException { 59 | Span root = Tracer.spanBuilder("stream") 60 | .setSpanKind(SpanKind.SERVER) 61 | .setAttribute("class", Tracer.class.getSimpleName()) 62 | .setAttribute("date", System.currentTimeMillis()) 63 | .startSpan(); 64 | 65 | try (Scope scope = root.makeCurrent()) { 66 | Span span1 = Tracer.spanBuilder("process1") 67 | .setSpanKind(SpanKind.SERVER) 68 | .startSpan(); 69 | span1.end(); 70 | } finally { 71 | root.end(); 72 | } 73 | Thread.sleep(10000000); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.util; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude.Include; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.JavaType; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | 9 | import java.io.IOException; 10 | import java.text.SimpleDateFormat; 11 | import java.util.List; 12 | 13 | public class JsonUtil { 14 | private static ObjectMapper MAPPER; 15 | 16 | private JsonUtil() { 17 | } 18 | 19 | public static T fromJson(String json, Class clazz) throws IOException { 20 | return MAPPER.readValue(json, clazz); 21 | } 22 | 23 | public static T fromJson(String json, TypeReference valueTypeRef) throws IOException { 24 | return MAPPER.readValue(json, valueTypeRef); 25 | } 26 | 27 | public static List fromJson(String json, Class collection, Class clazz) throws IOException { 28 | return (List) MAPPER.readValue(json, getCollectionType(MAPPER, collection, clazz)); 29 | } 30 | 31 | public static String toJson(T src) throws IOException { 32 | return src instanceof String ? (String) src : MAPPER.writeValueAsString(src); 33 | } 34 | 35 | public static String toJson(T src, Include inclusion) throws IOException { 36 | if (src instanceof String) { 37 | return (String) src; 38 | } else { 39 | ObjectMapper customMapper = generateMapper(inclusion); 40 | return customMapper.writeValueAsString(src); 41 | } 42 | } 43 | 44 | public static String toJson(T src, ObjectMapper mapper) throws IOException { 45 | if (null != mapper) { 46 | return src instanceof String ? (String) src : mapper.writeValueAsString(src); 47 | } else { 48 | return null; 49 | } 50 | } 51 | 52 | private static ObjectMapper generateMapper(Include include) { 53 | ObjectMapper customMapper = new ObjectMapper(); 54 | customMapper.setSerializationInclusion(include); 55 | customMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 56 | customMapper.configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, true); 57 | customMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); 58 | return customMapper; 59 | } 60 | 61 | private static JavaType getCollectionType(ObjectMapper mapper, Class collectionClass, Class... elementClasses) { 62 | return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses); 63 | } 64 | 65 | static { 66 | MAPPER = generateMapper(Include.ALWAYS); 67 | } 68 | 69 | public enum Some { 70 | A("a", 1), B("b", 2); 71 | 72 | private String name; 73 | private int id; 74 | 75 | Some(String name, int id) { 76 | this.name = name; 77 | this.id = id; 78 | } 79 | 80 | public String getName() { 81 | return name; 82 | } 83 | 84 | public int getId() { 85 | return id; 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/util/OsUtils.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.util; 2 | 3 | 4 | import io.netty.channel.EventLoopGroup; 5 | import io.netty.channel.epoll.EpollEventLoopGroup; 6 | import io.netty.channel.epoll.EpollServerSocketChannel; 7 | import io.netty.channel.epoll.EpollSocketChannel; 8 | import io.netty.channel.kqueue.KQueueEventLoopGroup; 9 | import io.netty.channel.kqueue.KQueueServerSocketChannel; 10 | import io.netty.channel.kqueue.KQueueSocketChannel; 11 | import io.netty.channel.nio.NioEventLoopGroup; 12 | import io.netty.channel.socket.ServerSocketChannel; 13 | import io.netty.channel.socket.SocketChannel; 14 | import io.netty.channel.socket.nio.NioServerSocketChannel; 15 | import io.netty.channel.socket.nio.NioSocketChannel; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import java.lang.management.ManagementFactory; 20 | import java.util.Optional; 21 | 22 | public class OsUtils { 23 | private static final Logger logger = LoggerFactory.getLogger(OsUtils.class); 24 | private static final OS os = parseOS(); 25 | 26 | 27 | public static Class serverSocketChannelClazz() { 28 | return os.serverSocketChannelClazz; 29 | } 30 | 31 | public static Class socketChannelClazz() { 32 | return os.socketChannelClazz; 33 | } 34 | 35 | public static EventLoopGroup buildEventLoopGroup(int num) { 36 | return os.buildEventLoopGroup(num); 37 | } 38 | 39 | public static boolean isUnix() { 40 | return os.equals(OS.Unix); 41 | } 42 | 43 | public static boolean isWindows() { 44 | return os.equals(OS.Windows); 45 | } 46 | 47 | public static boolean isMac() { 48 | return os.equals(OS.MacOS); 49 | } 50 | 51 | private enum OS { 52 | MacOS("mac", KQueueServerSocketChannel.class, KQueueSocketChannel.class) { 53 | @Override 54 | EventLoopGroup buildEventLoopGroup(int num) { 55 | return new KQueueEventLoopGroup(num); 56 | } 57 | }, 58 | Unix("unix", EpollServerSocketChannel.class, EpollSocketChannel.class) { 59 | @Override 60 | EventLoopGroup buildEventLoopGroup(int num) { 61 | return new EpollEventLoopGroup(num); 62 | } 63 | }, 64 | Windows("windows", NioServerSocketChannel.class, NioSocketChannel.class) { 65 | @Override 66 | EventLoopGroup buildEventLoopGroup(int num) { 67 | return new NioEventLoopGroup(num); 68 | } 69 | }, 70 | Other("other", NioServerSocketChannel.class, NioSocketChannel.class) { 71 | @Override 72 | EventLoopGroup buildEventLoopGroup(int num) { 73 | return new NioEventLoopGroup(num); 74 | } 75 | }; 76 | 77 | String name; 78 | Class serverSocketChannelClazz; 79 | Class socketChannelClazz; 80 | 81 | abstract EventLoopGroup buildEventLoopGroup(int num); 82 | 83 | OS(String name, Class serverSocketChannelClass, Class socketChannelClass) { 84 | this.name = name; 85 | this.serverSocketChannelClazz = serverSocketChannelClass; 86 | this.socketChannelClazz = socketChannelClass; 87 | } 88 | } 89 | 90 | private static OsUtils.OS parseOS() { 91 | String osName = System.getProperty("os.name"); 92 | logger.info("当前系统为: " + osName); 93 | String name = ManagementFactory.getRuntimeMXBean().getName(); 94 | String pid = name.split("@")[0]; 95 | logger.info("该进程pid= " + pid); 96 | osName = Optional.ofNullable(osName).orElse("").toLowerCase(); 97 | if ((osName.contains("win"))) { 98 | return OS.Windows; 99 | } else if (osName.contains("mac")) { 100 | return OS.MacOS; 101 | } else if (osName.contains("nix") || osName.contains("nux") || osName.indexOf("aix") > 0) { 102 | return OS.Unix; 103 | } else { 104 | return OS.Other; 105 | } 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/util/RenderUtil.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.util; 2 | 3 | import com.arloor.forwardproxy.vo.RenderParam; 4 | import org.thymeleaf.TemplateEngine; 5 | import org.thymeleaf.context.Context; 6 | import org.thymeleaf.templatemode.TemplateMode; 7 | import org.thymeleaf.templateresolver.StringTemplateResolver; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | public class RenderUtil { 13 | private final static TemplateEngine textEngine = new TemplateEngine(); 14 | private final static TemplateEngine htmlEngine = new TemplateEngine(); 15 | 16 | static { 17 | StringTemplateResolver textResolver = new StringTemplateResolver(); 18 | textResolver.setOrder(1); 19 | textResolver.setTemplateMode(TemplateMode.TEXT); 20 | // TODO Cacheable or Not ? 21 | textResolver.setCacheable(true); 22 | textEngine.setTemplateResolver(textResolver); 23 | 24 | StringTemplateResolver templateResolver = new StringTemplateResolver(); 25 | templateResolver.setOrder(1); 26 | templateResolver.setTemplateMode(TemplateMode.HTML); 27 | // TODO Cacheable or Not ? 28 | templateResolver.setCacheable(true); 29 | htmlEngine.setTemplateResolver(templateResolver); 30 | } 31 | 32 | /** 33 | * 使用 Thymeleaf 渲染 Text模版 34 | * Text模版语法见:https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#textual-syntax 35 | * 36 | * @param template 模版 37 | * @param renderParam 参数 38 | * @return 渲染后的Text 39 | */ 40 | public static String text(String template, RenderParam renderParam) { 41 | 42 | Context context = new Context(); 43 | context.setVariables(renderParam.getContent()); 44 | return textEngine.process(template, context); 45 | } 46 | 47 | /** 48 | * 使用 Thymeleaf 渲染 Html模版 49 | * 50 | * @param template Html模版 51 | * @param renderParam 参数 52 | * @return 渲染后的html 53 | */ 54 | public static String html(String template, RenderParam renderParam) { 55 | Context context = new Context(); 56 | context.setVariables(renderParam.getContent()); 57 | return htmlEngine.process(template, context); 58 | } 59 | 60 | /** 61 | * 测试用,展示如何使用 62 | * 63 | * @param args 64 | */ 65 | public static void main(String[] args) { 66 | // 渲染String 67 | String string_template = "这是[(${name.toString()})]"; // 直接name其实就行了,这里就是展示能调用java对象的方法 68 | String value = RenderUtil.text(string_template, new RenderParam().add("name", "ARLOOR")); 69 | System.out.println(value); 70 | 71 | // 渲染List 72 | /** 73 | * [# th:each="item : ${items}"] 74 | * - [(${item})] 75 | * [/] 76 | */ 77 | String list_template = """ 78 | [# th:each="item : ${items}"] 79 | - [(${item})] 80 | [/]"""; 81 | String value1 = RenderUtil.text(list_template, new RenderParam().add("items", List.of("第一个", "第二个", "第三个"))); 82 | System.out.println(value1); 83 | 84 | // 渲染Map 85 | /** 86 | * [# th:each="key : ${map.keySet()}"] 87 | * - [(${map.get(key)})] 88 | * [/] 89 | */ 90 | String map_template = """ 91 | [# th:each="key : ${map.keySet()}"] 92 | 这是 - [(${map.get(key)})] 93 | [/]"""; 94 | String value2 = RenderUtil.text(map_template, new RenderParam().add("map", Map.of("a", "b", "c", "d"))); 95 | System.out.println(value2); 96 | 97 | String html_template = "这是"; 98 | System.out.println(RenderUtil.html(html_template, new RenderParam().add("name", "ARLOOR"))); 99 | 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/util/SocksServerUtils.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 | * http://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 com.arloor.forwardproxy.util; 17 | 18 | import io.netty.buffer.Unpooled; 19 | import io.netty.channel.Channel; 20 | import io.netty.channel.ChannelFutureListener; 21 | 22 | public final class SocksServerUtils { 23 | 24 | /** 25 | * Closes the specified channel after all queued write requests are flushed. 26 | */ 27 | public static void closeOnFlush(Channel ch) { 28 | if (ch.isActive()) { 29 | ch.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); 30 | } 31 | } 32 | 33 | private SocksServerUtils() { } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/vo/Config.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.vo; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.util.*; 5 | import java.util.stream.Collectors; 6 | 7 | public class Config { 8 | private static final String TRUE = "true"; 9 | 10 | public static boolean ask4Authcate = false; 11 | private static final String POUND_SIGN = "\u00A3"; // £ 12 | 13 | private SslConfig sslConfig; 14 | private HttpConfig httpConfig; 15 | 16 | public SslConfig ssl() { 17 | return sslConfig; 18 | } 19 | 20 | public HttpConfig http() { 21 | return httpConfig; 22 | } 23 | 24 | public static Config parse(Properties properties) { 25 | 26 | Config config = new Config(); 27 | ask4Authcate = TRUE.equals(properties.getProperty("ask4Authcate")); 28 | 29 | String httpsEnable = properties.getProperty("https.enable"); 30 | if (TRUE.equals(httpsEnable)) { 31 | String httpsPortStr = properties.getProperty("https.port"); 32 | Integer port = Integer.parseInt(httpsPortStr); 33 | String auth = properties.getProperty("https.auth"); 34 | Map users = new HashMap<>(); 35 | if (auth != null && auth.length() != 0) { 36 | for (String user : auth.split(",")) { 37 | users.computeIfAbsent(genBasicAuth(user), (cell) -> user); 38 | users.computeIfAbsent(genBasicAuthWithOut£(user), (cell) -> user); 39 | } 40 | } 41 | String fullchain = properties.getProperty("https.fullchain.pem"); 42 | String privkey = properties.getProperty("https.privkey.pem"); 43 | SslConfig sslConfig = new SslConfig(port, users, fullchain, privkey); 44 | config.sslConfig = sslConfig; 45 | } 46 | 47 | String httpEnable = properties.getProperty("http.enable"); 48 | if (TRUE.equals(httpEnable)) { 49 | String httpPortStr = properties.getProperty("http.port"); 50 | Integer port = Integer.parseInt(httpPortStr); 51 | String auth = properties.getProperty("http.auth"); 52 | Map users = new HashMap<>(); 53 | if (auth != null && auth.length() != 0) { 54 | for (String user : auth.split(",")) { 55 | users.computeIfAbsent(genBasicAuth(user), (cell) -> user); 56 | users.computeIfAbsent(genBasicAuthWithOut£(user), (cell) -> user); 57 | } 58 | } 59 | String whiteDomains = properties.getProperty("http.proxy.white.domain", ""); 60 | config.httpConfig = new HttpConfig(port, users, Arrays.stream(whiteDomains.split(",")).filter(s -> s != null && s.length() != 0).collect(Collectors.toSet())); 61 | ; 62 | } 63 | 64 | return config; 65 | } 66 | 67 | /** 68 | * https://datatracker.ietf.org/doc/html/rfc7617 69 | * The user's name is "test", and the password is the string "123" 70 | * followed by the Unicode character U+00A3 (POUND SIGN). Using the 71 | * character encoding scheme UTF-8, the user-pass becomes: 72 | *

73 | * 't' 'e' 's' 't' ':' '1' '2' '3' pound 74 | * 74 65 73 74 3A 31 32 33 C2 A3 75 | *

76 | * Encoding this octet sequence in Base64 ([RFC4648], Section 4) yields: 77 | *

78 | * dGVzdDoxMjPCow== 79 | * 80 | * @param user 81 | * @return 82 | */ 83 | private static String genBasicAuth(String user) { 84 | user += POUND_SIGN; 85 | return "Basic " + Base64.getEncoder().encodeToString(user.getBytes(StandardCharsets.UTF_8)); 86 | } 87 | 88 | 89 | private static String genBasicAuthWithOut£(String user) { 90 | return "Basic " + Base64.getEncoder().encodeToString(user.getBytes(StandardCharsets.UTF_8)); 91 | } 92 | 93 | 94 | } -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/vo/HttpConfig.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.vo; 2 | 3 | import java.util.HashSet; 4 | import java.util.Map; 5 | import java.util.Set; 6 | 7 | public class HttpConfig { 8 | private Integer port; 9 | private Map auth; // base64 - raw 10 | private Set domainWhiteList = new HashSet<>(); 11 | 12 | public HttpConfig(Integer port, Map auth, Set domainWhiteList) { 13 | this.port = port; 14 | this.auth = auth; 15 | if (domainWhiteList != null) { 16 | this.domainWhiteList = domainWhiteList; 17 | } 18 | } 19 | 20 | public Integer getPort() { 21 | return port; 22 | } 23 | 24 | public String getAuth(String base64Auth) { 25 | return auth.get(base64Auth); 26 | } 27 | 28 | public Map getAuthMap() { 29 | return auth; 30 | } 31 | 32 | public boolean needAuth() { 33 | return auth != null && auth.size() != 0; 34 | } 35 | 36 | public Set getDomainWhiteList() { 37 | return domainWhiteList; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/vo/RenderParam.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.vo; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public final class RenderParam { 7 | private Map map = new HashMap<>(); 8 | 9 | public Map getContent() { 10 | return map; 11 | } 12 | 13 | public RenderParam add(String key, Object value) { 14 | map.put(key, value); 15 | return this; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/vo/SslConfig.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.vo; 2 | 3 | import java.util.Map; 4 | 5 | public class SslConfig { 6 | private Integer port; 7 | private Map auth; // base64 - raw 8 | private String fullchain; 9 | private String privkey; 10 | 11 | public SslConfig(Integer port, Map auth, String fullchain, String privkey) { 12 | this.port = port; 13 | this.auth = auth; 14 | this.fullchain = fullchain; 15 | this.privkey = privkey; 16 | } 17 | 18 | public Integer getPort() { 19 | return port; 20 | } 21 | 22 | public String getAuth(String base64Auth) { 23 | return auth.get(base64Auth); 24 | } 25 | 26 | public Map getAuthMap() { 27 | return auth; 28 | } 29 | 30 | public String getFullchain() { 31 | return fullchain; 32 | } 33 | 34 | public String getPrivkey() { 35 | return privkey; 36 | } 37 | 38 | public boolean needAuth() { 39 | return auth != null && auth.size() != 0; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/web/Dispatcher.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.web; 2 | 3 | import com.arloor.forwardproxy.HttpProxyServer; 4 | import com.arloor.forwardproxy.handler.SessionHandShakeHandler; 5 | import com.arloor.forwardproxy.monitor.GlobalTrafficMonitor; 6 | import com.arloor.forwardproxy.monitor.MonitorService; 7 | import com.arloor.forwardproxy.util.SocksServerUtils; 8 | import com.arloor.forwardproxy.vo.Config; 9 | import io.netty.buffer.ByteBuf; 10 | import io.netty.buffer.Unpooled; 11 | import io.netty.channel.*; 12 | import io.netty.handler.codec.http.*; 13 | import io.netty.handler.stream.ChunkedFile; 14 | import io.netty.handler.stream.ChunkedWriteHandler; 15 | import org.apache.logging.log4j.util.TriConsumer; 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | 19 | import java.io.*; 20 | import java.net.*; 21 | import java.nio.charset.StandardCharsets; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.Objects; 25 | 26 | import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION; 27 | import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; 28 | import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE; 29 | import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND; 30 | 31 | public class Dispatcher { 32 | private static final Logger log = LoggerFactory.getLogger("web"); 33 | private static byte[] favicon = new byte[0]; 34 | private static byte[] echarts_min_js = new byte[0]; 35 | private static final String SERVER_NAME = "github.com/arloor/HttpProxy"; 36 | private static final String MAGIC_HEADER = "arloor"; 37 | private static final MonitorService MONITOR_SERVICE = MonitorService.getInstance(); 38 | private static Map> handler = new HashMap>() {{ 39 | put("/favicon.ico", Dispatcher::favicon); 40 | put("/ip", Dispatcher::ip); 41 | put("/net", Dispatcher::net); 42 | put("/metrics", Dispatcher::metrics); 43 | put("/echarts.min.js", Dispatcher::echarts); 44 | }}; 45 | 46 | private static void echarts(HttpRequest request, ChannelHandlerContext ctx, boolean ifNeedClose) { 47 | ByteBuf buffer = ctx.alloc().buffer(); 48 | buffer.writeBytes(echarts_min_js); 49 | final FullHttpResponse response = new DefaultFullHttpResponse( 50 | HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer); 51 | response.headers().set("Server", SERVER_NAME); 52 | response.headers().set("Content-Length", echarts_min_js.length); 53 | response.headers().set("Cache-Control", "max-age=86400"); 54 | if (ifNeedClose) { 55 | response.headers().set(CONNECTION, CLOSE); 56 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 57 | } else { 58 | ctx.writeAndFlush(response); 59 | } 60 | 61 | } 62 | 63 | private static void metrics(HttpRequest httpRequest, ChannelHandlerContext ctx, boolean ifNeedClose) { 64 | String html = MONITOR_SERVICE.metrics(); 65 | ByteBuf buffer = ctx.alloc().buffer(); 66 | buffer.writeBytes(html.getBytes()); 67 | final FullHttpResponse response = new DefaultFullHttpResponse( 68 | HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer); 69 | response.headers().set("Server", SERVER_NAME); 70 | response.headers().set("Content-Length", html.getBytes().length); 71 | response.headers().set("Content-Type", "text/text; charset=utf-8"); 72 | if (ifNeedClose) { 73 | response.headers().set(CONNECTION, CLOSE); 74 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 75 | } else { 76 | ctx.writeAndFlush(response); 77 | } 78 | } 79 | 80 | static { 81 | try (BufferedInputStream stream = new BufferedInputStream(Objects.requireNonNull(HttpProxyServer.class.getClassLoader().getResourceAsStream("favicon.ico")))) { 82 | favicon = readAll(stream); 83 | } catch (IOException e) { 84 | e.printStackTrace(); 85 | } catch (NullPointerException e) { 86 | log.error("缺少favicon.ico"); 87 | } 88 | 89 | try (BufferedInputStream stream = new BufferedInputStream(Objects.requireNonNull(HttpProxyServer.class.getClassLoader().getResourceAsStream("echarts.min.js")))) { 90 | echarts_min_js = readAll(stream); 91 | } catch (Throwable e) { 92 | log.error("加载echart.min.js失败"); 93 | } 94 | } 95 | 96 | public static byte[] readAll(InputStream input) throws IOException { 97 | ByteArrayOutputStream output = new ByteArrayOutputStream(); 98 | input.transferTo(output); 99 | return output.toByteArray(); 100 | } 101 | 102 | public static void handle(HttpRequest request, ChannelHandlerContext ctx, boolean ifNeedClose) { 103 | SocketAddress socketAddress = ctx.channel().remoteAddress(); 104 | boolean fromLocalAddress = ((InetSocketAddress) socketAddress).getAddress().isSiteLocalAddress(); 105 | boolean fromLocalHost = ((InetSocketAddress) socketAddress).getAddress().isLoopbackAddress(); 106 | // 以下允许处理: 107 | // 1. 来自局域网 2.无被探测风险 3. 请求头包含特定字符串 108 | if (fromLocalAddress || fromLocalHost || !Config.ask4Authcate || request.headers().contains(MAGIC_HEADER)) { 109 | log(request, ctx); 110 | handler.getOrDefault(request.uri(), Dispatcher::other).accept(request, ctx, ifNeedClose); 111 | } else { 112 | refuse(request, ctx); 113 | } 114 | } 115 | 116 | private static void other(HttpRequest request, ChannelHandlerContext ctx, boolean ifNeedClose) { 117 | String path = getPath(request); 118 | String contentType = URLConnection.getFileNameMap().getContentTypeFor(path); 119 | try { 120 | RandomAccessFile randomAccessFile = new RandomAccessFile(path, "r"); 121 | long fileLength = randomAccessFile.length(); 122 | ChunkedFile chunkedFile = new ChunkedFile(randomAccessFile, 0, fileLength, 8192); 123 | // 针对其他需要读取文件的请求,增加ChunkedWriteHandler,防止OOM 124 | if (ctx.pipeline().get("chunked") == null) { 125 | ctx.pipeline().addBefore(SessionHandShakeHandler.NAME, "chunked", new ChunkedWriteHandler()); 126 | } 127 | HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); 128 | response.headers().set("Server", SERVER_NAME); 129 | response.headers().set("Content-Length", fileLength); 130 | response.headers().set("Cache-Control", "max-age=1800"); 131 | response.headers().set("Content-Type", contentType + "; charset=utf-8"); 132 | response.headers().set(CONNECTION, ifNeedClose ? CLOSE : KEEP_ALIVE); 133 | 134 | 135 | ctx.write(response); 136 | ChannelFuture sendFileFuture = null; 137 | sendFileFuture = ctx.write(chunkedFile, ctx.newProgressivePromise()); 138 | sendFileFuture.addListener(new ChannelProgressiveFutureListener() { 139 | @Override 140 | public void operationComplete(ChannelProgressiveFuture future) 141 | throws Exception { 142 | log.debug("Transfer complete."); 143 | } 144 | 145 | @Override 146 | public void operationProgressed(ChannelProgressiveFuture future, 147 | long progress, long total) throws Exception { 148 | if (total < 0) 149 | log.debug("Transfer progress: " + progress); 150 | else 151 | log.debug("Transfer progress: " + progress + "/" + total); 152 | } 153 | }); 154 | 155 | ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); 156 | if (ifNeedClose) { 157 | lastContentFuture.addListener(ChannelFutureListener.CLOSE); 158 | } 159 | } catch (FileNotFoundException fnfd) { 160 | r404(ctx); 161 | } catch (IOException e) { 162 | log.error("", e); 163 | sendError(ctx, HttpResponseStatus.INTERNAL_SERVER_ERROR); 164 | } 165 | 166 | } 167 | 168 | private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) { 169 | FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, 170 | Unpooled.copiedBuffer("Failure: " + status.toString() + "\r\n", StandardCharsets.UTF_8)); 171 | response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/html;charset=UTF-8"); 172 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 173 | } 174 | 175 | // 文件后缀与contentType映射见 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types 176 | private static String getContentType(String path) { 177 | final int i = path.lastIndexOf("."); 178 | if (i == -1) return "text/text"; 179 | String end = path.substring(i); 180 | return switch (end) { 181 | case ".aac" -> "audio/aac"; 182 | case ".abw" -> "application/x-abiword"; 183 | case ".arc" -> "application/x-freearc"; 184 | case ".avi" -> "video/x-msvideo"; 185 | case ".azw" -> "application/vnd.amazon.ebook"; 186 | case ".bin" -> "application/octet-stream"; 187 | case ".bmp" -> "image/bmp"; 188 | case ".bz" -> "application/x-bzip"; 189 | case ".bz2" -> "application/x-bzip2"; 190 | case ".csh" -> "application/x-csh"; 191 | case ".css" -> "text/css"; 192 | case ".csv" -> "text/csv"; 193 | case ".doc" -> "application/msword"; 194 | case ".docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; 195 | case ".eot" -> "application/vnd.ms-fontobject"; 196 | case ".epub" -> "application/epub+zip"; 197 | case ".gif" -> "image/gif"; 198 | case ".htm" -> "text/html"; 199 | case ".html" -> "text/html"; 200 | case ".ico" -> "image/vnd.microsoft.icon"; 201 | case ".ics" -> "text/calendar"; 202 | case ".jar" -> "application/java-archive"; 203 | case ".jpeg" -> "image/jpeg"; 204 | case ".jpg" -> "image/jpeg"; 205 | case ".js" -> "text/javascript"; 206 | case ".json" -> "application/json"; 207 | case ".jsonld" -> "application/ld+json"; 208 | case ".mid" -> "audio/midi"; 209 | case ".midi" -> "audio/midi"; 210 | case ".mjs" -> "text/javascript"; 211 | case ".mp3" -> "audio/mpeg"; 212 | case ".mpeg" -> "video/mpeg"; 213 | case ".mpkg" -> "application/vnd.apple.installer+xml"; 214 | case ".odp" -> "application/vnd.oasis.opendocument.presentation"; 215 | case ".ods" -> "application/vnd.oasis.opendocument.spreadsheet"; 216 | case ".odt" -> "application/vnd.oasis.opendocument.text"; 217 | case ".oga" -> "audio/ogg"; 218 | case ".ogv" -> "video/ogg"; 219 | case ".ogx" -> "application/ogg"; 220 | case ".otf" -> "font/otf"; 221 | case ".png" -> "image/png"; 222 | case ".pdf" -> "application/pdf"; 223 | case ".ppt" -> "application/vnd.ms-powerpoint"; 224 | case ".pptx" -> "application/vnd.openxmlformats-officedocument.presentationml.presentation"; 225 | case ".rar" -> "application/x-rar-compressed"; 226 | case ".rtf" -> "application/rtf"; 227 | case ".sh" -> "application/x-sh"; 228 | case ".svg" -> "image/svg+xml"; 229 | case ".swf" -> "application/x-shockwave-flash"; 230 | case ".tar" -> "application/x-tar"; 231 | case ".tif" -> "image/tiff"; 232 | case ".tiff" -> "image/tiff"; 233 | case ".ttf" -> "font/ttf"; 234 | case ".txt" -> "text/plain"; 235 | case ".vsd" -> "application/vnd.visio"; 236 | case ".wav" -> "audio/wav"; 237 | case ".weba" -> "audio/webm"; 238 | case ".webm" -> "video/webm"; 239 | case ".webp" -> "image/webp"; 240 | case ".woff" -> "font/woff"; 241 | case ".woff2" -> "font/woff2"; 242 | case ".xhtml" -> "application/xhtml+xml"; 243 | case ".xls" -> "application/vnd.ms-excel"; 244 | case ".xlsx" -> "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; 245 | case ".xml" -> "application/xml"; 246 | case ".xul" -> "application/vnd.mozilla.xul+xml"; 247 | case ".zip" -> "application/zip"; 248 | case ".3gp" -> "video/3gpp"; 249 | case ".3g2" -> "video/3gpp2"; 250 | case ".7z" -> "application/x-7z-compressed"; 251 | default -> "text/text"; 252 | }; 253 | 254 | } 255 | 256 | private static String getPath(HttpRequest request) { 257 | String uri = request.uri(); 258 | uri = URLDecoder.decode(uri, StandardCharsets.UTF_8); 259 | if (uri.endsWith("/")) { 260 | uri += "index.html"; 261 | } 262 | if (uri.startsWith("/")) { 263 | uri = uri.substring(1); 264 | } 265 | return uri; 266 | } 267 | 268 | private static void r404(ChannelHandlerContext ctx) { 269 | String notFound = "404 not found"; 270 | ByteBuf buffer = ctx.alloc().buffer(); 271 | buffer.writeBytes(notFound.getBytes()); 272 | final FullHttpResponse response = new DefaultFullHttpResponse( 273 | HttpVersion.HTTP_1_1, NOT_FOUND, buffer); 274 | response.headers().set("Server", SERVER_NAME); 275 | response.headers().set("Content-Length", notFound.getBytes().length); 276 | response.headers().set(CONNECTION, CLOSE); 277 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 278 | } 279 | 280 | private static void refuse(HttpRequest request, ChannelHandlerContext ctx) { 281 | String hostAndPortStr = request.headers().get("Host"); 282 | if (hostAndPortStr == null) { 283 | SocksServerUtils.closeOnFlush(ctx.channel()); 284 | } 285 | String[] hostPortArray = hostAndPortStr.split(":"); 286 | String host = hostPortArray[0]; 287 | String portStr = hostPortArray.length == 2 ? hostPortArray[1] : "80"; 288 | int port = Integer.parseInt(portStr); 289 | String clientHostname = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress(); 290 | log.info("refuse!! {} {} {} {}", clientHostname, request.method(), request.uri(), String.format("{%s:%s}", host, port)); 291 | ctx.close(); 292 | } 293 | 294 | private static void ip(HttpRequest request, ChannelHandlerContext ctx, boolean ifNeedClose) { 295 | String clientHostname = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress(); 296 | ByteBuf buffer = ctx.alloc().buffer(); 297 | buffer.writeBytes(clientHostname.getBytes()); 298 | final FullHttpResponse response = new DefaultFullHttpResponse( 299 | HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer); 300 | response.headers().set("Server", SERVER_NAME); 301 | response.headers().set("Content-Length", clientHostname.getBytes().length); 302 | response.headers().set("Content-Type", "text/html; charset=utf-8"); 303 | if (ifNeedClose) { 304 | response.headers().set(CONNECTION, CLOSE); 305 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 306 | } else { 307 | ctx.writeAndFlush(response); 308 | } 309 | } 310 | 311 | private static void net(HttpRequest request, ChannelHandlerContext ctx, boolean ifNeedClose) { 312 | String html = GlobalTrafficMonitor.html(false); 313 | ByteBuf buffer = ctx.alloc().buffer(); 314 | buffer.writeBytes(html.getBytes(StandardCharsets.UTF_8)); 315 | final FullHttpResponse response = new DefaultFullHttpResponse( 316 | HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer); 317 | response.headers().set("Server", SERVER_NAME); 318 | response.headers().set("Content-Length", html.getBytes(StandardCharsets.UTF_8).length); 319 | response.headers().set("Content-Type", "text/html; charset=utf-8"); 320 | if (ifNeedClose) { 321 | response.headers().set(CONNECTION, CLOSE); 322 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 323 | } else { 324 | ctx.writeAndFlush(response); 325 | } 326 | } 327 | 328 | 329 | private static void favicon(HttpRequest request, ChannelHandlerContext ctx, boolean ifNeedClose) { 330 | ByteBuf buffer = ctx.alloc().buffer(); 331 | buffer.writeBytes(favicon); 332 | final FullHttpResponse response = new DefaultFullHttpResponse( 333 | HttpVersion.HTTP_1_1, HttpResponseStatus.OK, buffer); 334 | response.headers().set("Server", SERVER_NAME); 335 | response.headers().set("Content-Length", favicon.length); 336 | response.headers().set("Cache-Control", "max-age=86400"); 337 | if (ifNeedClose) { 338 | response.headers().set(CONNECTION, CLOSE); 339 | ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); 340 | } else { 341 | ctx.writeAndFlush(response); 342 | } 343 | } 344 | 345 | 346 | private static final void log(HttpRequest request, ChannelHandlerContext ctx) { 347 | //获取Host和port 348 | String hostAndPortStr = request.headers().get("Host"); 349 | if (hostAndPortStr == null) { 350 | SocksServerUtils.closeOnFlush(ctx.channel()); 351 | } 352 | String[] hostPortArray = hostAndPortStr.split(":"); 353 | String host = hostPortArray[0]; 354 | String portStr = hostPortArray.length == 2 ? hostPortArray[1] : "80"; 355 | int port = Integer.parseInt(portStr); 356 | String clientHostname = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress(); 357 | log.info("{} {} {} {}", clientHostname, request.method(), request.uri(), String.format("{%s:%s}", host, port)); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/main/java/com/arloor/forwardproxy/web/ResourceReader.java: -------------------------------------------------------------------------------- 1 | package com.arloor.forwardproxy.web; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.IOException; 7 | import java.nio.file.Files; 8 | import java.nio.file.Path; 9 | 10 | public class ResourceReader { 11 | private static final Logger logger = LoggerFactory.getLogger(ResourceReader.class); 12 | 13 | public static byte[] readFile(String path) { 14 | try { 15 | return Files.readAllBytes(Path.of(path)); 16 | } catch (IOException e) { 17 | logger.error("error read file {}", path); 18 | } 19 | return null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.arloor.forwardproxy.monitor.MonitorService: -------------------------------------------------------------------------------- 1 | com.arloor.forwardproxy.monitor.PromMonitorImpl -------------------------------------------------------------------------------- /src/main/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arloor/HttpProxy/44b171bc591d562266cd47e145197d38d57a2e8b/src/main/resources/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/log4j2-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | /data/var/log/proxy 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | /data/var/log/proxy 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 58 | 59 | 60 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level - %msg%n 10 | UTF-8 11 | 12 | 13 | 14 | 15 | ${LOG_HOME}/proxy/proxy.log 16 | 17 | 18 | ${LOG_HOME}/proxy/proxy.log-%d{yyyy-MM-dd}.%i 19 | 20 | 200MB 21 | 60 22 | 20GB 23 | 24 | 25 | 26 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level - %msg%n 27 | UTF-8 28 | 29 | 30 | 31 | ${LOG_HOME}/proxy/web.log 32 | 33 | 34 | ${LOG_HOME}/proxy/web.log-%d{yyyy-MM-dd}.%i 35 | 36 | 200MB 37 | 60 38 | 20GB 39 | 40 | 41 | 42 | %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level - %msg%n 43 | UTF-8 44 | 45 | 46 | 47 | 48 | 49 | 0 50 | 51 | 256 52 | 53 | 54 | 55 | 56 | 57 | 58 | 0 59 | 60 | 256 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/resources/proxy.properties: -------------------------------------------------------------------------------- 1 | # true 则可能被探测到是https代理 2 | ask4Authcate=true 3 | # http代理配置 4 | http.enable=true 5 | http.port=8888 6 | ## 白名单模式优先级大于auth模式,即配置了白名单则只有白名单内的域名才可通过 7 | #http.proxy.white.domain=github.com 8 | #http.auth=arloor:httpforarloor 9 | # over Tls配置 10 | https.enable=true 11 | https.port=443 12 | #https.auth=arloor:httpforarloor 13 | https.fullchain.pem=cert.pem 14 | https.privkey.pem=privkey.pem 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/test/java/MemoryMonitorTest.java: -------------------------------------------------------------------------------- 1 | import io.netty.util.internal.PlatformDependent; 2 | 3 | import java.lang.management.BufferPoolMXBean; 4 | import java.lang.management.ManagementFactory; 5 | import java.lang.management.MemoryMXBean; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.TreeMap; 9 | 10 | public class MemoryMonitorTest { 11 | public static void main(String[] args) { 12 | Map map = new TreeMap<>(); 13 | map.put("direct_netty", PlatformDependent.usedDirectMemory()); 14 | map.put("heap", memoryMXBean.getHeapMemoryUsage().getUsed()); 15 | map.put("non_heap", memoryMXBean.getNonHeapMemoryUsage().getUsed()); 16 | for (BufferPoolMXBean bufferPool : bufferPoolMXBeans) { 17 | map.put("buffer_pool_" + fixName(bufferPool.getName()), bufferPool.getMemoryUsed()); 18 | } 19 | for (Map.Entry entry : map.entrySet()) { 20 | System.out.println(String.format("%18s %s",entry.getKey(),entry.getValue())); 21 | } 22 | } 23 | 24 | private static String fixName(String name) { 25 | return name.replaceAll(" ", "_").replaceAll("'", "").replaceAll("-", "_"); 26 | } 27 | 28 | private static MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); 29 | private static List bufferPoolMXBeans = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class); 30 | } 31 | -------------------------------------------------------------------------------- /实时网速.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arloor/HttpProxy/44b171bc591d562266cd47e145197d38d57a2e8b/实时网速.png -------------------------------------------------------------------------------- /性能测试.md: -------------------------------------------------------------------------------- 1 | ## 性能测试 2 | 3 | 环境:一台2核4G的服务器(华为云 Intel(R) Xeon(R) Gold 6278C CPU @ 2.60GHz)(好像是目前最强的服务器芯片) 4 | 5 | 代理客户端:[connect](https://github.com/arloor/connect)项目 6 | 7 | 软件:proxychains-ng iperf3 8 | 9 | 实验原理: iperf3是专业的测速软件。通过`proxychains`将iperf3测速的流量走`httpproxy`,从而测试httpproxy的性能。流量传输如下描述: 10 | 11 | ```shell 12 | iperf3-client ---> connect ---> httpproxy ---> iperf3-server 13 | ``` 14 | 15 | 在测速期间,以上四个进程使用2个cpu核心。 16 | 17 | connect的jvm参数: 18 | 19 | ```shell script 20 | # 分别为最小堆,最大堆,新生代大小,触发gc的元空间大小(一般是fullgc)两个survivor与eden区比值(=4,则2:4,默认为8即每个survivor为1/10的年轻代大小) 21 | heap_option='-Xms2000m -Xmx2000m -Xmn600m -XX:MetaspaceSize=40M -XX:SurvivorRatio=8' 22 | ``` 23 | 24 | httpproxy的jvm参数: 25 | 26 | ```shell script 27 | # 分别为最小堆,最大堆,新生代大小,触发gc的元空间大小(一般是fullgc)两个survivor与eden区比值(=6,则2:6,默认为8即每个survivor为1/10的年轻代大小) 28 | heap_option='-Xms2000m -Xmx2000m -Xmn600m -XX:MetaspaceSize=40M -XX:SurvivorRatio=8' 29 | ``` 30 | 31 | 从GC日志上看,没有看到fullGC,为减少youngGC,年轻代的大小刻意地设置地比较大,副作用用jvm占用的内存比较大(500MB,来自top的res字段),实际使用场景下,并不需要这么大的年轻代。 32 | 33 | ### 性能测试结果 34 | 35 | HttpProxy上行速度(iperf3 295秒测试结果):10.1 Gbits/sec ——**单线程单tcp连接跑满万兆网卡** 36 | 37 | ```shell script 38 | [ ID] Interval Transfer Bandwidth 39 | [ 9] 0.00-295.72 sec 0.00 Bytes 0.00 bits/sec sender 40 | [ 9] 0.00-295.72 sec 349 GBytes 10.1 Gbits/sec receiver 41 | iperf3: interrupt - the client has terminated 42 | ``` 43 | 44 | HttpProxy上行速度(iperf3 361秒测试结果):10.3 Gbits/sec ——**单线程单tcp连接跑满万兆网卡** 45 | 46 | ```shell script 47 | [ ID] Interval Transfer Bandwidth Retr 48 | [ 9] 0.00-361.09 sec 431 GBytes 10.3 Gbits/sec 72 sender 49 | [ 9] 0.00-361.09 sec 0.00 Bytes 0.00 bits/sec receiver 50 | iperf3: interrupt - the client has terminated 51 | ``` 52 | 53 | 资源占用: 54 | 55 | ```shell script 56 | top - 14:34:36 up 180 days, 21:39, 6 users, load average: 3.82, 3.05, 2.56 57 | Tasks: 130 total, 2 running, 128 sleeping, 0 stopped, 0 zombie 58 | %Cpu(s): 47.2 us, 45.4 sy, 0.0 ni, 2.8 id, 0.0 wa, 0.5 hi, 4.0 si, 0.0 st 59 | MiB Mem : 3940.4 total, 272.3 free, 2348.8 used, 1319.4 buff/cache 60 | MiB Swap: 4069.0 total, 4029.8 free, 39.2 used. 1107.6 avail Mem 61 | 62 | PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 63 | 2175578 root 20 0 4619020 758548 19876 S 79.7 18.8 10:04.91 java 64 | 2175835 root 20 0 4579832 587452 20880 S 77.1 14.6 2:28.55 java 65 | 2167795 root 20 0 10264 2748 2496 R 15.0 0.1 3:59.31 iperf3 66 | 2175851 root 20 0 21076 2544 2348 S 13.6 0.1 0:24.98 iperf3 67 | ``` --------------------------------------------------------------------------------