├── .gitignore
├── Fgproxy.png
├── LICENSE
├── README.md
├── haproxy.png
├── log.png
└── proxy
├── __init__.py
├── adb.py
├── api.py
├── bridge.py
├── command.py
├── deploy
└── cli.py
├── header.py
├── network.py
└── structure.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .coverage
42 | .coverage.*
43 | .cache
44 | nosetests.xml
45 | coverage.xml
46 | *.cover
47 | .hypothesis/
48 | .pytest_cache/
49 |
50 | # Translations
51 | *.mo
52 | *.pot
53 |
54 | # Django stuff:
55 | *.log
56 | local_settings.py
57 | db.sqlite3
58 |
59 | # Flask stuff:
60 | instance/
61 | .webassets-cache
62 |
63 | # Scrapy stuff:
64 | .scrapy
65 |
66 | # Sphinx documentation
67 | docs/_build/
68 |
69 | # PyBuilder
70 | target/
71 |
72 | # Jupyter Notebook
73 | .ipynb_checkpoints
74 |
75 | # pyenv
76 | .python-version
77 |
78 | # celery beat schedule file
79 | celerybeat-schedule
80 |
81 | # SageMath parsed files
82 | *.sage.py
83 |
84 | # Environments
85 | .env
86 | .venv
87 | env/
88 | venv/
89 | ENV/
90 | env.bak/
91 | venv.bak/
92 |
93 | # Spyder project settings
94 | .spyderproject
95 | .spyproject
96 |
97 | # Rope project settings
98 | .ropeproject
99 |
100 | # mkdocs documentation
101 | /site
102 |
103 | # mypy
104 | .mypy_cache/
105 |
106 | # Mac os
107 | .DS_Store
108 | */.DS_Store
109 |
110 | # pycharm
111 | .idea
112 | .env
113 |
--------------------------------------------------------------------------------
/Fgproxy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kr1s77/FgSurfing/c1704c30476431bd36e0a6a240a91e32e6723dce/Fgproxy.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 FGSurfing
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
FGProxy
2 |
3 |

