simple-proxy 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simple-proxy-0.0.1/LICENSE +21 -0
- simple-proxy-0.0.1/PKG-INFO +140 -0
- simple-proxy-0.0.1/README.md +114 -0
- simple-proxy-0.0.1/setup.cfg +14 -0
- simple-proxy-0.0.1/setup.py +51 -0
- simple-proxy-0.0.1/simple_proxy/__init__.py +639 -0
- simple-proxy-0.0.1/simple_proxy/version.py +1 -0
- simple-proxy-0.0.1/simple_proxy.egg-info/PKG-INFO +140 -0
- simple-proxy-0.0.1/simple_proxy.egg-info/SOURCES.txt +12 -0
- simple-proxy-0.0.1/simple_proxy.egg-info/dependency_links.txt +1 -0
- simple-proxy-0.0.1/simple_proxy.egg-info/entry_points.txt +2 -0
- simple-proxy-0.0.1/simple_proxy.egg-info/requires.txt +3 -0
- simple-proxy-0.0.1/simple_proxy.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Hao Ruan
|
|
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.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: simple-proxy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A very simple NIO TCP proxy server
|
|
5
|
+
Home-page: https://github.com/ruanhao/simple-proxy
|
|
6
|
+
Author: Hao Ruan
|
|
7
|
+
Author-email: ruanhao1116@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: network,tcp,non-blocking,proxy
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.7, <4
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
|
|
27
|
+
# simple-proxy :rocket:
|
|
28
|
+
|
|
29
|
+
A very simple TCP proxy tool (not http proxy) empowered by nio tcp framework [py-netty](https://pypi.org/project/py-netty/)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install simple-proxy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
Usage: simple-proxy [OPTIONS]
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
-l, --local-server TEXT Local server address [default: localhost]
|
|
48
|
+
-lp, --local-port INTEGER Local port [default: 8080]
|
|
49
|
+
-r, --remote-server TEXT Remote server address [default: localhost]
|
|
50
|
+
-rp, --remote-port INTEGER Remote port [default: 80]
|
|
51
|
+
-g, --global Listen on 0.0.0.0
|
|
52
|
+
-c, --tcp-flow Dump tcp flow on to console
|
|
53
|
+
-f, --save-tcp-flow Save tcp flow to file
|
|
54
|
+
-s, --tls Denote remote server listening on secure
|
|
55
|
+
port
|
|
56
|
+
-ss Denote local sever listening on secure port
|
|
57
|
+
-kf, --key-file PATH Key file for local server
|
|
58
|
+
-cf, --cert-file PATH Certificate file for local server
|
|
59
|
+
--speed-monitor Print speed info to console for established
|
|
60
|
+
connection
|
|
61
|
+
--speed-monitor-interval INTEGER
|
|
62
|
+
Speed monitor interval [default: 5]
|
|
63
|
+
-dti, --disguise-tls-ip TEXT Disguise TLS IP
|
|
64
|
+
-dtp, --disguise-tls-port INTEGER
|
|
65
|
+
Disguise TLS port [default: 443]
|
|
66
|
+
-wl, --white-list TEXT IP White list for incoming connections
|
|
67
|
+
(comma separated)
|
|
68
|
+
--run-mock-tls-server Run mock TLS server
|
|
69
|
+
-v, --verbose Verbose mode
|
|
70
|
+
-h, --help Show this message and exit.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
### Basic proxy (TLS termination)
|
|
76
|
+
```commandline
|
|
77
|
+
> simple-proxy --tls -r www.google.com -rp 443 -lp 8080
|
|
78
|
+
Proxy server started listening: localhost:8080 => www.google.com:443(TLS) ...
|
|
79
|
+
console:False, file:False, disguise:n/a, whitelist:*
|
|
80
|
+
> curl -I -H 'Host: www.google.com' http://localhost:8080
|
|
81
|
+
HTTP/1.1 200 OK
|
|
82
|
+
...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```commandline
|
|
86
|
+
> simple-proxy -r www.google.com -rp 80 -lp 8443 -ss
|
|
87
|
+
Proxy server started listening: localhost:8443(TLS) => www.google.com:80 ...
|
|
88
|
+
console:False, file:False, disguise:n/a, whitelist:*
|
|
89
|
+
> curl -I -H 'Host: www.google.com' -k https://localhost:8443
|
|
90
|
+
HTTP/1.1 200 OK
|
|
91
|
+
...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Dump TCP flow
|
|
95
|
+
TCP flow can be dumped into console or files (under directory __tcpflow__)
|
|
96
|
+
```commandline
|
|
97
|
+
> simple-proxy -r www.google.com -rp 443 -lp 8443 -ss -s -c -f
|
|
98
|
+
Proxy server started listening: localhost:8443(TLS) => www.google.com:443(TLS) ...
|
|
99
|
+
console:True, file:True, disguise:n/a, whitelist:*
|
|
100
|
+
> curl -k -I -H 'Host: www.google.com' https://localhost:8443
|
|
101
|
+
```
|
|
102
|
+

|
|
103
|
+
|
|
104
|
+
### Connection status monitor
|
|
105
|
+
```commandline
|
|
106
|
+
> $ simple-proxy -r echo-server.proxy.com -rp 8080 -lp 48080 --speed-monitor
|
|
107
|
+
Proxy server started listening: localhost:48080 => echo-server.proxy.com:8080 ...
|
|
108
|
+
console:False, file:False, disguise:n/a, whitelist:*
|
|
109
|
+
Connection opened: ('127.0.0.1', 60937)
|
|
110
|
+
Connection opened: ('127.0.0.1', 60938)
|
|
111
|
+
Connection opened: ('127.0.0.1', 60939)
|
|
112
|
+
Connection opened: ('127.0.0.1', 60940)
|
|
113
|
+
Connection opened: ('127.0.0.1', 60941)
|
|
114
|
+
Connection opened: ('127.0.0.1', 60942)
|
|
115
|
+
Connection opened: ('127.0.0.1', 60943)
|
|
116
|
+
Connection opened: ('127.0.0.1', 60944)
|
|
117
|
+
---------------------------2024-02-12 17:43:02.337268 (total:8, rounds:1)---------------------------
|
|
118
|
+
[ 1] | 127.0.0.1:60937 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:235.00 K | duration: 7s
|
|
119
|
+
[ 2] | 127.0.0.1:60938 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
120
|
+
[ 3] | 127.0.0.1:60939 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
121
|
+
[ 4] | 127.0.0.1:60940 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
122
|
+
[ 5] | 127.0.0.1:60941 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
123
|
+
[ 6] | 127.0.0.1:60942 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:234.00 K Tx:234.00 K | duration: 7s
|
|
124
|
+
[ 7] | 127.0.0.1:60943 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:234.00 K Tx:234.00 K | duration: 7s
|
|
125
|
+
[ 8] | 127.0.0.1:60944 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:234.00 K Tx:234.00 K | duration: 7s
|
|
126
|
+
Average Read Speed: 32765.0 bytes/s, Average Write Speed: 32752.88 bytes/s
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Disguise as HTTPS server with whitelist
|
|
130
|
+
Any connection beyond whitelist will be served by a mock https server. Real service can thus be hided.
|
|
131
|
+
|
|
132
|
+
For example, you can protect your Scurrying Squirrel against attack from Grim Foolish Weasel.
|
|
133
|
+
|
|
134
|
+
```commandline
|
|
135
|
+
> simple-proxy -rp 8388 -lp 443 -g --run-mock-tls-server -wl=<your ip>,<your wife's ip>,<your friend's wife's ip>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+

|
|
139
|
+
|
|
140
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# simple-proxy :rocket:
|
|
2
|
+
|
|
3
|
+
A very simple TCP proxy tool (not http proxy) empowered by nio tcp framework [py-netty](https://pypi.org/project/py-netty/)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install simple-proxy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
Usage: simple-proxy [OPTIONS]
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-l, --local-server TEXT Local server address [default: localhost]
|
|
22
|
+
-lp, --local-port INTEGER Local port [default: 8080]
|
|
23
|
+
-r, --remote-server TEXT Remote server address [default: localhost]
|
|
24
|
+
-rp, --remote-port INTEGER Remote port [default: 80]
|
|
25
|
+
-g, --global Listen on 0.0.0.0
|
|
26
|
+
-c, --tcp-flow Dump tcp flow on to console
|
|
27
|
+
-f, --save-tcp-flow Save tcp flow to file
|
|
28
|
+
-s, --tls Denote remote server listening on secure
|
|
29
|
+
port
|
|
30
|
+
-ss Denote local sever listening on secure port
|
|
31
|
+
-kf, --key-file PATH Key file for local server
|
|
32
|
+
-cf, --cert-file PATH Certificate file for local server
|
|
33
|
+
--speed-monitor Print speed info to console for established
|
|
34
|
+
connection
|
|
35
|
+
--speed-monitor-interval INTEGER
|
|
36
|
+
Speed monitor interval [default: 5]
|
|
37
|
+
-dti, --disguise-tls-ip TEXT Disguise TLS IP
|
|
38
|
+
-dtp, --disguise-tls-port INTEGER
|
|
39
|
+
Disguise TLS port [default: 443]
|
|
40
|
+
-wl, --white-list TEXT IP White list for incoming connections
|
|
41
|
+
(comma separated)
|
|
42
|
+
--run-mock-tls-server Run mock TLS server
|
|
43
|
+
-v, --verbose Verbose mode
|
|
44
|
+
-h, --help Show this message and exit.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Features
|
|
49
|
+
### Basic proxy (TLS termination)
|
|
50
|
+
```commandline
|
|
51
|
+
> simple-proxy --tls -r www.google.com -rp 443 -lp 8080
|
|
52
|
+
Proxy server started listening: localhost:8080 => www.google.com:443(TLS) ...
|
|
53
|
+
console:False, file:False, disguise:n/a, whitelist:*
|
|
54
|
+
> curl -I -H 'Host: www.google.com' http://localhost:8080
|
|
55
|
+
HTTP/1.1 200 OK
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```commandline
|
|
60
|
+
> simple-proxy -r www.google.com -rp 80 -lp 8443 -ss
|
|
61
|
+
Proxy server started listening: localhost:8443(TLS) => www.google.com:80 ...
|
|
62
|
+
console:False, file:False, disguise:n/a, whitelist:*
|
|
63
|
+
> curl -I -H 'Host: www.google.com' -k https://localhost:8443
|
|
64
|
+
HTTP/1.1 200 OK
|
|
65
|
+
...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Dump TCP flow
|
|
69
|
+
TCP flow can be dumped into console or files (under directory __tcpflow__)
|
|
70
|
+
```commandline
|
|
71
|
+
> simple-proxy -r www.google.com -rp 443 -lp 8443 -ss -s -c -f
|
|
72
|
+
Proxy server started listening: localhost:8443(TLS) => www.google.com:443(TLS) ...
|
|
73
|
+
console:True, file:True, disguise:n/a, whitelist:*
|
|
74
|
+
> curl -k -I -H 'Host: www.google.com' https://localhost:8443
|
|
75
|
+
```
|
|
76
|
+

|
|
77
|
+
|
|
78
|
+
### Connection status monitor
|
|
79
|
+
```commandline
|
|
80
|
+
> $ simple-proxy -r echo-server.proxy.com -rp 8080 -lp 48080 --speed-monitor
|
|
81
|
+
Proxy server started listening: localhost:48080 => echo-server.proxy.com:8080 ...
|
|
82
|
+
console:False, file:False, disguise:n/a, whitelist:*
|
|
83
|
+
Connection opened: ('127.0.0.1', 60937)
|
|
84
|
+
Connection opened: ('127.0.0.1', 60938)
|
|
85
|
+
Connection opened: ('127.0.0.1', 60939)
|
|
86
|
+
Connection opened: ('127.0.0.1', 60940)
|
|
87
|
+
Connection opened: ('127.0.0.1', 60941)
|
|
88
|
+
Connection opened: ('127.0.0.1', 60942)
|
|
89
|
+
Connection opened: ('127.0.0.1', 60943)
|
|
90
|
+
Connection opened: ('127.0.0.1', 60944)
|
|
91
|
+
---------------------------2024-02-12 17:43:02.337268 (total:8, rounds:1)---------------------------
|
|
92
|
+
[ 1] | 127.0.0.1:60937 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:235.00 K | duration: 7s
|
|
93
|
+
[ 2] | 127.0.0.1:60938 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
94
|
+
[ 3] | 127.0.0.1:60939 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
95
|
+
[ 4] | 127.0.0.1:60940 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
96
|
+
[ 5] | 127.0.0.1:60941 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
97
|
+
[ 6] | 127.0.0.1:60942 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:234.00 K Tx:234.00 K | duration: 7s
|
|
98
|
+
[ 7] | 127.0.0.1:60943 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:234.00 K Tx:234.00 K | duration: 7s
|
|
99
|
+
[ 8] | 127.0.0.1:60944 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:234.00 K Tx:234.00 K | duration: 7s
|
|
100
|
+
Average Read Speed: 32765.0 bytes/s, Average Write Speed: 32752.88 bytes/s
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Disguise as HTTPS server with whitelist
|
|
104
|
+
Any connection beyond whitelist will be served by a mock https server. Real service can thus be hided.
|
|
105
|
+
|
|
106
|
+
For example, you can protect your Scurrying Squirrel against attack from Grim Foolish Weasel.
|
|
107
|
+
|
|
108
|
+
```commandline
|
|
109
|
+
> simple-proxy -rp 8388 -lp 443 -g --run-mock-tls-server -wl=<your ip>,<your wife's ip>,<your friend's wife's ip>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
114
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# setup.py
|
|
2
|
+
from setuptools import setup, find_packages
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
this_directory = Path(__file__).parent
|
|
6
|
+
long_description = (this_directory / "README.md").read_text()
|
|
7
|
+
# install_requires = (this_directory / 'requirements.txt').read_text().splitlines()
|
|
8
|
+
|
|
9
|
+
__version__ = None
|
|
10
|
+
|
|
11
|
+
exec(open("simple_proxy/version.py").read())
|
|
12
|
+
|
|
13
|
+
config = {
|
|
14
|
+
'name': 'simple-proxy',
|
|
15
|
+
'url': 'https://github.com/ruanhao/simple-proxy',
|
|
16
|
+
'license': 'MIT',
|
|
17
|
+
"long_description": long_description,
|
|
18
|
+
"long_description_content_type": 'text/markdown',
|
|
19
|
+
'description': 'A very simple NIO TCP proxy server',
|
|
20
|
+
'author' : 'Hao Ruan',
|
|
21
|
+
'author_email': 'ruanhao1116@gmail.com',
|
|
22
|
+
'keywords': ['network', 'tcp', 'non-blocking', 'proxy'],
|
|
23
|
+
'version': __version__,
|
|
24
|
+
'packages': find_packages(),
|
|
25
|
+
'install_requires': ['click', 'py-netty==0.0.38', 'cryptography'],
|
|
26
|
+
'python_requires': ">=3.7, <4",
|
|
27
|
+
'setup_requires': ['wheel'],
|
|
28
|
+
'package_data': {'simple_proxy': ['*']},
|
|
29
|
+
'entry_points': {
|
|
30
|
+
'console_scripts': [
|
|
31
|
+
'simple-proxy = simple_proxy.__init__:_run',
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
'classifiers': [
|
|
35
|
+
"Intended Audience :: Developers",
|
|
36
|
+
'License :: OSI Approved :: MIT License',
|
|
37
|
+
"Natural Language :: English",
|
|
38
|
+
"Operating System :: OS Independent",
|
|
39
|
+
"Programming Language :: Python",
|
|
40
|
+
"Programming Language :: Python :: 3",
|
|
41
|
+
"Programming Language :: Python :: 3.7",
|
|
42
|
+
"Programming Language :: Python :: 3.8",
|
|
43
|
+
"Programming Language :: Python :: 3.9",
|
|
44
|
+
"Programming Language :: Python :: 3.10",
|
|
45
|
+
"Programming Language :: Python :: 3.11",
|
|
46
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
47
|
+
"Topic :: Software Development :: Libraries",
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setup(**config)
|
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
import socket
|
|
2
|
+
import random
|
|
3
|
+
import http.server
|
|
4
|
+
import ssl
|
|
5
|
+
from functools import wraps, partial
|
|
6
|
+
from cryptography.hazmat.backends import default_backend
|
|
7
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
8
|
+
from cryptography import x509
|
|
9
|
+
from cryptography.x509.oid import NameOID
|
|
10
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
11
|
+
from py_netty.handler import LoggingChannelHandler
|
|
12
|
+
from py_netty import Bootstrap, ServerBootstrap, EventLoopGroup
|
|
13
|
+
import inspect
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import traceback
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
import os
|
|
19
|
+
import click
|
|
20
|
+
from datetime import datetime, timedelta
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
import codecs
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
import time
|
|
26
|
+
from attrs import define, field
|
|
27
|
+
import threading
|
|
28
|
+
import itertools
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
logger.setLevel(logging.WARNING)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _setup_logging(level=logging.INFO):
|
|
36
|
+
logging.basicConfig(
|
|
37
|
+
handlers=[
|
|
38
|
+
logging.StreamHandler(), # default to stderr
|
|
39
|
+
],
|
|
40
|
+
level=level,
|
|
41
|
+
format='%(asctime)s.%(msecs)03d - %(name)s - %(levelname)s - %(threadName)s - %(message)s',
|
|
42
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
__ALL__ = ['run_proxy']
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_speed_monitor = True
|
|
50
|
+
_counter = itertools.count()
|
|
51
|
+
_stderr = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _submit_daemon_thread(func, *args, **kwargs) -> threading.Thread:
|
|
55
|
+
if isinstance(func, partial):
|
|
56
|
+
func_name = func.func.__name__
|
|
57
|
+
else:
|
|
58
|
+
func_name = func.__name__
|
|
59
|
+
|
|
60
|
+
def _worker():
|
|
61
|
+
func(*args, **kwargs)
|
|
62
|
+
|
|
63
|
+
t = threading.Thread(target=_worker, name=f'{func_name}-daemon-{next(_counter)}', daemon=True)
|
|
64
|
+
t.start()
|
|
65
|
+
return t
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _random_sentence():
|
|
69
|
+
nouns = ("puppy", "car", "rabbit", "girl", "monkey")
|
|
70
|
+
verbs = ("runs", "hits", "jumps", "drives", "barfs")
|
|
71
|
+
adv = ("crazily.", "dutifully.", "foolishly.", "merrily.", "occasionally.")
|
|
72
|
+
return nouns[random.randrange(0, 5)] + ' ' + \
|
|
73
|
+
verbs[random.randrange(0, 5)] + ' ' + \
|
|
74
|
+
adv[random.randrange(0, 5)] + '\n'
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def pstderr(msg):
|
|
78
|
+
logger.debug(msg)
|
|
79
|
+
if _stderr:
|
|
80
|
+
click.echo(msg, err=True)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def pfatal(msg):
|
|
84
|
+
logger.critical(msg)
|
|
85
|
+
exit(1)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _pretty_duration(seconds: int) -> str:
|
|
89
|
+
TIME_DURATION_UNITS = (
|
|
90
|
+
('W', 60 * 60 * 24 * 7),
|
|
91
|
+
('D', 60 * 60 * 24),
|
|
92
|
+
('H', 60 * 60),
|
|
93
|
+
('M', 60),
|
|
94
|
+
('S', 1)
|
|
95
|
+
)
|
|
96
|
+
if seconds == 0:
|
|
97
|
+
return '0S'
|
|
98
|
+
parts = []
|
|
99
|
+
for unit, div in TIME_DURATION_UNITS:
|
|
100
|
+
amount, seconds = divmod(int(seconds), div)
|
|
101
|
+
if amount > 0:
|
|
102
|
+
parts.append('{}{}'.format(amount, unit))
|
|
103
|
+
return ', '.join(parts)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _format_bytes(size, scale=1):
|
|
107
|
+
# 2**10 = 1024
|
|
108
|
+
size = int(size)
|
|
109
|
+
power = 2**10
|
|
110
|
+
n = 0
|
|
111
|
+
power_labels = {0 : '', 1: 'K', 2: 'M', 3: 'G', 4: 'T'}
|
|
112
|
+
while size > power:
|
|
113
|
+
size /= power
|
|
114
|
+
size = round(size, scale)
|
|
115
|
+
n += 1
|
|
116
|
+
return size, power_labels[n]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@define(slots=True, kw_only=True, order=True)
|
|
120
|
+
class _Client():
|
|
121
|
+
last_read_time: float = field(factory=time.time)
|
|
122
|
+
total_read_bytes: int = field(default=0)
|
|
123
|
+
cumulative_read_bytes: int = field(default=0) # bytes
|
|
124
|
+
cumulative_read_time: float = field(default=0.0) # seconds
|
|
125
|
+
rbps: float = field(default=0.0)
|
|
126
|
+
born_time: float = field(factory=time.time)
|
|
127
|
+
|
|
128
|
+
last_write_time: float = field(factory=time.time)
|
|
129
|
+
total_write_bytes: int = field(default=0)
|
|
130
|
+
cumulative_write_bytes: int = field(default=0) # bytes
|
|
131
|
+
cumulative_write_time: float = field(default=0.0) # seconds
|
|
132
|
+
wbps: float = field(default=0.0)
|
|
133
|
+
|
|
134
|
+
def pretty_born_time(self):
|
|
135
|
+
return _pretty_duration(time.time() - self.born_time)
|
|
136
|
+
|
|
137
|
+
def pretty_speed(self):
|
|
138
|
+
v, unit = _format_bytes(self.rbps)
|
|
139
|
+
return f"{v:.2f} {unit}/s"
|
|
140
|
+
|
|
141
|
+
def pretty_wspeed(self):
|
|
142
|
+
v, unit = _format_bytes(self.wbps)
|
|
143
|
+
return f"{v:.2f} {unit}/s"
|
|
144
|
+
|
|
145
|
+
def pretty_total(self):
|
|
146
|
+
v, unit = _format_bytes(self.total_read_bytes)
|
|
147
|
+
if unit:
|
|
148
|
+
return f"{v:.2f} {unit}"
|
|
149
|
+
else:
|
|
150
|
+
return f"{v} B"
|
|
151
|
+
|
|
152
|
+
def pretty_wtotal(self):
|
|
153
|
+
v, unit = _format_bytes(self.total_write_bytes)
|
|
154
|
+
if unit:
|
|
155
|
+
return f"{v:.2f} {unit}"
|
|
156
|
+
else:
|
|
157
|
+
return f"{v} B"
|
|
158
|
+
|
|
159
|
+
def read(self, size):
|
|
160
|
+
current_time = time.time()
|
|
161
|
+
self.cumulative_read_time += (current_time - self.last_read_time)
|
|
162
|
+
self.last_read_time = current_time
|
|
163
|
+
self.total_read_bytes += size
|
|
164
|
+
self.cumulative_read_bytes += size
|
|
165
|
+
if self.cumulative_read_time > 1:
|
|
166
|
+
self.rbps = int(self.cumulative_read_bytes / self.cumulative_read_time) # bytes per second
|
|
167
|
+
self.cumulative_read_time = 0
|
|
168
|
+
self.cumulative_read_bytes = 0
|
|
169
|
+
|
|
170
|
+
def write(self, size):
|
|
171
|
+
current_time = time.time()
|
|
172
|
+
self.cumulative_write_time += (current_time - self.last_write_time)
|
|
173
|
+
self.last_write_time = current_time
|
|
174
|
+
self.total_write_bytes += size
|
|
175
|
+
self.cumulative_write_bytes += size
|
|
176
|
+
if self.cumulative_write_time > 1:
|
|
177
|
+
self.wbps = int(self.cumulative_write_bytes / self.cumulative_write_time) # bytes per second
|
|
178
|
+
self.cumulative_write_time = 0
|
|
179
|
+
self.cumulative_write_bytes = 0
|
|
180
|
+
|
|
181
|
+
def check(self):
|
|
182
|
+
if time.time() - self.last_read_time > 2:
|
|
183
|
+
self.rbps = 0
|
|
184
|
+
self.cumulative_read_time = 0
|
|
185
|
+
self.cumulative_read_bytes = 0
|
|
186
|
+
self.wbps = 0
|
|
187
|
+
self.cumulative_write_time = 0
|
|
188
|
+
self.cumulative_write_bytes = 0
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
_clients = defaultdict(_Client)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _check_patterns(patterns, s):
|
|
195
|
+
for pattern in patterns:
|
|
196
|
+
if re.search(pattern, s):
|
|
197
|
+
logger.debug(f"pattern {pattern} matched {s}")
|
|
198
|
+
return True
|
|
199
|
+
logger.warning(f"no pattern matched {s}")
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _pattern_to_regex(pattern: str) -> str:
|
|
204
|
+
regex_pattern = re.escape(pattern)
|
|
205
|
+
regex_pattern = regex_pattern.replace(r'\*', r'.*')
|
|
206
|
+
return regex_pattern
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _all_args_repr(args, kw):
|
|
210
|
+
try:
|
|
211
|
+
args_repr = [repr(arg) for arg in args]
|
|
212
|
+
kws = [f"{k}={repr(v)}" for k, v in kw.items()]
|
|
213
|
+
return ', '.join(args_repr + kws)
|
|
214
|
+
except Exception:
|
|
215
|
+
return "(?)"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def sneaky():
|
|
219
|
+
|
|
220
|
+
def decorate(func):
|
|
221
|
+
@wraps(func)
|
|
222
|
+
def wrapper(*args, **kw):
|
|
223
|
+
all_args = _all_args_repr(args, kw)
|
|
224
|
+
try:
|
|
225
|
+
return func(*args, **kw)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
emsg = f"[{e}] sneaky call: {func.__name__}({all_args})"
|
|
228
|
+
if logger:
|
|
229
|
+
logger.exception(emsg)
|
|
230
|
+
print(emsg, traceback.format_exc(), file=sys.stderr, sep=os.linesep, flush=True)
|
|
231
|
+
return wrapper
|
|
232
|
+
return decorate
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _from_cwd(*args):
|
|
236
|
+
absolute = Path(os.path.join(os.getcwd(), *args))
|
|
237
|
+
absolute.parent.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
return absolute
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _module_path(mod=None):
|
|
242
|
+
if not mod:
|
|
243
|
+
frm = inspect.stack()[1]
|
|
244
|
+
mod = inspect.getmodule(frm[0])
|
|
245
|
+
return os.path.dirname(mod.__file__)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _from_module(filename=None):
|
|
249
|
+
frm = inspect.stack()[1]
|
|
250
|
+
mod = inspect.getmodule(frm[0])
|
|
251
|
+
if not filename:
|
|
252
|
+
return _module_path(mod)
|
|
253
|
+
return os.path.join(_module_path(mod), filename)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@sneaky()
|
|
257
|
+
def _handle(buffer, direction, src, dst, print_content, to_file):
|
|
258
|
+
src_ip, src_port = src.getpeername()
|
|
259
|
+
dst_ip, dst_port = dst.getpeername()
|
|
260
|
+
|
|
261
|
+
raddr = (src_ip, src_port) if direction else (dst_ip, dst_port)
|
|
262
|
+
client = _clients[raddr]
|
|
263
|
+
if buffer:
|
|
264
|
+
if direction:
|
|
265
|
+
client.read(len(buffer))
|
|
266
|
+
else:
|
|
267
|
+
client.write(len(buffer))
|
|
268
|
+
else: # EOF
|
|
269
|
+
del _clients[raddr]
|
|
270
|
+
return buffer
|
|
271
|
+
|
|
272
|
+
if not print_content and not to_file:
|
|
273
|
+
return buffer
|
|
274
|
+
content = buffer.decode('ascii', errors='using_dot')
|
|
275
|
+
src_ip = src_ip.replace(':', '_')
|
|
276
|
+
dst_ip = dst_ip.replace(':', '_')
|
|
277
|
+
filename = ('L' if direction else 'R') + f'_{src_ip}_{src_port}_{dst_ip}_{dst_port}.log'
|
|
278
|
+
if to_file:
|
|
279
|
+
with _from_cwd('__tcpflow__', filename).open('a') as f:
|
|
280
|
+
f.write(content)
|
|
281
|
+
if print_content:
|
|
282
|
+
click.secho(content, fg='green' if direction else 'yellow')
|
|
283
|
+
return buffer
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _clients_check(interval):
|
|
287
|
+
ever = False
|
|
288
|
+
zzz = 0
|
|
289
|
+
rounds = 0
|
|
290
|
+
while True and _speed_monitor:
|
|
291
|
+
clients_snapshot = _clients.copy()
|
|
292
|
+
items = list(clients_snapshot.items())
|
|
293
|
+
items.sort(key=lambda x: x[1].born_time)
|
|
294
|
+
total = len(clients_snapshot)
|
|
295
|
+
if total:
|
|
296
|
+
rounds += 1
|
|
297
|
+
pstderr(f'{datetime.now()} (total:{total}, rounds:{rounds})'.center(100, '-'))
|
|
298
|
+
ever = True
|
|
299
|
+
zzz = 0
|
|
300
|
+
else:
|
|
301
|
+
if zzz % 60 == 0 and ever:
|
|
302
|
+
rounds += 1
|
|
303
|
+
pstderr(f"{datetime.now()} No client connected (rounds:{rounds})".center(100, '-'))
|
|
304
|
+
zzz += 1
|
|
305
|
+
|
|
306
|
+
count = 1
|
|
307
|
+
for address, client in items:
|
|
308
|
+
client.check()
|
|
309
|
+
ip, port = address
|
|
310
|
+
ipport = f"{ip}:{port}"
|
|
311
|
+
pspeed = client.pretty_speed()
|
|
312
|
+
ptotal = client.pretty_total()
|
|
313
|
+
pwspeed = client.pretty_wspeed()
|
|
314
|
+
pwtotal = client.pretty_wtotal()
|
|
315
|
+
duration = client.pretty_born_time().lower()
|
|
316
|
+
pstderr(f"[{count:3}] | {ipport:21} | Speed Rx:{pspeed:10} Tx:{pwspeed:10} | Total Rx:{ptotal:10} Tx:{pwtotal:10} | duration: {duration}")
|
|
317
|
+
count += 1
|
|
318
|
+
if total:
|
|
319
|
+
average_speed = round(sum([c.rbps for c in clients_snapshot.values()]) / total, 2)
|
|
320
|
+
average_wspeed = round(sum([c.wbps for c in clients_snapshot.values()]) / total, 2)
|
|
321
|
+
pstderr(f"{'Average Read Speed:':<20} {average_speed} bytes/s, {'Average Write Speed:':<20} {average_wspeed} bytes/s")
|
|
322
|
+
|
|
323
|
+
time.sleep(interval)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ProxyChannelHandler(LoggingChannelHandler):
|
|
327
|
+
def __init__(
|
|
328
|
+
self,
|
|
329
|
+
remote_host, remote_port,
|
|
330
|
+
client_eventloop_group,
|
|
331
|
+
tls=False, content=False, to_file=False,
|
|
332
|
+
disguise_tls_ip=None, disguise_tls_port=None,
|
|
333
|
+
white_list=None,
|
|
334
|
+
):
|
|
335
|
+
self._remote_host = remote_host
|
|
336
|
+
self._remote_port = remote_port
|
|
337
|
+
self._client_eventloop_group = client_eventloop_group
|
|
338
|
+
self._tls = tls
|
|
339
|
+
self._client = None
|
|
340
|
+
self._content = content
|
|
341
|
+
self._to_file = to_file
|
|
342
|
+
|
|
343
|
+
self._disguise_tls_ip = disguise_tls_ip
|
|
344
|
+
self._disguise_tls_port = disguise_tls_port
|
|
345
|
+
self._white_list = white_list
|
|
346
|
+
|
|
347
|
+
def _client_channel(self, ctx0, ip, port):
|
|
348
|
+
|
|
349
|
+
class _ChannelHandler(LoggingChannelHandler):
|
|
350
|
+
|
|
351
|
+
def channel_read(this, ctx, bytebuf):
|
|
352
|
+
_handle(bytebuf, False, ctx.channel().socket(), ctx0.channel().socket(), self._content, self._to_file)
|
|
353
|
+
ctx0.write(bytebuf)
|
|
354
|
+
|
|
355
|
+
def channel_inactive(this, ctx):
|
|
356
|
+
super().channel_inactive(ctx)
|
|
357
|
+
ctx0.close()
|
|
358
|
+
|
|
359
|
+
if self._client is None:
|
|
360
|
+
self._client = Bootstrap(
|
|
361
|
+
eventloop_group=self._client_eventloop_group,
|
|
362
|
+
handler_initializer=_ChannelHandler,
|
|
363
|
+
tls=self._tls,
|
|
364
|
+
verify=False
|
|
365
|
+
).connect(ip, port, True).sync().channel()
|
|
366
|
+
return self._client
|
|
367
|
+
|
|
368
|
+
def exception_caught(self, ctx, exception):
|
|
369
|
+
super().exception_caught(ctx, exception)
|
|
370
|
+
ctx.close()
|
|
371
|
+
|
|
372
|
+
def channel_active(self, ctx):
|
|
373
|
+
super().channel_active(ctx)
|
|
374
|
+
self.raddr = ctx.channel().socket().getpeername()
|
|
375
|
+
pstderr(f"Connection opened: {self.raddr}")
|
|
376
|
+
_clients[self.raddr]
|
|
377
|
+
|
|
378
|
+
def channel_read(self, ctx, bytebuf):
|
|
379
|
+
super().channel_read(ctx, bytebuf)
|
|
380
|
+
if self._client is None:
|
|
381
|
+
if self._white_list and not _check_patterns(self._white_list, ctx.channel().socket().getpeername()[0]):
|
|
382
|
+
pstderr(f"malicious visitor: {ctx.channel().socket().getpeername()}")
|
|
383
|
+
self._client_channel(ctx, self._disguise_tls_ip, self._disguise_tls_port)
|
|
384
|
+
else:
|
|
385
|
+
self._client_channel(ctx, self._remote_host, self._remote_port)
|
|
386
|
+
|
|
387
|
+
_handle(bytebuf, True, ctx.channel().socket(), self._client.socket(), self._content, self._to_file)
|
|
388
|
+
self._client.write(bytebuf)
|
|
389
|
+
|
|
390
|
+
def channel_inactive(self, ctx):
|
|
391
|
+
super().channel_inactive(ctx)
|
|
392
|
+
if hasattr(self, 'raddr'):
|
|
393
|
+
pstderr(f"Connection closed: {self.raddr}")
|
|
394
|
+
del _clients[self.raddr]
|
|
395
|
+
if self._client:
|
|
396
|
+
self._client.close()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class MyHttpHandler(http.server.BaseHTTPRequestHandler):
|
|
400
|
+
def log_message(self, format, *args):
|
|
401
|
+
# no log
|
|
402
|
+
pass
|
|
403
|
+
|
|
404
|
+
def do_GET(self):
|
|
405
|
+
self.send_response(200)
|
|
406
|
+
self.send_header('Content-type', 'text/plain')
|
|
407
|
+
self.end_headers()
|
|
408
|
+
self.wfile.write(_random_sentence().encode('utf-8'))
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@click.command(short_help="Simple proxy", context_settings=dict(help_option_names=['-h', '--help']))
|
|
412
|
+
@click.option('--local-server', '-l', default='localhost', help='Local server address', show_default=True)
|
|
413
|
+
@click.option('--local-port', '-lp', type=int, default=8080, help='Local port', show_default=True)
|
|
414
|
+
@click.option('--remote-server', '-r', default='localhost', help='Remote server address', show_default=True)
|
|
415
|
+
@click.option('--remote-port', '-rp', type=int, default=80, help='Remote port', show_default=True)
|
|
416
|
+
@click.option('--global', '-g', 'using_global', is_flag=True, help='Listen on 0.0.0.0')
|
|
417
|
+
@click.option('--tcp-flow', '-c', 'content', is_flag=True, help='Dump tcp flow on to console')
|
|
418
|
+
@click.option('--save-tcp-flow', '-f', 'to_file', is_flag=True, help='Save tcp flow to file')
|
|
419
|
+
@click.option('--tls', '-s', is_flag=True, help='Denote remote server listening on secure port')
|
|
420
|
+
@click.option('-ss', is_flag=True, help='Denote local sever listening on secure port')
|
|
421
|
+
@click.option('--key-file', '-kf', help='Key file for local server', type=click.Path(exists=True))
|
|
422
|
+
@click.option('--cert-file', '-cf', help='Certificate file for local server', type=click.Path(exists=True))
|
|
423
|
+
@click.option('--speed-monitor', is_flag=True, help='Print speed info to console for established connection')
|
|
424
|
+
@click.option('--speed-monitor-interval', type=int, default=5, help='Speed monitor interval', show_default=True)
|
|
425
|
+
@click.option('--disguise-tls-ip', '-dti', help='Disguise TLS IP')
|
|
426
|
+
@click.option('--disguise-tls-port', '-dtp', type=int, help='Disguise TLS port', default=443, show_default=True)
|
|
427
|
+
@click.option('--white-list', '-wl', help='IP White list for incoming connections (comma separated)')
|
|
428
|
+
@click.option('--run-mock-tls-server', is_flag=True, help='Run mock TLS server')
|
|
429
|
+
@click.option('--verbose', '-v', is_flag=True, help='Verbose mode')
|
|
430
|
+
def _cli(verbose, **kwargs):
|
|
431
|
+
if verbose:
|
|
432
|
+
_setup_logging(logging.INFO)
|
|
433
|
+
logger.setLevel(logging.DEBUG)
|
|
434
|
+
run_proxy(**kwargs)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def run_proxy(
|
|
438
|
+
local_server, local_port,
|
|
439
|
+
remote_server, remote_port,
|
|
440
|
+
using_global,
|
|
441
|
+
content, to_file,
|
|
442
|
+
tls, ss,
|
|
443
|
+
key_file, cert_file,
|
|
444
|
+
speed_monitor, speed_monitor_interval,
|
|
445
|
+
disguise_tls_ip, disguise_tls_port,
|
|
446
|
+
white_list,
|
|
447
|
+
run_mock_tls_server
|
|
448
|
+
):
|
|
449
|
+
if tls and (disguise_tls_ip or run_mock_tls_server):
|
|
450
|
+
pfatal("'--tls/-s' is not applicable if disguise is used!")
|
|
451
|
+
if not white_list and (disguise_tls_ip or run_mock_tls_server):
|
|
452
|
+
pstderr("[WARN] disguise is not took effect if '--white-list/-wl' is not specified")
|
|
453
|
+
|
|
454
|
+
white_list0 = white_list or ''
|
|
455
|
+
if white_list:
|
|
456
|
+
white_list = white_list.split(',')
|
|
457
|
+
white_list = [_pattern_to_regex(x) for x in white_list]
|
|
458
|
+
|
|
459
|
+
if using_global:
|
|
460
|
+
local_server = '0.0.0.0'
|
|
461
|
+
|
|
462
|
+
codecs.register_error('using_dot', lambda e: ('.', e.start + 1))
|
|
463
|
+
|
|
464
|
+
cf = None
|
|
465
|
+
kf = None
|
|
466
|
+
if ss:
|
|
467
|
+
assert (key_file and cert_file) or (not key_file and not cert_file), "Both key and cert files are required"
|
|
468
|
+
if key_file and cert_file:
|
|
469
|
+
kf = key_file
|
|
470
|
+
cf = cert_file
|
|
471
|
+
else:
|
|
472
|
+
kf, cf = create_temp_key_cert()
|
|
473
|
+
|
|
474
|
+
if run_mock_tls_server:
|
|
475
|
+
disguise_tls_ip = 'localhost'
|
|
476
|
+
disguise_tls_port = _free_port()
|
|
477
|
+
server_address = (disguise_tls_ip, disguise_tls_port)
|
|
478
|
+
kf_mock, cf_mock = create_temp_key_cert(True)
|
|
479
|
+
httpd = http.server.HTTPServer(server_address, MyHttpHandler)
|
|
480
|
+
httpd.socket = ssl.wrap_socket(httpd.socket,
|
|
481
|
+
server_side=True,
|
|
482
|
+
certfile=cf_mock,
|
|
483
|
+
keyfile=kf_mock,
|
|
484
|
+
ssl_version=ssl.PROTOCOL_TLS)
|
|
485
|
+
_submit_daemon_thread(httpd.serve_forever)
|
|
486
|
+
|
|
487
|
+
client_eventloop_group = EventLoopGroup(1, 'Client')
|
|
488
|
+
sb = ServerBootstrap(
|
|
489
|
+
parant_group=EventLoopGroup(1, 'Boss'),
|
|
490
|
+
child_group=EventLoopGroup(1, 'Worker'),
|
|
491
|
+
child_handler_initializer=lambda: ProxyChannelHandler(
|
|
492
|
+
remote_server, remote_port,
|
|
493
|
+
client_eventloop_group,
|
|
494
|
+
tls=tls,
|
|
495
|
+
content=content, to_file=to_file,
|
|
496
|
+
disguise_tls_ip=disguise_tls_ip, disguise_tls_port=disguise_tls_port,
|
|
497
|
+
white_list=white_list
|
|
498
|
+
),
|
|
499
|
+
certfile=cf,
|
|
500
|
+
keyfile=kf,
|
|
501
|
+
)
|
|
502
|
+
disguise = f"https://{disguise_tls_ip}:{disguise_tls_port}" if disguise_tls_ip else 'n/a'
|
|
503
|
+
pstderr(f"Proxy server started listening: {local_server}:{local_port}{'(TLS)' if ss else ''} => {remote_server}:{remote_port}{'(TLS)' if tls else ''} ...")
|
|
504
|
+
pstderr(f"console:{content}, file:{to_file}, disguise:{disguise}, whitelist:{white_list0 or '*'}")
|
|
505
|
+
|
|
506
|
+
if speed_monitor:
|
|
507
|
+
import signal
|
|
508
|
+
_submit_daemon_thread(_clients_check, speed_monitor_interval)
|
|
509
|
+
|
|
510
|
+
def _signal_handler(sig, frame):
|
|
511
|
+
global _speed_monitor
|
|
512
|
+
_speed_monitor = False
|
|
513
|
+
signal.default_int_handler(sig, frame)
|
|
514
|
+
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
515
|
+
|
|
516
|
+
signal.signal(signal.SIGINT, _signal_handler)
|
|
517
|
+
sb.bind(address=local_server, port=local_port).close_future().sync()
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def generate_private_key():
|
|
521
|
+
private_key = rsa.generate_private_key(
|
|
522
|
+
public_exponent=65537,
|
|
523
|
+
key_size=2048,
|
|
524
|
+
backend=default_backend()
|
|
525
|
+
)
|
|
526
|
+
return private_key
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def generate_self_signed_cert(private_key, subject_name, valid_days=365):
|
|
530
|
+
subject = x509.Name([
|
|
531
|
+
x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
|
|
532
|
+
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"CA"),
|
|
533
|
+
x509.NameAttribute(NameOID.LOCALITY_NAME, u"San Francisco"),
|
|
534
|
+
x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"Simple Proxy"),
|
|
535
|
+
x509.NameAttribute(NameOID.COMMON_NAME, subject_name),
|
|
536
|
+
])
|
|
537
|
+
|
|
538
|
+
issuer = subject
|
|
539
|
+
|
|
540
|
+
cert = x509.CertificateBuilder().subject_name(
|
|
541
|
+
subject
|
|
542
|
+
).issuer_name(
|
|
543
|
+
issuer
|
|
544
|
+
).public_key(
|
|
545
|
+
private_key.public_key()
|
|
546
|
+
).serial_number(
|
|
547
|
+
x509.random_serial_number()
|
|
548
|
+
).not_valid_before(
|
|
549
|
+
datetime.utcnow()
|
|
550
|
+
).not_valid_after(
|
|
551
|
+
datetime.utcnow() + timedelta(days=valid_days)
|
|
552
|
+
).add_extension(
|
|
553
|
+
x509.SubjectAlternativeName([
|
|
554
|
+
x509.DNSName(subject_name),
|
|
555
|
+
]),
|
|
556
|
+
critical=False,
|
|
557
|
+
).sign(
|
|
558
|
+
private_key,
|
|
559
|
+
algorithm=hashes.SHA256(),
|
|
560
|
+
backend=default_backend()
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
return cert
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def save_key_and_cert(private_key, cert, key_file_path, cert_file_path):
|
|
567
|
+
# Save private key
|
|
568
|
+
with open(key_file_path, 'wb') as key_file:
|
|
569
|
+
key_file.write(
|
|
570
|
+
private_key.private_bytes(
|
|
571
|
+
encoding=serialization.Encoding.PEM,
|
|
572
|
+
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
573
|
+
encryption_algorithm=serialization.NoEncryption()
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
# Save certificate
|
|
578
|
+
with open(cert_file_path, 'wb') as cert_file:
|
|
579
|
+
cert_file.write(
|
|
580
|
+
cert.public_bytes(serialization.Encoding.PEM)
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def create_temp_file(filename):
|
|
585
|
+
temp_dir = tempfile.mkdtemp()
|
|
586
|
+
file_path = os.path.join(temp_dir, filename)
|
|
587
|
+
with open(file_path, 'w'):
|
|
588
|
+
pass
|
|
589
|
+
return file_path
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def create_temp_key_cert(is_for_mock=False):
|
|
593
|
+
kf_obj = generate_private_key()
|
|
594
|
+
cf_obj = generate_self_signed_cert(kf_obj, 'localhost')
|
|
595
|
+
kf = create_temp_file('key.pem')
|
|
596
|
+
cf = create_temp_file('cert.pem')
|
|
597
|
+
if is_for_mock:
|
|
598
|
+
logger.debug(f"[Mock] Generated key and cert: {kf}, {cf}")
|
|
599
|
+
else:
|
|
600
|
+
logger.debug(f"Generated key and cert: {kf}, {cf}")
|
|
601
|
+
save_key_and_cert(kf_obj, cf_obj, kf, cf)
|
|
602
|
+
return kf, cf
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _free_port():
|
|
606
|
+
temp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
607
|
+
temp_socket.bind(('localhost', 0))
|
|
608
|
+
_, port = temp_socket.getsockname()
|
|
609
|
+
temp_socket.close()
|
|
610
|
+
return port
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _run():
|
|
614
|
+
global _stderr
|
|
615
|
+
_stderr = True
|
|
616
|
+
_cli()
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _test_pattern_to_regex():
|
|
620
|
+
assert _pattern_to_regex('*.example.com') == r'.*\.example\.com'
|
|
621
|
+
assert _pattern_to_regex('example.com') == r'example\.com'
|
|
622
|
+
assert _pattern_to_regex('example.*.com') == r'example\..*\.com'
|
|
623
|
+
assert _pattern_to_regex('example.com.*') == r'example\.com\..*'
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
if __name__ == '__main__':
|
|
627
|
+
_test_pattern_to_regex()
|
|
628
|
+
_stderr = True
|
|
629
|
+
run_proxy(
|
|
630
|
+
local_server='localhost', local_port=8080,
|
|
631
|
+
remote_server='www.google.com', remote_port=80,
|
|
632
|
+
tls=False, ss=False,
|
|
633
|
+
content=False, to_file=False,
|
|
634
|
+
key_file='', cert_file='',
|
|
635
|
+
speed_monitor=False, speed_monitor_interval=5,
|
|
636
|
+
disguise_tls_ip='', disguise_tls_port=0,
|
|
637
|
+
white_list=None,
|
|
638
|
+
using_global=False,
|
|
639
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: simple-proxy
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A very simple NIO TCP proxy server
|
|
5
|
+
Home-page: https://github.com/ruanhao/simple-proxy
|
|
6
|
+
Author: Hao Ruan
|
|
7
|
+
Author-email: ruanhao1116@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: network,tcp,non-blocking,proxy
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Natural Language :: English
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.7, <4
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
|
|
27
|
+
# simple-proxy :rocket:
|
|
28
|
+
|
|
29
|
+
A very simple TCP proxy tool (not http proxy) empowered by nio tcp framework [py-netty](https://pypi.org/project/py-netty/)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install simple-proxy
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
Usage: simple-proxy [OPTIONS]
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
-l, --local-server TEXT Local server address [default: localhost]
|
|
48
|
+
-lp, --local-port INTEGER Local port [default: 8080]
|
|
49
|
+
-r, --remote-server TEXT Remote server address [default: localhost]
|
|
50
|
+
-rp, --remote-port INTEGER Remote port [default: 80]
|
|
51
|
+
-g, --global Listen on 0.0.0.0
|
|
52
|
+
-c, --tcp-flow Dump tcp flow on to console
|
|
53
|
+
-f, --save-tcp-flow Save tcp flow to file
|
|
54
|
+
-s, --tls Denote remote server listening on secure
|
|
55
|
+
port
|
|
56
|
+
-ss Denote local sever listening on secure port
|
|
57
|
+
-kf, --key-file PATH Key file for local server
|
|
58
|
+
-cf, --cert-file PATH Certificate file for local server
|
|
59
|
+
--speed-monitor Print speed info to console for established
|
|
60
|
+
connection
|
|
61
|
+
--speed-monitor-interval INTEGER
|
|
62
|
+
Speed monitor interval [default: 5]
|
|
63
|
+
-dti, --disguise-tls-ip TEXT Disguise TLS IP
|
|
64
|
+
-dtp, --disguise-tls-port INTEGER
|
|
65
|
+
Disguise TLS port [default: 443]
|
|
66
|
+
-wl, --white-list TEXT IP White list for incoming connections
|
|
67
|
+
(comma separated)
|
|
68
|
+
--run-mock-tls-server Run mock TLS server
|
|
69
|
+
-v, --verbose Verbose mode
|
|
70
|
+
-h, --help Show this message and exit.
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Features
|
|
75
|
+
### Basic proxy (TLS termination)
|
|
76
|
+
```commandline
|
|
77
|
+
> simple-proxy --tls -r www.google.com -rp 443 -lp 8080
|
|
78
|
+
Proxy server started listening: localhost:8080 => www.google.com:443(TLS) ...
|
|
79
|
+
console:False, file:False, disguise:n/a, whitelist:*
|
|
80
|
+
> curl -I -H 'Host: www.google.com' http://localhost:8080
|
|
81
|
+
HTTP/1.1 200 OK
|
|
82
|
+
...
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```commandline
|
|
86
|
+
> simple-proxy -r www.google.com -rp 80 -lp 8443 -ss
|
|
87
|
+
Proxy server started listening: localhost:8443(TLS) => www.google.com:80 ...
|
|
88
|
+
console:False, file:False, disguise:n/a, whitelist:*
|
|
89
|
+
> curl -I -H 'Host: www.google.com' -k https://localhost:8443
|
|
90
|
+
HTTP/1.1 200 OK
|
|
91
|
+
...
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Dump TCP flow
|
|
95
|
+
TCP flow can be dumped into console or files (under directory __tcpflow__)
|
|
96
|
+
```commandline
|
|
97
|
+
> simple-proxy -r www.google.com -rp 443 -lp 8443 -ss -s -c -f
|
|
98
|
+
Proxy server started listening: localhost:8443(TLS) => www.google.com:443(TLS) ...
|
|
99
|
+
console:True, file:True, disguise:n/a, whitelist:*
|
|
100
|
+
> curl -k -I -H 'Host: www.google.com' https://localhost:8443
|
|
101
|
+
```
|
|
102
|
+

|
|
103
|
+
|
|
104
|
+
### Connection status monitor
|
|
105
|
+
```commandline
|
|
106
|
+
> $ simple-proxy -r echo-server.proxy.com -rp 8080 -lp 48080 --speed-monitor
|
|
107
|
+
Proxy server started listening: localhost:48080 => echo-server.proxy.com:8080 ...
|
|
108
|
+
console:False, file:False, disguise:n/a, whitelist:*
|
|
109
|
+
Connection opened: ('127.0.0.1', 60937)
|
|
110
|
+
Connection opened: ('127.0.0.1', 60938)
|
|
111
|
+
Connection opened: ('127.0.0.1', 60939)
|
|
112
|
+
Connection opened: ('127.0.0.1', 60940)
|
|
113
|
+
Connection opened: ('127.0.0.1', 60941)
|
|
114
|
+
Connection opened: ('127.0.0.1', 60942)
|
|
115
|
+
Connection opened: ('127.0.0.1', 60943)
|
|
116
|
+
Connection opened: ('127.0.0.1', 60944)
|
|
117
|
+
---------------------------2024-02-12 17:43:02.337268 (total:8, rounds:1)---------------------------
|
|
118
|
+
[ 1] | 127.0.0.1:60937 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:235.00 K | duration: 7s
|
|
119
|
+
[ 2] | 127.0.0.1:60938 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
120
|
+
[ 3] | 127.0.0.1:60939 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
121
|
+
[ 4] | 127.0.0.1:60940 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
122
|
+
[ 5] | 127.0.0.1:60941 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:235.00 K Tx:234.00 K | duration: 7s
|
|
123
|
+
[ 6] | 127.0.0.1:60942 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:234.00 K Tx:234.00 K | duration: 7s
|
|
124
|
+
[ 7] | 127.0.0.1:60943 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:234.00 K Tx:234.00 K | duration: 7s
|
|
125
|
+
[ 8] | 127.0.0.1:60944 | Speed Rx:32.00 K/s Tx:32.00 K/s | Total Rx:234.00 K Tx:234.00 K | duration: 7s
|
|
126
|
+
Average Read Speed: 32765.0 bytes/s, Average Write Speed: 32752.88 bytes/s
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Disguise as HTTPS server with whitelist
|
|
130
|
+
Any connection beyond whitelist will be served by a mock https server. Real service can thus be hided.
|
|
131
|
+
|
|
132
|
+
For example, you can protect your Scurrying Squirrel against attack from Grim Foolish Weasel.
|
|
133
|
+
|
|
134
|
+
```commandline
|
|
135
|
+
> simple-proxy -rp 8388 -lp 443 -g --run-mock-tls-server -wl=<your ip>,<your wife's ip>,<your friend's wife's ip>
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+

|
|
139
|
+
|
|
140
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.cfg
|
|
4
|
+
setup.py
|
|
5
|
+
simple_proxy/__init__.py
|
|
6
|
+
simple_proxy/version.py
|
|
7
|
+
simple_proxy.egg-info/PKG-INFO
|
|
8
|
+
simple_proxy.egg-info/SOURCES.txt
|
|
9
|
+
simple_proxy.egg-info/dependency_links.txt
|
|
10
|
+
simple_proxy.egg-info/entry_points.txt
|
|
11
|
+
simple_proxy.egg-info/requires.txt
|
|
12
|
+
simple_proxy.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
simple_proxy
|