├── .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 | 
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 | 
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 extends ServerSocketChannel> serverSocketChannelClazz() {
28 | return os.serverSocketChannelClazz;
29 | }
30 |
31 | public static Class extends SocketChannel> 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 extends ServerSocketChannel> serverSocketChannelClazz;
79 | Class extends SocketChannel> socketChannelClazz;
80 |
81 | abstract EventLoopGroup buildEventLoopGroup(int num);
82 |
83 | OS(String name, Class extends ServerSocketChannel> serverSocketChannelClass, Class extends SocketChannel> 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 | ```
--------------------------------------------------------------------------------