5 |
6 |
7 | ##### FGProxy 是一个企业级 `4G` 代理程序。它是一个比树莓派实现的4g代理程序更稳定的代理程序。它摒弃了物联网卡的缺点,使用了真正的 `4G` 手机卡来实现,部署实现都非常的简单。
8 |
9 | #### 文档可能比较简洁,部署失败的可以邮件联系我,或者找我远程协助搭建,有啥问题也可以在 issues 里面提出。
10 | ##### 有兴趣的同学可以尝试使用 haproxy + prometheus + grafana 做个代理的监控,还是非常不错的!
11 |
12 | #### ✅[bridge.py](https://github.com/Kr1s77/FgSurfing/blob/main/proxy/bridge.py) 可以单独运行在任何服务器上,那么运行 `bridge.py` 的服务器也可以作为代理使用,如果你想测试,可以通过这个方式
13 |
14 | #### Overall structure
15 |
16 | The following logic is then triggered:
17 | 
18 |
19 |
20 | ##### HAPROXY STATUS
21 | 
22 |
23 | ---
24 |
25 | #### Before You Begin
26 | 1. [Install lineageos](https://wiki.lineageos.org/devices/sailfish/install) at `Google Pixel`
27 | 2. The mobile phone must use a mobile phone that can execute `adb root`, I use [Google Pixel](https://en.wikipedia.org/wiki/Pixel_(1st_generation)),My system is [lineageos](https://www.lineageos.org/)
28 | 3. Before using this program, you need to confirm that your phone has been configured according to the link below [How-do-I-run-python-on-Android-devices](https://kr1s77.github.io/2021/7/12/How-do-I-run-python-on-Android-devices/)
29 | 4. Then you can execute the Fgproxy program. After the execution is completed, the port will be mapped to 30000, 30001, 30002... The number of ports is the number of devices
30 | 5. After that, configure haproxy for load balancing [Haproxy](https://github.com/haproxy/haproxy)
31 | 6. Since the machine is in our local area, we need to do intranet penetration to forward the local haproxy load balancing port, here we need [FRP](https://github.com/fatedier/frp)
32 |
33 | The above is the overall configuration process. After the configuration is completed, the frp exit is our proxy port, which can be used in the crawler.
34 |
35 | #### Create
36 |
37 | > ```shell
38 | > $ git clone https://github.com/Kr1s77/FgSurfing.git
39 | > $ cd FgSurfing/proxy
40 | > $ python3 api.py
41 | > >> [2021-07-15 14:22:32,522 INFO] -> Count: [1] Devices Found
42 | > >> [2021-07-15 14:22:32,522 INFO] -> Deploy device: FAXXXXXXX 1/1
43 | > ```
44 |
45 | ##### Currently most of it has been completed
46 | > Anyone is welcome to participate and improve
47 | > one person can go fast, but a group of people can go further
48 |
49 |
--------------------------------------------------------------------------------
/haproxy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kr1s77/FgSurfing/c1704c30476431bd36e0a6a240a91e32e6723dce/haproxy.png
--------------------------------------------------------------------------------
/log.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Kr1s77/FgSurfing/c1704c30476431bd36e0a6a240a91e32e6723dce/log.png
--------------------------------------------------------------------------------
/proxy/__init__.py:
--------------------------------------------------------------------------------
1 | # _*_ coding: utf-8 _*_
2 | __author__ = 'Kris '
3 | __version__ = '1.0.0'
4 | __site__ = 'https://github.com/Kr1s77/FgSurfing'
5 |
--------------------------------------------------------------------------------
/proxy/adb.py:
--------------------------------------------------------------------------------
1 | # _*_ coding: utf-8 _*_
2 | """
3 | FGSurfing.api
4 | ~~~~~~~~~~~~
5 | Use this module connect your mobile by android debug bridge
6 | :copyright: (c) 2021 by Kris
7 | """
8 | import os
9 | import re
10 | import time
11 | from network import Network
12 | # from func_timeout import exceptions
13 | # from func_timeout import func_set_timeout
14 | from command import CmdExecute, AdbCommand
15 |
16 |
17 | class AdbTools(object):
18 | """Can use adb cmd...
19 | """
20 |
21 | def __init__(self, device_id):
22 | self.command = AdbCommand(device_id=device_id)
23 | self.cmd = CmdExecute(self.command)
24 |
25 | def check_airplane_status(self):
26 | # :Check airplane mode is open
27 | return True if int(self.cmd.execute('CHECK_AIRPLANE_MODE')) else False
28 |
29 | def forward_tcp_port(self, local_port, device_port):
30 | return self.cmd.execute('FORWARD_DEVICE_PORT', f'tcp:{local_port} tcp:{device_port}')
31 |
32 | def turn_on_airplane_mode(self):
33 | self.cmd.execute('TURN_ON_AIRPLANE_MODE')
34 | self.cmd.execute('SET_AIRPLANE_MODE_ON')
35 | time.sleep(3)
36 | return None
37 |
38 | def close_airplane_mode(self):
39 | self.cmd.execute('CLOSE_AIRPLANE_MODE')
40 | self.cmd.execute('SET_AIRPLANE_MODE_OFF')
41 | return None
42 |
43 | def push(self, filepath: str, remote_path: str) -> str:
44 | return self.cmd.execute('ADB_PUSH', filepath, remote_path)
45 |
46 | def decompress(self, remote_path: str, remote_file: str) -> str:
47 | return self.cmd.execute('GZ_DECOMPRESS', remote_path, '-C', remote_file)
48 |
49 | def delete_and_create_dir(self, remote_path: str) -> str:
50 | self.cmd.execute('RM_DIR', remote_path)
51 | return self.cmd.execute('MK_DIR', remote_path)
52 |
53 | def running_server(self, host, port):
54 | return self.cmd.execute('RUN_SERVER', host, str(port), '&>/dev/null', '&')
55 |
56 | def get_server_pid(self, port: int):
57 | return self.cmd.execute('PROCESS_ID', str(port), '| grep python')
58 |
59 | def check_remote_port(self, port: int) -> bool:
60 | return True if self.get_server_pid(port) else False
61 |
62 | def start_as_root(self):
63 | flag = self.cmd.execute('ROOT')
64 | time.sleep(2)
65 | return flag
66 |
67 | def bridge_process(self):
68 | return self.cmd.execute('GREP')
69 |
70 | def kill_port_process(self, port):
71 | # info = self.get_server_pid(port=port)
72 | # # 'ps -ef | grep {port} | grep -v grep | awk "{print $2}"| sed -e "s/^/kill -9 /g" | sh'
73 | #
74 | # pid = info.split(' ')[-1].strip().split('/')[0]
75 | pid = self.bridge_process()
76 | if not pid:
77 | return
78 | return self.cmd.execute('KILL_PROCESS', pid)
79 |
80 | def remove_forward(self, port):
81 | return self.cmd.execute('FORWARD_DEVICE_PORT', f'--remove tcp:{port}')
82 |
83 | @staticmethod
84 | def kill_master_process(port):
85 | pid = os.popen(f'lsof -t -i:{port}').read()
86 | if not pid:
87 | return
88 | command = f'kill -9 {pid}'
89 | print(f'[Command]: {command}')
90 | return os.popen(command).read()
91 |
92 | def ping_test(self):
93 | ping = self.cmd.execute('PING_TEST')
94 | if 'ping: unknown host baidu.com' in ping:
95 | # 不通杀死端口
96 | return False
97 |
98 | match = re.search(r'time=(.*?) ms', ping)
99 | if not match:
100 | return False
101 |
102 | return True
103 |
104 | @staticmethod
105 | def check_local_port(port: int):
106 | """检测本地端口"""
107 | result = os.popen(f'echo "" | telnet 127.0.0.1 {port}').read()
108 | if "Escape character is '^]'" not in result:
109 | return False
110 | return True
111 |
112 |
113 | class Device(object):
114 | """Android Device...
115 | """
116 |
117 | def __init__(
118 | self,
119 | device_id: str,
120 | port: int = 8118,
121 | ip: str = '0.0.0.0',
122 | network: Network = Network()
123 | ):
124 | self.ip = ip
125 | self.port = port
126 | self.network = network
127 | self.device_id = device_id
128 | self.adb = AdbTools(device_id=device_id) # adb
129 |
130 | self.airplane_mode_is_open = False # airplane mode status
131 | self.transfer_port_is_open = False # device port status
132 |
133 | self.adb.start_as_root() # start device as root
134 | self.initialize_device() # init device message
135 |
136 | self.is_change_ip = False
137 |
138 | def initialize_device(self) -> tuple:
139 | self.airplane_mode_is_open = self.adb.check_airplane_status()
140 | self.transfer_port_is_open = self.adb.check_remote_port(
141 | port=self.port
142 | )
143 | return self.airplane_mode_is_open, self.transfer_port_is_open
144 |
--------------------------------------------------------------------------------
/proxy/api.py:
--------------------------------------------------------------------------------
1 | # _*_ coding: utf-8 _*_
2 | """
3 | Master Process
4 |
5 |
6 | Why cluster running at ip6-localhost:8899?
7 | /bin/bash$ adb shell cat /etc/hosts
8 | 127.0.0.1 localhost
9 | ::1 ip6-localhost
10 |
11 | It can also run in ::1:8899.
12 | 下一步计划:检查 4g 网络是否可用
13 |
14 | # 设备检测,如果设备不见了那么需要将设备踢出去
15 | # devices_str = init_all_devices()
16 | # log.info(f'Now running device count: {len(devices_str)}')
17 | # if device.device_id not in devices_str:
18 | # kill_master_port(device, port=master_port)
19 | # return device
20 | """
21 | import time
22 | import logging
23 | from adb import Device
24 | from functools import partial
25 | from command import CmdExecute
26 | from multiprocessing import Queue
27 | from multiprocessing import Process
28 | from deploy.cli import deploy_to_remote
29 | from __init__ import __author__, __version__, __site__
30 |
31 | log = logging.getLogger(__name__)
32 |
33 |
34 | def configure_logging(level):
35 | logging.basicConfig(
36 | level=level,
37 | format='[%(asctime)s %(levelname)s] -> %(message)s',
38 | )
39 |
40 |
41 | _set_debug_logging = partial(configure_logging, logging.DEBUG)
42 | _set_info_logging = partial(configure_logging, logging.INFO)
43 |
44 | IP_SWITCHING_TIME = 30 * 60 # second
45 | MASTER_PORT_START = 30000
46 | HEALTH_CHECK_TIME = 1 * 60 # second
47 | WAIT_AIRPLANE_MODE_TIME = 8 # second
48 |
49 |
50 | def _init_msg(debug: bool) -> None:
51 | if debug: # set debug level
52 | _set_debug_logging()
53 | else:
54 | _set_info_logging()
55 |
56 | print(f'\nauthor: [{__author__}] site: [{__site__}]')
57 | print(f"""
58 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
59 | ' ███████╗ ██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗██╗ ██╗ '
60 | ' ██╔════╝██╔════╝ ██╔══██╗██╔══██╗██╔═══██╗╚██╗██╔╝╚██╗ ██╔╝ '
61 | ' █████╗ ██║ ███╗██████╔╝██████╔╝██║ ██║ ╚███╔╝ ╚████╔╝ '
62 | ' ██╔══╝ ██║ ██║██╔═══╝ ██╔══██╗██║ ██║ ██╔██╗ ╚██╔╝ '
63 | ' ██║ ╚██████╔╝██║ ██║ ██║╚██████╔╝██╔╝ ██╗ ██║ '
64 | ' ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ '
65 | {__site__}
66 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
67 | """)
68 | return None
69 |
70 |
71 | def init_all_devices():
72 | """
73 | List of devices attached
74 | 1daa96050207 device
75 | 1daa96050207 device
76 | :return: [1daa96050207, 1daa96050207]
77 | """
78 | devices_string = CmdExecute.execute_command('adb devices')
79 | time.sleep(3)
80 | devices_with_str = devices_string.split('\n')[1:-2]
81 | online_devices = list()
82 | for device_str in devices_with_str:
83 | if 'device' in device_str:
84 | online_devices.append(device_str)
85 | devices = list(map(lambda x: x.split('\t')[0].strip(), online_devices))
86 | log.info(f'Count: [{len(devices)}] Devices Found')
87 | return devices
88 |
89 |
90 | def _forward_tcp_port(device: Device, local_port, remote_port):
91 | # nginx complex balanced local port
92 | return device.adb.forward_tcp_port(local_port, remote_port)
93 |
94 |
95 | def kill_master_port(device: Device, port: int):
96 | return device.adb.kill_master_process(port)
97 |
98 |
99 | def run_server(device, master_port):
100 | """
101 | if int(device.adb.check_remote_port(ip, port).strip()) != 0:
102 | The port is already in use
103 | """
104 | # kill_master_port(device, master_port)
105 |
106 | device.adb.remove_forward(master_port)
107 | device.adb.kill_port_process(device.port)
108 |
109 | # :If airplane mode is opened, first need close airplane mode
110 | if device.airplane_mode_is_open:
111 | device.adb.close_airplane_mode()
112 | time.sleep(WAIT_AIRPLANE_MODE_TIME)
113 |
114 | # :Running Proxy server command
115 | device.adb.running_server(host=device.ip, port=device.port)
116 | # : wait server running
117 | time.sleep(2)
118 | device.initialize_device()
119 | # waite init device
120 | time.sleep(1)
121 | if not device.transfer_port_is_open:
122 | return
123 |
124 | _forward_tcp_port(device, master_port, device.port)
125 |
126 |
127 | def _change_ip(master_port: int, cluster_device: Device, change_ip_queue):
128 | """
129 | # :Close master port for nginx
130 | # :Get device info
131 | # :Check proxy running port
132 | # :Time to change ip
133 | # :Open master port for nginx
134 | """
135 | time.sleep(IP_SWITCHING_TIME)
136 | change_ip_queue.put_nowait(cluster_device.device_id)
137 |
138 | cluster_device.adb.remove_forward(master_port)
139 | cluster_device.adb.kill_port_process(cluster_device.port)
140 | cluster_device.initialize_device()
141 |
142 | if not cluster_device.airplane_mode_is_open:
143 | cluster_device.adb.turn_on_airplane_mode()
144 | cluster_device.adb.close_airplane_mode()
145 |
146 | time.sleep(WAIT_AIRPLANE_MODE_TIME)
147 | # if cluster_device.transfer_port_is_open is False:
148 | cluster_device.adb.running_server(
149 | host=cluster_device.ip,
150 | port=cluster_device.port
151 | )
152 | time.sleep(1)
153 | if cluster_device.adb.bridge_process().strip():
154 | _forward_tcp_port(
155 | device=cluster_device,
156 | local_port=master_port,
157 | remote_port=cluster_device.port
158 | )
159 | time.sleep(1)
160 |
161 | if not cluster_device.adb.ping_test():
162 | cluster_device.adb.remove_forward(port=master_port)
163 |
164 | if not cluster_device.adb.check_local_port(master_port):
165 | cluster_device.adb.remove_forward(port=master_port)
166 |
167 | change_ip_queue.get_nowait()
168 |
169 |
170 | def _health_check(master_port: int, device: Device, change_ip_queue):
171 | # 如果正在切换 IP 那么此设备不会经过健康检测,否则会出现问题
172 | time.sleep(HEALTH_CHECK_TIME)
173 | if not change_ip_queue.empty():
174 | if device.device_id == change_ip_queue.get_nowait():
175 | change_ip_queue.put_nowait(device.device_id)
176 | time.sleep(HEALTH_CHECK_TIME)
177 | return None
178 |
179 | change_ip_queue.put_nowait(device.device_id)
180 | time.sleep(HEALTH_CHECK_TIME)
181 |
182 | device.initialize_device()
183 | if not device.transfer_port_is_open:
184 | device.adb.remove_forward(port=master_port)
185 |
186 | if device.airplane_mode_is_open:
187 | device.adb.remove_forward(port=master_port)
188 |
189 | if not device.adb.ping_test():
190 | device.adb.remove_forward(port=master_port)
191 |
192 | return None
193 |
194 |
195 | class Daemon(object):
196 | def __init__(self, devices: list):
197 | self.devices = devices
198 | self.change_ip_queue = Queue()
199 |
200 | def worker(self, work_func, change_ip_queue):
201 | while True:
202 | for index, device in enumerate(self.devices):
203 | master_port = MASTER_PORT_START + index
204 | work_func(master_port, device, change_ip_queue)
205 |
206 | def run_forever(self):
207 | """Running change ip and health check"""
208 | process = [Process(target=self.worker, args=(_change_ip, self.change_ip_queue))]
209 |
210 | [p.start() for p in process]
211 | [p.join() for p in process]
212 |
213 |
214 | def _deploy_all_device(devices: list) -> None:
215 | for index, device in enumerate(devices):
216 | log.info(f'Deploy device: {device.device_id} {len(devices)}/{index + 1}')
217 | log.info(f'Device: {device.device_id} transfer port running is {device.transfer_port_is_open}')
218 | log.info(f'Device: {device.device_id} transfer airplane mode open is {device.airplane_mode_is_open}')
219 | # :Deploy and run server...
220 | deploy_to_remote(device=device)
221 | master_port = MASTER_PORT_START + index
222 | run_server(device=device, master_port=master_port)
223 | time.sleep(2)
224 |
225 | daemon = Daemon(devices=devices)
226 | daemon.run_forever()
227 |
228 |
229 | def runner(debug: bool = True, ip: str = '0.0.0.0', port: int = 30000) -> None:
230 | """master entry
231 | 1. deploy the application to the phone.
232 | 2. get all device and init all device.
233 | 3. kill all proxy running port
234 | 4. running application and output log.
235 | """
236 | _init_msg(debug=debug)
237 | devices_str = init_all_devices()
238 |
239 | if not devices_str:
240 | log.warning('No connected mobile phone was found')
241 | exit(1)
242 |
243 | devices = [Device(device_id=device_id, port=port, ip=ip) for device_id in devices_str]
244 | _deploy_all_device(devices)
245 |
246 | log.info(f'FGProxy {__version__} remote running at {ip}:{port}')
247 |
248 |
249 | if __name__ == '__main__':
250 | runner(True, 'ip6-localhost', 10000)
251 |
--------------------------------------------------------------------------------
/proxy/bridge.py:
--------------------------------------------------------------------------------
1 | # _*_ coding: utf-8 _*_
2 | """
3 | Socket bridge // Send & Receive
4 | http.server: https://docs.python.org/3/library/http.server.html
5 | """
6 | import os
7 | import ssl
8 | import socket
9 | import select
10 | import sys
11 | import threading
12 | import time
13 | from socketserver import ThreadingMixIn
14 | from urllib.parse import urlsplit
15 |
16 | from header import filter_header
17 | import http.client as http_lib
18 | from subprocess import Popen, PIPE
19 | from http.server import BaseHTTPRequestHandler, HTTPServer
20 |
21 | REQUEST_TIMEOUT = 20
22 | DEFAULT_CHARSET = 'UTF-8'
23 | RECEIVE_BUFFER_SIZE = 8192
24 |
25 |
26 | class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
27 | address_family = socket.AF_INET6
28 | daemon_threads = True
29 |
30 | def handle_error(self, request, client_address):
31 | cls, e = sys.exc_info()[:2]
32 | if cls is socket.error or cls is ssl.SSLError:
33 | pass
34 | else:
35 | return HTTPServer.handle_error(self, request, client_address)
36 |
37 |
38 | def join_with_script_dir(path):
39 | return os.path.join(os.path.dirname(os.path.abspath(__file__)), path)
40 |
41 |
42 | class ProxyBridge(BaseHTTPRequestHandler):
43 | lock = threading.Lock()
44 | cakey = join_with_script_dir('ca.key')
45 | ca_cert = join_with_script_dir('ca.crt')
46 | cert_key = join_with_script_dir('cert.key')
47 | cert_dir = join_with_script_dir('certs/')
48 | timeout = 5
49 |
50 | def __init__(self, *args, **kwargs):
51 | self.tls = threading.local()
52 | self.tls.conns = dict()
53 | self._headers_buffer = list()
54 | super(ProxyBridge, self).__init__(*args, **kwargs)
55 |
56 | def parse_request(self) -> bool:
57 | symbol = super(ProxyBridge, self).parse_request()
58 | self.command = self.command.lower()
59 | return symbol
60 |
61 | def do_connect(self):
62 | """HANDLE HTTPS CONNECT REQUEST
63 | """
64 | if os.path.isfile(self.cakey) and os.path.isfile(self.ca_cert) and os.path.isfile(
65 | self.cert_key) and os.path.isdir(self.cert_dir):
66 | self.connect_intercept()
67 | else:
68 | self.exec_connect_options()
69 |
70 | def connect_intercept(self):
71 | hostname = self.path.split(':')[0]
72 | cert_path = "%s/%s.crt" % (self.cert_dir.rstrip('/'), hostname)
73 |
74 | with self.lock:
75 | if not os.path.isfile(cert_path):
76 | epoch = "%d" % (time.time() * 1000)
77 | p1 = Popen(["openssl", "req", "-new", "-key", self.cert_key, "-subj", "/CN=%s" % hostname], stdout=PIPE)
78 | p2 = Popen(["openssl", "x509", "-req", "-days", "3650", "-CA", self.ca_cert, "-CAkey", self.cakey,
79 | "-set_serial", epoch, "-out", cert_path], stdin=p1.stdout, stderr=PIPE)
80 | p2.communicate()
81 |
82 | self.wfile.write(
83 | "%s %d %s\r\n" % (self.protocol_version, 200, 'Connection Established')
84 | )
85 | self.end_headers()
86 |
87 | self.connection = ssl.wrap_socket(self.connection, keyfile=self.cert_key, certfile=cert_path, server_side=True)
88 | self.rfile = self.connection.makefile("rb", self.rbufsize)
89 | self.wfile = self.connection.makefile("wb", self.wbufsize)
90 |
91 | conn_type = self.headers.get('Proxy-Connection', '')
92 | if self.protocol_version == "HTTP/1.1" and conn_type.lower() != 'close':
93 | self.close_connection = False
94 | else:
95 | self.close_connection = True
96 |
97 | def do_get(self):
98 | """Handle all request protocol and all request method
99 | """
100 | return self.handle_request()
101 |
102 | def handle_request(self) -> None:
103 | req = self
104 | content_length = int(req.headers.get('Content-Length', 0))
105 | req_body = self.rfile.read(content_length) if content_length else None
106 |
107 | if req.path[0] == '/':
108 | if isinstance(self.connection, ssl.SSLSocket):
109 | req.path = "https://%s%s" % (req.headers['Host'], req.path)
110 | else:
111 | req.path = "http://%s%s" % (req.headers['Host'], req.path)
112 |
113 | u = urlsplit(req.path)
114 | scheme, netloc, path = u.scheme, u.netloc, (u.path + '?' + u.query if u.query else u.path)
115 | assert scheme in ('http', 'https')
116 | if netloc:
117 | req.headers['Host'] = netloc
118 | setattr(req, 'headers', filter_header(req.headers))
119 | origin = (scheme, netloc)
120 |
121 | try:
122 | if origin not in self.tls.conns:
123 | if scheme == 'https':
124 | self.tls.conns[origin] = http_lib.HTTPSConnection(netloc, timeout=self.timeout)
125 | else:
126 | self.tls.conns[origin] = http_lib.HTTPConnection(netloc, timeout=self.timeout)
127 |
128 | conn = self.tls.conns[origin]
129 | conn.request(self.command.upper(), path, req_body, dict(req.headers))
130 | res = conn.getresponse()
131 | version_table = {10: 'HTTP/1.0', 11: 'HTTP/1.1'}
132 | setattr(res, 'response_version', version_table[res.version])
133 | print("Headers: ", res.headers)
134 | if 'Content-Length' not in res.headers and 'no-store' in res.headers.get('Cache-Control', '') \
135 | and 'Content-Encoding' not in res.headers:
136 | self.relay_streaming(res)
137 | return
138 |
139 | res_body = res.read()
140 | except Exception as e:
141 | if origin in self.tls.conns:
142 | del self.tls.conns[origin]
143 | self.send_error(502, str(e))
144 | return
145 |
146 | if req_body is False:
147 | self.send_error(403)
148 | return
149 |
150 | setattr(res, 'headers', filter_header(res.headers))
151 |
152 | self.wfile.write(f'{self.protocol_version} {res.status} {res.reason}\r\n'.encode(DEFAULT_CHARSET))
153 | for line in res.headers:
154 | self.wfile.write((line + ': ' + res.headers[line] + '\r\n').encode(DEFAULT_CHARSET))
155 |
156 | # self.end_headers()
157 | self._headers_buffer = [b"\r\n"]
158 | self.flush_headers()
159 |
160 | self.wfile.write(res_body)
161 | self.wfile.flush()
162 |
163 | def write_and_flush_headers(self, headers: list):
164 | self._headers_buffer += headers
165 | self.flush_headers()
166 |
167 | def relay_streaming(self, res):
168 | """查看client 是否断开链接"""
169 | self.wfile.write(f'{self.protocol_version} {res.status} {res.reason}\r\n'.encode(DEFAULT_CHARSET))
170 | for line in res.headers.headers:
171 | self.wfile.write(line)
172 | self.end_headers()
173 | try:
174 | while True:
175 | chunk = res.read(8192)
176 | if not chunk:
177 | break
178 | self.wfile.write(chunk)
179 | self.wfile.flush()
180 | except socket.error:
181 | print('connection closed by client')
182 |
183 | def exec_connect_options(self):
184 | host, port = self.path.split(':', 1)
185 | port = int(port) or 443
186 | try:
187 | server = socket.create_connection((host, port), timeout=REQUEST_TIMEOUT)
188 | except Exception as e:
189 | return self.send_error(502, str(e))
190 |
191 | self.send_response(200, 'Connection Established')
192 | self.end_headers()
193 |
194 | conns = [self.connection, server]
195 | self.close_connection = False
196 | while not self.close_connection:
197 | read_list, write_list, error_list = select.select(conns, [], conns, self.timeout)
198 |
199 | if not read_list or error_list:
200 | break
201 |
202 | for read in read_list:
203 | symbol = self.exec_read_fd(read_conn=read, conns=conns)
204 | if not symbol:
205 | break
206 |
207 | def exec_read_fd(self, read_conn, conns) -> bool:
208 | other = conns[1] if read_conn is conns[0] else conns[0]
209 | buffer = read_conn.recv(RECEIVE_BUFFER_SIZE)
210 | if not buffer:
211 | self.close_connection = True
212 | return False
213 |
214 | other.sendall(buffer)
215 | return True
216 |
217 |
218 | def run(server_class=ThreadingHTTPServer, handler_class=ProxyBridge):
219 | host = sys.argv[1] if len(sys.argv) > 1 else '127.0.0.1'
220 | port = int(sys.argv[2]) if len(sys.argv) > 2 else 10000
221 | print(f'server running at {host}:{port}')
222 | server_address = (host, int(port))
223 | httpd = server_class(server_address, handler_class)
224 | httpd.serve_forever()
225 |
226 |
227 | if __name__ == '__main__':
228 | run()
229 |
--------------------------------------------------------------------------------
/proxy/command.py:
--------------------------------------------------------------------------------
1 | import os
2 | from typing import Optional
3 | from abc import ABCMeta, abstractmethod
4 | from collections import namedtuple
5 |
6 |
7 | Cmd = namedtuple('Cmd', 'name command')
8 |
9 | ADB_PREFIX = 'adb -s {} '
10 |
11 | ADB = {
12 | 'TURN_ON_AIRPLANE_MODE': 'settings put global airplane_mode_on 1',
13 | 'SET_AIRPLANE_MODE_ON': 'am broadcast -a android.intent.action.AIRPLANE_MODE --ez state true',
14 | 'CLOSE_AIRPLANE_MODE': 'settings put global airplane_mode_on 0',
15 | 'SET_AIRPLANE_MODE_OFF': 'am broadcast -a android.intent.action.AIRPLANE_MODE --ez state false',
16 | 'CHECK_AIRPLANE_MODE': 'settings get global airplane_mode_on',
17 | 'PING_BAIDU': 'ping -c 1 baidu.com',
18 | 'FORWARD_DEVICE_PORT': 'forward',
19 | 'ADB_DEVICES': 'devices',
20 | 'ADB_PUSH': 'push',
21 | 'GZ_DECOMPRESS': 'tar -zxf',
22 | 'RM_DIR': 'rm -rf',
23 | 'MK_DIR': 'mkdir',
24 | 'RUN_SERVER': 'python /data/local/tmp/proxy/bridge.py',
25 | 'CHECK_PORT': 'python /data/local/tmp/proxy/network.py',
26 | 'ROOT': 'root',
27 | 'PROCESS_ID': 'netstat -ntlp | grep',
28 | 'KILL_PROCESS': 'kill -9',
29 | 'KILL_MASTER_PORT': 'kill -9 $(lsof -t -i:',
30 | 'PING_TEST': 'ping -c 1 baidu.com',
31 | 'GREP': "ps -ef | grep bridge.py | grep -v 'grep' | awk '{print $2}'"
32 | }
33 |
34 |
35 | Commands = {
36 | 'TURN_ON_AIRPLANE_MODE': Cmd('TURN_ON_AIRPLANE_MODE', ADB['TURN_ON_AIRPLANE_MODE']),
37 | 'SET_AIRPLANE_MODE_ON': Cmd('SET_AIRPLANE_MODE_ON', ADB['SET_AIRPLANE_MODE_ON']),
38 | 'CLOSE_AIRPLANE_MODE': Cmd('CLOSE_AIRPLANE_MODE', ADB['CLOSE_AIRPLANE_MODE']),
39 | 'SET_AIRPLANE_MODE_OFF': Cmd('SET_AIRPLANE_MODE_OFF', ADB['SET_AIRPLANE_MODE_OFF']),
40 | 'CHECK_AIRPLANE_MODE': Cmd('CHECK_AIRPLANE_MODE', ADB['CHECK_AIRPLANE_MODE']),
41 | 'PING_BAIDU': Cmd('PING_BAIDU', ADB['CHECK_AIRPLANE_MODE']),
42 | 'FORWARD_DEVICE_PORT': Cmd('FORWARD_DEVICE_PORT', ADB['FORWARD_DEVICE_PORT']),
43 | 'ADB_DEVICES': Cmd('ADB_DEVICES', ADB['ADB_DEVICES']),
44 | 'ADB_PUSH': Cmd('ADB_PUSH', ADB['ADB_PUSH']),
45 | 'GZ_DECOMPRESS': Cmd('GZ_DECOMPRESS', ADB['GZ_DECOMPRESS']),
46 | 'RM_DIR': Cmd('RM_DIR', ADB['RM_DIR']),
47 | 'MK_DIR': Cmd('MK_DIR', ADB['MK_DIR']),
48 | 'RUN_SERVER': Cmd('RUN_SERVER', ADB['RUN_SERVER']),
49 | 'CHECK_PORT': Cmd('CHECK_PORT', ADB['CHECK_PORT']),
50 | 'ROOT': Cmd('ROOT', ADB['ROOT']),
51 | 'PROCESS_ID': Cmd('PROCESS_ID', ADB['PROCESS_ID']),
52 | 'KILL_PROCESS': Cmd('KILL_PROCESS', ADB['KILL_PROCESS']),
53 | 'KILL_MASTER_PORT': Cmd('KILL_MASTER_PORT', ADB['KILL_MASTER_PORT']),
54 | 'PING_TEST': Cmd('PING_TEST', ADB['PING_TEST']),
55 | 'GREP': Cmd('GREP', ADB['GREP']),
56 | }
57 |
58 |
59 | class Command(metaclass=ABCMeta):
60 | without_shell_list = ['push', 'forward', 'root']
61 |
62 | def __init__(self, device_id: str = None):
63 | self.device_id = device_id
64 |
65 | @abstractmethod
66 | def gen_cmd(self, name: Optional[str], *args) -> Optional[str]:
67 | """generate command...
68 | """
69 | pass
70 |
71 |
72 | class AdbCommand(Command):
73 | def __init__(self, device_id: str = None):
74 | super(AdbCommand, self).__init__(device_id=device_id)
75 | self.device_id = device_id
76 |
77 | def gen_cmd(self, name: Optional[str], *args) -> Optional[str]:
78 | """ Types:
79 | # adb -s '...' push ././. ././.
80 | # adb -s '...' shell ...
81 | """
82 | command = Commands[name].command # gen command
83 |
84 | if command in self.without_shell_list:
85 | cmd = ''.join([ADB_PREFIX.format(self.device_id), ' '.join([command, *args])])
86 | else:
87 | cmd = ''.join([ADB_PREFIX.format(self.device_id), 'shell ', ' '.join([command, *args])])
88 |
89 | return cmd
90 |
91 |
92 | class CmdExecute():
93 | def __init__(self, command: Command):
94 | self.command = command
95 |
96 | def get_state(self):
97 | pass
98 |
99 | def execute(self, name: Optional[str], *args) -> Optional[str]:
100 | cmd = self.command.gen_cmd(name, *args)
101 | print('[Command]:', cmd)
102 | return self.execute_command(command=cmd)
103 |
104 | @staticmethod
105 | def execute_command(command: Optional[str]):
106 | value = os.popen(command).read()
107 | return value
108 |
--------------------------------------------------------------------------------
/proxy/deploy/cli.py:
--------------------------------------------------------------------------------
1 | import os
2 | import tarfile
3 | import zipfile
4 | from adb import Device
5 |
6 | # Default dir path
7 | current_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8 | proxy_dir = os.path.join(current_dir, 'proxy')
9 |
10 |
11 | def make_gz(output_filename: str, source_dir):
12 | with tarfile.open(output_filename, "w:gz") as tar:
13 | tar.add(source_dir, arcname=os.path.basename(source_dir))
14 |
15 |
16 | def make_zip(output_filename: str, source_dir):
17 | zip_file = zipfile.ZipFile(output_filename, 'w')
18 | pre_len = len(os.path.dirname(source_dir))
19 | for parent, _, filenames in os.walk(source_dir):
20 | for filename in filenames:
21 | path_file = os.path.join(parent, filename)
22 | name = path_file[pre_len:].strip(os.path.sep) # ads path
23 | zip_file.write(path_file, name)
24 | zip_file.close()
25 |
26 |
27 | def push_to_mobile(filepath: str, remote_path: str, device: Device) -> str:
28 | device.adb.push(
29 | filepath=filepath,
30 | remote_path=remote_path
31 | )
32 | return remote_path
33 |
34 |
35 | def decompress_mobile_file(remote_path: str, remote_file: str, device: Device) -> str:
36 | device.adb.decompress(
37 | remote_path=remote_path,
38 | remote_file=remote_file,
39 | )
40 | return remote_file
41 |
42 |
43 | def rm_and_mk_dir(remote_path: str, device: Device) -> str:
44 | device.adb.delete_and_create_dir(
45 | remote_path=remote_path
46 | )
47 | return remote_path
48 |
49 |
50 | def deploy_to_remote(device: Device):
51 | output_filename = os.path.basename(proxy_dir) + '.tar.gz'
52 | make_gz(output_filename, proxy_dir)
53 |
54 | filepath = os.path.join(proxy_dir, output_filename)
55 | mobile_file_path = push_to_mobile(
56 | filepath=filepath,
57 | remote_path='/data/local/tmp/',
58 | device=device
59 | )
60 | # print(f'*{output_filename}* was pushed to mobile *{mobile_file_path}*')
61 | mobile_compress_file = os.path.join(mobile_file_path, output_filename)
62 | mobile_script_file = os.path.join(mobile_file_path, os.path.basename(proxy_dir))
63 |
64 | # decompress script file
65 | # rm and mk dir
66 | rm_and_mk_dir(remote_path=mobile_script_file, device=device)
67 |
68 | decompress_mobile_file(
69 | remote_path=mobile_compress_file,
70 | remote_file=mobile_file_path,
71 | device=device
72 | )
73 |
74 | # clean local compress file
75 | os.popen(f'rm -rf {filepath}')
76 |
--------------------------------------------------------------------------------
/proxy/header.py:
--------------------------------------------------------------------------------
1 | # _*_ coding: utf-8 _*_
2 | """
3 | FgSurfing.api
4 | ~~~~~~~~~~~~
5 | Use this module to replace the request headers
6 | :copyright: (c) 2021 by Kris
7 | """
8 | import re
9 |
10 | # :Filter request headers segments
11 | FILTER_HEADER_SEGMENTS = (
12 | 'connection',
13 | 'keep-alive',
14 | 'proxy-authenticate',
15 | 'proxy-authorization',
16 | 'te',
17 | 'trailers',
18 | 'transfer-encoding',
19 | 'upgrade'
20 | )
21 |
22 |
23 | def _to_capitalize(word: str):
24 | """ :Header segment to capitalize
25 | """
26 | return '-'.join([i.capitalize() for i in word.split('-')])
27 |
28 |
29 | FILTER_HEADER_SEGMENTS_UPPER = tuple([i.upper() for i in FILTER_HEADER_SEGMENTS])
30 | FILTER_HEADER_SEGMENTS_CAPITALIZE = tuple([_to_capitalize(i) for i in FILTER_HEADER_SEGMENTS])
31 |
32 |
33 | def filter_header(headers):
34 | """
35 | :Remove irrelevant request headers to prevent the
36 | :agent from being discovered.....................
37 | """
38 | for key, upper_key, capitalize_key in zip(
39 | FILTER_HEADER_SEGMENTS,
40 | FILTER_HEADER_SEGMENTS_UPPER,
41 | FILTER_HEADER_SEGMENTS_CAPITALIZE
42 | ):
43 | del headers[key]
44 | del headers[upper_key]
45 | del headers[capitalize_key]
46 |
47 | if 'Accept-Encoding' in headers:
48 | ae = headers['Accept-Encoding']
49 | filtered_encodings = [x for x in re.split(r',\s*', ae) if x in ('identity', 'gzip', 'x-gzip', 'deflate')]
50 | headers['Accept-Encoding'] = ', '.join(filtered_encodings)
51 |
52 | return headers
53 |
--------------------------------------------------------------------------------
/proxy/network.py:
--------------------------------------------------------------------------------
1 | # _*_ coding: utf-8 _*_
2 | """
3 | Slaver Process
4 | ~~~~~~~~~~~~
5 | Use this module to replace the request headers
6 | :copyright: (c) 2021 by Kris
7 | """
8 | import sys
9 | import socket
10 |
11 |
12 | class Network(object):
13 | def check_local_port(self, port: int, host='localhost'):
14 | port_msg = dict()
15 | port_msg['host'] = host
16 | port_msg['port'] = port
17 | return self._check_tcp_port(port_msg)
18 |
19 | @staticmethod
20 | def _check_tcp_port(port_msg: dict, timeout=2):
21 | cs = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
22 | address = (str(port_msg["host"]), int(port_msg["port"]))
23 | cs.settimeout(timeout)
24 | status = cs.connect_ex(address)
25 | return 1 if status == 0 else 0
26 |
27 | def check_remote_port(self, host: str, port: int):
28 | return self.check_local_port(port, host)
29 |
30 |
31 | def check():
32 | host = 'localhost'
33 | port = int(sys.argv[1]) if len(sys.argv) > 1 else 30000
34 | network = Network()
35 | print(network.check_remote_port(host, port))
36 |
37 |
38 | if __name__ == '__main__':
39 | check()
40 |
--------------------------------------------------------------------------------
/proxy/structure.py:
--------------------------------------------------------------------------------
1 | # _*_ coding: utf-8 _*_
2 |
--------------------------------------------------------------------------------