├── .gitignore
├── .mvn
└── wrapper
│ ├── MavenWrapperDownloader.java
│ └── maven-wrapper.properties
├── README.md
├── build.sh
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
└── main
├── java
└── cn
│ └── iinti
│ └── proxycompose
│ ├── Bootstrap.java
│ ├── Settings.java
│ ├── auth
│ ├── AuthRules.java
│ └── IpTrie.java
│ ├── loop
│ ├── Looper.java
│ ├── ParallelExecutor.java
│ └── ValueCallback.java
│ ├── proxy
│ ├── ProxyCompose.java
│ ├── ProxyServer.java
│ ├── RuntimeIpSource.java
│ ├── Session.java
│ ├── inbound
│ │ ├── detector
│ │ │ ├── HttpProxyMatcher.java
│ │ │ ├── HttpsProxyMatcher.java
│ │ │ ├── ProtocolDetector.java
│ │ │ ├── ProtocolMatcher.java
│ │ │ └── Socks5ProxyMatcher.java
│ │ └── handlers
│ │ │ ├── ProxyHttpHandler.java
│ │ │ ├── ProxySocks5Handler.java
│ │ │ └── RelayHandler.java
│ ├── outbound
│ │ ├── ActiveProxyIp.java
│ │ ├── IpPool.java
│ │ ├── OutboundOperator.java
│ │ ├── downloader
│ │ │ ├── DownloadProxyIp.java
│ │ │ ├── IpDownloader.java
│ │ │ └── IpOfferStep.java
│ │ └── handshark
│ │ │ ├── AbstractUpstreamHandShaker.java
│ │ │ ├── HandShakerHttps.java
│ │ │ ├── HandShakerSocks5.java
│ │ │ ├── Protocol.java
│ │ │ └── ProtocolManager.java
│ └── switcher
│ │ ├── EatErrorFilter.java
│ │ ├── OutboundConnectTask.java
│ │ └── UpstreamHandSharkCallback.java
│ ├── resource
│ ├── DropReason.java
│ ├── IpAndPort.java
│ ├── IpResourceParser.java
│ ├── ProxyIp.java
│ └── SmartParser.java
│ ├── trace
│ ├── Recorder.java
│ ├── impl
│ │ ├── DiskRecorders.java
│ │ ├── SubscribeRecorders.java
│ │ └── WheelSlotFilter.java
│ └── utils
│ │ ├── StringSplitter.java
│ │ └── ThrowablePrinter.java
│ └── utils
│ ├── AsyncHttpInvoker.java
│ ├── ConsistentHashUtil.java
│ ├── IniConfig.java
│ ├── IpUtils.java
│ ├── NettyThreadPools.java
│ ├── NettyUtil.java
│ ├── PortSpaceParser.java
│ └── ResourceUtil.java
└── resources
├── config.ini
└── logback.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | target/
3 | node_modules/
4 | _book/
5 | *.DS_Store
6 | .logs/
--------------------------------------------------------------------------------
/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed to the Apache Software Foundation (ASF) under one
3 | or more contributor license agreements. See the NOTICE file
4 | distributed with this work for additional information
5 | regarding copyright ownership. The ASF licenses this file
6 | to you under the Apache License, Version 2.0 (the
7 | "License"); you may not use this file except in compliance
8 | with the License. You may obtain a copy of the License at
9 |
10 | https://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing,
13 | software distributed under the License is distributed on an
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | KIND, either express or implied. See the License for the
16 | specific language governing permissions and limitations
17 | under the License.
18 | */
19 |
20 | import java.io.File;
21 | import java.io.FileInputStream;
22 | import java.io.FileOutputStream;
23 | import java.io.IOException;
24 | import java.net.URL;
25 | import java.nio.channels.Channels;
26 | import java.nio.channels.ReadableByteChannel;
27 | import java.util.Properties;
28 |
29 | public class MavenWrapperDownloader {
30 |
31 | /**
32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
33 | */
34 | private static final String DEFAULT_DOWNLOAD_URL =
35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar";
36 |
37 | /**
38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
39 | * use instead of the default one.
40 | */
41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
42 | ".mvn/wrapper/maven-wrapper.properties";
43 |
44 | /**
45 | * Path where the maven-wrapper.jar will be saved to.
46 | */
47 | private static final String MAVEN_WRAPPER_JAR_PATH =
48 | ".mvn/wrapper/maven-wrapper.jar";
49 |
50 | /**
51 | * Name of the property which should be used to override the default download url for the wrapper.
52 | */
53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
54 |
55 | public static void main(String args[]) {
56 | System.out.println("- Downloader started");
57 | File baseDirectory = new File(args[0]);
58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
59 |
60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
61 | // wrapperUrl parameter.
62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
63 | String url = DEFAULT_DOWNLOAD_URL;
64 | if(mavenWrapperPropertyFile.exists()) {
65 | FileInputStream mavenWrapperPropertyFileInputStream = null;
66 | try {
67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
68 | Properties mavenWrapperProperties = new Properties();
69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
71 | } catch (IOException e) {
72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
73 | } finally {
74 | try {
75 | if(mavenWrapperPropertyFileInputStream != null) {
76 | mavenWrapperPropertyFileInputStream.close();
77 | }
78 | } catch (IOException e) {
79 | // Ignore ...
80 | }
81 | }
82 | }
83 | System.out.println("- Downloading from: : " + url);
84 |
85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
86 | if(!outputFile.getParentFile().exists()) {
87 | if(!outputFile.getParentFile().mkdirs()) {
88 | System.out.println(
89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'");
90 | }
91 | }
92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
93 | try {
94 | downloadFileFromURL(url, outputFile);
95 | System.out.println("Done");
96 | System.exit(0);
97 | } catch (Throwable e) {
98 | System.out.println("- Error downloading");
99 | e.printStackTrace();
100 | System.exit(1);
101 | }
102 | }
103 |
104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
105 | URL website = new URL(urlString);
106 | ReadableByteChannel rbc;
107 | rbc = Channels.newChannel(website.openStream());
108 | FileOutputStream fos = new FileOutputStream(destination);
109 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
110 | fos.close();
111 | rbc.close();
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ProxyCompose
2 |
3 | 这是malenia的开源分支,主要用于给商业代码引流😊
4 |
5 | ProxyCompose是一个对代理IP池进行二次组合的工具,用户可以将多个采购的IP资源进行二次组合,使用统一的账户密码,服务器端口等信息进行统一访问。
6 |
7 | 交流群: 加微信:(iinti_cn)拉入微信交流群
8 |
9 | ## 特性
10 |
11 | 1. 访问统一:如论是什么IP资源供应商,业务代码均接入ProxyCompose,IP资源供应商的变动不会影响业务代码
12 | 2. 池间路由:对于多个IP资源供应商,支持浮动流量比例动态调控,即根据IP池健康评估,弹性伸缩各个IP池的流量。某个IP池挂了不影响整体业务
13 | 3. 协议转换:你可以使用ProxyCompose实现http/https/socks5几种代理协议的转换,这样即使采购的代理资源仅支持socks5,也能转换为https代理
14 | 4. 池化加速:ProxyCompose内置了一个高效的IP池模块,可以对IP资源的访问进行探测、评分、连接池等工作,提高IP资源使用成功率
15 |
16 | ## 使用
17 |
18 | ### 构建
19 |
20 | - 安装Java
21 | - 安装maven
22 | - Linux/mac下,执行脚本:``build.sh``,得到文件``target/proxy-compose.zip``即为产出文件
23 | - 配置: 请根据实际情况配置代理资源 ``conf/config.ini``
24 | - 运行脚本:``bin/ProxyComposed.sh`` 或 ``bin/ProxyComposed.bat``
25 | - 代理测试:``curl -x iinti:iinti@127.0.0.1:36000 https://www.baidu.com/``
26 |
27 | **如不方便构建,可以使用我们构建好的发布包:[https://oss.iinti.cn/malenia/proxy-compose.zip](https://oss.iinti.cn/malenia/proxy-compose.zip)**
28 |
29 | ### 最简配置
30 |
31 | ```ini
32 | [global]
33 | # 鉴权用户,即用户连接到proxy_compose的鉴权
34 | auth_username=iinti
35 | # 鉴权密码
36 | auth_password=iinti
37 |
38 | # 定义IP资源,即从IP供应商采购的IP资源,要求至少配置一个IP资源
39 | [source:dailiyun]
40 | # IP资源下载连接
41 | loadURL=http://修改这里.user.xiecaiyun.com/api/proxies?action=getText&key=修改这里&count=修改这里&word=&rand=false&norepeat=false&detail=false<ime=0
42 | # Ip供应提供的代理账户(如果是白名单,后者无鉴权,则无需配置)
43 | upstreamAuthUser=修改这里
44 | # Ip供应提供的代密码
45 | upstreamAuthPassword=修改这里
46 | # IP池大小,重要
47 | poolSize=10
48 | ```
49 |
50 | ### 完整配置
51 |
52 | ```ini
53 | [global]
54 | # 开启debug将会有更加丰富的日志
55 | debug=false
56 | # 对代理IP质量进行探测的URL
57 | proxyHttpTestURL = https://iinti.cn/conn/getPublicIp?scene=proxy_compose
58 | # 代理服务器启动端口,本系统将会在配置端口范围连续启动多个代理服务器
59 | mappingSpace=36000-36010
60 | # 是否启用随机隧道,启用随机隧道之后,每次代理请求将会使用随机的IP出口
61 | randomTurning=false
62 | # 是否启用池间路由,池间路由支持根据IP池的健康状态在多个IP池之间动态调整比例
63 | enableFloatIpSourceRatio=true
64 | # failover次数,即系统为失败的代理转发进行的充实
65 | maxFailoverCount=3
66 | # 代理连接超时时间
67 | handleSharkConnectionTimeout=5000
68 | # 鉴权用户,即用户连接到proxy_compose的鉴权
69 | auth_username=iinti
70 | # 鉴权密码
71 | auth_password=iinti
72 | # 使用IP白名单,或者IP端的方式进行鉴权
73 | auth_white_ips=122.23.43.0/24,29.23.45.65
74 |
75 | # 定义IP资源,即从IP供应商采购的IP资源,要求至少配置一个IP资源
76 | # section 要求以 《source:》开始
77 | [source:dailiyun]
78 | # 本资源是否启用,如果希望临时关闭本资源,但是不希望删除配置,可以使用本开关
79 | enable=true
80 | # IP资源下载连接
81 | loadURL=http://修改这里.user.xiecaiyun.com/api/proxies?action=getText&key=修改这里&count=修改这里&word=&rand=false&norepeat=false&detail=false<ime=0
82 | # IP资源格式,目前支持plain,json两种格式,其中json格式需要满足json格式要求 cn.iinti.proxycompose.resource.ProxyIp
83 | resourceFormat=plain
84 | # Ip供应提供的代理账户(如果是白名单,后者无鉴权,则无需配置)
85 | upstreamAuthUser=修改这里
86 | # Ip供应提供的代密码
87 | upstreamAuthPassword=修改这里
88 | # IP池大小,非常重要,此字段为您的IP供应商单次提取返回的节点数
89 | poolSize=10
90 | # 本IP资源池是否需要探测IP质量,如开启,则IP需要被验证可用后方可加入IP池
91 | needTest=true
92 | # IP资源下载间隔时间,单位秒
93 | reloadInterval=240
94 | # IP资源入库后最长存活时间,单位秒,达到此时间后,对应IP资源将会从IP池中移除,除非被重新下载到IP池中
95 | maxAlive=300
96 | # 当前IP资源支持的代理协议(建议至少选择支持socks5的资源)
97 | supportProtocol=socks5,https,http
98 | # 连接池连接空转时间,单位秒,IP池将会提前创建到代理真实代理服务器的连接,给业务使用提供加速功能
99 | connIdleSeconds=20
100 | # 提前创建连接的时间间隔,单位秒
101 | makeConnInterval=20
102 | # 当前IP池在池间流量比例,当存在多个Ip资源配置时,本配置有效,即业务按照此比例对多个IP池进行流量情切
103 | ratio=1
104 | ```
105 |
106 | ## 特别说明
107 |
108 | ComposeProxy本身能做的工作非常丰富,更多想象空间可以参考我们对应的商业分支:[malenia](https://malenia.iinti.cn/malenia-doc/)
109 | 用户如提交任何新的功能(即使和商业分支重叠)均可以被接收
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 |
4 | now_dir=`pwd`
5 | cd `dirname $0`
6 |
7 | shell_dir=`pwd`
8 |
9 | mvn -Dmaven.test.skip=true package appassembler:assemble
10 |
11 | if [[ $? != 0 ]] ;then
12 | echo "build sekiro jar failed"
13 | exit 2
14 | fi
15 |
16 | chmod +x target/proxy-compose/bin/ProxyComposed.sh
17 | proxy_compose_dir=target/proxy-compose
18 |
19 | cd ${proxy_compose_dir}
20 |
21 | zip -r proxy-compose.zip ./*
22 |
23 | mv proxy-compose.zip ../
24 |
25 | cd ${now_dir}
26 |
27 | echo "the output zip file: target/proxy-compose.zip"
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven2 Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /etc/mavenrc ] ; then
40 | . /etc/mavenrc
41 | fi
42 |
43 | if [ -f "$HOME/.mavenrc" ] ; then
44 | . "$HOME/.mavenrc"
45 | fi
46 |
47 | fi
48 |
49 | # OS specific support. $var _must_ be set to either true or false.
50 | cygwin=false;
51 | darwin=false;
52 | mingw=false
53 | case "`uname`" in
54 | CYGWIN*) cygwin=true ;;
55 | MINGW*) mingw=true;;
56 | Darwin*) darwin=true
57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
59 | if [ -z "$JAVA_HOME" ]; then
60 | if [ -x "/usr/libexec/java_home" ]; then
61 | export JAVA_HOME="`/usr/libexec/java_home`"
62 | else
63 | export JAVA_HOME="/Library/Java/Home"
64 | fi
65 | fi
66 | ;;
67 | esac
68 |
69 | if [ -z "$JAVA_HOME" ] ; then
70 | if [ -r /etc/gentoo-release ] ; then
71 | JAVA_HOME=`java-config --jre-home`
72 | fi
73 | fi
74 |
75 | if [ -z "$M2_HOME" ] ; then
76 | ## resolve links - $0 may be a link to maven's home
77 | PRG="$0"
78 |
79 | # need this for relative symlinks
80 | while [ -h "$PRG" ] ; do
81 | ls=`ls -ld "$PRG"`
82 | link=`expr "$ls" : '.*-> \(.*\)$'`
83 | if expr "$link" : '/.*' > /dev/null; then
84 | PRG="$link"
85 | else
86 | PRG="`dirname "$PRG"`/$link"
87 | fi
88 | done
89 |
90 | saveddir=`pwd`
91 |
92 | M2_HOME=`dirname "$PRG"`/..
93 |
94 | # make it fully qualified
95 | M2_HOME=`cd "$M2_HOME" && pwd`
96 |
97 | cd "$saveddir"
98 | # echo Using m2 at $M2_HOME
99 | fi
100 |
101 | # For Cygwin, ensure paths are in UNIX format before anything is touched
102 | if $cygwin ; then
103 | [ -n "$M2_HOME" ] &&
104 | M2_HOME=`cygpath --unix "$M2_HOME"`
105 | [ -n "$JAVA_HOME" ] &&
106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
107 | [ -n "$CLASSPATH" ] &&
108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
109 | fi
110 |
111 | # For Mingw, ensure paths are in UNIX format before anything is touched
112 | if $mingw ; then
113 | [ -n "$M2_HOME" ] &&
114 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
115 | [ -n "$JAVA_HOME" ] &&
116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
117 | # TODO classpath?
118 | fi
119 |
120 | if [ -z "$JAVA_HOME" ]; then
121 | javaExecutable="`which javac`"
122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
123 | # readlink(1) is not available as standard on Solaris 10.
124 | readLink=`which readlink`
125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
126 | if $darwin ; then
127 | javaHome="`dirname \"$javaExecutable\"`"
128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
129 | else
130 | javaExecutable="`readlink -f \"$javaExecutable\"`"
131 | fi
132 | javaHome="`dirname \"$javaExecutable\"`"
133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
134 | JAVA_HOME="$javaHome"
135 | export JAVA_HOME
136 | fi
137 | fi
138 | fi
139 |
140 | if [ -z "$JAVACMD" ] ; then
141 | if [ -n "$JAVA_HOME" ] ; then
142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
143 | # IBM's JDK on AIX uses strange locations for the executables
144 | JAVACMD="$JAVA_HOME/jre/sh/java"
145 | else
146 | JAVACMD="$JAVA_HOME/bin/java"
147 | fi
148 | else
149 | JAVACMD="`which java`"
150 | fi
151 | fi
152 |
153 | if [ ! -x "$JAVACMD" ] ; then
154 | echo "Error: JAVA_HOME is not defined correctly." >&2
155 | echo " We cannot execute $JAVACMD" >&2
156 | exit 1
157 | fi
158 |
159 | if [ -z "$JAVA_HOME" ] ; then
160 | echo "Warning: JAVA_HOME environment variable is not set."
161 | fi
162 |
163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
164 |
165 | # traverses directory structure from process work directory to filesystem root
166 | # first directory with .mvn subdirectory is considered project base directory
167 | find_maven_basedir() {
168 |
169 | if [ -z "$1" ]
170 | then
171 | echo "Path not specified to find_maven_basedir"
172 | return 1
173 | fi
174 |
175 | basedir="$1"
176 | wdir="$1"
177 | while [ "$wdir" != '/' ] ; do
178 | if [ -d "$wdir"/.mvn ] ; then
179 | basedir=$wdir
180 | break
181 | fi
182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
183 | if [ -d "${wdir}" ]; then
184 | wdir=`cd "$wdir/.."; pwd`
185 | fi
186 | # end of workaround
187 | done
188 | echo "${basedir}"
189 | }
190 |
191 | # concatenates all lines of a file
192 | concat_lines() {
193 | if [ -f "$1" ]; then
194 | echo "$(tr -s '\n' ' ' < "$1")"
195 | fi
196 | }
197 |
198 | BASE_DIR=`find_maven_basedir "$(pwd)"`
199 | if [ -z "$BASE_DIR" ]; then
200 | exit 1;
201 | fi
202 |
203 | ##########################################################################################
204 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
205 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
206 | ##########################################################################################
207 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
208 | if [ "$MVNW_VERBOSE" = true ]; then
209 | echo "Found .mvn/wrapper/maven-wrapper.jar"
210 | fi
211 | else
212 | if [ "$MVNW_VERBOSE" = true ]; then
213 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
214 | fi
215 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
216 | while IFS="=" read key value; do
217 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
218 | esac
219 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
220 | if [ "$MVNW_VERBOSE" = true ]; then
221 | echo "Downloading from: $jarUrl"
222 | fi
223 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
224 |
225 | if command -v wget > /dev/null; then
226 | if [ "$MVNW_VERBOSE" = true ]; then
227 | echo "Found wget ... using wget"
228 | fi
229 | wget "$jarUrl" -O "$wrapperJarPath"
230 | elif command -v curl > /dev/null; then
231 | if [ "$MVNW_VERBOSE" = true ]; then
232 | echo "Found curl ... using curl"
233 | fi
234 | curl -o "$wrapperJarPath" "$jarUrl"
235 | else
236 | if [ "$MVNW_VERBOSE" = true ]; then
237 | echo "Falling back to using Java to download"
238 | fi
239 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
240 | if [ -e "$javaClass" ]; then
241 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
242 | if [ "$MVNW_VERBOSE" = true ]; then
243 | echo " - Compiling MavenWrapperDownloader.java ..."
244 | fi
245 | # Compiling the Java class
246 | ("$JAVA_HOME/bin/javac" "$javaClass")
247 | fi
248 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
249 | # Running the downloader
250 | if [ "$MVNW_VERBOSE" = true ]; then
251 | echo " - Running MavenWrapperDownloader.java ..."
252 | fi
253 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
254 | fi
255 | fi
256 | fi
257 | fi
258 | ##########################################################################################
259 | # End of extension
260 | ##########################################################################################
261 |
262 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
263 | if [ "$MVNW_VERBOSE" = true ]; then
264 | echo $MAVEN_PROJECTBASEDIR
265 | fi
266 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
267 |
268 | # For Cygwin, switch paths to Windows format before running java
269 | if $cygwin; then
270 | [ -n "$M2_HOME" ] &&
271 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
272 | [ -n "$JAVA_HOME" ] &&
273 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
274 | [ -n "$CLASSPATH" ] &&
275 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
276 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
277 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
278 | fi
279 |
280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
281 |
282 | exec "$JAVACMD" \
283 | $MAVEN_OPTS \
284 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
285 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
286 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
287 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM http://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven2 Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
124 | FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO (
125 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
126 | )
127 |
128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
130 | if exist %WRAPPER_JAR% (
131 | echo Found %WRAPPER_JAR%
132 | ) else (
133 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
134 | echo Downloading from: %DOWNLOAD_URL%
135 | powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
136 | echo Finished downloading %WRAPPER_JAR%
137 | )
138 | @REM End of extension
139 |
140 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
141 | if ERRORLEVEL 1 goto error
142 | goto end
143 |
144 | :error
145 | set ERROR_CODE=1
146 |
147 | :end
148 | @endlocal & set ERROR_CODE=%ERROR_CODE%
149 |
150 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
151 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
152 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
153 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
154 | :skipRcPost
155 |
156 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
157 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
158 |
159 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
160 |
161 | exit /B %ERROR_CODE%
162 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 | 4.0.0
6 | cn.iinti.proxycompose
7 | proxycompose
8 | 0.0.1
9 | jar
10 |
11 | 1.8
12 | 1.8
13 | 31.1-jre
14 | 4.1.60.Final
15 |
16 |
17 |
18 | com.google.code.findbugs
19 | jsr305
20 | 3.0.2
21 |
22 |
23 | org.ini4j
24 | ini4j
25 | 0.5.2
26 |
27 |
28 | ch.qos.logback
29 | logback-classic
30 | 1.2.9
31 |
32 |
33 | com.alibaba
34 | fastjson
35 | 1.2.79
36 |
37 |
38 | org.slf4j
39 | jcl-over-slf4j
40 | 1.7.30
41 |
42 |
43 | org.slf4j
44 | log4j-over-slf4j
45 | 1.7.30
46 |
47 |
48 | org.projectlombok
49 | lombok
50 | 1.18.24
51 | provided
52 |
53 |
54 | com.google.guava
55 | guava
56 | 31.1-jre
57 |
58 |
59 | org.apache.commons
60 | commons-lang3
61 | 3.12.0
62 |
63 |
64 | commons-io
65 | commons-io
66 | 2.10.0
67 |
68 |
69 |
70 |
71 | io.netty
72 | netty-codec-http
73 | ${netty.version}
74 |
75 |
76 | io.netty
77 | netty-tcnative
78 | 1.1.33.Fork26
79 | true
80 |
81 |
82 | io.netty
83 | netty-codec-socks
84 | ${netty.version}
85 |
86 |
87 |
88 | org.asynchttpclient
89 | async-http-client
90 | 2.12.3
91 |
92 |
93 |
94 | ch.hsr
95 | geohash
96 | 1.4.0
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | org.codehaus.mojo
105 | appassembler-maven-plugin
106 | 1.1.1
107 |
108 | flat
109 | lib
110 | src/main/resources
111 | conf
112 | true
113 | true
114 | ${project.build.directory}/proxy-compose
115 |
116 |
117 | -Dfile.encoding=utf-8 -Duser.timezone=GMT+08 -XX:-OmitStackTraceInFastThrow
118 |
119 |
120 | .sh
121 |
122 |
123 | windows
124 | unix
125 |
126 |
127 |
128 | cn.iinti.proxycompose.Bootstrap
129 | ProxyComposed
130 |
131 |
132 |
133 |
134 |
135 | org.apache.maven.plugins
136 | maven-compiler-plugin
137 | 3.8.0
138 |
139 | ${maven.compiler.source}
140 | ${maven.compiler.target}
141 | UTF-8
142 | 1.8
143 | UTF-8
144 |
145 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/Bootstrap.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose;
2 |
3 |
4 | import cn.iinti.proxycompose.proxy.ProxyCompose;
5 | import cn.iinti.proxycompose.proxy.RuntimeIpSource;
6 | import cn.iinti.proxycompose.utils.PortSpaceParser;
7 |
8 | import java.io.File;
9 | import java.net.URL;
10 | import java.util.List;
11 | import java.util.TreeSet;
12 | import java.util.stream.Collectors;
13 |
14 | public class Bootstrap {
15 | private static final String IntMessage =
16 | "welcome use ProxyComposed framework, for more support please visit our website: https://iinti.cn/";
17 |
18 | static {
19 | URL configURL = Bootstrap.class.getClassLoader().getResource("config.ini");
20 | if (configURL != null && configURL.getProtocol().equals("file")) {
21 | File classPathDir = new File(configURL.getFile()).getParentFile();
22 | String absolutePath = classPathDir.getAbsolutePath();
23 | if (absolutePath.endsWith("target/classes") || absolutePath.endsWith("conf")) {
24 | System.setProperty("LOG_DIR", new File(classPathDir.getParentFile(), "logs").getAbsolutePath());
25 | }
26 | }
27 | }
28 |
29 | public static void main(String[] args) {
30 | List sourceList = Settings.ipSourceList;
31 | if (sourceList.isEmpty()) {
32 | System.err.println("no ipSource defined");
33 | return;
34 | }
35 |
36 | List runtimeIpSources = sourceList.stream()
37 | .map(RuntimeIpSource::new)
38 | .collect(Collectors.toList());
39 |
40 | TreeSet ports = PortSpaceParser.parsePortSpace(Settings.global.mappingSpace.value);
41 | if (ports.isEmpty()) {
42 | System.err.println("proxy server port not config");
43 | return;
44 | }
45 | new ProxyCompose(runtimeIpSources, ports);
46 | System.out.println(IntMessage);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/Settings.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose;
2 |
3 | import cn.iinti.proxycompose.auth.AuthRules;
4 | import cn.iinti.proxycompose.utils.IniConfig;
5 | import cn.iinti.proxycompose.utils.IpUtils;
6 | import cn.iinti.proxycompose.utils.ResourceUtil;
7 | import lombok.Getter;
8 | import lombok.SneakyThrows;
9 | import org.apache.commons.lang3.StringUtils;
10 | import org.ini4j.ConfigParser;
11 |
12 | import java.io.InputStream;
13 | import java.net.SocketException;
14 | import java.util.ArrayList;
15 | import java.util.List;
16 |
17 | /**
18 | * 配置文件解析和配置内容定义
19 | */
20 | public class Settings {
21 |
22 | /**
23 | * 全局配置
24 | */
25 | public static Global global;
26 | /**
27 | * IP资源定义,系统至少包含1个IP资源
28 | */
29 | public static List ipSourceList = new ArrayList<>();
30 |
31 | static {
32 | load();
33 | }
34 |
35 | @SneakyThrows
36 | private static void load() {
37 | InputStream stream = ResourceUtil.openResource("config.ini");
38 |
39 | ConfigParser config = new ConfigParser();
40 | config.read(stream);
41 |
42 | global = new Global(config);
43 |
44 | List sections = config.sections();
45 | for (String sectionName : sections) {
46 | if (sectionName.startsWith("source:")) {
47 | ipSourceList.add(new IpSource(config, sectionName));
48 | }
49 | }
50 | }
51 |
52 | @Getter
53 | public static class IpSource extends IniConfig {
54 |
55 | public IpSource(ConfigParser config, String section) {
56 | super(config, section);
57 | name = section.substring("source:".length());
58 | }
59 |
60 | public final String name;
61 |
62 | public final StringConfigValue loadURL = new StringConfigValue(
63 | "loadURL", "");
64 |
65 | /**
66 | * 资源格式,目前支持两种
67 | * plain:文本分割,如 proxy.iinti.cn:8000-9000,proxy1.iinti.cn:5817,proxy2.iinti.cn:5817
68 | * json: json格式,满足 {@link cn.iinti.proxycompose.resource.ProxyIp}的json对象
69 | */
70 | public final StringConfigValue resourceFormat = new StringConfigValue(
71 | "resourceFormat", "plain"
72 | );
73 |
74 | public final BooleanConfigValue enable = new BooleanConfigValue(
75 | "enable", false
76 | );
77 |
78 | public final StringConfigValue upstreamAuthUser = new StringConfigValue(
79 | "upstreamAuthUser", "");
80 |
81 | public final StringConfigValue upstreamAuthPassword = new StringConfigValue(
82 | "upstreamAuthPassword", "");
83 |
84 | public final IntegerConfigValue poolSize = new IntegerConfigValue(
85 | "poolSize", 0
86 | );
87 |
88 | public final BooleanConfigValue needTest = new BooleanConfigValue(
89 | "needTest", true
90 | );
91 | public final IntegerConfigValue reloadInterval = new IntegerConfigValue(
92 | "reloadInterval", 60);
93 |
94 | public IntegerConfigValue maxAlive = new IntegerConfigValue(
95 | "maxAlive", 1200
96 | );
97 |
98 | public final StringConfigValue supportProtocol = new StringConfigValue(
99 | "supportProtocol", "socks5");
100 |
101 | public final IntegerConfigValue connIdleSeconds = new IntegerConfigValue(
102 | "connIdleSeconds", 5);
103 |
104 | public final IntegerConfigValue makeConnInterval = new IntegerConfigValue(
105 | "makeConnInterval", 500);
106 |
107 | public final IntegerConfigValue ratio = new IntegerConfigValue(
108 | "ratio", 1
109 | );
110 |
111 | }
112 |
113 | public static class Global extends IniConfig {
114 |
115 | public BooleanConfigValue debug = new BooleanConfigValue(
116 | "debug", false
117 | );
118 |
119 | /**
120 | * 后端探测接口,探测代理ip是否可用以及解析出口ip地址
121 | */
122 | public StringConfigValue proxyHttpTestURL = new StringConfigValue(
123 | "proxyHttpTestURL", "https://iinti.cn/conn/getPublicIp?scene=proxy_compose");
124 |
125 |
126 | public StringConfigValue mappingSpace = new StringConfigValue(
127 | "mappingSpace", "36000-36010"
128 | );
129 |
130 | public BooleanConfigValue randomTurning = new BooleanConfigValue(
131 | "randomTurning", false
132 | );
133 |
134 | public BooleanConfigValue enableFloatIpSourceRatio = new BooleanConfigValue(
135 | "enableFloatIpSourceRatio", true
136 | );
137 |
138 | public IntegerConfigValue maxFailoverCount = new IntegerConfigValue(
139 | "maxFailoverCount", 3
140 | );
141 |
142 | public IntegerConfigValue handleSharkConnectionTimeout = new IntegerConfigValue(
143 | "handleSharkConnectionTimeout", 5_000
144 | );
145 |
146 | public final AuthRules authRules = new AuthRules();
147 |
148 | public String listenIp = "0.0.0.0";
149 |
150 | public Global(ConfigParser config) {
151 | super(config, "global");
152 | acceptConfig("auth_username", authRules::setAuthAccount);
153 | acceptConfig("auth_password", authRules::setAuthPassword);
154 | acceptConfig("auth_white_ips", s -> {
155 | for (String str : StringUtils.split(s, ",")) {
156 | if (StringUtils.isBlank(str)) {
157 | continue;
158 | }
159 | authRules.addCidrIpConfig(str.trim());
160 | }
161 | });
162 | acceptConfig("listen_type", listenType -> listenIp = parseListenType(listenType));
163 | }
164 | }
165 |
166 | private static String parseListenType(String listenType) {
167 | listenType = StringUtils.trimToEmpty(listenType);
168 | if (IpUtils.isIpV4(listenType)) {
169 | return listenType;
170 | }
171 | switch (listenType) {
172 | //lo,private,public,all
173 | case "lo":
174 | return "127.0.0.1";
175 |
176 | case "all":
177 | return "0.0.0.0";
178 | default:
179 | try {
180 | return IpUtils.fetchIp(listenType);
181 | } catch (SocketException e) {
182 | //ignore
183 | }
184 | }
185 | return "0.0.0.0";
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/auth/AuthRules.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.auth;
2 |
3 | import lombok.Getter;
4 | import lombok.Setter;
5 | import org.apache.commons.lang3.StringUtils;
6 |
7 | public class AuthRules {
8 | /**
9 | * IP匹配前缀树
10 | */
11 | private final IpTrie ipTrie = new IpTrie();
12 |
13 |
14 | @Getter
15 | @Setter
16 | private String authAccount;
17 |
18 | @Getter
19 | @Setter
20 | private String authPassword;
21 |
22 | public void addCidrIpConfig(String ipConfig) {
23 | ipTrie.insert(ipConfig);
24 | }
25 |
26 |
27 | public boolean doAuth(String ip) {
28 | return ipTrie.has(ip);
29 | }
30 |
31 | public boolean doAuth(String authUserName, String authPassword) {
32 | return StringUtils.equals(this.authAccount, authUserName)
33 | && StringUtils.equals(this.authPassword, authPassword);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/auth/IpTrie.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.auth;
2 |
3 | import org.apache.commons.lang3.StringUtils;
4 |
5 | public class IpTrie {
6 | private IpTrie left;
7 | private IpTrie right;
8 | private boolean has = false;
9 | private static final String localhostStr = "localhost";
10 | private static final String localhost = "127.0.0.1";
11 |
12 |
13 | public void insert(String ipConfig) {
14 | String ip;
15 | int cidr = 32;
16 | if (ipConfig.contains("/")) {
17 | String[] split = ipConfig.split("/");
18 | ip = StringUtils.trim(split[0]);
19 | cidr = Integer.parseInt(StringUtils.trim(split[1]));
20 | } else {
21 | ip = ipConfig.trim();
22 | }
23 |
24 | insert(0, ip2Int(ip), cidr);
25 | }
26 |
27 | private void insert(int deep, long ip, int cidr) {
28 | if (deep >= cidr) {
29 | has = true;
30 | return;
31 | }
32 |
33 | int bit = (int) ((ip >>> (32 - deep)) & 0x01);
34 | if (bit == 0) {
35 | if (left == null) {
36 | left = new IpTrie();
37 | }
38 | left.insert(deep + 1, ip, cidr);
39 | } else {
40 | if (right == null) {
41 | right = new IpTrie();
42 | }
43 | right.insert(deep + 1, ip, cidr);
44 | }
45 | }
46 |
47 | private static long ip2Int(String ip) {
48 | if (localhostStr.equals(ip)) {
49 | ip = localhost;
50 | }
51 | String[] split = ip.split("\\.");
52 | return ((Long.parseLong(split[0]) << 24
53 | | Long.parseLong(split[1]) << 16
54 | | Long.parseLong(split[2]) << 8
55 | | Long.parseLong(split[3])));
56 | }
57 |
58 |
59 | public boolean has(String ip) {
60 | if (ip.contains("/")) {
61 | // 这里可能输入了一个cidr的ip规则
62 | ip = ip.substring(0, ip.indexOf('/'));
63 | }
64 |
65 | return has(ip2Int(ip), 0);
66 | }
67 |
68 |
69 | private boolean has(long ip, int deep) {
70 | if (has) {
71 | return true;
72 | }
73 | int bit = (int) ((ip >>> (32 - deep)) & 0x01);
74 | if (bit == 0) {
75 | if (left == null) {
76 | return false;
77 | }
78 | return left.has(ip, deep + 1);
79 | } else {
80 | if (right == null) {
81 | return false;
82 | }
83 | return right.has(ip, deep + 1);
84 | }
85 | }
86 |
87 | public void remove(String ipConfig) {
88 | String ip;
89 | int cidr = 32;
90 | if (ipConfig.contains("/")) {
91 | String[] split = ipConfig.split("/");
92 | cidr = Integer.parseInt(StringUtils.trim(split[1]));
93 | ip = StringUtils.trim(split[0]);
94 | } else {
95 | ip = ipConfig.trim();
96 | }
97 | remove(0, ip2Int(ip), cidr);
98 | }
99 |
100 | private void remove(int deep, long ip, int cidr) {
101 | if (deep >= cidr) {
102 | has = false;
103 | return;
104 | }
105 |
106 | int bit = (int) ((ip >>> (32 - deep)) & 0x01);
107 | if (bit == 0) {
108 | if (left == null) {
109 | return;
110 | }
111 | left.remove(deep + 1, ip, cidr);
112 | } else {
113 | if (right == null) {
114 | return;
115 | }
116 | right.remove(deep + 1, ip, cidr);
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/loop/Looper.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.loop;
2 |
3 |
4 | import lombok.extern.slf4j.Slf4j;
5 |
6 | import javax.annotation.Nonnull;
7 | import java.util.concurrent.*;
8 |
9 | /**
10 | * 单线程事件循环模型,用来避免一致性问题
11 | */
12 | @Slf4j
13 | public class Looper implements Executor {
14 |
15 | private final LinkedBlockingDeque taskQueue = new LinkedBlockingDeque<>();
16 |
17 | private final LoopThread loopThread;
18 | private final long createTimestamp = System.currentTimeMillis();
19 |
20 | private static Looper lowPriorityLooper;
21 |
22 |
23 | /**
24 | * 获取一个全局的低优looper
25 | *
26 | * @return looper
27 | */
28 | public static Looper getLowPriorityLooper() {
29 | if (lowPriorityLooper != null) {
30 | return lowPriorityLooper;
31 | }
32 | synchronized (Looper.class) {
33 | lowPriorityLooper = new Looper("lowPriorityLooper").startLoop();
34 | }
35 | return lowPriorityLooper;
36 | }
37 |
38 | /**
39 | * 让looper拥有延时任务的能力
40 | */
41 | private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
42 |
43 | public Looper(String looperName) {
44 | loopThread = new LoopThread(looperName);
45 | loopThread.setDaemon(true);
46 | }
47 |
48 |
49 | public Looper startLoop() {
50 | loopThread.start();
51 | return this;
52 | }
53 |
54 | public void post(Runnable runnable) {
55 | post(runnable, false);
56 | }
57 |
58 | public void offerLast(Runnable runnable) {
59 | taskQueue.addLast(runnable);
60 | }
61 |
62 | public void post(Runnable runnable, boolean first) {
63 | if (!loopThread.isAlive()) {
64 | if (System.currentTimeMillis() - createTimestamp > 60000) {
65 | log.warn("post task before looper startup,do you call :startLoop??", new Throwable());
66 | }
67 | runnable.run();
68 | return;
69 | }
70 | if (inLooper()) {
71 | runnable.run();
72 | return;
73 | }
74 | if (first) {
75 | taskQueue.offerFirst(runnable);
76 | } else {
77 | taskQueue.add(runnable);
78 | }
79 | }
80 |
81 | public void postDelay(Runnable runnable, long delay) {
82 | if (delay <= 0) {
83 | post(runnable);
84 | return;
85 | }
86 | if (!loopThread.isAlive()) {
87 | //todo 这是应该是有bug,先加上这一行日志
88 | if (System.currentTimeMillis() - createTimestamp > 60000) {
89 | log.warn("post task before looper startup,do you call :startLoop??", new Throwable());
90 | }
91 | }
92 | scheduler.schedule(() -> post(runnable), delay, TimeUnit.MILLISECONDS);
93 | }
94 |
95 | public Looper fluentScheduleWithRate(Runnable runnable, long rate) {
96 | scheduleWithRate(runnable, rate);
97 | return this;
98 | }
99 |
100 | public FixRateScheduleHandle scheduleWithRate(Runnable runnable, long rate) {
101 | return scheduleWithRate(runnable, Long.valueOf(rate));
102 | }
103 |
104 | /**
105 | * 这个接口,可以支持非固定速率
106 | */
107 | public FixRateScheduleHandle scheduleWithRate(Runnable runnable, Number rate) {
108 | FixRateScheduleHandle fixRateScheduleHandle = new FixRateScheduleHandle(runnable, rate);
109 | // if (rate.longValue() > 0) {
110 | // post(runnable);
111 | // }
112 | postDelay(fixRateScheduleHandle, rate.longValue());
113 | return fixRateScheduleHandle;
114 | }
115 |
116 | @Override
117 | public void execute(@Nonnull Runnable command) {
118 | if (inLooper()) {
119 | command.run();
120 | return;
121 | }
122 | post(command);
123 | }
124 |
125 |
126 | public class FixRateScheduleHandle implements Runnable {
127 | private final Runnable runnable;
128 | private final Number rate;
129 | private boolean running;
130 |
131 |
132 | FixRateScheduleHandle(Runnable runnable, Number rate) {
133 | this.runnable = runnable;
134 | this.running = true;
135 | this.rate = rate;
136 | }
137 |
138 | public void cancel() {
139 | this.running = false;
140 | }
141 |
142 | @Override
143 | public void run() {
144 | if (running && rate.longValue() > 0) {
145 | postDelay(this, rate.longValue());
146 | }
147 | runnable.run();
148 | }
149 |
150 | }
151 |
152 | public boolean inLooper() {
153 | return Thread.currentThread().equals(loopThread)
154 | || !loopThread.isAlive();
155 | }
156 |
157 | public void checkLooper() {
158 | if (!inLooper()) {
159 | throw new IllegalStateException("run task not in looper");
160 | }
161 | }
162 |
163 | private class LoopThread extends Thread {
164 | LoopThread(String name) {
165 | super(name);
166 | }
167 |
168 | private boolean run = true;
169 |
170 | @Override
171 | public void run() {
172 | while (run) {
173 | try {
174 | taskQueue.take().run();
175 | } catch (InterruptedException interruptedException) {
176 | return;
177 | } catch (Throwable throwable) {
178 | log.error("group event loop error", throwable);
179 | }
180 | }
181 | }
182 | }
183 |
184 | public void close() {
185 | loopThread.run = false;
186 | loopThread.interrupt();
187 | }
188 | }
189 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/loop/ParallelExecutor.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.loop;
2 |
3 | public class ParallelExecutor implements ValueCallback {
4 | private final Looper looper;
5 |
6 | private final int eventSize;
7 | private int eventIndex = 0;
8 | private boolean success = false;
9 | private final ParallelConnectEvent parallelConnectEvent;
10 |
11 | public ParallelExecutor(Looper looper, int eventSize, ParallelConnectEvent parallelConnectEvent) {
12 | this.looper = looper;
13 | this.eventSize = eventSize;
14 | this.parallelConnectEvent = parallelConnectEvent;
15 | }
16 |
17 | @Override
18 | public void onReceiveValue(Value value) {
19 | looper.execute(() -> {
20 | eventIndex++;
21 |
22 | if (value.isSuccess()) {
23 | if (!success) {
24 | success = true;
25 | parallelConnectEvent.firstSuccess(value);
26 | } else {
27 | parallelConnectEvent.secondSuccess(value);
28 | }
29 | return;
30 | }
31 |
32 | if (!success && eventIndex >= eventSize) {
33 | parallelConnectEvent.finalFailed(value.e);
34 | }
35 | });
36 |
37 |
38 | }
39 |
40 | //private List<>
41 |
42 | public interface ParallelConnectEvent {
43 | void firstSuccess(Value value);
44 |
45 | void secondSuccess(Value value);
46 |
47 | void finalFailed(Throwable throwable);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/loop/ValueCallback.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.loop;
2 |
3 |
4 | public interface ValueCallback {
5 | void onReceiveValue(Value value);
6 |
7 |
8 | static void success(ValueCallback callback, T t) {
9 | callback.onReceiveValue(Value.success(t));
10 | }
11 |
12 | static void failed(ValueCallback callback, Throwable e) {
13 | callback.onReceiveValue(Value.failed(e));
14 | }
15 |
16 | static void failed(ValueCallback callback, String message) {
17 | callback.onReceiveValue(Value.failed(message));
18 | }
19 |
20 | class Value {
21 |
22 | public T v;
23 | public Throwable e;
24 |
25 | public boolean isSuccess() {
26 | return e == null;
27 | }
28 |
29 | public static Value failed(Throwable e) {
30 | Value value = new Value<>();
31 | value.e = e;
32 | return value;
33 | }
34 |
35 | public static Value failed(String message) {
36 | return failed(new RuntimeException(message));
37 | }
38 |
39 | public static Value success(T t) {
40 | Value value = new Value<>();
41 | value.v = t;
42 | return value;
43 | }
44 |
45 | public Value errorTransfer() {
46 | Value value = new Value<>();
47 | value.e = e;
48 | return value;
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/ProxyCompose.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy;
2 |
3 | import cn.iinti.proxycompose.Settings;
4 | import cn.iinti.proxycompose.proxy.outbound.ActiveProxyIp;
5 | import cn.iinti.proxycompose.utils.ConsistentHashUtil;
6 | import cn.iinti.proxycompose.loop.Looper;
7 | import cn.iinti.proxycompose.loop.ValueCallback;
8 | import cn.iinti.proxycompose.trace.Recorder;
9 | import cn.iinti.proxycompose.trace.impl.SubscribeRecorders;
10 | import com.alibaba.fastjson.JSONObject;
11 | import com.google.common.cache.Cache;
12 | import com.google.common.cache.CacheBuilder;
13 | import com.google.common.cache.RemovalListener;
14 | import com.google.common.collect.Lists;
15 | import com.google.common.collect.Maps;
16 | import io.netty.channel.Channel;
17 | import lombok.Getter;
18 | import lombok.extern.slf4j.Slf4j;
19 |
20 | import java.util.*;
21 | import java.util.concurrent.TimeUnit;
22 |
23 | @Slf4j
24 | public class ProxyCompose {
25 | /**
26 | * 所有的代理IP池,IP池代表malenia的上有资源池,他来自各种代理ip供应商。如代理云、芝麻代理、快代理等
27 | * malenia通过对IpSource的抽象,完成对所有代理供应商的透明屏蔽
28 | */
29 | private final Map ipSources = Maps.newHashMap();
30 |
31 | /**
32 | * 本产品下的所有代理服务器
33 | */
34 | private final TreeMap proxyServerTreeMap = Maps.newTreeMap();
35 |
36 | private TreeMap ipSourceKeyMap = new TreeMap<>();
37 | private List ipSourceKeyList = Lists.newArrayList();
38 | /**
39 | * 路由缓存,当一个隧道访问过程使用过某个ip资源,那么系统优先尝试使用曾今的隧道,如此尽可能保证ip出口不变
40 | * ps:需要做这个缓存更重要的原因是,malenia在运作过程会有failover,failover过程会有不可预期的隧道映射关系重置
41 | * 此时无法根据固定的规则进行mapping计算
42 | */
43 | private final Cache routeCache = CacheBuilder.newBuilder()
44 | .removalListener((RemovalListener) notification -> {
45 | Long sessionHash = notification.getKey();
46 | ActiveProxyIp activeProxyIp = notification.getValue();
47 | // auto decrease refCount of proxyIp
48 | if (sessionHash != null && activeProxyIp != null) {
49 | activeProxyIp.refreshRefSessionHash(sessionHash, false);
50 | }
51 | })
52 | .expireAfterAccess(1, TimeUnit.MINUTES).build();
53 |
54 |
55 | @Getter
56 | private final Looper composeWorkThead;
57 |
58 | public ProxyCompose(List ipSources, Set proxyServePorts) {
59 | composeWorkThead = new Looper("compose").startLoop();
60 | for (Integer port : proxyServePorts) {
61 | ProxyServer proxyServer = new ProxyServer(port, ProxyCompose.this);
62 | log.info("start proxy server for port: {}", port);
63 | proxyServerTreeMap.put(port, proxyServer);
64 | }
65 |
66 | // config ipSource
67 | Map ipSourceWithRatio = Maps.newHashMap();
68 | for (RuntimeIpSource runtimeIpSource : ipSources) {
69 | String sourceKey = runtimeIpSource.getName();
70 | this.ipSources.put(sourceKey, runtimeIpSource);
71 | ipSourceWithRatio.put(sourceKey, runtimeIpSource.getRatio().value);
72 | }
73 | Map ratioConfig = Collections.unmodifiableMap(ipSourceWithRatio);
74 | reloadIpSourceRatio(ratioConfig);
75 | composeWorkThead.scheduleWithRate(() -> reloadIpSourceRatio(ratioConfig), 300_000);
76 | }
77 |
78 |
79 | private Map floatRatio(Map configRule) {
80 | configRule = new HashMap<>(configRule);
81 |
82 | boolean hasVerySmallConfig = configRule.values().stream().anyMatch(it -> it.equals(1) || it.equals(2));
83 | if (hasVerySmallConfig) {
84 | // expand ratio, because the float ratio component maybe decease config
85 | for (String ipSourceKey : Lists.newArrayList(configRule.keySet())) {
86 | configRule.put(ipSourceKey, configRule.get(ipSourceKey) * 3);
87 | }
88 | }
89 |
90 | // scale ratio config by health score
91 | for (String ipSourceKey : Lists.newArrayList(configRule.keySet())) {
92 | //IP池健康指数,正常值为100,最大值一般不超过150,小于100认为不健康,最低为0(代表此IP资源已经完全挂了)
93 | RuntimeIpSource runtimeIpSource = ipSources.get(ipSourceKey);
94 | if (runtimeIpSource == null) {
95 | continue;
96 | }
97 | Double healthScore = runtimeIpSource.healthScore();
98 |
99 | Integer configuredRatio = configRule.get(ipSourceKey);
100 | configuredRatio = (int) (configuredRatio * healthScore / 100);
101 | if (configuredRatio <= 1) {
102 | configuredRatio = 1;
103 | }
104 | configRule.put(ipSourceKey, configuredRatio);
105 | }
106 | return configRule;
107 | }
108 |
109 | public void reloadIpSourceRatio(Map ratio) {
110 | composeWorkThead.post(() -> {
111 | Map configRule = Settings.global.enableFloatIpSourceRatio.value ?
112 | floatRatio(ratio) : ratio;
113 |
114 | TreeMap newIpSources = new TreeMap<>();
115 | List newIpSourceLists = Lists.newArrayList(new TreeSet<>(configRule.keySet()));
116 |
117 | for (String ipSourceKey : configRule.keySet()) {
118 | int ratio1 = configRule.get(ipSourceKey);
119 | if (ratio1 == 0) {
120 | continue;
121 | }
122 | for (int i = 1; i <= ratio1; i++) {
123 | long murHash = ConsistentHashUtil.murHash(ipSourceKey + "_##_" + i);
124 | newIpSources.put(murHash, ipSourceKey);
125 | }
126 | }
127 | ipSourceKeyMap = newIpSources;
128 | ipSourceKeyList = newIpSourceLists;
129 | });
130 | }
131 |
132 |
133 | public void destroy() {
134 | composeWorkThead.post(() -> {
135 | proxyServerTreeMap.values().forEach(ProxyServer::destroy);
136 | composeWorkThead.close();
137 | });
138 | }
139 |
140 | public void connectToOutbound(
141 | Session session, long sessionHash, String tag,
142 | ActiveProxyIp.ActivityProxyIpBindObserver observer,
143 | ValueCallback callback) {
144 | SubscribeRecorders.SubscribeRecorder recorder = session.getRecorder();
145 |
146 | allocateIpSource(sessionHash, tag, session, ipSourceValue -> {
147 | if (!ipSourceValue.isSuccess()) {
148 | recorder.recordEvent(() -> tag + "allocate IpSource failed", ipSourceValue.e);
149 | ValueCallback.failed(callback, ipSourceValue.e);
150 | return;
151 | }
152 |
153 | RuntimeIpSource ipSource = ipSourceValue.v;
154 | // 拿到IP源,此时ip源是根据分流比例控制的
155 | recorder.recordMosaicMsg(() -> tag + "allocate IpSource success: " + ipSource.getName());
156 | // 在ip源上分配代理
157 | ipSource.getIpPool().allocateIp(sessionHash, session.getRecorder(), ipWrapper -> {
158 | if (!ipWrapper.isSuccess()) {
159 | recorder.recordEvent(() -> tag + "allocate IP failed");
160 | ValueCallback.failed(callback, ipWrapper.e);
161 | return;
162 | }
163 |
164 | ActiveProxyIp activeProxyIp = ipWrapper.v;
165 | recorder.recordMosaicMsg(() -> tag + "allocate IP success:" +
166 | JSONObject.toJSONString(activeProxyIp.getDownloadProxyIp())
167 | + " begin to borrow connection"
168 | );
169 | activeProxyIp.borrowConnect(recorder, tag, observer, callback);
170 | });
171 | });
172 | }
173 |
174 | public void allocateIpSource(long sessionHash, String tag, Session session, ValueCallback valueCallback) {
175 | composeWorkThead.post(() -> {
176 | // 如果用户有指定了代理ip源,那么使用特定的代理ip
177 | String ipSourceKey = ConsistentHashUtil.fetchConsistentRing(ipSourceKeyMap, sessionHash);
178 | if (ipSourceKey == null) {
179 | // not happen
180 | valueCallback.onReceiveValue(ValueCallback.Value.failed("no ipSources mapping for "));
181 | return;
182 | }
183 | session.getRecorder().recordMosaicMsg(() -> tag + "map route to ipSource: " + ipSourceKey);
184 | allocateIpSource0(session.getRecorder(), tag, ipSourceKey, ipSourceKeyList, valueCallback);
185 | });
186 | }
187 |
188 | private void allocateIpSource0(Recorder recorder, String tag, String prefer, List candidate,
189 | ValueCallback valueCallback) {
190 | RuntimeIpSource runtimeIpSource = ipSources.get(prefer);
191 |
192 | if (runtimeIpSource == null) {
193 | valueCallback.onReceiveValue(ValueCallback.Value.failed("no ipSources: " + prefer + " defined"));
194 | return;
195 | }
196 | if (isValidIpSource(runtimeIpSource)) {
197 | ValueCallback.success(valueCallback, runtimeIpSource);
198 | return;
199 | }
200 | if (ipSources.size() > 1) {
201 | recorder.recordEvent(() -> tag + "IpSourceManager this ipSource not have ip resource, begin ratio to next ip source");
202 | int length = candidate.size();
203 | int start = Math.abs(prefer.hashCode()) + candidate.size();
204 | for (int i = 0; i < length; i++) {
205 | String newSourceKey = candidate.get((start + i) % candidate.size());
206 | if (newSourceKey.equals(prefer)) {
207 | continue;
208 | }
209 | runtimeIpSource = ipSources.get(newSourceKey);
210 | if (isValidIpSource(runtimeIpSource)) {
211 | ValueCallback.success(valueCallback, runtimeIpSource);
212 | return;
213 | }
214 | }
215 | }
216 |
217 | ValueCallback.failed(valueCallback, "can not find online IpSourceKey");
218 | }
219 |
220 | public static boolean isValidIpSource(RuntimeIpSource runtimeIpSource) {
221 | return runtimeIpSource != null && !runtimeIpSource.poolEmpty();
222 | }
223 |
224 | public void fetchCachedSession(Session session, ValueCallback callback) {
225 | composeWorkThead.post(() -> {
226 | ActiveProxyIp sessionIp = routeCache.getIfPresent(session.getSessionHash());
227 | if (sessionIp == null) {
228 | ValueCallback.failed(callback, "not exist");
229 | return;
230 | }
231 |
232 | if (sessionIp.getActiveStatus() != ActiveProxyIp.ActiveStatus.DESTROY) {
233 | ValueCallback.success(callback, sessionIp);
234 | return;
235 | }
236 | routeCache.invalidate(session.getSessionHash());
237 | ValueCallback.failed(callback, "not exist");
238 | });
239 |
240 | }
241 |
242 | public void markSessionUse(Session session, ActiveProxyIp activeProxyIp) {
243 | if (Settings.global.randomTurning.value) {
244 | return;
245 | }
246 | session.getRecorder().recordEvent(() -> "add sessionId route mapping ");
247 | activeProxyIp.refreshRefSessionHash(session.getSessionHash(), true);
248 | composeWorkThead.post(() -> routeCache.put(session.getSessionHash(), activeProxyIp));
249 | }
250 | }
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/ProxyServer.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy;
2 |
3 | import cn.iinti.proxycompose.proxy.inbound.detector.HttpProxyMatcher;
4 | import cn.iinti.proxycompose.proxy.inbound.detector.HttpsProxyMatcher;
5 | import cn.iinti.proxycompose.proxy.inbound.detector.ProtocolDetector;
6 | import cn.iinti.proxycompose.proxy.inbound.detector.Socks5ProxyMatcher;
7 | import cn.iinti.proxycompose.proxy.inbound.handlers.ProxyHttpHandler;
8 | import cn.iinti.proxycompose.proxy.inbound.handlers.ProxySocks5Handler;
9 | import cn.iinti.proxycompose.utils.NettyThreadPools;
10 | import cn.iinti.proxycompose.utils.NettyUtil;
11 | import cn.iinti.proxycompose.trace.Recorder;
12 | import io.netty.bootstrap.ServerBootstrap;
13 | import io.netty.buffer.Unpooled;
14 | import io.netty.channel.*;
15 | import io.netty.channel.socket.SocketChannel;
16 | import io.netty.channel.socket.nio.NioServerSocketChannel;
17 | import lombok.Getter;
18 | import lombok.extern.slf4j.Slf4j;
19 |
20 | import java.lang.ref.WeakReference;
21 | import java.util.concurrent.TimeUnit;
22 |
23 |
24 |
25 | /**
26 | * 描述一个代理服务器
27 | */
28 | @Slf4j
29 | public class ProxyServer {
30 | public static final byte[] UN_SUPPORT_PROTOCOL_MSG = "unknown protocol".getBytes();
31 |
32 | @Getter
33 | public int port;
34 |
35 |
36 | private Channel serverChannel;
37 |
38 | @Getter
39 | private final ProxyCompose proxyCompose;
40 |
41 | public ProxyServer(int port, ProxyCompose proxyCompose) {
42 | this.port = port;
43 | this.proxyCompose = proxyCompose;
44 | startProxy(buildProxyServerConfig(), 20);
45 | }
46 |
47 | private void startProxy(ServerBootstrap serverBootstrap, int leftRetry) {
48 | if (leftRetry < 0) {
49 | log.error("the proxy server start failed finally!!:{}", port);
50 | return;
51 | }
52 | serverBootstrap.bind(port).addListener((ChannelFutureListener) future -> {
53 | if (future.isSuccess()) {
54 | log.info("proxy server start success: {}", port);
55 | serverChannel = future.channel();
56 | return;
57 | }
58 | log.info("proxy server start failed, will be retry after 5s", future.cause());
59 | future.channel().eventLoop().schedule(() -> startProxy(
60 | serverBootstrap, leftRetry - 1), 5, TimeUnit.SECONDS
61 | );
62 | });
63 | }
64 |
65 | public boolean enable() {
66 | return serverChannel != null && serverChannel.isActive();
67 | }
68 |
69 |
70 | private ServerBootstrap buildProxyServerConfig() {
71 | return new ServerBootstrap()
72 | .group(NettyThreadPools.proxyServerBossGroup, NettyThreadPools.proxyServerWorkerGroup)
73 | .channel(NioServerSocketChannel.class)
74 | .childHandler(new ChannelInitializer() {
75 | @Override
76 | protected void initChannel(SocketChannel ch) {
77 | Session session = Session.touch(ch, ProxyServer.this);
78 | ch.pipeline().addLast(
79 | buildProtocolDetector(session.getRecorder())
80 | );
81 | setupProtocolTimeoutCheck(ch, session.getRecorder());
82 | }
83 | });
84 | }
85 |
86 | private static void setupProtocolTimeoutCheck(Channel channel, Recorder recorder) {
87 | WeakReference ref = new WeakReference<>(channel);
88 | EventLoop eventLoop = channel.eventLoop();
89 |
90 | eventLoop.schedule(() ->
91 | doHandlerTimeoutCheck(ref, ProtocolDetector.class, recorder, "protocol detect timeout, close this channel"),
92 | 5, TimeUnit.SECONDS);
93 |
94 | eventLoop.schedule(() -> {
95 | doHandlerTimeoutCheck(ref, ProxyHttpHandler.class, recorder, "http proxy init timeout");
96 | doHandlerTimeoutCheck(ref, ProxySocks5Handler.class, recorder, "socks5 proxy init timeout");
97 | }, 2, TimeUnit.MINUTES);
98 |
99 | }
100 |
101 | private static void doHandlerTimeoutCheck(
102 | WeakReference ref, Class handlerClazz, Recorder recorder, String msgHit
103 | ) {
104 | Channel ch = ref.get();
105 | if (ch == null) {
106 | return;
107 | }
108 | T handler = ch.pipeline().get(handlerClazz);
109 | if (handler != null) {
110 | recorder.recordEvent(msgHit);
111 | ch.close();
112 | }
113 | }
114 |
115 | public static ProtocolDetector buildProtocolDetector(Recorder recorder) {
116 | return new ProtocolDetector(
117 | recorder,
118 | (ctx, buf) -> {
119 | recorder.recordEvent("unsupported protocol:" + NettyUtil.formatByteBuf(ctx, "detect", buf));
120 | buf.release();
121 | ctx.channel().writeAndFlush(Unpooled.wrappedBuffer(UN_SUPPORT_PROTOCOL_MSG))
122 | .addListener(ChannelFutureListener.CLOSE);
123 | },
124 | new HttpProxyMatcher(),
125 | new HttpsProxyMatcher(),
126 | new Socks5ProxyMatcher()
127 | );
128 | }
129 |
130 | public void destroy() {
131 | NettyUtil.closeIfActive(serverChannel);
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/RuntimeIpSource.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy;
2 |
3 | import cn.iinti.proxycompose.Settings;
4 | import cn.iinti.proxycompose.resource.IpResourceParser;
5 | import cn.iinti.proxycompose.loop.Looper;
6 | import cn.iinti.proxycompose.proxy.outbound.IpPool;
7 | import cn.iinti.proxycompose.proxy.outbound.downloader.IpDownloader;
8 | import cn.iinti.proxycompose.proxy.outbound.handshark.Protocol;
9 | import cn.iinti.proxycompose.trace.Recorder;
10 | import cn.iinti.proxycompose.trace.impl.SubscribeRecorders;
11 | import com.google.common.base.Splitter;
12 | import com.google.common.collect.Lists;
13 | import lombok.Getter;
14 | import lombok.experimental.Delegate;
15 | import org.apache.commons.lang3.StringUtils;
16 |
17 | import java.util.Collections;
18 | import java.util.List;
19 |
20 | public class RuntimeIpSource {
21 | @Delegate
22 | private final Settings.IpSource ipSource;
23 |
24 | /**
25 | * 代理IP池,真正存储IP资源,以及缓存的可用tcp连接
26 | */
27 | @Getter
28 | private final IpPool ipPool;
29 |
30 | /**
31 | * 从代理源加载ip资源的下载器
32 | */
33 | private final IpDownloader ipDownloader;
34 |
35 | /**
36 | * 绑定ip池的线程,使用单线程模型完成资源分发
37 | */
38 | @Getter
39 | private final Looper looper;
40 |
41 | @Getter
42 | private final Recorder recorder;
43 | /**
44 | * 代理资源文本解析器
45 | */
46 | @Getter
47 | private final IpResourceParser resourceParser;
48 |
49 | public RuntimeIpSource(Settings.IpSource ipSource) {
50 | this.ipSource = ipSource;
51 | this.supportProtocolList = Collections.unmodifiableList(parseSupportProtocol(ipSource.supportProtocol.value));
52 | String sourceKey = ipSource.name;
53 | String beanId = "IpSource-" + sourceKey;
54 |
55 | looper = new Looper(beanId).startLoop();
56 | recorder = SubscribeRecorders.IP_SOURCE.acquireRecorder(beanId, Settings.global.debug.value, sourceKey);
57 | ipPool = new IpPool(RuntimeIpSource.this, looper, recorder, sourceKey);
58 | ipDownloader = new IpDownloader(RuntimeIpSource.this, recorder, sourceKey);
59 | resourceParser = IpResourceParser.resolve(ipSource.resourceFormat.value);
60 |
61 | validCheck();
62 |
63 | // 这里会启动下载任务,所以最后执行
64 | looper.scheduleWithRate(RuntimeIpSource.this::scheduleIpDownload,
65 | ipSource.reloadInterval.value * 1000);
66 |
67 | looper.scheduleWithRate(RuntimeIpSource.this::scheduleMakeConnCache,
68 | ipSource.makeConnInterval.value * 1000);
69 |
70 | looper.postDelay(RuntimeIpSource.this::scheduleIpDownload, 500);
71 | }
72 |
73 | void validCheck() {
74 | //todo
75 | }
76 |
77 | /**
78 | * 本IP池支持的代理协议:http/https/socks5
79 | */
80 | @Getter
81 | private final List supportProtocolList;
82 |
83 |
84 | public boolean needAuth() {
85 | return StringUtils.isNoneBlank(
86 | ipSource.getUpstreamAuthUser().value,
87 | ipSource.getUpstreamAuthPassword().value
88 | );
89 | }
90 |
91 | private static final Splitter splitter = Splitter.on(',').omitEmptyStrings().trimResults();
92 |
93 | public static List parseSupportProtocol(String config) {
94 | List protocols = Lists.newArrayList();
95 | for (String protocolStr : splitter.split(config)) {
96 | Protocol protocol = Protocol.get(protocolStr);
97 | if (protocol == null) {
98 | throw new IllegalArgumentException("error support protocol config:" + protocolStr);
99 | }
100 | protocols.add(protocol);
101 | }
102 | protocols.sort((o1, o2) -> Integer.compare(o2.getPriority(), o1.getPriority()));
103 | return protocols;
104 | }
105 |
106 | public void recordComposedEvent(Recorder userTraceRecorder, Recorder.MessageGetter messageGetter) {
107 | userTraceRecorder.recordEvent(messageGetter);
108 | recorder.recordEvent(messageGetter);
109 | }
110 |
111 | public void recordComposedMosaicEvent(Recorder userTraceRecorder, Recorder.MessageGetter messageGetter) {
112 | userTraceRecorder.recordMosaicMsgIfSubscribeRecorder(messageGetter);
113 | recorder.recordEvent(messageGetter);
114 | }
115 |
116 |
117 | private void scheduleIpDownload() {
118 | looper.checkLooper();
119 | recorder.recordEvent("begin download ip");
120 | ipDownloader.downloadIp();
121 | }
122 |
123 | public void scheduleMakeConnCache() {
124 | ipPool.makeCache();
125 | }
126 |
127 | public boolean poolEmpty() {
128 | return getIpPool().poolEmpty();
129 | }
130 | public double healthScore() {
131 | return getIpPool().healthScore();
132 | }
133 |
134 | public void destroy() {
135 | looper.execute(() -> {
136 | ipPool.destroy();
137 | looper.postDelay(looper::close, 30_000);
138 | });
139 | }
140 |
141 |
142 | }
143 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/Session.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy;
2 |
3 | import cn.iinti.proxycompose.Settings;
4 | import cn.iinti.proxycompose.proxy.inbound.handlers.RelayHandler;
5 | import cn.iinti.proxycompose.proxy.outbound.handshark.Protocol;
6 | import cn.iinti.proxycompose.resource.IpAndPort;
7 | import cn.iinti.proxycompose.utils.ConsistentHashUtil;
8 | import cn.iinti.proxycompose.trace.impl.SubscribeRecorders;
9 | import io.netty.channel.Channel;
10 | import io.netty.util.AttributeKey;
11 | import lombok.Getter;
12 | import lombok.Setter;
13 |
14 | import java.util.UUID;
15 |
16 | public class Session {
17 |
18 | @Getter
19 | private final String sessionId = UUID.randomUUID().toString();
20 |
21 | @Getter
22 | private final SubscribeRecorders.SubscribeRecorder recorder = SubscribeRecorders.USER_SESSION
23 | .acquireRecorder(sessionId, Settings.global.debug.value, "default");
24 |
25 | @Getter
26 | private final Channel inboundChannel;
27 |
28 | @Getter
29 | private final ProxyServer proxyServer;
30 |
31 | @Getter
32 | @Setter
33 | private boolean authed = false;
34 |
35 |
36 | public static Session touch(Channel inboundChannel, ProxyServer proxyServer) {
37 | return new Session(inboundChannel, proxyServer);
38 | }
39 |
40 | private Session(Channel inboundChannel, ProxyServer proxyServer) {
41 | this.inboundChannel = inboundChannel;
42 | this.proxyServer = proxyServer;
43 |
44 | recorder.recordEvent("new request from: " + inboundChannel);
45 | attach(inboundChannel);
46 | inboundChannel.closeFuture().addListener(future -> recorder.recordEvent("user connection closed"));
47 | }
48 |
49 | private static final AttributeKey SESSION_ATTRIBUTE_KEY = AttributeKey.newInstance("SESSION_ATTRIBUTE_KEY");
50 |
51 | private void attach(Channel channel) {
52 | channel.attr(SESSION_ATTRIBUTE_KEY).set(this);
53 | }
54 |
55 | public static Session get(Channel channel) {
56 | return channel.attr(SESSION_ATTRIBUTE_KEY).get();
57 | }
58 |
59 |
60 | public void replay(Channel upstreamChannel) {
61 | upstreamChannel.pipeline()
62 | .addLast(new RelayHandler(inboundChannel, "replay-outbound:", recorder));
63 |
64 | inboundChannel.pipeline()
65 | .addLast(new RelayHandler(upstreamChannel, "replay-inbound:", recorder));
66 | }
67 |
68 |
69 | // 代理的最终目标,从代理请求中提取,可以理解为一定存在(只要是合法的代理请求一定能解析到)
70 | @Getter
71 | private IpAndPort connectTarget;
72 |
73 | @Getter
74 | private Long sessionHash;
75 | @Getter
76 | private Protocol inboundProtocol;
77 |
78 |
79 | public void onProxyTargetResolved(IpAndPort ipAndPort, Protocol inboundProtocol) {
80 | String sessionIdKey = Settings.global.randomTurning.value ?
81 | String.valueOf(System.currentTimeMillis()) + getProxyServer().getPort() :
82 | String.valueOf(getProxyServer().getPort());
83 |
84 | this.sessionHash = ConsistentHashUtil.murHash(sessionIdKey);
85 |
86 | this.connectTarget = ipAndPort;
87 | this.inboundProtocol = inboundProtocol;
88 |
89 |
90 | recorder.recordEvent(() -> "proxy target resolved -> \nsessionHash: " + sessionHash
91 | + "\ntarget:" + ipAndPort
92 | + "\ninbound protocol: " + inboundProtocol
93 | + "\n"
94 | );
95 | }
96 |
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/inbound/detector/HttpProxyMatcher.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.inbound.detector;
2 |
3 |
4 | import cn.iinti.proxycompose.proxy.inbound.handlers.ProxyHttpHandler;
5 | import cn.iinti.proxycompose.trace.Recorder;
6 | import com.google.common.collect.Sets;
7 | import io.netty.buffer.ByteBuf;
8 | import io.netty.channel.ChannelHandlerContext;
9 | import io.netty.handler.codec.http.HttpServerCodec;
10 |
11 | import java.util.Set;
12 |
13 | import static java.nio.charset.StandardCharsets.US_ASCII;
14 |
15 | /**
16 | * Matcher for plain http request.
17 | */
18 | public class HttpProxyMatcher extends ProtocolMatcher {
19 |
20 |
21 | private static final Set methods = Sets.newHashSet("GET", "POST", "PUT", "HEAD", "OPTIONS", "PATCH", "DELETE",
22 | "TRACE");
23 |
24 |
25 | @Override
26 | public int match(ByteBuf buf) {
27 | if (buf.readableBytes() < 8) {
28 | return PENDING;
29 | }
30 |
31 | int index = buf.indexOf(0, 8, (byte) ' ');
32 | if (index < 0) {
33 | return MISMATCH;
34 | }
35 |
36 | int firstURIIndex = index + 1;
37 | if (buf.readableBytes() < firstURIIndex + 1) {
38 | return PENDING;
39 | }
40 |
41 | String method = buf.toString(0, index, US_ASCII);
42 | char firstURI = (char) (buf.getByte(firstURIIndex + buf.readerIndex()) & 0xff);
43 | if (!methods.contains(method) || firstURI == '/') {
44 | return MISMATCH;
45 | }
46 |
47 | return MATCH;
48 | }
49 |
50 | @Override
51 | protected void handleMatched( Recorder recorder, ChannelHandlerContext ctx) {
52 | recorder.recordEvent("new mock http request ");
53 | ctx.pipeline().addLast(
54 | new HttpServerCodec(),
55 | new ProxyHttpHandler(recorder, false)
56 | );
57 | }
58 |
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/inbound/detector/HttpsProxyMatcher.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.inbound.detector;
2 |
3 |
4 | import cn.iinti.proxycompose.proxy.inbound.handlers.ProxyHttpHandler;
5 | import cn.iinti.proxycompose.trace.Recorder;
6 | import io.netty.buffer.ByteBuf;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.handler.codec.http.HttpServerCodec;
9 |
10 | import static java.nio.charset.StandardCharsets.US_ASCII;
11 |
12 | /**
13 | * Matcher for http proxy connect tunnel.
14 | */
15 | public class HttpsProxyMatcher extends ProtocolMatcher {
16 |
17 | @Override
18 | public int match(ByteBuf buf) {
19 | if (buf.readableBytes() < 8) {
20 | return PENDING;
21 | }
22 |
23 | String method = buf.toString(0, 8, US_ASCII);
24 | if (!"CONNECT ".equalsIgnoreCase(method)) {
25 | return MISMATCH;
26 | }
27 |
28 | return MATCH;
29 | }
30 |
31 |
32 | @Override
33 | protected void handleMatched(Recorder recorder, ChannelHandlerContext ctx) {
34 | ctx.pipeline().addLast(
35 | new HttpServerCodec(),
36 | new ProxyHttpHandler(recorder, true)
37 | );
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/inbound/detector/ProtocolDetector.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.inbound.detector;
2 |
3 |
4 | import cn.iinti.proxycompose.utils.NettyUtil;
5 | import cn.iinti.proxycompose.trace.Recorder;
6 | import io.netty.buffer.ByteBuf;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.channel.ChannelInboundHandlerAdapter;
9 | import io.netty.handler.codec.ByteToMessageDecoder;
10 | import io.netty.util.ReferenceCountUtil;
11 | import lombok.extern.slf4j.Slf4j;
12 |
13 | import java.io.IOException;
14 |
15 | import static io.netty.handler.codec.ByteToMessageDecoder.MERGE_CUMULATOR;
16 |
17 | /**
18 | * Switcher to distinguish different protocols
19 | */
20 | @Slf4j
21 | public class ProtocolDetector extends ChannelInboundHandlerAdapter {
22 |
23 | private final ByteToMessageDecoder.Cumulator cumulator = MERGE_CUMULATOR;
24 | private final ProtocolMatcher[] matcherList;
25 |
26 | private ByteBuf buf;
27 |
28 | private final MatchMissHandler missHandler;
29 |
30 | private final Recorder recorder;
31 | private boolean hasData = false;
32 |
33 |
34 | public ProtocolDetector( Recorder recorder, MatchMissHandler missHandler, ProtocolMatcher... matchers) {
35 | this.recorder = recorder;
36 | this.missHandler = missHandler;
37 | if (matchers.length == 0) {
38 | throw new IllegalArgumentException("No matcher for ProtocolDetector");
39 | }
40 | this.matcherList = matchers;
41 | }
42 |
43 | @Override
44 | public void channelRead(ChannelHandlerContext ctx, Object msg) {
45 | if (!(msg instanceof ByteBuf)) {
46 | ReferenceCountUtil.release(msg);
47 | recorder.recordEvent("unexpected message type for ProtocolDetector: " + msg.getClass());
48 | NettyUtil.closeOnFlush(ctx.channel());
49 | return;
50 | }
51 | hasData = true;
52 |
53 | ByteBuf in = (ByteBuf) msg;
54 | if (buf == null) {
55 | buf = in;
56 | } else {
57 | buf = cumulator.cumulate(ctx.alloc(), buf, in);
58 | }
59 | // the buf maybe null after recorder acquire variable
60 | ByteBuf tmpBuf = buf;
61 | recorder.recordEvent(() -> "begin protocol detect with header: " + tmpBuf.readableBytes());
62 | boolean hasPending = false;
63 | for (ProtocolMatcher matcher : matcherList) {
64 | int match = matcher.match(buf.duplicate());
65 | if (match == ProtocolMatcher.MATCH) {
66 | recorder.recordEvent("matched by " + matcher.getClass().getName());
67 | matcher.handleMatched( recorder, ctx);
68 | ctx.pipeline().remove(this);
69 | ctx.fireChannelRead(buf);
70 | return;
71 | }
72 |
73 | if (match == ProtocolMatcher.PENDING) {
74 | recorder.recordEvent("match " + matcher.getClass() + " pending..");
75 | hasPending = true;
76 | }
77 | }
78 | if (hasPending) {
79 | return;
80 | }
81 |
82 | // all miss
83 | missHandler.onAllMatchMiss(ctx, buf);
84 | buf = null;
85 | }
86 |
87 | @Override
88 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
89 | if (buf != null) {
90 | buf.release();
91 | buf = null;
92 | }
93 | if (!hasData && cause instanceof IOException) {
94 | // 有LBS负载均衡的服务,通过探测端口是否开启来判断服务是否存活,
95 | // 他们只开启端口,然后就会关闭隧道,此时这里就会有IOException: java.io.IOException: Connection reset by peer
96 | recorder.recordEvent(() -> "exception: " + cause.getClass() + " ->" + cause.getMessage() + " before any data receive");
97 | } else {
98 | recorder.recordEvent("protocol detect error", cause);
99 | }
100 | NettyUtil.closeOnFlush(ctx.channel());
101 | }
102 |
103 | public interface MatchMissHandler {
104 | void onAllMatchMiss(ChannelHandlerContext ctx, ByteBuf buf);
105 | }
106 |
107 | }
108 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/inbound/detector/ProtocolMatcher.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.inbound.detector;
2 |
3 |
4 | import cn.iinti.proxycompose.trace.Recorder;
5 | import io.netty.buffer.ByteBuf;
6 | import io.netty.channel.ChannelHandlerContext;
7 |
8 | /**
9 | * Matcher for protocol.
10 | */
11 | public abstract class ProtocolMatcher {
12 |
13 | static int MATCH = 1;
14 | static int MISMATCH = -1;
15 | static int PENDING = 0;
16 |
17 | /**
18 | * If match the protocol.
19 | *
20 | * @return 1:match, -1:not match, 0:still can not judge now
21 | */
22 | protected abstract int match(ByteBuf buf);
23 |
24 | /**
25 | * Deal with the pipeline when matched
26 | */
27 | protected void handleMatched(Recorder recorder, ChannelHandlerContext ctx) {
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/inbound/detector/Socks5ProxyMatcher.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.inbound.detector;
2 |
3 |
4 | import cn.iinti.proxycompose.proxy.inbound.handlers.ProxySocks5Handler;
5 | import cn.iinti.proxycompose.trace.Recorder;
6 | import io.netty.buffer.ByteBuf;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.handler.codec.socks.SocksInitRequestDecoder;
9 | import io.netty.handler.codec.socks.SocksMessageEncoder;
10 |
11 | /**
12 | * Matcher for socks5 proxy protocol
13 | */
14 | public class Socks5ProxyMatcher extends ProtocolMatcher {
15 |
16 |
17 | @Override
18 | public int match(ByteBuf buf) {
19 | if (buf.readableBytes() < 2) {
20 | return PENDING;
21 | }
22 | byte first = buf.getByte(buf.readerIndex());
23 | byte second = buf.getByte(buf.readerIndex() + 1);
24 | if (first == 5) {
25 | return MATCH;
26 | }
27 | return MISMATCH;
28 | }
29 |
30 |
31 | @Override
32 | protected void handleMatched(Recorder recorder, ChannelHandlerContext ctx) {
33 | ctx.pipeline().addLast(
34 | new SocksInitRequestDecoder(),
35 | new SocksMessageEncoder(),
36 | new ProxySocks5Handler()
37 | );
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/inbound/handlers/ProxySocks5Handler.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.inbound.handlers;
2 |
3 |
4 | import cn.iinti.proxycompose.Settings;
5 | import cn.iinti.proxycompose.resource.IpAndPort;
6 | import cn.iinti.proxycompose.proxy.Session;
7 | import cn.iinti.proxycompose.proxy.outbound.handshark.Protocol;
8 | import cn.iinti.proxycompose.proxy.switcher.OutboundConnectTask;
9 | import cn.iinti.proxycompose.proxy.switcher.UpstreamHandSharkCallback;
10 | import cn.iinti.proxycompose.utils.NettyUtil;
11 | import cn.iinti.proxycompose.utils.IpUtils;
12 | import cn.iinti.proxycompose.trace.Recorder;
13 | import io.netty.channel.*;
14 | import io.netty.handler.codec.socks.*;
15 |
16 | import java.util.List;
17 |
18 | public class ProxySocks5Handler extends SimpleChannelInboundHandler
19 | implements UpstreamHandSharkCallback {
20 | private ChannelHandlerContext ctx;
21 | private SocksCmdRequest req;
22 | private Session session;
23 | private Recorder recorder;
24 |
25 |
26 | @Override
27 | protected void channelRead0(ChannelHandlerContext ctx, SocksRequest socksRequest) {
28 | this.ctx = ctx;
29 | if (this.session == null) {
30 | this.session = Session.get(ctx.channel());
31 | this.recorder = session.getRecorder();
32 | }
33 | recorder.recordEvent(() -> "inbound handle socks request:" + socksRequest.requestType());
34 | switch (socksRequest.requestType()) {
35 | case INIT:
36 | handleInit((SocksInitRequest) socksRequest);
37 | break;
38 | case AUTH:
39 | handleAuth(ctx, (SocksAuthRequest) socksRequest);
40 | break;
41 | case CMD:
42 | handleCmd(ctx, socksRequest);
43 | break;
44 | case UNKNOWN:
45 | default:
46 | ctx.close();
47 | }
48 | }
49 |
50 | private void handleAuth(ChannelHandlerContext ctx, SocksAuthRequest socksRequest) {
51 | if (session.isAuthed()) {
52 | ctx.pipeline().addFirst(new SocksCmdRequestDecoder());
53 | ctx.writeAndFlush(new SocksAuthResponse(SocksAuthStatus.SUCCESS));
54 | return;
55 | }
56 |
57 | String username = socksRequest.username();
58 | String password = socksRequest.password();
59 | session.setAuthed(Settings.global.authRules.doAuth(username, password));
60 |
61 | if (session.isAuthed()) {
62 | ctx.pipeline().addFirst(new SocksCmdRequestDecoder());
63 | ctx.writeAndFlush(new SocksAuthResponse(SocksAuthStatus.SUCCESS));
64 | return;
65 | }
66 |
67 | ctx.pipeline().addFirst(new SocksAuthRequestDecoder());
68 | ctx.writeAndFlush(new SocksAuthResponse(SocksAuthStatus.FAILURE));
69 | }
70 |
71 |
72 | private void handleInit(SocksInitRequest socksInitRequest) {
73 | // 对于socks代理来说,ip鉴权和密码鉴权是两个步骤,所以需要提前判定,先鉴权IP,然后鉴权密码
74 | if (session.isAuthed()) {
75 | recorder.recordEvent(() -> "has been authed");
76 | ctx.pipeline().addFirst(new SocksCmdRequestDecoder());
77 | ctx.writeAndFlush(new SocksInitResponse(SocksAuthScheme.NO_AUTH));
78 | return;
79 | }
80 |
81 | List socksAuthSchemes = socksInitRequest.authSchemes();
82 | for (SocksAuthScheme socksAuthScheme : socksAuthSchemes) {
83 | if (socksAuthScheme == SocksAuthScheme.AUTH_PASSWORD) {
84 | // 如果客户端明确说明支持密码鉴权,那么要求提供密码
85 | ctx.pipeline().addFirst(new SocksAuthRequestDecoder());
86 | ctx.writeAndFlush(new SocksInitResponse(SocksAuthScheme.AUTH_PASSWORD));
87 | return;
88 | }
89 |
90 | }
91 |
92 | recorder.recordEvent(() -> "require password auth");
93 | ctx.pipeline().addFirst(new SocksAuthRequestDecoder());
94 | ctx.writeAndFlush(new SocksInitResponse(SocksAuthScheme.AUTH_PASSWORD));
95 |
96 | }
97 |
98 | private void handleCmd(ChannelHandlerContext ctx, SocksRequest socksRequest) {
99 | req = (SocksCmdRequest) socksRequest;
100 | //TODO 实现UDP转发
101 | if (req.cmdType() != SocksCmdType.CONNECT) {
102 | recorder.recordEvent(() -> "not support s5 cmd: " + req.cmdType());
103 | ctx.close();
104 | return;
105 | }
106 | session.onProxyTargetResolved(new IpAndPort(req.host(), req.port()), Protocol.SOCKS5);
107 |
108 | OutboundConnectTask.startConnectOutbound(session, this);
109 | }
110 |
111 |
112 | @Override
113 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
114 | recorder.recordEvent(() -> "Socks5InboundHandler handler error", cause);
115 | }
116 |
117 | @Override
118 | public void onHandSharkFinished(Channel upstreamChannel, Protocol outboundProtocol) {
119 | NettyUtil.loveOther(session.getInboundChannel(), upstreamChannel);
120 | ctx.channel().eventLoop().execute(() -> {
121 | IpAndPort connectTarget = session.getConnectTarget();
122 | SocksCmdResponse socksCmdResponse = new SocksCmdResponse(SocksCmdStatus.SUCCESS,
123 | IpUtils.isIpV4(connectTarget.getIp()) ? SocksAddressType.IPv4 : SocksAddressType.DOMAIN,
124 | connectTarget.getIp(), connectTarget.getPort()
125 | );
126 | ctx.channel().writeAndFlush(socksCmdResponse)
127 | .addListener(future -> {
128 | if (!future.isSuccess()) {
129 | recorder.recordEvent(() -> "socket closed when write socks success", future.cause());
130 | return;
131 | }
132 | ChannelPipeline pipeline = ctx.pipeline();
133 | pipeline.remove(SocksMessageEncoder.class);
134 | pipeline.remove(ProxySocks5Handler.class);
135 | recorder.recordEvent(() -> "start socks5 replay tuning");
136 | session.replay(upstreamChannel);
137 | });
138 | });
139 |
140 | }
141 |
142 | @Override
143 | public void onHandSharkError(Throwable e) {
144 | ctx.channel().writeAndFlush(new SocksCmdResponse(SocksCmdStatus.FAILURE, req.addressType()))
145 | .addListener(ChannelFutureListener.CLOSE);
146 | }
147 |
148 | }
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/inbound/handlers/RelayHandler.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.inbound.handlers;
2 |
3 |
4 | import cn.iinti.proxycompose.trace.Recorder;
5 | import io.netty.buffer.Unpooled;
6 | import io.netty.channel.Channel;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.channel.ChannelInboundHandlerAdapter;
9 | import io.netty.util.ReferenceCountUtil;
10 |
11 | public final class RelayHandler extends ChannelInboundHandlerAdapter {
12 |
13 | private final Channel nextChannel;
14 | private final String TAG;
15 |
16 | private final Recorder recorder;
17 |
18 | public RelayHandler(Channel relayChannel, String tag, Recorder recorder) {
19 | this.nextChannel = relayChannel;
20 | this.TAG = tag;
21 | this.recorder = recorder;
22 | }
23 |
24 | @Override
25 | public void channelActive(ChannelHandlerContext ctx) {
26 | ctx.writeAndFlush(Unpooled.EMPTY_BUFFER);
27 | }
28 |
29 | @Override
30 | public void channelInactive(ChannelHandlerContext ctx) throws Exception {
31 | recorder.recordEvent("channel channelInactive");
32 | super.channelInactive(ctx);
33 | }
34 |
35 | @Override
36 | public void channelRead(ChannelHandlerContext ctx, Object msg) {
37 | recorder.recordEvent(TAG + ": receive message: " + msg);
38 | if (nextChannel.isActive()) {
39 | nextChannel.writeAndFlush(msg);
40 | } else {
41 | ReferenceCountUtil.release(msg);
42 | }
43 | }
44 |
45 |
46 | @Override
47 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
48 | recorder.recordEvent(() -> TAG + ": replay exception", cause);
49 | if (nextChannel.isActive()) {
50 | nextChannel.write(Unpooled.EMPTY_BUFFER)
51 | .addListener(future -> nextChannel.close());
52 | } else {
53 | ctx.close();
54 | }
55 |
56 | }
57 | }
58 |
59 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/ActiveProxyIp.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound;
2 |
3 | import cn.iinti.proxycompose.proxy.outbound.downloader.DownloadProxyIp;
4 | import cn.iinti.proxycompose.utils.ConsistentHashUtil;
5 | import cn.iinti.proxycompose.utils.NettyUtil;
6 | import cn.iinti.proxycompose.resource.DropReason;
7 | import cn.iinti.proxycompose.loop.Looper;
8 | import cn.iinti.proxycompose.loop.ValueCallback;
9 | import cn.iinti.proxycompose.trace.Recorder;
10 | import com.google.common.collect.Lists;
11 | import com.google.common.io.BaseEncoding;
12 | import io.netty.channel.Channel;
13 | import io.netty.util.AttributeKey;
14 | import lombok.Getter;
15 | import org.apache.commons.lang3.StringUtils;
16 |
17 | import java.lang.ref.WeakReference;
18 | import java.nio.charset.StandardCharsets;
19 | import java.util.HashSet;
20 | import java.util.LinkedList;
21 | import java.util.Set;
22 | import java.util.concurrent.ConcurrentHashMap;
23 |
24 | public class ActiveProxyIp {
25 |
26 | @Getter
27 | private final DownloadProxyIp downloadProxyIp;
28 |
29 | private final Looper workThread;
30 | private final Recorder recorder;
31 | @Getter
32 | private final IpPool ipPool;
33 |
34 | @Getter
35 | private final long murHash;
36 | @Getter
37 | private final long seq;
38 |
39 | @Getter
40 | private volatile ActiveStatus activeStatus;
41 |
42 |
43 | private final Set usedChannels = ConcurrentHashMap.newKeySet();
44 |
45 | private final CacheHandle cachedHandle = new CacheHandle();
46 |
47 | private final Set usedHash = new HashSet<>();
48 |
49 |
50 | private Runnable destroyFun;
51 |
52 | public ActiveProxyIp(IpPool ipPool, DownloadProxyIp downloadProxyIp, long seq) {
53 | this.ipPool = ipPool;
54 | this.seq = seq;
55 | this.downloadProxyIp = downloadProxyIp;
56 | this.workThread = ipPool.getRuntimeIpSource().getLooper();
57 | this.recorder = ipPool.getRuntimeIpSource().getRecorder();
58 | this.activeStatus = ActiveStatus.ONLINE;
59 | this.murHash = ConsistentHashUtil.murHash(downloadProxyIp.getResourceId());
60 | }
61 |
62 | public void destroy(DropReason dropReason) {
63 | if (activeStatus == ActiveStatus.DESTROY) {
64 | return;
65 | }
66 | workThread.execute(() -> {
67 | if (activeStatus == ActiveStatus.DESTROY) {
68 | return;
69 | }
70 | if (activeStatus == ActiveStatus.ONLINE) {
71 | activeStatus = ActiveStatus.OFFLINE;
72 | }
73 |
74 | destroyFun = () -> {
75 | workThread.checkLooper();
76 | if (activeStatus == ActiveStatus.DESTROY) {
77 | return;
78 | }
79 | activeStatus = ActiveStatus.DESTROY;
80 | destroyFun = null;
81 | cachedHandle.destroy();
82 | };
83 | if (usedHash.isEmpty() || dropReason == DropReason.IP_SERVER_UNAVAILABLE) {
84 | // 如果当前没有用户占用本ip,则立即销毁
85 | destroyFun.run();
86 | return;
87 | }
88 | // 否则给一个60s的延时时间,给业务继续使用
89 | // 请注意整个60s的延时不是确定的,如果业务提前判断全部离开本ip,则清理动作可以提前
90 | workThread.postDelay(destroyFun, 60_000);
91 | });
92 |
93 |
94 | }
95 |
96 |
97 | public void borrowConnect(Recorder recorder, String tag,
98 | ActivityProxyIpBindObserver observer,
99 | ValueCallback valueCallback) {
100 | observer.onBind(this);
101 | workThread.execute(() -> {
102 | cachedHandle.onConnBorrowed();
103 |
104 | // 尝试使用缓存的ip资源
105 | while (true) {
106 | Channel one = cachedHandle.cachedChannels.poll();
107 | if (one == null) {
108 | break;
109 | }
110 | if (one.isActive()) {
111 | recorder.recordEvent(() -> tag + "conn cache pool hinted");
112 | ValueCallback.success(valueCallback, one);
113 | return;
114 | }
115 | }
116 | recorder.recordEvent(() -> tag + "begin to create connection immediately");
117 |
118 | createUpstreamConnection(valueCallback, recorder);
119 |
120 | });
121 |
122 | }
123 |
124 | public boolean isIdle() {
125 | return usedChannels.isEmpty();
126 | }
127 |
128 | private static final AttributeKey ACTIVITY_PROXY_IP_KEY = AttributeKey.newInstance("ACTIVITY_PROXY_IP");
129 |
130 | private void createUpstreamConnection(ValueCallback valueCallback, Recorder userRecorder) {
131 | OutboundOperator.connectToServer(downloadProxyIp.getProxyHost(), downloadProxyIp.getProxyPort(), value -> {
132 | if (!value.isSuccess()) {
133 | // 这里失败我们我们不再执行立即替换出口ip的逻辑,这是因为在并发极高的情况下
134 | // 失败可能是我们自己的网络不通畅导致的,我们不能以链接失败就判定ip存在问题
135 | ipPool.onCreateConnectionFailed(ActiveProxyIp.this, value.e, userRecorder);
136 | ValueCallback.failed(valueCallback, value.e);
137 | return;
138 | }
139 |
140 | // setup meta info
141 | Channel channel = value.v;
142 | channel.attr(ACTIVITY_PROXY_IP_KEY).set(this);
143 | channel.closeFuture().addListener(it -> usedChannels.remove(channel));
144 | cachedHandle.scheduleCleanIdleCache(channel, ipPool.getRuntimeIpSource().getConnIdleSeconds().value);
145 |
146 | ValueCallback.success(valueCallback, channel);
147 | });
148 | }
149 |
150 | public static void restoreCache(Channel channel) {
151 | ActiveProxyIp activeProxyIp = getBinding(channel);
152 | if (activeProxyIp == null) {
153 | channel.close();
154 | return;
155 | }
156 | activeProxyIp.cachedHandle.restoreCache(channel);
157 | }
158 |
159 | public static void offlineBindingProxy(Channel channel, IpPool.OfflineLevel level, Recorder userRecorder) {
160 | ActiveProxyIp activeProxyIp = getBinding(channel);
161 | if (activeProxyIp == null) {
162 | return;
163 | }
164 | activeProxyIp.getIpPool().offlineProxy(activeProxyIp, level, userRecorder);
165 | }
166 |
167 | public static ActiveProxyIp getBinding(Channel channel) {
168 | return channel.attr(ACTIVITY_PROXY_IP_KEY).get();
169 | }
170 |
171 | public void makeCache() {
172 | if (activeStatus != ActiveStatus.ONLINE) {
173 | return;
174 | }
175 | cachedHandle.doCreateCacheTask();
176 | }
177 |
178 | private class CacheHandle {
179 | private final LinkedList cachedChannels = Lists.newLinkedList();
180 | /**
181 | * 用户请求的平均时间间隔,代表这个ip资源处理请求的qps,但是我们使用一个高效计算方案评估这个指标
182 | * 这个值将会约等于最近10次请求时间间隔的平均数
183 | */
184 | private double avgInterval = 1000;
185 |
186 | private long lastRequestConnection = 0;
187 |
188 | private long lastCreateCacheConnection = 0;
189 |
190 | private void destroy() {
191 | NettyUtil.closeAll(cachedChannels);
192 | cachedChannels.clear();
193 | }
194 |
195 | public void restoreCache(Channel channel) {
196 | workThread.execute(() -> {
197 | if (activeStatus == ActiveStatus.DESTROY) {
198 | channel.close();
199 | return;
200 | }
201 | cachedChannels.addFirst(channel);
202 | });
203 | }
204 |
205 | public void onConnBorrowed() {
206 | long now = System.currentTimeMillis();
207 | if (lastRequestConnection == 0L) {
208 | lastRequestConnection = now;
209 | return;
210 | }
211 |
212 | long thisInterval = now - lastRequestConnection;
213 | lastRequestConnection = now;
214 | avgInterval = (thisInterval * 9 + avgInterval) / 10;
215 | }
216 |
217 | void scheduleCleanIdleCache(Channel cacheChannel, Integer idleSeconds) {
218 | // 为了避免gc hold,所有延时任务里面不允许直接访问channel对象,而使用WeakReference
219 | WeakReference channelRef = new WeakReference<>(cacheChannel);
220 | workThread.postDelay(() -> {
221 | Channel gcChannel = channelRef.get();
222 | if (gcChannel == null || !gcChannel.isActive()) {
223 | return;
224 | }
225 | for (Channel ch : cachedChannels) {
226 | if (ch.equals(gcChannel)) {
227 | gcChannel.close();
228 | return;
229 | }
230 | }
231 | }, idleSeconds * 1000);
232 | }
233 |
234 |
235 | public void doCreateCacheTask() {
236 | if (cachedChannels.size() > 3) {
237 | recorder.recordEvent(() -> "skip make conn cache because of cache overflow:" + cachedChannels.size());
238 | //理论上真实流量一半的速率,cachedChannels.size()应该为0或者1
239 | return;
240 | }
241 |
242 | long now = System.currentTimeMillis();
243 | if (lastCreateCacheConnection != 0 && (now - lastRequestConnection) < avgInterval / 2) {
244 | // 流量为真实速率的一半
245 | return;
246 | }
247 | lastCreateCacheConnection = now;
248 |
249 | recorder.recordEvent(() -> "fire conn cache make task");
250 | createUpstreamConnection(value -> {
251 | if (value.isSuccess()) {
252 | workThread.execute(() -> cachedChannels.addFirst(value.v));
253 | }
254 | }, Recorder.nop);
255 | }
256 |
257 | }
258 |
259 | public boolean canUserPassAuth() {
260 | return StringUtils.isNoneBlank(
261 | downloadProxyIp.getUserName(),
262 | downloadProxyIp.getPassword()
263 | );
264 | }
265 |
266 |
267 | public String buildHttpAuthenticationInfo() {
268 | if (canUserPassAuth()) {
269 | String authorizationBody = downloadProxyIp.getUserName() + ":" + downloadProxyIp.getPassword();
270 | return "Basic " + BaseEncoding.base64().encode(authorizationBody.getBytes(StandardCharsets.UTF_8));
271 | }
272 |
273 | return null;
274 | }
275 |
276 | public void refreshRefSessionHash(long sessionHash, boolean add) {
277 | workThread.execute(() -> {
278 | if (add) {
279 | usedHash.add(sessionHash);
280 | return;
281 | }
282 | usedHash.remove(sessionHash);
283 | if (usedHash.isEmpty() && destroyFun != null) {
284 | destroyFun.run();
285 | }
286 | });
287 | }
288 |
289 |
290 |
291 | public enum ActiveStatus {
292 | ONLINE,
293 | OFFLINE,
294 | DESTROY
295 | }
296 |
297 | public interface ActivityProxyIpBindObserver {
298 | void onBind(ActiveProxyIp activeProxyIp);
299 | }
300 |
301 |
302 | }
303 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/IpPool.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound;
2 |
3 | import cn.iinti.proxycompose.resource.DropReason;
4 | import cn.iinti.proxycompose.loop.Looper;
5 | import cn.iinti.proxycompose.loop.ValueCallback;
6 | import cn.iinti.proxycompose.proxy.outbound.downloader.DownloadProxyIp;
7 | import cn.iinti.proxycompose.proxy.RuntimeIpSource;
8 | import cn.iinti.proxycompose.utils.ConsistentHashUtil;
9 | import cn.iinti.proxycompose.trace.Recorder;
10 | import com.alibaba.fastjson.JSONObject;
11 | import lombok.Getter;
12 |
13 | import java.lang.ref.WeakReference;
14 | import java.util.*;
15 | import java.util.concurrent.atomic.AtomicLong;
16 | import java.util.function.Function;
17 |
18 | public class IpPool {
19 | @Getter
20 | private final RuntimeIpSource runtimeIpSource;
21 | private final Looper workThread;
22 | private final Recorder recorder;
23 |
24 | /**
25 | * 一致性哈希ip池,提供在池中随机(固定session)的能力
26 | */
27 | private final TreeMap poolWithMurHash = new TreeMap<>();
28 |
29 |
30 | /**
31 | * 非ip池,一个独立的,用于track代理资源入库时间的存储域
32 | */
33 | private final TreeMap poolWithCreateSequence = new TreeMap<>();
34 | private final AtomicLong outboundInc = new AtomicLong(0);
35 | /**
36 | * 备用ip资源,溢出的额外代理ip资源
37 | */
38 | private final LinkedHashMap cachedProxies = new LinkedHashMap() {
39 | @Override
40 | protected boolean removeEldestEntry(Map.Entry eldest) {
41 | // 自动删除超量ip,曾经因为没有处理这个变量,导致内存有两百万的ip资源
42 | // 范围为 10 - 1024之间的,配置ip池化大小的3倍容量
43 | Integer configPoolSize = runtimeIpSource.getPoolSize().value;
44 | int maxCacheSize = Math.min(configPoolSize * 3, 1024);
45 | maxCacheSize = Math.max(10, maxCacheSize);
46 | return size() > maxCacheSize;
47 | }
48 | };
49 |
50 | public IpPool(RuntimeIpSource runtimeIpSource, Looper workThread, Recorder recorder, String sourceKey) {
51 | this.runtimeIpSource = runtimeIpSource;
52 | this.workThread = workThread;
53 | this.recorder = recorder;
54 | }
55 |
56 | public void allocateIp(long hash, Recorder userRecorder,
57 | ValueCallback valueCallback) {
58 | workThread.execute(() -> {
59 | if (poolWithMurHash.isEmpty()) {
60 | ValueCallback.failed(valueCallback, "no ip in pool");
61 | return;
62 | }
63 | runtimeIpSource.recordComposedEvent(userRecorder, () -> "allocateOutbound request" +
64 | " hash: " + hash +
65 | " pool size:" + poolWithMurHash.size()
66 | );
67 |
68 | ActiveProxyIp activeProxyIp;
69 |
70 | runtimeIpSource.recordComposedEvent(userRecorder, () -> "default session hash plan");
71 | // 默认策略,在当前ip池中进行一致性哈希绑定
72 | activeProxyIp = ConsistentHashUtil.fetchConsistentRing(poolWithMurHash, hash);
73 |
74 | if (activeProxyIp == null) {
75 | ValueCallback.failed(valueCallback, "no suitable ip in the pool");
76 | } else {
77 | ValueCallback.success(valueCallback, activeProxyIp);
78 | }
79 | });
80 | }
81 |
82 |
83 | public void onCreateConnectionFailed(ActiveProxyIp activeProxyIp, Throwable cause, Recorder userRecorder) {
84 | runtimeIpSource.recordComposedEvent(userRecorder, () ->
85 | "连接创建失败->,进入IP资源下线决策阶段,"
86 | + "当前系统缓存池大小:" + cachedProxies.size()
87 | );
88 | runtimeIpSource.recordComposedMosaicEvent(userRecorder, cause::getMessage);
89 |
90 |
91 | String message = cause.getMessage();
92 | OfflineLevel offlineLevel;
93 | String offlineReason = "未知原因";
94 | if (message.contains("Connection refused") || message.contains("拒绝连接")) {
95 | // 明确这个ip资源无法使用了,强制下线
96 | offlineLevel = OfflineLevel.MUST;
97 | offlineReason = "拒绝连接";
98 | } else if (message.contains("connection timed out") || message.contains("连接超时")) {
99 | // 连接超时,可能是防火墙拦截,也可能是对方服务器负载高无法处理我们的请求
100 | // 此时根据ip池的容量大小决定是否要下线
101 | offlineLevel = OfflineLevel.STRONG;
102 | offlineReason = "连接超时";
103 | } else {
104 | // 其他失败原因,在ip池非常富裕的条件下执行下线
105 | offlineLevel = OfflineLevel.SUGGEST;
106 | }
107 |
108 |
109 | String finalOfflineReason = offlineReason;
110 | runtimeIpSource.recordComposedEvent(userRecorder, () -> "资产下线触发强度:" + offlineLevel + " 下线原因:" + finalOfflineReason);
111 |
112 | offlineProxy(activeProxyIp, offlineLevel, userRecorder);
113 | }
114 |
115 | public boolean poolEmpty() {
116 | return poolWithMurHash.isEmpty();
117 | }
118 |
119 | public double healthScore() {
120 | return (poolWithMurHash.size() + cachedProxies.size()) * 100.0
121 | / runtimeIpSource.getPoolSize().value;
122 | }
123 |
124 | public enum OfflineLevel {
125 | MUST("MUST"),// 必须下线,不关心什么原因
126 | STRONG("STRONG"),// 强烈要求下线,除非系统状态bad
127 | SUGGEST("SUGGEST")// 建议下线,轻微请求,除非系统非常富裕
128 | ;
129 | // 加一个name是为了避免混淆修改class的名字,毕竟添加keep很容易导致keep范围被放大
130 | public final String name;
131 |
132 | OfflineLevel(String name) {
133 | this.name = name;
134 | }
135 | }
136 |
137 | public void offlineProxy(ActiveProxyIp activeProxyIp, OfflineLevel level, Recorder userRecorder) {
138 |
139 | runtimeIpSource.recordComposedEvent(userRecorder, () -> "offline proxy by pool status: " + level.name);
140 | runtimeIpSource.recordComposedMosaicEvent(userRecorder, () -> " proxy: " + activeProxyIp.getDownloadProxyIp().getResourceId());
141 | if (poolWithMurHash.size() > runtimeIpSource.getPoolSize().value) {
142 | // ip池大小变更,直接下线
143 | offlineProxy(activeProxyIp, level == OfflineLevel.MUST ? DropReason.IP_SERVER_UNAVAILABLE : DropReason.IP_IDLE_POOL_OVERFLOW, userRecorder);
144 | return;
145 | }
146 | switch (level) {
147 | case MUST:
148 | offlineProxy(activeProxyIp, DropReason.IP_SERVER_UNAVAILABLE, userRecorder);
149 | return;
150 | case STRONG:
151 | if (!cachedProxies.isEmpty()) {
152 | offlineProxy(activeProxyIp, DropReason.IP_QUALITY_BAD, userRecorder);
153 | return;
154 | }
155 | break;
156 | case SUGGEST:
157 | if (cachedProxies.size() > runtimeIpSource.getPoolSize().value / 3) {
158 | offlineProxy(activeProxyIp, DropReason.IP_IDLE_POOL_OVERFLOW, userRecorder);
159 | return;
160 | }
161 | break;
162 | }
163 | runtimeIpSource.recordComposedEvent(userRecorder, () -> "do not offline proxy finally");
164 | }
165 |
166 | private void offlineProxy(ActiveProxyIp activeProxyIp, DropReason dropReason, Recorder userRecorder) {
167 | if (activeProxyIp.getActiveStatus() != ActiveProxyIp.ActiveStatus.ONLINE) {
168 | return;
169 | }
170 | workThread.post(() -> {
171 | if (activeProxyIp.getActiveStatus() != ActiveProxyIp.ActiveStatus.ONLINE) {
172 | return;
173 | }
174 | DownloadProxyIp remove = cachedProxies.remove(activeProxyIp.getDownloadProxyIp().getResourceId());
175 | runtimeIpSource.recordComposedEvent(userRecorder, () -> "cache existed: " + (remove != null));
176 |
177 | removeIfEq(activeProxyIp, ActiveProxyIp::getSeq, poolWithCreateSequence);
178 | removeIfEq(activeProxyIp, ActiveProxyIp::getMurHash, poolWithMurHash);
179 | // removeIfEq(activeProxyIp, ActiveProxyIp::getGeoHashCode, poolWithGeoLocation);
180 | // removeIfEq(activeProxyIp, ActiveProxyIp::getAdminCode, poolWithAdmin);
181 |
182 | activeProxyIp.destroy(dropReason);
183 |
184 | if (poolWithMurHash.size() > runtimeIpSource.getPoolSize().value) {
185 | return;
186 | }
187 | DownloadProxyIp newProxy = detachCacheCachedProxy();
188 | if (newProxy == null) {
189 | runtimeIpSource.recordComposedEvent(userRecorder, () -> "no cache proxy resource exist, pool size decreased!!");
190 | return;
191 | }
192 |
193 | runtimeIpSource.recordComposedEvent(userRecorder, () -> "online new proxy: ");
194 | runtimeIpSource.recordComposedMosaicEvent(userRecorder, () -> JSONObject.toJSONString(newProxy));
195 |
196 | onlineProxyResource(newProxy);
197 | }, true);
198 | // IP下线代码需要立即执行,保证其他任务不会使用到被下线了的ip,特别是ip重试的时候
199 | }
200 |
201 | private void removeIfEq(ActiveProxyIp activeProxyIp, Function kSupplier, Map map) {
202 | T k = kSupplier.apply(activeProxyIp);
203 | if (k == null) {
204 | return;
205 | }
206 | ActiveProxyIp remove = map.remove(k);
207 | if (remove == null) {
208 | return;
209 | }
210 | if (remove != activeProxyIp) {
211 | map.put(k, remove);
212 | }
213 | }
214 |
215 | private DownloadProxyIp detachCacheCachedProxy() {
216 | Iterator> iterator = cachedProxies.entrySet().iterator();
217 | DownloadProxyIp newProxy = null;
218 | if (iterator.hasNext()) {
219 | Map.Entry firstEntry = iterator.next();
220 | newProxy = firstEntry.getValue();
221 | iterator.remove();
222 | }
223 | return newProxy;
224 | }
225 |
226 | public void offerProxy(DownloadProxyIp downloadProxyIp) {
227 | if (downloadProxyIp.getExpireTime() != null
228 | && downloadProxyIp.getExpireTime() < System.currentTimeMillis()
229 | ) {
230 | // 加入的时候已经过期了
231 | recorder.recordEvent("offer an expired proxy item");
232 | return;
233 | }
234 | workThread.post(() -> {
235 | if (duplicate(downloadProxyIp)) {
236 | return;
237 | }
238 |
239 | Integer configPoolSize = runtimeIpSource.getPoolSize().value;
240 | if (poolWithMurHash.size() < configPoolSize) {
241 | onlineProxyResource(downloadProxyIp);
242 | return;
243 | }
244 |
245 | cachedProxies.put(downloadProxyIp.getResourceId(), downloadProxyIp);
246 |
247 | // 当缓存的ip资源比较大,超过了ip池的一半,那么我们直接插入替换
248 | // 最早加入的,当前没有为业务服务的资源,并把他替换到二级资源池
249 | if (cachedProxies.size() > Math.max(configPoolSize * 0.5, 1)) {
250 | ActiveProxyIp toReplaceActiveProxyIp = null;
251 | int count = 0;
252 | for (ActiveProxyIp activeProxyIp : poolWithCreateSequence.values()) {
253 | if (activeProxyIp.isIdle()) {
254 | toReplaceActiveProxyIp = activeProxyIp;
255 | break;
256 | }
257 | count++;
258 | if (count > 50) {
259 | break;
260 | }
261 | }
262 | if (toReplaceActiveProxyIp != null) {
263 | recorder.recordEvent("proxy cache overflow , offline old proxy");
264 | offlineProxy(toReplaceActiveProxyIp, DropReason.IP_IDLE_POOL_OVERFLOW, Recorder.nop);
265 | }
266 | }
267 | });
268 | }
269 |
270 | private boolean duplicate(DownloadProxyIp downloadProxyIp) {
271 | if (poolWithMurHash.containsKey(ConsistentHashUtil.murHash(downloadProxyIp.getResourceId()))) {
272 | return true;
273 | }
274 | return cachedProxies.containsKey(downloadProxyIp.getResourceId());
275 | }
276 |
277 | private void onlineProxyResource(DownloadProxyIp downloadProxyIp) {
278 | long lifeTime;
279 | if (downloadProxyIp.getExpireTime() != null) {
280 | lifeTime = downloadProxyIp.getExpireTime() - System.currentTimeMillis();
281 | } else {
282 | Integer maxAlive = runtimeIpSource.getMaxAlive().value;
283 | if (maxAlive == null) {
284 | maxAlive = 300;
285 | }
286 | lifeTime = maxAlive * 1000L;
287 | }
288 | if (lifeTime <= 0) {
289 | return;
290 | }
291 |
292 |
293 | ActiveProxyIp activeProxyIp = new ActiveProxyIp(this, downloadProxyIp, outboundInc.incrementAndGet());
294 |
295 | // register in multiple function ip pool
296 | poolWithMurHash.put(activeProxyIp.getMurHash(), activeProxyIp);
297 | poolWithCreateSequence.put(activeProxyIp.getSeq(), activeProxyIp);
298 |
299 | // 该ip配置了有效时间,且代理系统无法感知ip失效,
300 | // 故手动完成ip资源的下线
301 | WeakReference weakReference = new WeakReference<>(activeProxyIp);
302 | workThread.postDelay(() -> {
303 | // 可能已经提前下线了,所以这里用软引用
304 | ActiveProxyIp proxyNotGc = weakReference.get();
305 | if (proxyNotGc != null) {
306 | offlineProxy(proxyNotGc, DropReason.IP_ALIVE_TIME_REACHED, Recorder.nop);
307 | }
308 | }, lifeTime);
309 | }
310 |
311 | public void makeCache() {
312 | LinkedList outbounds = new LinkedList<>(poolWithMurHash.values());
313 | for (ActiveProxyIp outbound : outbounds) {
314 | outbound.makeCache();
315 | }
316 | }
317 |
318 | public void destroy() {
319 | poolWithMurHash.values().forEach(activeProxyIp -> activeProxyIp.destroy(DropReason.IP_RESOURCE_CLOSE));
320 | }
321 | }
322 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/OutboundOperator.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound;
2 |
3 | import cn.iinti.proxycompose.loop.ValueCallback;
4 | import cn.iinti.proxycompose.utils.NettyThreadPools;
5 | import cn.iinti.proxycompose.proxy.Session;
6 | import io.netty.bootstrap.Bootstrap;
7 | import io.netty.channel.*;
8 | import io.netty.channel.socket.nio.NioSocketChannel;
9 |
10 | public class OutboundOperator {
11 | private static final Bootstrap outboundBootstrap = buildOutboundBootstrap();
12 |
13 | private static Bootstrap buildOutboundBootstrap() {
14 | return new Bootstrap()
15 | .group(NettyThreadPools.outboundGroup)
16 | .channelFactory(NioSocketChannel::new)
17 | //上游链接在不确定协议的时候,无法确定处理器,我们使用一个插桩替代,在获取链接成功之后我们再手动构造
18 | .handler(new StubHandler())
19 | // 到代理ip的连接时间,设置短一些,设置为5s,如果不成功那么通过其他代理重试
20 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);
21 | }
22 |
23 | @ChannelHandler.Sharable
24 | public static class StubHandler extends ChannelInboundHandlerAdapter {
25 | @Override
26 | public void channelRegistered(ChannelHandlerContext ctx) {
27 | // just make netty framework happy
28 | ctx.pipeline().remove(this);
29 | }
30 | }
31 |
32 | public static void connectToServer(String host, int port, ValueCallback callback) {
33 | // 这里切换线程池的原因是,connect过程会有socket warming up,实践看来他在有些时候会有一定时间的延迟
34 | outboundBootstrap.config().group().execute(() -> outboundBootstrap.connect(
35 | host, port
36 | ).addListener((ChannelFutureListener) future -> {
37 | if (!future.isSuccess()) {
38 | ValueCallback.failed(callback, future.cause());
39 | } else {
40 | ValueCallback.success(callback, future.channel());
41 | }
42 | }));
43 | }
44 |
45 | public static void connectToOutbound(
46 | Session session, long sessionHash, String tag,
47 | ActiveProxyIp.ActivityProxyIpBindObserver observer,
48 | ValueCallback callback) {
49 | session.getProxyServer().getProxyCompose()
50 | .connectToOutbound(session, sessionHash, tag, observer, callback);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/downloader/DownloadProxyIp.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound.downloader;
2 |
3 | import cn.iinti.proxycompose.resource.ProxyIp;
4 | import com.alibaba.fastjson.JSONObject;
5 | import lombok.Getter;
6 | import lombok.Setter;
7 | import lombok.experimental.Delegate;
8 |
9 | import javax.annotation.Nullable;
10 |
11 | @Setter
12 | @Getter
13 | public class DownloadProxyIp {
14 |
15 | /**
16 | * 测试耗时
17 | */
18 | @Nullable
19 | private Long testCost;
20 |
21 | /**
22 | * 加入队列时间
23 | */
24 | private long enQueueTime;
25 |
26 |
27 | @Delegate
28 | private final ProxyIp proxyIp;
29 |
30 | public DownloadProxyIp(ProxyIp proxyIp) {
31 | this.proxyIp = proxyIp;
32 | }
33 |
34 | @Override
35 | public String toString() {
36 | return "DownloadProxyIp{" +
37 | "testCost=" + testCost +
38 | ", enQueueTime=" + enQueueTime +
39 | ", proxyIp=" + JSONObject.toJSONString(proxyIp) +
40 | '}';
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/downloader/IpDownloader.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound.downloader;
2 |
3 | import cn.iinti.proxycompose.Settings;
4 | import cn.iinti.proxycompose.proxy.outbound.handshark.Protocol;
5 | import cn.iinti.proxycompose.proxy.RuntimeIpSource;
6 | import cn.iinti.proxycompose.utils.AsyncHttpInvoker;
7 | import cn.iinti.proxycompose.resource.ProxyIp;
8 | import cn.iinti.proxycompose.loop.Looper;
9 | import cn.iinti.proxycompose.loop.ValueCallback;
10 | import cn.iinti.proxycompose.trace.Recorder;
11 | import cn.iinti.proxycompose.trace.impl.SubscribeRecorders;
12 | import cn.iinti.proxycompose.utils.IpUtils;
13 | import org.apache.commons.lang3.BooleanUtils;
14 | import org.apache.commons.lang3.StringUtils;
15 | import org.asynchttpclient.Realm;
16 | import org.asynchttpclient.proxy.ProxyServer;
17 | import org.asynchttpclient.proxy.ProxyType;
18 |
19 | import java.util.List;
20 | import java.util.function.Consumer;
21 | import java.util.function.Predicate;
22 | import java.util.stream.Collectors;
23 |
24 | public class IpDownloader {
25 | private final RuntimeIpSource runtimeIpSource;
26 | private final Looper workThread;
27 | private volatile boolean isDownloading = false;
28 | private final Recorder recorder;
29 |
30 |
31 | public IpDownloader(RuntimeIpSource runtimeIpSource, Recorder recorder, String sourceKey) {
32 | this.runtimeIpSource = runtimeIpSource;
33 | // downloader里面全是异步,但是可能会调用dns服务,并且可能存在对无效的代理域名进行dns解析,
34 | // 这可能存在些许耗时,所以这里我新建一个线程来处理
35 | this.workThread = new Looper("Downloader-" + sourceKey).startLoop();
36 | this.recorder = recorder;
37 | }
38 |
39 | private static final String dbName = "GeoLite2-City.mmdb";
40 |
41 |
42 | public void downloadIp() {
43 | workThread.execute(() -> {
44 | String loadUrl = runtimeIpSource.getLoadURL().value;
45 | if (!isHTTPLink(loadUrl)) {
46 | onDownloadResponse(loadUrl);
47 | return;
48 | }
49 |
50 | if (isDownloading) {
51 | return;
52 | }
53 | isDownloading = true;
54 |
55 | AsyncHttpInvoker.get(loadUrl, runtimeIpSource.getRecorder(), value -> workThread.post(() -> {
56 | isDownloading = false;
57 |
58 | if (!value.isSuccess()) {
59 | recorder.recordEvent(() -> "ip source download failed", value.e);
60 | return;
61 | }
62 | onDownloadResponse(value.v);
63 | }));
64 | });
65 |
66 | }
67 |
68 | @SuppressWarnings("all")
69 | private static boolean isHTTPLink(String url) {
70 | return StringUtils.startsWithAny(url, "http://", "https://");
71 | }
72 |
73 | private void onDownloadResponse(String response) {
74 | workThread.execute(() -> {
75 | recorder.recordEvent(() -> "download ip response:\n" + response + "\n");
76 | List proxyIps = runtimeIpSource.getResourceParser()
77 | .parse(response)
78 | .stream()
79 | .filter(proxyIp -> {
80 | if (!proxyIp.isValid()) {
81 | recorder.recordEvent(() -> "invalid parsed proxyIp");
82 | return false;
83 | }
84 | return true;
85 | }).collect(Collectors.toList());
86 |
87 | if (proxyIps.isEmpty()) {
88 | return;
89 | }
90 | // fill password from ip source config
91 | String upUserPassword = runtimeIpSource.getUpstreamAuthPassword().value;
92 | if (StringUtils.isNotBlank(upUserPassword)) {
93 | proxyIps.forEach(proxyIpResourceItem -> {
94 | if (StringUtils.isBlank(proxyIpResourceItem.getPassword())) {
95 | proxyIpResourceItem.setPassword(upUserPassword);
96 | }
97 | });
98 | }
99 | String upUserName = runtimeIpSource.getUpstreamAuthUser().value;
100 | if (StringUtils.isNotBlank(upUserName)) {
101 | proxyIps.forEach(proxyIpResourceItem -> {
102 | if (StringUtils.isBlank(proxyIpResourceItem.getUserName())) {
103 | proxyIpResourceItem.setUserName(upUserName);
104 | }
105 | });
106 | }
107 |
108 |
109 | Boolean needTest = runtimeIpSource.getNeedTest().value;
110 | recorder.recordEvent(() -> "this ip source configure test switch: " + needTest);
111 | Consumer action = BooleanUtils.isFalse(needTest) ?
112 | this::offerIpResource :
113 | this::runIpQualityTest;
114 |
115 | // 2024年03约21日,医药魔方:
116 | // 公司降本增效,将malenia服务器压缩到4G内存,同时跑malenia+mysql+python服务,导致物理内存不够
117 | // 最终排查在这里可能并发发出几百个网络情况(1毫秒内),这导致系统极短时间内存快速分配,进而引发oom
118 | // fix此问题使用如下策略:ip下载完成,进行分批延时探测入库,对此网络行为进行削峰填谷,10个ip一批并发多次进行ip质量探测
119 | //////////////////////////////////////////////////////////////////////////////////////////////////////////
120 | // step = ( interval * 1000 * 0.3) / size
121 | // 步长 = (加载间隔 * 1000毫秒 * 在前30%时间内完成探测) / 本次加载数量
122 | long offerStepInMills = runtimeIpSource.getReloadInterval().value * 300 / proxyIps.size();
123 |
124 | new IpOfferStep(workThread, offerStepInMills, proxyIps, action).execute();
125 | });
126 | }
127 |
128 |
129 | private void offerIpResource(DownloadProxyIp downloadProxyIp) {
130 | // 请注意,这里必须确保线程正确,因为InetAddress的解析可能比较耗时
131 | workThread.execute(() -> {
132 | recorder.recordEvent(() -> "prepare enpool proxy ip: " + downloadProxyIp);
133 | runtimeIpSource.getIpPool().offerProxy(downloadProxyIp);
134 | });
135 |
136 | }
137 |
138 | private void runIpQualityTest(DownloadProxyIp downloadProxyIp) {
139 | recorder.recordEvent(() -> "[QualityTest] begin test proxy quality: " + downloadProxyIp);
140 | Recorder recorderForTester = SubscribeRecorders.IP_TEST.acquireRecorder(
141 | downloadProxyIp.getResourceId() + "_" + System.currentTimeMillis(),
142 | Settings.global.debug.value, runtimeIpSource.getName()
143 | );
144 | recorderForTester.recordEvent(() -> "[QualityTest] begin to test proxy:" + downloadProxyIp);
145 |
146 | String url = Settings.global.proxyHttpTestURL.value;
147 | recorder.recordEvent(() -> "[QualityTest] ip test with url: " + url);
148 | long startTestTimestamp = System.currentTimeMillis();
149 |
150 | ProxyServer.Builder proxyBuilder = new ProxyServer.Builder(downloadProxyIp.getProxyHost(), downloadProxyIp.getProxyPort());
151 |
152 | if (runtimeIpSource.needAuth()) {
153 | proxyBuilder.setRealm(new Realm.Builder(
154 | runtimeIpSource.getUpstreamAuthUser().value,
155 | runtimeIpSource.getUpstreamAuthPassword().value
156 | ).setScheme(Realm.AuthScheme.BASIC));
157 | }
158 |
159 | if (runtimeIpSource.getSupportProtocolList().stream().anyMatch(Predicate.isEqual(Protocol.SOCKS5))) {
160 | // 有代理资源只能支持socks5,所以如果代理支持socks5时,直接使用s5来代理,
161 | // 默认将会使用http协议族,然后malenia具备自动的协议转换能力
162 | recorder.recordEvent(() -> "[QualityTest] use test protocol SOCKS_V5");
163 | proxyBuilder.setProxyType(ProxyType.SOCKS_V5);
164 | } else {
165 | recorder.recordEvent(() -> "[QualityTest] use test protocol HTTP");
166 | }
167 |
168 | AsyncHttpInvoker.get(url, recorderForTester, proxyBuilder, value -> {
169 | if (value.isSuccess()) {
170 | if (!IpUtils.isValidIp(value.v)) {
171 | // 扭转成功状态,因为响应的内容不是ip,那么认为报文错误
172 | value = ValueCallback.Value.failed("response not ip format: " + value.v);
173 | }
174 | }
175 |
176 | if (!value.isSuccess()) {
177 | recorder.recordEvent(() -> "[QualityTest] ip test failed", value.e);
178 | recorderForTester.recordEvent(() -> "ip test failed", value.e);
179 | return;
180 | }
181 | recorder.recordEvent(() -> "[QualityTest] ip test success");
182 | downloadProxyIp.setOutIp(value.v);
183 | downloadProxyIp.setTestCost(System.currentTimeMillis() - startTestTimestamp);
184 | offerIpResource(downloadProxyIp);
185 | });
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/downloader/IpOfferStep.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound.downloader;
2 |
3 | import cn.iinti.proxycompose.resource.ProxyIp;
4 | import cn.iinti.proxycompose.loop.Looper;
5 |
6 | import java.util.List;
7 | import java.util.function.Consumer;
8 |
9 | public class IpOfferStep {
10 | private static final int offerBatch = 10;
11 | private int nowIndex;
12 | private final Looper workThread;
13 | private final long offerStepInMills;
14 | private final List downloadIps;
15 | private final Consumer offerAction;
16 |
17 | public IpOfferStep(Looper workThread, long offerStepInMills, List downloadIps,
18 | Consumer offerAction) {
19 | this.workThread = workThread;
20 | this.offerStepInMills = offerStepInMills;
21 | this.downloadIps = downloadIps;
22 | this.offerAction = offerAction;
23 | this.nowIndex = 0;
24 | }
25 |
26 | void execute() {
27 | int maxIndex = downloadIps.size() - 1;
28 | for (int i = 0; i < offerBatch; i++) {
29 | ProxyIp proxyIp = downloadIps.get(this.nowIndex).resolveId();
30 | offerAction.accept(new DownloadProxyIp(proxyIp));
31 | this.nowIndex++;
32 | if (this.nowIndex > maxIndex) {
33 | return;
34 | }
35 | }
36 | workThread.postDelay(this::execute, offerStepInMills * offerBatch);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/handshark/AbstractUpstreamHandShaker.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound.handshark;
2 |
3 |
4 | import cn.iinti.proxycompose.Settings;
5 | import cn.iinti.proxycompose.loop.ValueCallback;
6 | import cn.iinti.proxycompose.proxy.Session;
7 | import cn.iinti.proxycompose.proxy.outbound.ActiveProxyIp;
8 | import cn.iinti.proxycompose.proxy.outbound.IpPool;
9 | import cn.iinti.proxycompose.trace.impl.SubscribeRecorders;
10 | import io.netty.channel.Channel;
11 |
12 | import java.lang.ref.WeakReference;
13 | import java.util.HashSet;
14 | import java.util.Set;
15 | import java.util.concurrent.TimeUnit;
16 | import java.util.concurrent.atomic.AtomicBoolean;
17 |
18 | public abstract class AbstractUpstreamHandShaker {
19 | protected Session session;
20 | protected Channel outboundChannel;
21 | private final ValueCallback callback;
22 | protected final ActiveProxyIp activeProxyIp;
23 |
24 | protected SubscribeRecorders.SubscribeRecorder recorder;
25 | private final AtomicBoolean hasEmitResult = new AtomicBoolean(false);
26 |
27 | /**
28 | * @param session 对应隧道
29 | * @param callback 回掉函数,请注意他包裹一个Boolean对象,表示是否可以failover
30 | */
31 | public AbstractUpstreamHandShaker(Session session, Channel outboundChannel, ActiveProxyIp activeProxyIp, ValueCallback callback) {
32 | this.session = session;
33 | this.activeProxyIp = activeProxyIp;
34 | this.callback = value -> {
35 | if (!value.isSuccess()) {
36 | // 握手失败的链接,统一计数为失败
37 | outboundChannel.close();
38 | }
39 | callback.onReceiveValue(value);
40 | };
41 | this.recorder = session.getRecorder();
42 | this.outboundChannel = outboundChannel;
43 | setupHandSharkTimeout();
44 |
45 | }
46 |
47 | private void setupHandSharkTimeout() {
48 | // 看追踪日志,部分请求出现在发送了connect之后没有回应,所以这里我们做一次检测
49 | // 设定一定timeout之后,主动阻断请求,然后执行failover
50 | // 猜想出现这个的愿意如下:
51 | // 1. 代理服务器bug,导致代理服务器接受了connect但是不给响应
52 | // 2. 代理服务器在我们发送connect之后发生了重播,或者发生了线路无法通畅连接的问题
53 | // 3. 代理服务器到真实服务器的线路bug,或代理服务器本身代理socket创建过程timeout过长,或者代理服务器连接真实服务器失败了,但是代理服务器没有较好的处理这个问题
54 | //
55 | // 实际测试发现这个值普遍在2秒左右,我们默认值设置为5s
56 | // 请注意,这个逻辑是必要的,这是因为如果我们不设置超时。上游ip资源不关闭链接,将会导致链接泄漏,影响GC同时消耗FD资源
57 | // 第一代的malenia系统socks代理缺失了time-out检查,导致出现了200w FD资源无法回收的情况
58 | Integer connectTimeout = Settings.global.handleSharkConnectionTimeout.value;
59 | // min:1s max: 60s default: 5s
60 | if (connectTimeout == null || connectTimeout > 60 * 1000) {
61 | connectTimeout = 5000;
62 | }
63 | if (connectTimeout < 1000) {
64 | connectTimeout = 1000;
65 | }
66 | recorder.recordEvent("this connection timeout is: " + connectTimeout);
67 | WeakReference ref = new WeakReference<>(this);
68 | outboundChannel.eventLoop().schedule(() -> {
69 | AbstractUpstreamHandShaker upstreamHandShaker = ref.get();
70 | if (upstreamHandShaker == null) {
71 | return;
72 | }
73 | if (upstreamHandShaker.hasEmitResult.get()) {
74 | return;
75 | }
76 | upstreamHandShaker.recorder.recordEvent(() -> "timeout");
77 | upstreamHandShaker.emitFailed("upstream hand shark timeout, abort this connection", true);
78 | }, connectTimeout, TimeUnit.MILLISECONDS);
79 |
80 | }
81 |
82 | protected void emitSuccess() {
83 | if (hasEmitResult.compareAndSet(false, true)) {
84 | ValueCallback.success(callback, true);
85 | }
86 | }
87 |
88 | private static final Set notOfflineProxyMsgs = new HashSet() {
89 | {
90 | add("NETWORK_UNREACHABLE");
91 | }
92 | };
93 |
94 | protected void emitFailed(Object msg, boolean canRetry) {
95 | if (hasEmitResult.compareAndSet(false, true)) {
96 | ValueCallback.Value value;
97 | if (msg instanceof Throwable) {
98 | value = ValueCallback.Value.failed((Throwable) msg);
99 | } else {
100 | value = ValueCallback.Value.failed(msg.toString());
101 | }
102 | value.v = canRetry;
103 | String errorMsg = value.e.getMessage();
104 | recorder.recordEvent(() -> "HandShark failed", value.e);
105 | try {
106 | callback.onReceiveValue(value);
107 | } finally {
108 | boolean needOfflineProxy = true;
109 | for (String notOfflineProxyMsg : notOfflineProxyMsgs) {
110 | if (errorMsg.contains(notOfflineProxyMsg)) {
111 | // java.lang.RuntimeException: cmd failed: NETWORK_UNREACHABLE
112 | // 这个时候其实就是目标服务器连不上,和代理服务器没有关系
113 | needOfflineProxy = false;
114 | break;
115 | }
116 | }
117 | if (needOfflineProxy) {
118 | // 代理云ip过期之后,可能允许连接,但是鉴权失败,这是我们建议建议下线ip
119 | ActiveProxyIp.offlineBindingProxy(
120 | outboundChannel,
121 | IpPool.OfflineLevel.SUGGEST,
122 | recorder);
123 | }
124 | }
125 |
126 |
127 | }
128 | }
129 |
130 |
131 | public abstract void doHandShark();
132 | }
133 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/handshark/HandShakerHttps.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound.handshark;
2 |
3 | import cn.iinti.proxycompose.loop.ValueCallback;
4 | import cn.iinti.proxycompose.proxy.Session;
5 | import cn.iinti.proxycompose.proxy.outbound.ActiveProxyIp;
6 | import io.netty.channel.Channel;
7 | import io.netty.channel.ChannelHandlerContext;
8 | import io.netty.channel.ChannelPipeline;
9 | import io.netty.channel.SimpleChannelInboundHandler;
10 | import io.netty.handler.codec.http.*;
11 |
12 | /**
13 | * 完成https和上级代理的鉴权和握手
14 | */
15 | public class HandShakerHttps extends AbstractUpstreamHandShaker {
16 | private boolean hasSendAuthentication = false;
17 |
18 |
19 | /**
20 | * @param session 对应隧道
21 | * @param callback 回掉函数,请注意他包裹一个Boolean对象,表示是否可以failover
22 | */
23 | public HandShakerHttps(Session session, Channel outboundChannel, ActiveProxyIp activeProxyIp, ValueCallback callback) {
24 | super(session, outboundChannel, activeProxyIp, callback);
25 | }
26 |
27 | @Override
28 | public void doHandShark() {
29 | recorder.recordEvent(() -> "begin https upstream hand shark");
30 |
31 | ChannelPipeline pipeline = outboundChannel.pipeline();
32 | pipeline.addFirst(new HttpRequestEncoder());
33 | pipeline.addFirst(new HttpResponseDecoder());
34 |
35 | DefaultFullHttpRequest connectRequest = createConnectRequest();
36 | outboundChannel.writeAndFlush(connectRequest)
37 | .addListener(future -> {
38 | if (!future.isSuccess()) {
39 | recorder.recordEvent(() -> "write https connect request failed");
40 | emitFailed("write https connect request failed", true);
41 | return;
42 | }
43 | recorder.recordEvent(() -> "request write finish ,setupResponseHandler");
44 | outboundChannel.pipeline().addLast(new SimpleChannelInboundHandler() {
45 | @Override
46 | protected void channelRead0(ChannelHandlerContext ctx, HttpResponse msg) {
47 | handleResponse(msg, this);
48 | }
49 |
50 | @Override
51 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
52 | // 发connect报文过程发现对方关闭了链接,这个时候可以重试
53 | recorder.recordEvent(() -> "exception: ", cause);
54 | emitFailed(cause, true);
55 | }
56 | });
57 | });
58 |
59 | }
60 |
61 |
62 | private void handleResponse(HttpResponse httpResponse, SimpleChannelInboundHandler mHandler) {
63 | int code = httpResponse.status().code();
64 | recorder.recordEvent(() -> "ConnectHttpResponse:" + code);
65 | if (code == HttpResponseStatus.OK.code()) {
66 | recorder.recordEvent(() -> "hand shark success, remove upstream http netty handler");
67 | ChannelPipeline pipeline = outboundChannel.pipeline();
68 | pipeline.remove(HttpRequestEncoder.class);
69 | pipeline.remove(HttpResponseDecoder.class);
70 | pipeline.remove(mHandler);
71 | emitSuccess();
72 | return;
73 | }
74 |
75 | if (code >= 500) {
76 | recorder.recordEvent(() -> "receive 5xx from upstream" + httpResponse);
77 | emitFailed(httpResponse.status().reasonPhrase(), true);
78 | return;
79 | }
80 |
81 | if (code == 401) {
82 | // 401 芝麻代理 节点失效错误,这是在瞎搞
83 | recorder.recordEvent(() -> "401 on hand shark");
84 | emitFailed(httpResponse.status().reasonPhrase(), true);
85 | return;
86 | }
87 | if (code != HttpResponseStatus.PROXY_AUTHENTICATION_REQUIRED.code()) {
88 | recorder.recordEvent(() -> "not 407 response,hand shark failed");
89 | emitFailed(httpResponse.status().reasonPhrase(), false);
90 | return;
91 | }
92 |
93 | if (hasSendAuthentication) {
94 | recorder.recordEvent(() -> "upstream password error");
95 | emitFailed("upstream password error", false);
96 | return;
97 | }
98 |
99 | // 有部分代理服务器首次发送了代理鉴权内容但是他不认,依然返回407
100 | // 然后有一些网络库一旦首次发送过密码,即使407也不会重新发送带鉴权请求,而是直接报告失败
101 | // 所以我们这里屏蔽这个问题,如果407我们再发送一次报文.
102 | recorder.recordEvent(() -> "407 response, send user pass again");
103 | DefaultFullHttpRequest connectRequest = createConnectRequest();
104 |
105 | outboundChannel.writeAndFlush(connectRequest)
106 | .addListener(future -> {
107 | if (!future.isSuccess()) {
108 | recorder.recordEvent(() -> "write https connect request failed");
109 | emitFailed("write https connect request failed", true);
110 | return;
111 | }
112 | recorder.recordEvent(() -> "second auth request write finish");
113 | hasSendAuthentication = true;
114 | });
115 | }
116 |
117 |
118 | private DefaultFullHttpRequest createConnectRequest() {
119 | DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
120 | HttpMethod.CONNECT, session.getConnectTarget().getIpPort());
121 |
122 | String header = activeProxyIp.buildHttpAuthenticationInfo();
123 |
124 | if (header == null) {
125 | recorder.recordEvent(() -> "this ipSource do not need authentication");
126 | } else {
127 | request.headers().add(HttpHeaderNames.PROXY_AUTHORIZATION, header);
128 | recorder.recordMosaicMsg(() -> "fill authorizationContent: " + header);
129 | }
130 | return request;
131 | }
132 |
133 |
134 | }
135 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/handshark/HandShakerSocks5.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound.handshark;
2 |
3 | import cn.iinti.proxycompose.resource.IpAndPort;
4 | import cn.iinti.proxycompose.proxy.Session;
5 | import cn.iinti.proxycompose.proxy.outbound.ActiveProxyIp;
6 | import cn.iinti.proxycompose.utils.IpUtils;
7 | import cn.iinti.proxycompose.loop.ValueCallback;
8 | import com.google.common.collect.Lists;
9 | import io.netty.channel.Channel;
10 | import io.netty.channel.ChannelHandlerContext;
11 | import io.netty.channel.ChannelPipeline;
12 | import io.netty.channel.SimpleChannelInboundHandler;
13 | import io.netty.handler.codec.socks.*;
14 | import io.netty.util.ReferenceCountUtil;
15 |
16 | import java.util.List;
17 |
18 | public class HandShakerSocks5 extends AbstractUpstreamHandShaker {
19 | private final ChannelPipeline pipeline;
20 |
21 | /**
22 | * @param turning 对应隧道
23 | * @param callback 回掉函数,请注意他包裹一个Boolean对象,表示是否可以failover
24 | */
25 | public HandShakerSocks5(Session turning, Channel outboundChannel, ActiveProxyIp activeProxyIp, ValueCallback callback) {
26 | super(turning, outboundChannel, activeProxyIp, callback);
27 | pipeline = outboundChannel.pipeline();
28 | }
29 |
30 | @Override
31 | public void doHandShark() {
32 | recorder.recordEvent(() -> "begin socks5 hand shark");
33 |
34 | pipeline.addLast(new SocksMessageEncoder());
35 | List socksAuthSchemes = Lists.newLinkedList();
36 | socksAuthSchemes.add(SocksAuthScheme.NO_AUTH);
37 | if (activeProxyIp.canUserPassAuth()) {
38 | socksAuthSchemes.add(SocksAuthScheme.AUTH_PASSWORD);
39 | }
40 | SocksInitRequest socksInitRequest = new SocksInitRequest(socksAuthSchemes);
41 | recorder.recordEvent(() -> "write socksInitRequest");
42 |
43 | outboundChannel.writeAndFlush(socksInitRequest)
44 | .addListener(future -> {
45 | if (!future.isSuccess()) {
46 | recorder.recordEvent(() -> "write socksInitRequest failed");
47 | emitFailed(future.cause(), true);
48 | return;
49 | }
50 | recorder.recordEvent(() -> "setup socks InitResponse handler");
51 | pipeline.addFirst(new SocksInitResponseDecoder());
52 | pipeline.addLast(new SocksResponseHandler());
53 | });
54 | }
55 |
56 |
57 | private void doConnectToUpstream() {
58 | // use proxy switch ,this means we just need support ipv4 & tcp
59 | IpAndPort ipAndPort = session.getConnectTarget();
60 |
61 | SocksCmdRequest socksCmdRequest = new SocksCmdRequest(SocksCmdType.CONNECT,
62 | IpUtils.isIpV4(ipAndPort.getIp()) ? SocksAddressType.IPv4 : SocksAddressType.DOMAIN,
63 | ipAndPort.getIp(), ipAndPort.getPort());
64 |
65 | recorder.recordEvent(() -> "send cmd request to upstream");
66 | pipeline.addFirst(new SocksCmdResponseDecoder());
67 | outboundChannel.writeAndFlush(socksCmdRequest)
68 | .addListener(future -> {
69 | if (!future.isSuccess()) {
70 | recorder.recordEvent(() -> "write upstream socksCmdRequest failed");
71 | emitFailed(future.cause(), true);
72 | } else {
73 | recorder.recordEvent(() -> "write upstream socksCmdRequest finished");
74 | // now waiting cmd response
75 | }
76 | });
77 | }
78 |
79 | private void doAuth() {
80 | if (!activeProxyIp.canUserPassAuth()) {
81 | emitFailed("the upstream server need auth,but no auth configured for this ip source : "
82 | + activeProxyIp.getIpPool().getRuntimeIpSource().getName(), false);
83 | return;
84 | }
85 | recorder.recordMosaicMsg(() -> "send socks5 auth");
86 | pipeline.addFirst(new SocksAuthResponseDecoder());
87 | SocksAuthRequest socksAuthRequest = new SocksAuthRequest(
88 | activeProxyIp.getDownloadProxyIp().getUserName(),
89 | activeProxyIp.getDownloadProxyIp().getPassword()
90 | );
91 | outboundChannel.writeAndFlush(socksAuthRequest)
92 | .addListener(future -> {
93 | if (!future.isSuccess()) {
94 | emitFailed(future.cause(), true);
95 | return;
96 | }
97 | recorder.recordEvent(() -> "auth request send finish");
98 | });
99 | }
100 |
101 | private void handleInitResponse(SocksInitResponse response) {
102 | SocksAuthScheme socksAuthScheme = response.authScheme();
103 | switch (socksAuthScheme) {
104 | case NO_AUTH:
105 | doConnectToUpstream();
106 | break;
107 | case AUTH_PASSWORD:
108 | doAuth();
109 | break;
110 | default:
111 | emitFailed("no support auth method: " + socksAuthScheme, false);
112 | }
113 | }
114 |
115 | private void handleAuthResponse(SocksAuthResponse response) {
116 | recorder.recordEvent(() -> "socks5 handShark authResponse: " + response.authStatus());
117 | if (response.authStatus() != SocksAuthStatus.SUCCESS) {
118 | emitFailed("upstream auth failed", false);
119 | return;
120 | }
121 | doConnectToUpstream();
122 | }
123 |
124 | private void handleCMDResponse(SocksCmdResponse response) {
125 | recorder.recordEvent(() -> "upstream CMD Response: " + response);
126 | if (response.cmdStatus() != SocksCmdStatus.SUCCESS) {
127 | recorder.recordEvent(() -> "cmd failed: " + response.cmdStatus());
128 | emitFailed("cmd failed: " + response.cmdStatus(), true);
129 | return;
130 | }
131 | pipeline.remove(SocksMessageEncoder.class);
132 | pipeline.remove(SocksResponseHandler.class);
133 | recorder.recordEvent(() -> "upstream HandShark success finally");
134 | emitSuccess();
135 | }
136 |
137 | private void handleUpstreamSocksResponse(SocksResponse msg) {
138 | switch (msg.responseType()) {
139 | case INIT:
140 | handleInitResponse((SocksInitResponse) msg);
141 | break;
142 | case AUTH:
143 | handleAuthResponse((SocksAuthResponse) msg);
144 | break;
145 | case CMD:
146 | handleCMDResponse((SocksCmdResponse) msg);
147 | break;
148 | default:
149 | recorder.recordEvent(() -> "unknown socksResponse: " + msg);
150 | emitFailed("unknown socksResponse:" + msg, false);
151 | }
152 | }
153 |
154 | private class SocksResponseHandler extends SimpleChannelInboundHandler {
155 |
156 | @Override
157 | protected void channelRead0(ChannelHandlerContext ctx, SocksResponse msg) throws Exception {
158 | handleUpstreamSocksResponse(msg);
159 | }
160 |
161 | @Override
162 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
163 | try {
164 | emitFailed(cause, true);
165 | } finally {
166 | ReferenceCountUtil.release(cause);
167 | }
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/handshark/Protocol.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound.handshark;
2 |
3 | import lombok.Getter;
4 |
5 | public enum Protocol {
6 | HTTP(0),
7 | // 这里的https实际上指代connect,实践中都是作为https使用。但是实际上也是可以跑http为加密流量的
8 | HTTPS(1),
9 | SOCKS5(2);
10 |
11 | static {
12 | HTTP.supportOverlayProtocol = new Protocol[]{};
13 | // 理论上https也是可以支持转发到socks上面的,但是他只能转发socks的tcp部分,
14 | // 为了避免未来支持udp的时候出现问题,我们不配置socks流量跑到https信道上面来
15 | HTTPS.supportOverlayProtocol = new Protocol[]{HTTP};
16 | // socks4和sock5可以相互转换,如有有上游供应商只支持socks4,我们也可以提供全套的代理服务
17 | SOCKS5.supportOverlayProtocol = new Protocol[]{HTTP, HTTPS};
18 | }
19 |
20 | @Getter
21 | private Protocol[] supportOverlayProtocol;
22 |
23 | @Getter
24 | private final int priority;
25 |
26 | Protocol(int priority) {
27 | this.priority = priority;
28 | }
29 |
30 | public static Protocol get(String name) {
31 | for (Protocol protocol : Protocol.values()) {
32 | if (protocol.name().equalsIgnoreCase(name)) {
33 | return protocol;
34 | }
35 | }
36 | return null;
37 | }
38 |
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/outbound/handshark/ProtocolManager.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.outbound.handshark;
2 |
3 | import cn.iinti.proxycompose.loop.ValueCallback;
4 | import cn.iinti.proxycompose.proxy.Session;
5 | import cn.iinti.proxycompose.proxy.outbound.ActiveProxyIp;
6 | import io.netty.channel.Channel;
7 |
8 | import java.util.HashMap;
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | public class ProtocolManager {
13 |
14 | private static final Map upstreamHandShakerMap = new HashMap() {
15 | {
16 | put(Protocol.HTTP, (turning, outboundChannel, activeProxyIp, callback) ->
17 | new AbstractUpstreamHandShaker(turning, outboundChannel, activeProxyIp, callback) {
18 | @Override
19 | public void doHandShark() {
20 | // 请注意,不允许直接在这里操作callback,因为handshake需要统一管理他
21 | emitSuccess();
22 | }
23 | });
24 | put(Protocol.HTTPS, HandShakerHttps::new);
25 | put(Protocol.SOCKS5, HandShakerSocks5::new);
26 | }
27 | };
28 |
29 | private interface UpstreamHandShakerFactory {
30 | AbstractUpstreamHandShaker create(Session session, Channel outboundChannel, ActiveProxyIp activeProxyIp, ValueCallback callback);
31 | }
32 |
33 | public static AbstractUpstreamHandShaker createHandShaker(Protocol protocol, Session turning, Channel outboundChannel,
34 | ActiveProxyIp activeProxyIp, ValueCallback callback) {
35 | if (protocol != null) {
36 | UpstreamHandShakerFactory factory = upstreamHandShakerMap.get(protocol);
37 | if (factory != null) {
38 | return factory.create(turning, outboundChannel, activeProxyIp, callback);
39 | }
40 | }
41 | return new AbstractUpstreamHandShaker(turning, outboundChannel, activeProxyIp, callback) {
42 | @Override
43 | public void doHandShark() {
44 | ValueCallback.failed(callback, "unknown protocol: " + protocol);
45 | }
46 | };
47 | }
48 |
49 |
50 | public static Protocol chooseUpstreamProtocol(Protocol inboundProtocol, List supportList) {
51 | if (inboundProtocol == Protocol.HTTP) {
52 | // 特殊逻辑,如果是http,我们不让上游代理走http,而是尽量走socks
53 | // 在代理失败判定过程,纯http的判定需要侵入到http协议报文中感知,并且可能因为上游代理服务器的实现导致存在可能的误判
54 | // 这是因为http的鉴权、连接建立、业务请求发送是来自同一个请求流程。我们可以考虑实现http侵入感知,但是这会带来巨大的系统开销和编程难度
55 | // 并且在上游代理系统不标准实现的情况下,我们可能存在对代理质量的误判
56 | for (Protocol candidateProtocol : supportList) {
57 | if (candidateProtocol == Protocol.HTTP) {
58 | continue;
59 | }
60 | for (Protocol protocol : candidateProtocol.getSupportOverlayProtocol()) {
61 | if (protocol == inboundProtocol) {
62 | return candidateProtocol;
63 | }
64 | }
65 | }
66 | }
67 |
68 | for (Protocol support : supportList) {
69 | // 首先使用原生的上下游协议
70 | if (support == inboundProtocol) {
71 | return support;
72 | }
73 | }
74 |
75 | for (Protocol candidateProtocol : supportList) {
76 | for (Protocol protocol : candidateProtocol.getSupportOverlayProtocol()) {
77 | if (protocol == inboundProtocol) {
78 | return candidateProtocol;
79 | }
80 | }
81 | }
82 | return null;
83 | }
84 |
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/switcher/EatErrorFilter.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.switcher;
2 |
3 | import cn.iinti.proxycompose.proxy.outbound.handshark.Protocol;
4 | import cn.iinti.proxycompose.trace.Recorder;
5 | import io.netty.channel.Channel;
6 |
7 | public class EatErrorFilter implements UpstreamHandSharkCallback {
8 | private final UpstreamHandSharkCallback delegate;
9 | private final Recorder recorder;
10 |
11 | public EatErrorFilter(UpstreamHandSharkCallback delegate, Recorder recorder) {
12 | this.delegate = delegate;
13 | this.recorder = recorder;
14 | }
15 |
16 | private volatile boolean hasSendErrorMsg = false;
17 |
18 | @Override
19 | public void onHandSharkFinished(Channel upstreamChannel, Protocol outboundProtocol) {
20 | if (hasSendErrorMsg) {
21 | recorder.recordEvent(() -> "hasSendErrorMsg ignore callback onHandSharkFinished");
22 | return;
23 | }
24 | delegate.onHandSharkFinished(upstreamChannel,outboundProtocol);
25 | }
26 |
27 | @Override
28 | public void onHandSharkError(Throwable e) {
29 | if (hasSendErrorMsg) {
30 | recorder.recordEvent(() -> "duplicate callback on hand shark");
31 | return;
32 | }
33 | hasSendErrorMsg = true;
34 | delegate.onHandSharkError(e);
35 | }
36 | }
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/switcher/OutboundConnectTask.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.switcher;
2 |
3 |
4 | import cn.iinti.proxycompose.Settings;
5 | import cn.iinti.proxycompose.proxy.ProxyCompose;
6 | import cn.iinti.proxycompose.proxy.Session;
7 | import cn.iinti.proxycompose.proxy.outbound.ActiveProxyIp;
8 | import cn.iinti.proxycompose.proxy.outbound.OutboundOperator;
9 | import cn.iinti.proxycompose.proxy.outbound.handshark.AbstractUpstreamHandShaker;
10 | import cn.iinti.proxycompose.proxy.outbound.handshark.Protocol;
11 | import cn.iinti.proxycompose.proxy.outbound.handshark.ProtocolManager;
12 | import cn.iinti.proxycompose.utils.ConsistentHashUtil;
13 | import cn.iinti.proxycompose.loop.ParallelExecutor;
14 | import cn.iinti.proxycompose.loop.ValueCallback;
15 | import cn.iinti.proxycompose.trace.Recorder;
16 | import io.netty.channel.Channel;
17 |
18 | import java.util.List;
19 |
20 | /**
21 | * 这是本系统最核心的心脏部位,他决定了整个ip池的上下游连接选择过程
22 | */
23 | public class OutboundConnectTask implements ActiveProxyIp.ActivityProxyIpBindObserver {
24 | private final Session session;
25 | private final UpstreamHandSharkCallback callback;
26 | private final Recorder recorder;
27 | // 失败重试次数
28 | public int failoverCount = 0;
29 | private String resolvedIpSource = "unknown";
30 |
31 | public static void startConnectOutbound(Session session, UpstreamHandSharkCallback callback) {
32 | new OutboundConnectTask(session, callback).doStart();
33 | }
34 |
35 | private OutboundConnectTask(Session session, UpstreamHandSharkCallback callback) {
36 | this.session = session;
37 | this.recorder = session.getRecorder();
38 | this.callback = new EatErrorFilter(callback, recorder);
39 | }
40 |
41 | private void doStart() {
42 | proxyForward(null);
43 | }
44 |
45 |
46 | private void proxyForward(Throwable throwable) {
47 | if (failoverCount >= Settings.global.maxFailoverCount.value) {
48 | recorder.recordEvent(() -> "failoverCount count: " + failoverCount + " maxCount:" + Settings.global.maxFailoverCount.value);
49 | if (throwable == null) {
50 | throwable = new RuntimeException("get upstream failed");
51 | }
52 | callback.onHandSharkError(throwable);
53 | return;
54 | }
55 | failoverCount++;
56 |
57 | ValueCallback channelCallback = makeChannelFinishedEvent();
58 |
59 | long sessionHash = session.getSessionHash();
60 | ProxyCompose proxyCompose = session.getProxyServer().getProxyCompose();
61 |
62 | if (failoverCount == 1) {
63 | // 如果是第一次尝试创建连接,则使用单并发,并且尝试记录曾经的隧道规则
64 | // 尽可能保证隧道映射不变
65 | proxyCompose.fetchCachedSession(session, value -> {
66 | if (value.isSuccess()) {
67 | recorder.recordEvent(() -> "this request session hold, reuse cached ip");
68 | value.v.borrowConnect(recorder, "CachedIp", this, channelCallback);
69 | } else {
70 | recorder.recordEvent(() -> "fist choose ip resource");
71 | OutboundOperator.connectToOutbound(session, sessionHash, "first create-> ", this, channelCallback);
72 | }
73 | });
74 | return;
75 | }
76 | // 否则使用并发的方式,连续创建三个连接,并且记忆成功的隧道
77 | recorder.recordEvent(() -> "retry index: " + failoverCount);
78 | failoverConnect(channelCallback, sessionHash, proxyCompose);
79 | }
80 |
81 | @Override
82 | public void onBind(ActiveProxyIp activeProxyIp) {
83 | resolvedIpSource = activeProxyIp.getIpPool()
84 | .getRuntimeIpSource().getName();
85 | }
86 |
87 |
88 | private static class FailoverTaskHolder {
89 | final long hash;
90 | final Channel channel;
91 | final int index;
92 | final String tag;
93 |
94 | public FailoverTaskHolder(long hash, int index, Channel value, String tag) {
95 | this.hash = hash;
96 | this.index = index;
97 | this.channel = value;
98 | this.tag = tag;
99 | }
100 | }
101 |
102 |
103 | private class FailoverMsgEvent implements ParallelExecutor.ParallelConnectEvent {
104 | private final ValueCallback channelCallback;
105 |
106 | public FailoverMsgEvent(ValueCallback channelCallback) {
107 | this.channelCallback = channelCallback;
108 | }
109 |
110 | @Override
111 | public void firstSuccess(ValueCallback.Value value) {
112 | FailoverTaskHolder holder = value.v;
113 | recorder.recordEvent(holder.tag + "create connection success :");
114 | recorder.recordMosaicMsgIfSubscribeRecorder(() -> holder.tag + holder.channel);
115 | ValueCallback.success(channelCallback, holder.channel);
116 | }
117 |
118 | @Override
119 | public void secondSuccess(ValueCallback.Value value) {
120 | FailoverTaskHolder holder = value.v;
121 | recorder.recordEvent(() -> holder.tag + "create channel secondSuccess, restore cache channel");
122 | ActiveProxyIp.restoreCache(holder.channel);
123 | }
124 |
125 | @Override
126 | public void finalFailed(Throwable throwable) {
127 | recorder.recordEvent(() -> "all ip create connection failed", throwable);
128 | ValueCallback.failed(channelCallback, throwable);
129 | }
130 | }
131 |
132 | private static final int parallelSize = 3;
133 |
134 | private void failoverConnect(ValueCallback channelCallback, long sessionHash, ProxyCompose proxyCompose) {
135 | // 不是第一次,那么同时使用多个资源创建ip,谁成功以谁为准
136 | ParallelExecutor executor = new ParallelExecutor<>(proxyCompose.getComposeWorkThead(), parallelSize,
137 | new FailoverMsgEvent(channelCallback));
138 |
139 | for (int i = 0; i < parallelSize; i++) {
140 | long newHash = ConsistentHashUtil.murHash(String.valueOf(sessionHash) + i + failoverCount);
141 | String tag = Math.abs(newHash) + "_" + i + " -> ";
142 | recorder.recordEvent(() -> tag + "parallel create connection: ");
143 | int index = i;
144 | OutboundOperator.connectToOutbound(session, newHash, tag, this,
145 | value -> {
146 | if (value.isSuccess()) {
147 | executor.onReceiveValue(
148 | ValueCallback.Value.success(new FailoverTaskHolder(newHash, index, value.v, tag))
149 | );
150 | } else {
151 | executor.onReceiveValue(value.errorTransfer());
152 | }
153 | }
154 | );
155 | }
156 | }
157 |
158 | private ValueCallback makeChannelFinishedEvent() {
159 | return channelValue -> {
160 | if (!channelValue.isSuccess()) {
161 | // retry
162 | proxyForward(channelValue.e);
163 | return;
164 | }
165 | Channel channel = channelValue.v;
166 |
167 | // 成功之后,和上游ip进行鉴权
168 | ActiveProxyIp activeProxyIp = ActiveProxyIp.getBinding(channel);
169 | List supportProtocolList = activeProxyIp.getIpPool().getRuntimeIpSource().getSupportProtocolList();
170 |
171 | Protocol outboundProtocol = ProtocolManager.chooseUpstreamProtocol(session.getInboundProtocol(),
172 | supportProtocolList);
173 |
174 | if (outboundProtocol == null) {
175 | callback.onHandSharkError(new RuntimeException("no support protocol from upstream"));
176 | channel.close();
177 | return;
178 | }
179 |
180 | AbstractUpstreamHandShaker handShaker = ProtocolManager.createHandShaker(
181 | outboundProtocol, session, channel, activeProxyIp,
182 | value -> {
183 | if (value.isSuccess()) {
184 | recorder.recordEvent(() -> "HandShark success");
185 | callback.onHandSharkFinished(channel, outboundProtocol);
186 |
187 | session.getProxyServer().getProxyCompose().markSessionUse(session, activeProxyIp);
188 | return;
189 | }
190 | channel.close();
191 | if (value.v) {
192 | // 可以被重试,那么走重试逻辑
193 | proxyForward(value.e);
194 | return;
195 | }
196 | callback.onHandSharkError(value.e);
197 | });
198 |
199 | recorder.recordMosaicMsgIfSubscribeRecorder(() -> "begin HandShark with channel: " + channel);
200 | handShaker.doHandShark();
201 | };
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/proxy/switcher/UpstreamHandSharkCallback.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.proxy.switcher;
2 |
3 | import cn.iinti.proxycompose.proxy.outbound.handshark.Protocol;
4 | import io.netty.channel.Channel;
5 |
6 | import javax.annotation.Nullable;
7 |
8 | public interface UpstreamHandSharkCallback {
9 | void onHandSharkFinished(Channel upstreamChannel, @Nullable Protocol outboundProtocol);
10 |
11 | void onHandSharkError(Throwable e);
12 | }
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/resource/DropReason.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.resource;
2 |
3 | public enum DropReason {
4 | /**
5 | * IP入库前连通性检查失败
6 | */
7 | IP_TEST_FAILED,
8 | /**
9 | * 使用期间,发现ip资源服务不可用。
10 | */
11 | IP_SERVER_UNAVAILABLE,
12 | /**
13 | * 使用期间动态判断ip资源质量差
14 | */
15 | IP_QUALITY_BAD,
16 | /**
17 | * IP没有问题,但空转无使用,直到更加新的IP资源入库替换本资源
18 | */
19 | IP_IDLE_POOL_OVERFLOW,
20 | /**
21 | * IP没有问题,但使用时间达到用户配置/指定的存活时间而被自动下线
22 | */
23 | IP_ALIVE_TIME_REACHED,
24 | /**
25 | * 管理员关闭整个IP池资源,此时下线本ip池中所有的IP资源
26 | */
27 | IP_RESOURCE_CLOSE,
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/resource/IpAndPort.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.resource;
2 |
3 | import lombok.Getter;
4 |
5 | @Getter
6 | public class IpAndPort {
7 | private final String ip;
8 | private final Integer port;
9 | private final String ipPort;
10 |
11 |
12 | public IpAndPort(String ip, Integer port) {
13 | this.ip = ip;
14 | this.port = port;
15 | this.ipPort = ip + ":" + port;
16 | }
17 |
18 | @Override
19 | public String toString() {
20 | return ipPort;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/resource/IpResourceParser.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.resource;
2 |
3 | import com.alibaba.fastjson.JSONArray;
4 | import com.alibaba.fastjson.JSONObject;
5 | import com.google.common.collect.Lists;
6 | import org.apache.commons.lang3.StringUtils;
7 |
8 | import java.util.Collections;
9 | import java.util.List;
10 |
11 | public interface IpResourceParser {
12 | List parse(String responseText);
13 |
14 | public static IpResourceParser resolve(String format) {
15 | return "json".equalsIgnoreCase(format) ?
16 | JSONParser.instance : SmartParser.instance;
17 | }
18 |
19 | class JSONParser implements IpResourceParser {
20 |
21 | public static JSONParser instance = new JSONParser();
22 |
23 | @Override
24 | public List parse(String responseText) {
25 | responseText = StringUtils.trimToEmpty(responseText);
26 | if (responseText.startsWith("[")) {
27 | return JSONArray.parseArray(responseText, ProxyIp.class);
28 | }
29 | if (responseText.startsWith("{")) {
30 | return Lists.newArrayList(JSONObject.parseObject(responseText, ProxyIp.class));
31 | }
32 | return Collections.emptyList();
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/resource/ProxyIp.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.resource;
2 |
3 | import cn.iinti.proxycompose.utils.IpUtils;
4 | import lombok.Data;
5 | import org.apache.commons.lang3.StringUtils;
6 |
7 | import javax.annotation.Nonnull;
8 | import javax.annotation.Nullable;
9 | import java.util.Comparator;
10 |
11 | /**
12 | * 一个真实的代理ip资源
13 | *
14 | * @see IpUtils#check(ProxyIp)
15 | */
16 | @Data
17 | public class ProxyIp implements Comparable {
18 |
19 | private String proxyHost;
20 |
21 |
22 | private Integer proxyPort;
23 |
24 |
25 | // 以下为扩展内容,他不是必须的,用以应对各种特殊的ip供应商
26 | // (如芝麻代理,他ip有有效期,但是过期不拒绝,反而在http层返回错误内容,导致框架无法感知)
27 |
28 | /**
29 | * 鉴权用户名称,一般应该使用整个ipsource的鉴权
30 | */
31 | private String userName;
32 | /**
33 | * 鉴权用户密码,一般应该使用整个ipSource的鉴权
34 | */
35 | private String password;
36 |
37 | /**
38 | * 该ip资源下线时间戳,时区使用东八区
39 | */
40 | private Long expireTime;
41 |
42 | /**
43 | * 出口ip:如果您关闭了ip连通性检查,则证明您的ip供应具备非常高的质量,
44 | * 此时您应该主动提供出口ip资源(即在ResourceHandler中手动设置本字段)
45 | * 请注意基于国家/城市的分发、基于经纬度的距离分发两种特性均强依赖出口ip解析。
46 | * 如果最终缺失本字段,则会导致上诉两种算法策略不生效
47 | */
48 | @Nullable
49 | private String outIp;
50 |
51 | /**
52 | * 资源唯一ID,可选项,如扩展为空,则默认填充为-> ip:port
53 | */
54 | private String resourceId;
55 |
56 |
57 | @Override
58 | public int compareTo(@Nonnull ProxyIp o) {
59 | int i = Comparator.comparing(ProxyIp::getProxyHost).compare(this, o);
60 | if (i == 0) {
61 | i = proxyPort.compareTo(o.proxyPort);
62 | }
63 | return i;
64 | }
65 |
66 | private String getIpPort() {
67 | return proxyHost + ":" + proxyPort;
68 | }
69 |
70 | @Override
71 | public String toString() {
72 | return getIpPort();
73 | }
74 |
75 | /**
76 | * this method will be call by framework
77 | */
78 | public ProxyIp resolveId() {
79 | if (StringUtils.isNotBlank(resourceId)) {
80 | return this;
81 | }
82 | resourceId = getIpPort();
83 | return this;
84 | }
85 |
86 | public boolean isValid() {
87 | return StringUtils.isNotBlank(proxyHost) && proxyPort != null
88 | && proxyPort > 0 && proxyPort < 65535;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/resource/SmartParser.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.resource;
2 |
3 | import cn.iinti.proxycompose.utils.IpUtils;
4 | import com.google.common.base.CharMatcher;
5 | import com.google.common.base.Splitter;
6 | import com.google.common.collect.Lists;
7 |
8 | import java.util.List;
9 | import java.util.TreeSet;
10 |
11 | /**
12 | * 通用ip格式解析器,同时支持IpPortPlain和PortSpace
13 | */
14 | public class SmartParser implements IpResourceParser {
15 |
16 | public static SmartParser instance = new SmartParser();
17 |
18 | public static void main(String[] args) {
19 | instance.parse("182.244.169.248:57114\n" +
20 | "113.128.31.3:57114\n" +
21 | "36.102.173.123:57114\n" +
22 | "182.38.126.190:57114")
23 | .forEach(System.out::println);
24 | System.out.println("-------");
25 | instance.parse("haproxy1.dailiyun.com:20000-20200,haproxy2.dailiyun.com:20200-20500")
26 | .forEach(System.out::println);
27 | }
28 |
29 | private static final Splitter smartSplitter = Splitter.on(CharMatcher.anyOf("\n,")).omitEmptyStrings().trimResults();
30 | private static final Splitter portSplitter = Splitter.on(':').omitEmptyStrings().trimResults();
31 |
32 | @Override
33 | public List parse(String responseText) {
34 | TreeSet treeSet = new TreeSet<>();
35 | smartSplitter.split(responseText)
36 | .forEach(pair -> {
37 | List ipAndPortSpace = portSplitter.splitToList(pair);
38 | if (ipAndPortSpace.size() != 2) {
39 | return;
40 | }
41 | String ip = ipAndPortSpace.get(0);
42 | String portSpace = ipAndPortSpace.get(1);
43 | if (portSpace.contains("-")) {
44 | fillSpace(ip, portSpace, treeSet);
45 | } else {
46 | treeSet.add(IpUtils.fromIpPort(ip, Integer.parseInt(portSpace)));
47 | }
48 | });
49 | return Lists.newArrayList(treeSet);
50 | }
51 |
52 | private static void fillSpace(String ip, String portSpace, TreeSet treeSet) {
53 | int index = portSpace.indexOf("-");
54 | String startStr = portSpace.substring(0, index);
55 | String endStr = portSpace.substring(index + 1);
56 | int start = Integer.parseInt(startStr);
57 | int end = Integer.parseInt(endStr);
58 | for (int i = start; i <= end; i++) {
59 | treeSet.add(IpUtils.fromIpPort(ip, i));
60 | }
61 | }
62 |
63 | private SmartParser() {
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/trace/Recorder.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.trace;
2 |
3 |
4 |
5 | import cn.iinti.proxycompose.loop.Looper;
6 | import cn.iinti.proxycompose.trace.impl.SubscribeRecorders;
7 | import cn.iinti.proxycompose.trace.utils.StringSplitter;
8 | import cn.iinti.proxycompose.trace.utils.ThrowablePrinter;
9 |
10 | import java.util.Collection;
11 | import java.util.LinkedList;
12 | import java.util.function.Function;
13 |
14 | public abstract class Recorder {
15 | protected static final Looper thread = new Looper("eventRecorder").startLoop();
16 |
17 | public static Looper workThread() {
18 | return thread;
19 | }
20 |
21 |
22 | public void recordEvent(String message) {
23 | recordEvent(() -> message, (Throwable) null);
24 | }
25 |
26 | public void recordEvent(String message, Throwable throwable) {
27 | recordEvent(() -> message, throwable);
28 | }
29 |
30 | public void recordEvent(MessageGetter messageGetter) {
31 | recordEvent(messageGetter, (Throwable) null);
32 | }
33 |
34 | public void recordEvent(T t, Function fuc) {
35 | recordEvent(() -> fuc.apply(t));
36 | }
37 |
38 | public abstract void recordEvent(MessageGetter messageGetter, Throwable throwable);
39 |
40 | public void recordMosaicMsgIfSubscribeRecorder(MessageGetter message) {
41 | if (this instanceof SubscribeRecorders.SubscribeRecorder) {
42 | SubscribeRecorders.SubscribeRecorder s = (SubscribeRecorders.SubscribeRecorder) this;
43 | s.recordMosaicMsg(message);
44 | } else {
45 | recordEvent(message);
46 | }
47 | }
48 |
49 | protected Collection splitMsg(String msg, Throwable throwable) {
50 | Collection strings = StringSplitter.split(msg, '\n');
51 | if (throwable == null) {
52 | return strings;
53 | }
54 | if (strings.isEmpty()) {
55 | // 确保可以被编辑
56 | strings = new LinkedList<>();
57 | }
58 | ThrowablePrinter.printStackTrace(strings, throwable);
59 | return strings;
60 | }
61 |
62 | public interface MessageGetter {
63 | String getMessage();
64 | }
65 |
66 | public static final Recorder nop = new Recorder() {
67 | @Override
68 | public void recordEvent(MessageGetter messageGetter, Throwable throwable) {
69 |
70 | }
71 | };
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/trace/impl/DiskRecorders.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.trace.impl;
2 |
3 | import cn.iinti.proxycompose.trace.Recorder;
4 | import lombok.Getter;
5 | import org.slf4j.Logger;
6 | import org.slf4j.LoggerFactory;
7 | import org.slf4j.MDC;
8 |
9 | import java.util.Collection;
10 | import java.util.Collections;
11 | import java.util.LinkedList;
12 | import java.util.List;
13 | import java.util.function.Consumer;
14 |
15 | public class DiskRecorders {
16 |
17 | public static DiskRecorders USER_SESSION = new DiskRecorders("user", false);
18 | /**
19 | * 给debug调试使用,传递本参数可以打印全流程日志
20 | */
21 | public static DiskRecorders USER_DEBUG_TRACE = new DiskRecorders("user_trace", true);
22 | public static DiskRecorders IP_SOURCE = new DiskRecorders("ip_source", true);
23 | public static DiskRecorders IP_TEST = new DiskRecorders("ip_test", false);
24 | public static DiskRecorders MOCK_SESSION = new DiskRecorders("mock_proxy", false);
25 | public static DiskRecorders PORTAL = new DiskRecorders("portal", false);
26 | public static DiskRecorders OTHER = new DiskRecorders("other", false);
27 |
28 | private static final Logger log = LoggerFactory.getLogger("EventTrace");
29 |
30 | @Getter
31 | private final String tag;
32 | @Getter
33 | private final boolean all;
34 | private final WheelSlotFilter wheelSlotFilter;
35 |
36 |
37 | public DiskRecorders(String tag, boolean all) {
38 | this.tag = tag;
39 | this.all = all;
40 | this.wheelSlotFilter = new WheelSlotFilter(all);
41 | }
42 |
43 | public DiskRecorder acquireRecorder(String sessionId, boolean debug) {
44 | return this.wheelSlotFilter.acquireRecorder(debug) ?
45 | new DiskRecorderImpl(sessionId) : nopDiskRecorder;
46 | }
47 |
48 | public static abstract class DiskRecorder extends Recorder {
49 | abstract void takeHistory(Consumer> consumer);
50 |
51 | abstract void recordBatchEvent(Collection msgLines);
52 |
53 | abstract boolean enable();
54 | }
55 |
56 | private class DiskRecorderImpl extends DiskRecorder {
57 | private final String sessionId;
58 |
59 | private LinkedList historyMsg = new LinkedList<>();
60 | private static final int MAX_HISTORY = 100;
61 |
62 | private DiskRecorderImpl(String sessionId) {
63 | this.sessionId = sessionId;
64 | }
65 |
66 | @Override
67 | void recordBatchEvent(Collection msgLines) {
68 | thread.execute(() -> {
69 | MDC.put("Scene", tag);
70 | historyMsg.addAll(msgLines);
71 | int overflow = historyMsg.size() - MAX_HISTORY;
72 | for (int i = 0; i < overflow; i++) {
73 | historyMsg.removeFirst();
74 | }
75 | msgLines.forEach(s -> log.info("sessionId:" + sessionId + " -> " + s));
76 | });
77 | }
78 |
79 | @Override
80 | public void recordEvent(MessageGetter messageGetter, Throwable throwable) {
81 | recordBatchEvent(splitMsg(messageGetter.getMessage(), throwable));
82 | }
83 |
84 | public void takeHistory(Consumer> consumer) {
85 | thread.execute(() -> {
86 | LinkedList historySnapshot = historyMsg;
87 | historyMsg = new LinkedList<>();
88 | consumer.accept(historySnapshot);
89 | });
90 | }
91 |
92 | boolean enable() {
93 | return true;
94 | }
95 | }
96 |
97 |
98 | private final static DiskRecorder nopDiskRecorder = new DiskRecorder() {
99 | public void takeHistory(Consumer> consumer) {
100 | consumer.accept(Collections.emptyList());
101 | }
102 |
103 | @Override
104 | public void recordEvent(MessageGetter messageGetter, Throwable throwable) {
105 |
106 | }
107 |
108 | void recordBatchEvent(Collection msgLines) {
109 |
110 | }
111 |
112 | boolean enable() {
113 | return false;
114 | }
115 | };
116 | }
117 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/trace/impl/SubscribeRecorders.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.trace.impl;
2 |
3 | import cn.iinti.proxycompose.trace.Recorder;
4 | import com.google.common.collect.HashMultimap;
5 | import com.google.common.collect.Lists;
6 | import com.google.common.collect.Maps;
7 | import com.google.common.collect.Multimap;
8 | import lombok.Getter;
9 |
10 | import java.util.Collection;
11 | import java.util.List;
12 | import java.util.Map;
13 | import java.util.UUID;
14 | import java.util.function.Consumer;
15 |
16 | public class SubscribeRecorders {
17 | private static final Map recorderCategories = Maps.newHashMap();
18 |
19 | public static SubscribeRecorders fromTag( String tag) {
20 | return recorderCategories.get(tag.toLowerCase());
21 | }
22 |
23 | public static SubscribeRecorders USER_SESSION = new SubscribeRecorders(DiskRecorders.USER_SESSION, false);
24 | public static SubscribeRecorders USER_DEBUG_TRACE = new SubscribeRecorders(DiskRecorders.USER_DEBUG_TRACE, true);
25 | public static SubscribeRecorders IP_SOURCE = new SubscribeRecorders(DiskRecorders.IP_SOURCE, true);
26 | public static SubscribeRecorders IP_TEST = new SubscribeRecorders(DiskRecorders.IP_TEST, true);
27 | public static SubscribeRecorders MOCK_SESSION = new SubscribeRecorders(DiskRecorders.MOCK_SESSION, true);
28 | public static SubscribeRecorders PORTAL = new SubscribeRecorders(DiskRecorders.PORTAL, true);
29 | public static SubscribeRecorders OTHER = new SubscribeRecorders(DiskRecorders.OTHER, false);
30 |
31 |
32 | private final Multimap listenerRegistry = HashMultimap.create();
33 | private final Map scopeSlotFilters = Maps.newHashMap();
34 | private final DiskRecorders lowLevel;
35 |
36 | @Getter
37 | private final String tag;
38 |
39 |
40 | private final boolean lowLevelAll;
41 |
42 | @Getter
43 | private final boolean forAdmin;
44 |
45 | public SubscribeRecorders(DiskRecorders lowLevel, boolean forAdmin) {
46 | this.lowLevel = lowLevel;
47 | this.forAdmin = forAdmin;
48 | this.tag = lowLevel.getTag().toLowerCase();
49 | this.lowLevelAll = lowLevel.isAll();
50 | recorderCategories.put(tag, this);
51 | }
52 |
53 | public void registerListener(String scope, Listener listener) {
54 | Recorder.workThread().execute(() -> listenerRegistry.put(scope, listener));
55 | }
56 |
57 | public void unregisterListener(String scope, Listener listener) {
58 | Recorder.workThread().execute(() -> listenerRegistry.remove(scope, listener));
59 | }
60 |
61 | public SubscribeRecorder acquireRecorder(boolean debug) {
62 | return acquireRecorder(debug, "default");
63 | }
64 |
65 | public SubscribeRecorder acquireRecorder(boolean debug, String scope) {
66 | return acquireRecorder(UUID.randomUUID().toString(), debug, scope);
67 | }
68 |
69 | public SubscribeRecorder acquireRecorder(String sessionId, boolean debug, String scope) {
70 | return new SubscribeRecorder(sessionId, lowLevel.acquireRecorder(sessionId, debug), scope);
71 | }
72 |
73 | public class SubscribeRecorder extends Recorder {
74 |
75 | private final DiskRecorders.DiskRecorder lowLevel;
76 | private String[] scopes;
77 | private boolean subscribeTicket;
78 | private final String sessionId;
79 |
80 |
81 | public SubscribeRecorder(String sessionId, DiskRecorders.DiskRecorder lowLevel, String scope) {
82 | this.lowLevel = lowLevel;
83 | this.sessionId = sessionId;
84 | this.scopes = new String[]{scope};
85 | refreshSlotTick();
86 | }
87 |
88 | private void refreshSlotTick() {
89 | if (lowLevelAll) {
90 | subscribeTicket = true;
91 | return;
92 | }
93 | Recorder.workThread().execute(() -> {
94 | boolean hint = false;
95 | for (String scope : scopes) {
96 | boolean ticket = scopeSlotFilters
97 | .computeIfAbsent(scope, (k) -> new WheelSlotFilter(false))
98 | .acquireRecorder(false);
99 | if (ticket) {
100 | hint = true;
101 | }
102 | }
103 | subscribeTicket = hint;
104 | });
105 |
106 | }
107 |
108 | public void takeHistory(Consumer> consumer) {
109 | lowLevel.takeHistory(consumer);
110 | }
111 |
112 | public void recordBatchEvent(Collection msgLines) {
113 | lowLevel.recordBatchEvent(msgLines);
114 | }
115 |
116 | @Override
117 | public void recordEvent(MessageGetter messageGetter, Throwable throwable) {
118 | List targetListenerList = Lists.newArrayList();
119 | for (String scope : scopes) {
120 | Collection listeners = listenerRegistry.get(scope);
121 | targetListenerList.addAll(listeners);
122 | }
123 |
124 | if (!lowLevel.enable() && (targetListenerList.isEmpty() || !subscribeTicket)) {
125 | // this indicate no log print need
126 | return;
127 | }
128 |
129 | thread.execute(() -> {
130 | Collection msgLines = splitMsg(messageGetter.getMessage(), throwable);
131 | lowLevel.recordBatchEvent(msgLines);
132 |
133 | if (subscribeTicket) {
134 | targetListenerList.forEach(listener -> msgLines.forEach(s -> listener.onLogMsg(sessionId, s)));
135 | }
136 | });
137 | }
138 |
139 | public void changeScope(String... scope) {
140 | this.scopes = scope;
141 | refreshSlotTick();
142 | }
143 |
144 |
145 | public void recordMosaicMsg(MessageGetter message) {
146 | recordEvent(() -> NOT_SHOW_FOR_NORMAL_USER + message.getMessage());
147 | }
148 |
149 |
150 | }
151 |
152 | private static final String NOT_SHOW_FOR_NORMAL_USER = "nsfnu:";
153 |
154 | public static boolean isMosaicMsg(String msg) {
155 | return msg.startsWith(NOT_SHOW_FOR_NORMAL_USER);
156 | }
157 |
158 | public interface Listener {
159 | void onLogMsg(String sessionId, String line);
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/trace/impl/WheelSlotFilter.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.trace.impl;
2 |
3 | import java.util.ArrayList;
4 | import java.util.concurrent.atomic.AtomicLong;
5 |
6 | public class WheelSlotFilter {
7 |
8 | private final ArrayList slots;
9 | private final boolean all;
10 |
11 | public WheelSlotFilter(boolean all) {
12 | this.all = all;
13 | this.slots = new ArrayList<>(30);
14 | for (int i = 0; i < 30; i++) {
15 | this.slots.add(new AtomicLong());
16 | }
17 | }
18 |
19 | public boolean acquireRecorder(boolean debug) {
20 | if (all || debug) {
21 | return true;
22 | }
23 | long nowTime = System.currentTimeMillis();
24 | int slotIndex = ((int) ((nowTime / 1000) % 60)) / 2;
25 | long timeMinute = nowTime / 60000;
26 |
27 | AtomicLong slot = slots.get(slotIndex);
28 |
29 | long slotTime = slot.get();
30 | if (slotTime == timeMinute) {
31 | return false;
32 | }
33 | return slot.compareAndSet(slotTime, timeMinute);
34 | }
35 |
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/trace/utils/ThrowablePrinter.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.trace.utils;
2 |
3 | import java.util.Collection;
4 | import java.util.Collections;
5 | import java.util.IdentityHashMap;
6 | import java.util.Set;
7 |
8 | public class ThrowablePrinter {
9 | /**
10 | * Caption for labeling causative exception stack traces
11 | */
12 | private static final String CAUSE_CAPTION = "Caused by: ";
13 |
14 | /**
15 | * Caption for labeling suppressed exception stack traces
16 | */
17 | private static final String SUPPRESSED_CAPTION = "Suppressed: ";
18 |
19 |
20 |
21 | public static void printStackTrace(Collection out, Throwable throwable) {
22 | // Guard against malicious overrides of Throwable.equals by
23 | // using a Set with identity equality semantics.
24 | Set dejaVu =
25 | Collections.newSetFromMap(new IdentityHashMap());
26 | dejaVu.add(throwable);
27 |
28 |
29 | // Print our stack trace
30 | //s.println(this);
31 | out.add(throwable.toString());
32 | StackTraceElement[] trace = throwable.getStackTrace();
33 | for (StackTraceElement traceElement : trace)
34 | out.add("\tat " + traceElement);
35 |
36 | // Print suppressed exceptions, if any
37 | for (Throwable se : throwable.getSuppressed())
38 | printEnclosedStackTrace(out, se, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
39 |
40 | // Print cause, if any
41 | Throwable ourCause = throwable.getCause();
42 | if (ourCause != null)
43 | printEnclosedStackTrace(out, ourCause, trace, CAUSE_CAPTION, "", dejaVu);
44 |
45 | }
46 |
47 | /**
48 | * Print our stack trace as an enclosed exception for the specified
49 | * stack trace.
50 | */
51 | private static void printEnclosedStackTrace(Collection out, Throwable throwable,
52 | StackTraceElement[] enclosingTrace,
53 | String caption,
54 | String prefix,
55 | Set dejaVu) {
56 |
57 | if (dejaVu.contains(throwable)) {
58 | out.add("\t[CIRCULAR REFERENCE:" + throwable + "]");
59 | } else {
60 | dejaVu.add(throwable);
61 | // Compute number of frames in common between this and enclosing trace
62 | StackTraceElement[] trace = throwable.getStackTrace();
63 | int m = trace.length - 1;
64 | int n = enclosingTrace.length - 1;
65 | while (m >= 0 && n >= 0 && trace[m].equals(enclosingTrace[n])) {
66 | m--;
67 | n--;
68 | }
69 | int framesInCommon = trace.length - 1 - m;
70 |
71 | // Print our stack trace
72 | out.add(prefix + caption + throwable);
73 | for (int i = 0; i <= m; i++)
74 | out.add(prefix + "\tat " + trace[i]);
75 | if (framesInCommon != 0)
76 | out.add(prefix + "\t... " + framesInCommon + " more");
77 |
78 | // Print suppressed exceptions, if any
79 | for (Throwable se : throwable.getSuppressed())
80 | printEnclosedStackTrace(out, se, trace, SUPPRESSED_CAPTION,
81 | prefix + "\t", dejaVu);
82 |
83 | // Print cause, if any
84 | Throwable ourCause = throwable.getCause();
85 | if (ourCause != null)
86 | printEnclosedStackTrace(out, ourCause, trace, CAUSE_CAPTION, prefix, dejaVu);
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/utils/AsyncHttpInvoker.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.utils;
2 |
3 | import cn.iinti.proxycompose.loop.ValueCallback;
4 | import cn.iinti.proxycompose.trace.Recorder;
5 | import org.asynchttpclient.AsyncHttpClient;
6 | import org.asynchttpclient.BoundRequestBuilder;
7 | import org.asynchttpclient.DefaultAsyncHttpClientConfig;
8 | import org.asynchttpclient.proxy.ProxyServer;
9 |
10 | import java.nio.charset.StandardCharsets;
11 |
12 | import static org.asynchttpclient.Dsl.asyncHttpClient;
13 |
14 | /**
15 | * 请注意,为了避免日志刷屏,本模块强制要求使用recorder记录日志
16 | * 另外由于他是异步的,也要求使用recorder
17 | */
18 | public class AsyncHttpInvoker {
19 | public static final AsyncHttpClient httpclient = asyncHttpClient(
20 | new DefaultAsyncHttpClientConfig.Builder()
21 | .setKeepAlive(true)
22 | .setConnectTimeout(10000)
23 | .setReadTimeout(8000)
24 | .setPooledConnectionIdleTimeout(20000)
25 | .setEventLoopGroup(NettyThreadPools.asyncHttpWorkGroup)
26 | .build());
27 |
28 |
29 |
30 | public static void get(String url, Recorder recorder, ValueCallback callback) {
31 | get(url, recorder, null, callback);
32 | }
33 |
34 | public static void get(String url, Recorder recorder, ProxyServer.Builder proxyBuilder, ValueCallback callback) {
35 | recorder.recordEvent(() -> "begin async invoker: " + url);
36 | BoundRequestBuilder getRequest = httpclient.prepareGet(url);
37 | if (proxyBuilder != null) {
38 | getRequest.setProxyServer(proxyBuilder);
39 | }
40 |
41 | execute(getRequest, recorder, callback);
42 | }
43 |
44 | private static void execute(BoundRequestBuilder requestBuilder, Recorder recorder, ValueCallback callback) {
45 | requestBuilder.execute().toCompletableFuture()
46 | .whenCompleteAsync((response, throwable) -> {
47 | if (throwable != null) {
48 | ValueCallback.failed(callback, throwable);
49 | return;
50 | }
51 | try {
52 | String responseBody = response.getResponseBody(StandardCharsets.UTF_8).trim();
53 | recorder.recordEvent(() -> "async response:" + responseBody);
54 | ValueCallback.success(callback, responseBody);
55 | } catch (Exception e) {
56 | recorder.recordEvent(() -> "read response failed", e);
57 | ValueCallback.failed(callback, e);
58 | }
59 | });
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/utils/ConsistentHashUtil.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.utils;
2 |
3 |
4 | import com.google.common.hash.Hashing;
5 |
6 | import java.nio.charset.StandardCharsets;
7 | import java.util.Iterator;
8 | import java.util.Map;
9 | import java.util.SortedMap;
10 | import java.util.TreeMap;
11 |
12 | public class ConsistentHashUtil {
13 | public static long murHash(String key) {
14 | return Hashing.goodFastHash(128)
15 | .hashString(key, StandardCharsets.UTF_8).asLong();
16 | }
17 |
18 | public static T fetchConsistentRing(TreeMap treeMap, K key) {
19 | if (treeMap.isEmpty()) {
20 | return null;
21 | }
22 | SortedMap tailMap = treeMap.tailMap(key);
23 | if (tailMap.isEmpty()) {
24 | return treeMap.values().iterator().next();
25 | }
26 | return tailMap.values().iterator().next();
27 | }
28 |
29 | public static Iterable constantRingIt(TreeMap treeMap, K key) {
30 | Iterator> tailIterator = treeMap.tailMap(key).entrySet().iterator();
31 | Iterator> headIterator = treeMap.entrySet().iterator();
32 | return () -> new Iterator() {
33 | private T value;
34 |
35 | @Override
36 | public boolean hasNext() {
37 | if (tailIterator.hasNext()) {
38 | value = tailIterator.next().getValue();
39 | return true;
40 | }
41 | if (headIterator.hasNext()) {
42 | Map.Entry next = headIterator.next();
43 | K nextKey = next.getKey();
44 | if (treeMap.comparator().compare(nextKey, key) >= 0) {
45 | value = null;
46 | return false;
47 | }
48 | value = next.getValue();
49 | return true;
50 |
51 | }
52 | return false;
53 | }
54 |
55 | @Override
56 | public T next() {
57 | return value;
58 | }
59 | };
60 |
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/utils/IniConfig.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.utils;
2 |
3 | import com.alibaba.fastjson.parser.ParserConfig;
4 | import com.alibaba.fastjson.util.TypeUtils;
5 | import lombok.SneakyThrows;
6 | import org.apache.commons.lang3.StringUtils;
7 | import org.ini4j.ConfigParser;
8 |
9 | import java.lang.reflect.ParameterizedType;
10 | import java.lang.reflect.Type;
11 | import java.util.function.Consumer;
12 |
13 | public abstract class IniConfig {
14 | private final ConfigParser config;
15 | protected final String section;
16 |
17 | public IniConfig(ConfigParser config, String section) {
18 | this.config = config;
19 | this.section = section;
20 | }
21 |
22 | @SneakyThrows
23 | protected void acceptConfig(String key, Consumer consumer) {
24 | if (config.hasOption(section, key)) {
25 | consumer.accept(config.get(section, key));
26 | }
27 | }
28 |
29 | public abstract class ConfigValue {
30 | public final V value;
31 | public final String key;
32 |
33 | public ConfigValue(String key, V defaultValue) {
34 | this.key = key;
35 | this.value = calcValue(key, defaultValue);
36 | }
37 |
38 | @SneakyThrows
39 | private V calcValue(String key, V defaultValue) {
40 | key = key.toLowerCase();
41 | Class superClassGenericType = getSuperClassGenericType(getClass());
42 | if (config.hasOption(section, key)) {
43 | String config = IniConfig.this.config.get(section, key);
44 | if (StringUtils.isNotBlank(config)) {
45 | return TypeUtils.cast(config, superClassGenericType, ParserConfig.getGlobalInstance());
46 | }
47 | }
48 | return defaultValue;
49 | }
50 |
51 | @SuppressWarnings("unchecked")
52 | private Class getSuperClassGenericType(Class> clazz) {
53 | Type genType = clazz.getGenericSuperclass();
54 | if (!(genType instanceof ParameterizedType)) {
55 | return (Class) Object.class;
56 | }
57 | Type[] params = ((ParameterizedType) genType).getActualTypeArguments();
58 | if (0 == params.length) {
59 | return (Class) Object.class;
60 | } else if (!(params[0] instanceof Class)) {
61 | return (Class) Object.class;
62 | } else {
63 | return (Class) params[0];
64 | }
65 | }
66 | }
67 |
68 |
69 | public class StringConfigValue extends ConfigValue {
70 | public StringConfigValue(String configKey, String defaultValue) {
71 | super(configKey, defaultValue);
72 | }
73 | }
74 |
75 | public class BooleanConfigValue extends ConfigValue {
76 | public BooleanConfigValue(String configKey, Boolean defaultValue) {
77 | super(configKey, defaultValue);
78 | }
79 | }
80 |
81 | public class IntegerConfigValue extends ConfigValue {
82 | public IntegerConfigValue(String configKey, Integer defaultValue) {
83 | super(configKey, defaultValue);
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/utils/IpUtils.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.utils;
2 |
3 | import cn.iinti.proxycompose.resource.ProxyIp;
4 | import com.google.common.base.Splitter;
5 | import org.apache.commons.lang3.StringUtils;
6 | import org.apache.commons.lang3.math.NumberUtils;
7 |
8 | import java.net.*;
9 | import java.util.Enumeration;
10 | import java.util.List;
11 |
12 | public class IpUtils {
13 |
14 | public static ProxyIp fromIpPort(String ip, int port) {
15 | ProxyIp proxyIp = new ProxyIp();
16 | proxyIp.setProxyHost(ip);
17 | proxyIp.setProxyPort(port);
18 | return proxyIp;
19 | }
20 |
21 |
22 | public static String check(ProxyIp proxyIp) {
23 | String ip = proxyIp.getProxyHost();
24 | if (StringUtils.isBlank(ip)) {
25 | return "ip can not empty";
26 | }
27 | ip = ip.trim();
28 | proxyIp.setProxyHost(ip);
29 | if (!isIpV4(ip)) {
30 | try {
31 | InetAddress byName = InetAddress.getByName(ip);
32 | } catch (UnknownHostException e) {
33 | return "error domain:" + e.getMessage();
34 | }
35 | }
36 |
37 | Integer port = proxyIp.getProxyPort();
38 | if (port == null || port <= 0 || port > 65535) {
39 | return "port range error";
40 | }
41 |
42 | return null;
43 | }
44 |
45 | private static final Splitter dotSplitter = Splitter.on('.');
46 |
47 | public static boolean isIpV4(String input) {
48 | if (StringUtils.isBlank(input)) {
49 | return false;
50 | }
51 | // 3 * 4 + 3 = 15
52 | // 1 * 4 + 3 = 7
53 | if (input.length() > 15 || input.length() < 7) {
54 | return false;
55 | }
56 |
57 | List split = dotSplitter.splitToList(input);
58 | if (split.size() != 4) {
59 | return false;
60 | }
61 | for (String segment : split) {
62 | int i = NumberUtils.toInt(segment, -1);
63 | if (i < 0 || i > 255) {
64 | return false;
65 | }
66 | }
67 | return true;
68 | }
69 |
70 | public static boolean isValidIp(String ip) {
71 | return StringUtils.isNotBlank(ip) && isIpV4(ip);
72 | }
73 |
74 |
75 | public static String fetchIp(String type) throws SocketException {
76 | Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces();
77 | while (networkInterfaces.hasMoreElements()) {
78 | NetworkInterface networkInterface = networkInterfaces.nextElement();
79 | if (networkInterface.isLoopback()) {
80 | continue;
81 | }
82 |
83 | Enumeration inetAddresses = networkInterface.getInetAddresses();
84 | while (inetAddresses.hasMoreElements()) {
85 | InetAddress inetAddress = inetAddresses.nextElement();
86 | if (inetAddress instanceof Inet6Address) {
87 | continue;
88 | }
89 | Inet4Address inet4Address = (Inet4Address) inetAddress;
90 | byte[] address = inet4Address.getAddress();
91 | if (address.length != 4) {
92 | continue;
93 | }
94 | int firstByte = address[0] & 0xFF;
95 | boolean isPrivate = (firstByte == 192 || firstByte == 10 || firstByte == 172);
96 | if (type.equals("private")) {
97 | if (isPrivate) {
98 | return inet4Address.getHostAddress();
99 | }
100 | } else {
101 | if (!isPrivate) {
102 | return inet4Address.getHostAddress();
103 | }
104 | }
105 | }
106 | }
107 | return null;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/utils/NettyThreadPools.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.utils;
2 |
3 | import io.netty.channel.nio.NioEventLoopGroup;
4 | import io.netty.util.concurrent.DefaultThreadFactory;
5 |
6 | public class NettyThreadPools {
7 |
8 | public static final NioEventLoopGroup outboundGroup = newDefaultEventLoop("outbound");
9 |
10 | public static final NioEventLoopGroup proxyServerBossGroup = newDefaultEventLoop("Proxy-boss-group");
11 | public static final NioEventLoopGroup proxyServerWorkerGroup = newDefaultEventLoop("Proxy-worker-group");
12 |
13 | // 下载代理,通知外部等业务http调用
14 | public static final NioEventLoopGroup asyncHttpWorkGroup =
15 | new NioEventLoopGroup(1, createThreadFactory("async-http-invoker"));
16 | //newDefaultEventLoop("async-http-invoker");
17 |
18 |
19 | private static NioEventLoopGroup newDefaultEventLoop(String name) {
20 | return new NioEventLoopGroup(0, createThreadFactory(name));
21 | }
22 |
23 | private static DefaultThreadFactory createThreadFactory(String name) {
24 | return new DefaultThreadFactory(name + "-" + DefaultThreadFactory.toPoolName(NioEventLoopGroup.class));
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/utils/NettyUtil.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.utils;
2 |
3 | import cn.iinti.proxycompose.resource.IpAndPort;
4 | import io.netty.buffer.ByteBuf;
5 | import io.netty.buffer.Unpooled;
6 | import io.netty.channel.Channel;
7 | import io.netty.channel.ChannelFuture;
8 | import io.netty.channel.ChannelFutureListener;
9 | import io.netty.channel.ChannelHandlerContext;
10 | import io.netty.handler.codec.http.*;
11 | import org.apache.commons.lang3.math.NumberUtils;
12 |
13 | import java.nio.charset.StandardCharsets;
14 | import java.util.ArrayList;
15 | import java.util.Collection;
16 | import java.util.Collections;
17 | import java.util.List;
18 |
19 | import static io.netty.buffer.ByteBufUtil.appendPrettyHexDump;
20 | import static io.netty.util.internal.StringUtil.NEWLINE;
21 |
22 | public class NettyUtil {
23 |
24 | public static String throwableMsg(Throwable throwable) {
25 | String msg = throwable.getClass().getName() + ":" + throwable.getMessage();
26 | StackTraceElement[] stackTrace = throwable.getStackTrace();
27 | if (stackTrace.length > 0) {
28 | msg += "\n" + stackTrace[0].toString();
29 | }
30 | return msg;
31 | }
32 |
33 | public static void loveOther(Channel girl, Channel boy) {
34 | girl.closeFuture().addListener((ChannelFutureListener) future -> boy.close());
35 | boy.closeFuture().addListener((ChannelFutureListener) future -> girl.close());
36 | }
37 |
38 |
39 | /**
40 | * Closes the specified channel after all queued write requests are flushed.
41 | */
42 | public static void closeOnFlush(Channel channel) {
43 | if (channel.isActive()) {
44 | channel.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
45 | }
46 | }
47 |
48 | public static void closeIfActive(Channel channel) {
49 | if (channel == null) {
50 | return;
51 | }
52 | channel.close();
53 | }
54 |
55 | public static void closeAll(Collection channels) {
56 | if (channels == null) {
57 | return;
58 | }
59 | for (Channel channel : channels) {
60 | closeIfActive(channel);
61 | }
62 | }
63 |
64 | public static void httpResponseText(Channel httpChannel, HttpResponseStatus status, String body) {
65 | DefaultFullHttpResponse response;
66 | if (body != null) {
67 | byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
68 | ByteBuf content = Unpooled.copiedBuffer(bytes);
69 | response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, content);
70 | response.headers().set(HttpHeaders.Names.CONTENT_LENGTH, bytes.length);
71 | response.headers().set(HttpHeaders.Names.CONTENT_TYPE, "text/html; charset=utf-8");
72 | } else {
73 | response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status);
74 | }
75 | HttpUtil.setKeepAlive(response, false);
76 | httpChannel.writeAndFlush(response)
77 | .addListener(ChannelFutureListener.CLOSE);
78 | }
79 |
80 | public static void addVia(HttpMessage httpMessage, String alias) {
81 | String newViaHeader = String.valueOf(httpMessage.protocolVersion().majorVersion()) +
82 | '.' +
83 | httpMessage.protocolVersion().minorVersion() +
84 | ' ' +
85 | alias;
86 |
87 | List vias;
88 | if (httpMessage.headers().contains(HttpHeaderNames.VIA)) {
89 | List existingViaHeaders = httpMessage.headers().getAll(HttpHeaderNames.VIA);
90 | vias = new ArrayList<>(existingViaHeaders);
91 | vias.add(newViaHeader);
92 | } else {
93 | vias = Collections.singletonList(newViaHeader);
94 | }
95 |
96 | httpMessage.headers().set(HttpHeaderNames.VIA, vias);
97 | }
98 |
99 | public static void sendHttpResponse(ChannelHandlerContext ctx, HttpRequest req, DefaultFullHttpResponse res) {
100 | res.headers().set(HttpHeaderNames.CONTENT_LENGTH, res.content().readableBytes());
101 | ChannelFuture f = ctx.channel().writeAndFlush(res);
102 | if (!HttpUtil.isKeepAlive(req) || res.status().code() != 200) {
103 | f.addListener(ChannelFutureListener.CLOSE);
104 | }
105 | }
106 |
107 |
108 |
109 | /**
110 | * 打印ByteBuf的好工具
111 | */
112 | public static String formatByteBuf(ChannelHandlerContext ctx, String eventName, ByteBuf msg) {
113 | String chStr = ctx == null ? "debug" : ctx.channel().toString();
114 | if (msg == null) {
115 | msg = Unpooled.EMPTY_BUFFER;
116 | }
117 | int length = msg.readableBytes();
118 | if (length == 0) {
119 | return chStr + ' ' + eventName + ": 0B";
120 | } else {
121 | int outputLength = chStr.length() + 1 + eventName.length() + 2 + 10 + 1;
122 |
123 | int rows = length / 16 + (length % 15 == 0 ? 0 : 1) + 4;
124 | int hexDumpLength = 2 + rows * 80;
125 | outputLength += hexDumpLength;
126 |
127 | StringBuilder buf = new StringBuilder(outputLength);
128 | buf.append(chStr).append(' ').append(eventName).append(": ").append(length).append('B');
129 |
130 | buf.append(NEWLINE);
131 | appendPrettyHexDump(buf, msg);
132 | return buf.toString();
133 | }
134 | }
135 |
136 |
137 | public static IpAndPort parseProxyTarget(HttpRequest httpRequest,boolean isHttps) {
138 | String host = httpRequest.headers().get(HttpHeaderNames.HOST);
139 | if (host == null && isHttps) {
140 | host = httpRequest.uri();
141 | }
142 |
143 | if (host == null) {
144 | return null;
145 | }
146 |
147 | if (!host.contains(":")) {
148 | host += isHttps ? ":443" : ":80";
149 | }
150 |
151 | String[] split = host.split(":");
152 |
153 | int port = NumberUtils.toInt(split[1].trim(), -1);
154 | boolean illegal = port > 0 && port <= 65535;
155 |
156 | if (illegal) {
157 | return new IpAndPort(split[0], port);
158 | }
159 | return null;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/utils/PortSpaceParser.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.utils;
2 |
3 | import com.google.common.base.Splitter;
4 | import com.google.common.collect.Maps;
5 |
6 | import java.util.Map;
7 | import java.util.TreeSet;
8 |
9 | public class PortSpaceParser {
10 | private static final Map> cache = Maps.newConcurrentMap();
11 |
12 | public static TreeSet parsePortSpace(String config) {
13 | TreeSet treeSet = cache.get(config);
14 | if (treeSet != null) {
15 | return new TreeSet<>(treeSet);
16 | }
17 | treeSet = parsePortSpaceImpl(config);
18 | cache.put(config, treeSet);
19 | return new TreeSet<>(treeSet);
20 | }
21 |
22 | public static TreeSet parsePortSpaceImpl(String config) {
23 | TreeSet copyOnWriteTreeSet = new TreeSet<>();
24 | Iterable pairs = Splitter.on(":").split(config);
25 | for (String pair : pairs) {
26 | if (pair.contains("-")) {
27 | int index = pair.indexOf("-");
28 | String startStr = pair.substring(0, index);
29 | String endStr = pair.substring(index + 1);
30 | int start = Integer.parseInt(startStr);
31 | int end = Integer.parseInt(endStr);
32 | for (int i = start; i <= end; i++) {
33 | copyOnWriteTreeSet.add(i);
34 | }
35 | } else {
36 | copyOnWriteTreeSet.add(Integer.parseInt(pair));
37 | }
38 | }
39 | return copyOnWriteTreeSet;
40 |
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/cn/iinti/proxycompose/utils/ResourceUtil.java:
--------------------------------------------------------------------------------
1 | package cn.iinti.proxycompose.utils;
2 |
3 | import org.apache.commons.io.IOUtils;
4 |
5 | import java.io.IOException;
6 | import java.io.InputStream;
7 | import java.nio.charset.StandardCharsets;
8 | import java.util.List;
9 |
10 | public class ResourceUtil {
11 | public static List readLines(String resourceName) {
12 | try (InputStream inputStream = openResource(resourceName)) {
13 | return IOUtils.readLines(inputStream, StandardCharsets.UTF_8);
14 | } catch (IOException e) {
15 | throw new RuntimeException(e);
16 | }
17 | }
18 |
19 | public static String readText(String resourceName) {
20 | try (InputStream inputStream = openResource(resourceName)) {
21 | return IOUtils.toString(inputStream, StandardCharsets.UTF_8);
22 | } catch (IOException e) {
23 | throw new RuntimeException(e);
24 | }
25 | }
26 |
27 | public static byte[] readBytes(String resourceName) {
28 | try (InputStream inputStream = openResource(resourceName)) {
29 | return IOUtils.toByteArray(inputStream);
30 | } catch (IOException e) {
31 | throw new RuntimeException(e);
32 | }
33 | }
34 |
35 | public static InputStream openResource(String name) {
36 | InputStream resource = ResourceUtil.class.getClassLoader()
37 | .getResourceAsStream(name);
38 | if (resource == null) {
39 | throw new IllegalStateException("can not find resource: " + name);
40 | }
41 | return resource;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/resources/config.ini:
--------------------------------------------------------------------------------
1 | [global]
2 | auth_username=iinti
3 | auth_password=iinti
4 |
5 | [source:dailiyun]
6 | loadURL=http://修改这里.user.xiecaiyun.com/api/proxies?action=getText&key=修改这里&count=修改这里&word=&rand=false&norepeat=false&detail=false<ime=0
7 | upstreamAuthUser=修改这里
8 | upstreamAuthPassword=修改这里
9 | poolSize=10
--------------------------------------------------------------------------------
/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
14 |
15 |
16 |
17 | ${LOG_DIR}/${LOG_PROJECT_NAME}/${LOG_PROJECT_NAME}-service.log
18 |
19 | ${normal-pattern}
20 |
21 |
22 | ${LOG_DIR}/${LOG_PROJECT_NAME}/service/service-%d{yyyy-MM-dd}.zip
23 |
24 | 15
25 |
26 |
27 |
28 |
29 |
30 | Scene
31 | unknown
32 |
33 |
34 |
35 | ${LOG_DIR}/${LOG_PROJECT_NAME}/traces/${Scene}/event.log
36 |
37 |
38 | [%d] %m%n
39 |
40 |
41 | ${LOG_DIR}/${LOG_PROJECT_NAME}/traces/${Scene}/event-%d{yyyy-MM-dd}.zip
42 |
43 | ${maxHistory}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | ERROR
52 | ACCEPT
53 | DENY
54 |
55 | ${LOG_DIR}/${LOG_PROJECT_NAME}/${LOG_PROJECT_NAME}-error.log
56 |
57 | ${LOG_DIR}/${LOG_PROJECT_NAME}/errors/error-%d{yyyy-MM-dd}.zip
58 | ${errorMaxHistory}
59 |
60 |
61 | ${normal-pattern}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------