pyproxytools 0.3.2__py3-none-any.whl
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.
- benchmark/benchmark.py +165 -0
- benchmark/utils/__init__.py +0 -0
- benchmark/utils/html.py +179 -0
- benchmark/utils/req.py +43 -0
- pyproxy/__init__.py +13 -0
- pyproxy/handlers/__init__.py +0 -0
- pyproxy/handlers/client.py +126 -0
- pyproxy/handlers/http.py +197 -0
- pyproxy/handlers/https.py +308 -0
- pyproxy/modules/__init__.py +0 -0
- pyproxy/modules/cancel_inspect.py +83 -0
- pyproxy/modules/custom_header.py +78 -0
- pyproxy/modules/filter.py +151 -0
- pyproxy/modules/shortcuts.py +85 -0
- pyproxy/monitoring/__init__.py +0 -0
- pyproxy/monitoring/web.py +279 -0
- pyproxy/pyproxy.py +107 -0
- pyproxy/server.py +334 -0
- pyproxy/utils/__init__.py +0 -0
- pyproxy/utils/args.py +176 -0
- pyproxy/utils/config.py +110 -0
- pyproxy/utils/crypto.py +52 -0
- pyproxy/utils/http_req.py +53 -0
- pyproxy/utils/logger.py +46 -0
- pyproxy/utils/version.py +0 -0
- pyproxytools-0.3.2.dist-info/METADATA +130 -0
- pyproxytools-0.3.2.dist-info/RECORD +40 -0
- pyproxytools-0.3.2.dist-info/WHEEL +5 -0
- pyproxytools-0.3.2.dist-info/entry_points.txt +2 -0
- pyproxytools-0.3.2.dist-info/licenses/LICENSE +21 -0
- pyproxytools-0.3.2.dist-info/top_level.txt +3 -0
- tests/modules/__init__.py +0 -0
- tests/modules/test_cancel_inspect.py +67 -0
- tests/modules/test_custom_header.py +70 -0
- tests/modules/test_filter.py +185 -0
- tests/modules/test_shortcuts.py +119 -0
- tests/utils/__init__.py +0 -0
- tests/utils/test_crypto.py +110 -0
- tests/utils/test_http_req.py +69 -0
- tests/utils/test_logger.py +68 -0
pyproxy/server.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
server.py
|
|
3
|
+
|
|
4
|
+
This module defines a Python-based proxy server capable of handling both HTTP
|
|
5
|
+
and HTTPS requests. It forwards client requests to target servers, applies
|
|
6
|
+
filtering, serves custom 403 pages for blocked content, and logs access and
|
|
7
|
+
block events.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import socket
|
|
11
|
+
import threading
|
|
12
|
+
import logging
|
|
13
|
+
import multiprocessing
|
|
14
|
+
import os
|
|
15
|
+
import time
|
|
16
|
+
import ipaddress
|
|
17
|
+
|
|
18
|
+
from pyproxy import __slim__
|
|
19
|
+
from pyproxy.utils.logger import configure_file_logger, configure_console_logger
|
|
20
|
+
from pyproxy.handlers.client import ProxyHandlers
|
|
21
|
+
from pyproxy.modules.filter import filter_process
|
|
22
|
+
from pyproxy.modules.cancel_inspect import cancel_inspect_process
|
|
23
|
+
|
|
24
|
+
if not __slim__:
|
|
25
|
+
from pyproxy.modules.shortcuts import shortcuts_process
|
|
26
|
+
if not __slim__:
|
|
27
|
+
from pyproxy.modules.custom_header import custom_header_process
|
|
28
|
+
if not __slim__:
|
|
29
|
+
from pyproxy.monitoring.web import start_flask_server
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ProxyServer:
|
|
33
|
+
"""
|
|
34
|
+
A proxy server that forwards HTTP and HTTPS requests, blocks based on rules,
|
|
35
|
+
injects headers, and logs events.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_EXCLUDE_DEBUG_KEYS = {
|
|
39
|
+
"filter_proc",
|
|
40
|
+
"filter_queue",
|
|
41
|
+
"filter_result_queue",
|
|
42
|
+
"shortcuts_proc",
|
|
43
|
+
"shortcuts_queue",
|
|
44
|
+
"shortcuts_result_queue",
|
|
45
|
+
"cancel_inspect_proc",
|
|
46
|
+
"cancel_inspect_queue",
|
|
47
|
+
"cancel_inspect_result_queue",
|
|
48
|
+
"custom_header_proc",
|
|
49
|
+
"custom_header_queue",
|
|
50
|
+
"custom_header_result_queue",
|
|
51
|
+
"console_logger",
|
|
52
|
+
"access_logger",
|
|
53
|
+
"block_logger",
|
|
54
|
+
"authorized_ips",
|
|
55
|
+
"active_connections",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
host,
|
|
61
|
+
port,
|
|
62
|
+
debug,
|
|
63
|
+
logger_config,
|
|
64
|
+
filter_config,
|
|
65
|
+
html_403,
|
|
66
|
+
ssl_config,
|
|
67
|
+
shortcuts,
|
|
68
|
+
custom_header,
|
|
69
|
+
flask_port,
|
|
70
|
+
flask_pass,
|
|
71
|
+
proxy_enable,
|
|
72
|
+
proxy_host,
|
|
73
|
+
proxy_port,
|
|
74
|
+
authorized_ips,
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
Initialize the ProxyServer with configuration parameters.
|
|
78
|
+
"""
|
|
79
|
+
self.host_port = (host, port)
|
|
80
|
+
self.debug = debug
|
|
81
|
+
self.html_403 = html_403
|
|
82
|
+
self.active_connections = {}
|
|
83
|
+
|
|
84
|
+
self.logger_config = logger_config
|
|
85
|
+
self.filter_config = filter_config
|
|
86
|
+
self.ssl_config = ssl_config
|
|
87
|
+
|
|
88
|
+
# Monitoring
|
|
89
|
+
self.flask_port = flask_port
|
|
90
|
+
self.flask_pass = flask_pass
|
|
91
|
+
|
|
92
|
+
# Proxy
|
|
93
|
+
self.proxy_enable = proxy_enable
|
|
94
|
+
self.proxy_host = proxy_host
|
|
95
|
+
self.proxy_port = proxy_port
|
|
96
|
+
|
|
97
|
+
# Authorized IPS
|
|
98
|
+
self.authorized_ips = authorized_ips
|
|
99
|
+
self.allowed_subnets = None
|
|
100
|
+
|
|
101
|
+
# Process communication queues
|
|
102
|
+
self.filter_proc = None
|
|
103
|
+
self.filter_queue = multiprocessing.Queue()
|
|
104
|
+
self.filter_result_queue = multiprocessing.Queue()
|
|
105
|
+
self.shortcuts_proc = None
|
|
106
|
+
self.shortcuts_queue = multiprocessing.Queue()
|
|
107
|
+
self.shortcuts_result_queue = multiprocessing.Queue()
|
|
108
|
+
self.cancel_inspect_proc = None
|
|
109
|
+
self.cancel_inspect_queue = multiprocessing.Queue()
|
|
110
|
+
self.cancel_inspect_result_queue = multiprocessing.Queue()
|
|
111
|
+
self.custom_header_proc = None
|
|
112
|
+
self.custom_header_queue = multiprocessing.Queue()
|
|
113
|
+
self.custom_header_result_queue = multiprocessing.Queue()
|
|
114
|
+
|
|
115
|
+
# Logging
|
|
116
|
+
self.console_logger = configure_console_logger()
|
|
117
|
+
if not self.logger_config.no_logging_access:
|
|
118
|
+
self.logger_config.access_logger = configure_file_logger(
|
|
119
|
+
self.logger_config.access_log, "AccessLogger"
|
|
120
|
+
)
|
|
121
|
+
if not self.logger_config.no_logging_block:
|
|
122
|
+
self.logger_config.block_logger = configure_file_logger(
|
|
123
|
+
self.logger_config.block_log, "BlockLogger"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Configuration files
|
|
127
|
+
self.config_shortcuts = shortcuts
|
|
128
|
+
self.config_custom_header = custom_header
|
|
129
|
+
|
|
130
|
+
def _initialize_processes(self):
|
|
131
|
+
"""
|
|
132
|
+
Initializes and starts multiple processes for various tasks if their
|
|
133
|
+
respective configurations and conditions are met.
|
|
134
|
+
"""
|
|
135
|
+
if not self.filter_config.no_filter:
|
|
136
|
+
self.filter_proc = multiprocessing.Process(
|
|
137
|
+
target=filter_process,
|
|
138
|
+
args=(
|
|
139
|
+
self.filter_queue,
|
|
140
|
+
self.filter_result_queue,
|
|
141
|
+
self.filter_config.filter_mode,
|
|
142
|
+
self.filter_config.blocked_sites,
|
|
143
|
+
self.filter_config.blocked_url,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
self.filter_proc.start()
|
|
147
|
+
self.console_logger.debug("[*] Starting the filter process...")
|
|
148
|
+
|
|
149
|
+
if (
|
|
150
|
+
not __slim__
|
|
151
|
+
and self.config_shortcuts
|
|
152
|
+
and os.path.isfile(self.config_shortcuts)
|
|
153
|
+
):
|
|
154
|
+
self.shortcuts_proc = multiprocessing.Process(
|
|
155
|
+
target=shortcuts_process,
|
|
156
|
+
args=(
|
|
157
|
+
self.shortcuts_queue,
|
|
158
|
+
self.shortcuts_result_queue,
|
|
159
|
+
self.config_shortcuts,
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
self.shortcuts_proc.start()
|
|
163
|
+
self.console_logger.debug("[*] Starting the shortcuts process...")
|
|
164
|
+
|
|
165
|
+
if self.ssl_config.cancel_inspect and os.path.isfile(
|
|
166
|
+
self.ssl_config.cancel_inspect
|
|
167
|
+
):
|
|
168
|
+
self.cancel_inspect_proc = multiprocessing.Process(
|
|
169
|
+
target=cancel_inspect_process,
|
|
170
|
+
args=(
|
|
171
|
+
self.cancel_inspect_queue,
|
|
172
|
+
self.cancel_inspect_result_queue,
|
|
173
|
+
self.ssl_config.cancel_inspect,
|
|
174
|
+
),
|
|
175
|
+
)
|
|
176
|
+
self.cancel_inspect_proc.start()
|
|
177
|
+
self.console_logger.debug("[*] Starting the cancel inspection process...")
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
not __slim__
|
|
181
|
+
and self.config_custom_header
|
|
182
|
+
and os.path.isfile(self.config_custom_header)
|
|
183
|
+
):
|
|
184
|
+
self.custom_header_proc = multiprocessing.Process(
|
|
185
|
+
target=custom_header_process,
|
|
186
|
+
args=(
|
|
187
|
+
self.custom_header_queue,
|
|
188
|
+
self.custom_header_result_queue,
|
|
189
|
+
self.config_custom_header,
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
self.custom_header_proc.start()
|
|
193
|
+
self.console_logger.debug("[*] Starting the custom header process...")
|
|
194
|
+
|
|
195
|
+
def _clean_inspection_folder(self):
|
|
196
|
+
"""
|
|
197
|
+
Delete old inspection cert/key files if they exist.
|
|
198
|
+
"""
|
|
199
|
+
for file in os.listdir(self.ssl_config.inspect_certs_folder):
|
|
200
|
+
if file.endswith((".key", ".pem")):
|
|
201
|
+
file_path = os.path.join(self.ssl_config.inspect_certs_folder, file)
|
|
202
|
+
try:
|
|
203
|
+
os.remove(file_path)
|
|
204
|
+
except (FileNotFoundError, PermissionError, OSError) as e:
|
|
205
|
+
self.console_logger.debug("Error deleting %s: %s", file_path, e)
|
|
206
|
+
|
|
207
|
+
def _load_authorized_ips(self):
|
|
208
|
+
"""
|
|
209
|
+
Load authorized IPs/subnets from the file.
|
|
210
|
+
"""
|
|
211
|
+
self.allowed_subnets = None
|
|
212
|
+
|
|
213
|
+
if self.authorized_ips and os.path.isfile(self.authorized_ips):
|
|
214
|
+
with open(self.authorized_ips, "r", encoding="utf-8") as f:
|
|
215
|
+
lines = [line.strip() for line in f if line.strip()]
|
|
216
|
+
try:
|
|
217
|
+
self.allowed_subnets = [
|
|
218
|
+
ipaddress.ip_network(line, strict=False) for line in lines
|
|
219
|
+
]
|
|
220
|
+
self.console_logger.debug(
|
|
221
|
+
"[*] Loaded %d authorized IPs/subnets", len(self.allowed_subnets)
|
|
222
|
+
)
|
|
223
|
+
except ValueError as e:
|
|
224
|
+
self.console_logger.error(
|
|
225
|
+
"[*] Invalid IP/subnet in %s: %s", self.authorized_ips, e
|
|
226
|
+
)
|
|
227
|
+
self.allowed_subnets = None
|
|
228
|
+
|
|
229
|
+
def start(self):
|
|
230
|
+
"""
|
|
231
|
+
Start the proxy server and listen for incoming client connections.
|
|
232
|
+
Logs configuration if debug is enabled.
|
|
233
|
+
"""
|
|
234
|
+
self.console_logger.setLevel(logging.DEBUG if self.debug else logging.INFO)
|
|
235
|
+
|
|
236
|
+
if self.debug:
|
|
237
|
+
self.console_logger.debug("Configuration used:")
|
|
238
|
+
for key in sorted(vars(self)):
|
|
239
|
+
if key not in self._EXCLUDE_DEBUG_KEYS:
|
|
240
|
+
self.console_logger.debug("[*] %s = %s", key, getattr(self, key))
|
|
241
|
+
|
|
242
|
+
if self.ssl_config.ssl_inspect:
|
|
243
|
+
if not self.ssl_config.inspect_ca_cert or not os.path.isfile(
|
|
244
|
+
self.ssl_config.inspect_ca_cert
|
|
245
|
+
):
|
|
246
|
+
raise FileNotFoundError(
|
|
247
|
+
f"CA certificate not found: {self.ssl_config.inspect_ca_cert}"
|
|
248
|
+
)
|
|
249
|
+
if not self.ssl_config.inspect_ca_key or not os.path.isfile(
|
|
250
|
+
self.ssl_config.inspect_ca_key
|
|
251
|
+
):
|
|
252
|
+
raise FileNotFoundError(
|
|
253
|
+
f"CA key not found: {self.ssl_config.inspect_ca_key}"
|
|
254
|
+
)
|
|
255
|
+
os.makedirs(self.ssl_config.inspect_certs_folder, exist_ok=True)
|
|
256
|
+
self._clean_inspection_folder()
|
|
257
|
+
|
|
258
|
+
if self.filter_config.filter_mode == "local":
|
|
259
|
+
for file in [
|
|
260
|
+
self.filter_config.blocked_sites,
|
|
261
|
+
self.filter_config.blocked_url,
|
|
262
|
+
]:
|
|
263
|
+
if not os.path.exists(file):
|
|
264
|
+
with open(file, "w", encoding="utf-8"):
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
self._initialize_processes()
|
|
268
|
+
self._load_authorized_ips()
|
|
269
|
+
|
|
270
|
+
if not __slim__:
|
|
271
|
+
flask_thread = threading.Thread(
|
|
272
|
+
target=start_flask_server,
|
|
273
|
+
args=(self, self.flask_port, self.flask_pass, self.debug),
|
|
274
|
+
daemon=True,
|
|
275
|
+
)
|
|
276
|
+
flask_thread.start()
|
|
277
|
+
self.console_logger.debug("[*] Starting the monitoring process...")
|
|
278
|
+
|
|
279
|
+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
280
|
+
server.bind(self.host_port)
|
|
281
|
+
server.listen(10)
|
|
282
|
+
self.console_logger.info("Proxy server started on %s...", self.host_port)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
while True:
|
|
286
|
+
client_socket, addr = server.accept()
|
|
287
|
+
client_ip, client_port = addr
|
|
288
|
+
|
|
289
|
+
if self.allowed_subnets:
|
|
290
|
+
ip_obj = ipaddress.ip_address(client_ip)
|
|
291
|
+
if not any(ip_obj in net for net in self.allowed_subnets):
|
|
292
|
+
self.console_logger.debug(
|
|
293
|
+
"Unauthorized IP blocked: %s", client_ip
|
|
294
|
+
)
|
|
295
|
+
client_socket.close()
|
|
296
|
+
continue
|
|
297
|
+
|
|
298
|
+
self.console_logger.debug("Connection from %s", addr)
|
|
299
|
+
client = ProxyHandlers(
|
|
300
|
+
html_403=self.html_403,
|
|
301
|
+
logger_config=self.logger_config,
|
|
302
|
+
filter_config=self.filter_config,
|
|
303
|
+
ssl_config=self.ssl_config,
|
|
304
|
+
filter_queue=self.filter_queue,
|
|
305
|
+
filter_result_queue=self.filter_result_queue,
|
|
306
|
+
shortcuts_queue=self.shortcuts_queue,
|
|
307
|
+
shortcuts_result_queue=self.shortcuts_result_queue,
|
|
308
|
+
cancel_inspect_queue=self.cancel_inspect_queue,
|
|
309
|
+
cancel_inspect_result_queue=self.cancel_inspect_result_queue,
|
|
310
|
+
custom_header_queue=self.custom_header_queue,
|
|
311
|
+
custom_header_result_queue=self.custom_header_result_queue,
|
|
312
|
+
console_logger=self.console_logger,
|
|
313
|
+
shortcuts=self.config_shortcuts,
|
|
314
|
+
custom_header=self.config_custom_header,
|
|
315
|
+
proxy_enable=self.proxy_enable,
|
|
316
|
+
proxy_host=self.proxy_host,
|
|
317
|
+
proxy_port=self.proxy_port,
|
|
318
|
+
active_connections=self.active_connections,
|
|
319
|
+
)
|
|
320
|
+
client_handler = threading.Thread(
|
|
321
|
+
target=client.handle_client, args=(client_socket,), daemon=True
|
|
322
|
+
)
|
|
323
|
+
client_handler.start()
|
|
324
|
+
client_ip, client_port = addr
|
|
325
|
+
self.active_connections[client_handler.ident] = {
|
|
326
|
+
"client_ip": client_ip,
|
|
327
|
+
"client_port": client_port,
|
|
328
|
+
"start_time": time.time(),
|
|
329
|
+
"bytes_sent": 0,
|
|
330
|
+
"bytes_received": 0,
|
|
331
|
+
"thread_name": client_handler.name,
|
|
332
|
+
}
|
|
333
|
+
except KeyboardInterrupt:
|
|
334
|
+
self.console_logger.info("Proxy interrupted, shutting down.")
|
|
File without changes
|
pyproxy/utils/args.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyproxy.utils.args.py
|
|
3
|
+
|
|
4
|
+
This module allows you to read the program configuration file and return the values.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import configparser
|
|
8
|
+
import argparse
|
|
9
|
+
import os
|
|
10
|
+
from rich_argparse import MetavarTypeRichHelpFormatter
|
|
11
|
+
from pyproxy import __version__
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_args() -> argparse.Namespace:
|
|
15
|
+
"""
|
|
16
|
+
Parses command-line arguments and returns the parsed arguments as an object.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
None
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
argparse.Namespace: The object containing parsed command-line arguments.
|
|
23
|
+
"""
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
description="Lightweight and fast python web proxy",
|
|
26
|
+
formatter_class=MetavarTypeRichHelpFormatter,
|
|
27
|
+
)
|
|
28
|
+
parser.add_argument(
|
|
29
|
+
"-v", "--version", action="version", version=__version__, help="Show version"
|
|
30
|
+
) # noqa: E501
|
|
31
|
+
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
32
|
+
parser.add_argument("-H", "--host", type=str, help="IP address to listen on")
|
|
33
|
+
parser.add_argument("-P", "--port", type=int, help="Port to listen on")
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"-f",
|
|
36
|
+
"--config-file",
|
|
37
|
+
type=str,
|
|
38
|
+
default="./config.ini",
|
|
39
|
+
help="Path to config.ini file",
|
|
40
|
+
) # noqa: E501
|
|
41
|
+
parser.add_argument("--access-log", type=str, help="Path to the access log file")
|
|
42
|
+
parser.add_argument("--block-log", type=str, help="Path to the block log file")
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--html-403", type=str, help="Path to the custom 403 Forbidden HTML page"
|
|
45
|
+
)
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--no-filter", action="store_true", help="Disable URL and domain filtering"
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--filter-mode", type=str, choices=["local", "http"], help="Filter list mode"
|
|
51
|
+
) # noqa: E501
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--blocked-sites",
|
|
54
|
+
type=str,
|
|
55
|
+
help="Path to the text file containing the list of sites to block",
|
|
56
|
+
) # noqa: E501
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--blocked-url",
|
|
59
|
+
type=str,
|
|
60
|
+
help="Path to the text file containing the list of URLs to block",
|
|
61
|
+
) # noqa: E501
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--shortcuts",
|
|
64
|
+
type=str,
|
|
65
|
+
help="Path to the text file containing the list of shortcuts",
|
|
66
|
+
) # noqa: E501
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--custom-header",
|
|
69
|
+
type=str,
|
|
70
|
+
help="Path to the json file containing the list of custom headers",
|
|
71
|
+
) # noqa: E501
|
|
72
|
+
parser.add_argument(
|
|
73
|
+
"--authorized-ips",
|
|
74
|
+
type=str,
|
|
75
|
+
help="Path to the txt file containing the list of authorized ips",
|
|
76
|
+
) # noqa: E501
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--no-logging-access", action="store_true", help="Disable access logging"
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--no-logging-block", action="store_true", help="Disable block logging"
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--ssl-inspect", action="store_true", help="Enable SSL inspection"
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"--inspect-ca-cert", type=str, help="Path to the CA certificate"
|
|
88
|
+
)
|
|
89
|
+
parser.add_argument("--inspect-ca-key", type=str, help="Path to the CA key")
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"--inspect-certs-folder",
|
|
92
|
+
type=str,
|
|
93
|
+
help="Path to the generated certificates folder",
|
|
94
|
+
) # noqa: E501
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--cancel-inspect",
|
|
97
|
+
type=str,
|
|
98
|
+
help="Path to the text file containing the list of URLs without ssl inspection",
|
|
99
|
+
) # noqa: E501
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"--flask-port", type=int, help="Port to listen on for monitoring interface"
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--flask-pass", type=int, help="Default password to Flask interface"
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--proxy-enable", action="store_true", help="Enable proxy after PyProxy"
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument("--proxy-host", type=str, help="Proxy IP to use after PyProxy")
|
|
110
|
+
parser.add_argument(
|
|
111
|
+
"--proxy-port", type=int, help="Proxy Port to use after PyProxy"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return parser.parse_args()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_config(config_path: str) -> configparser.ConfigParser:
|
|
118
|
+
"""
|
|
119
|
+
Loads the configuration file and returns the parsed config object.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
config_path (str): The path to the configuration file to load.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
configparser.ConfigParser: The parsed configuration object.
|
|
126
|
+
"""
|
|
127
|
+
config = configparser.ConfigParser()
|
|
128
|
+
config.read(config_path)
|
|
129
|
+
return config
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_config_value(
|
|
133
|
+
args: argparse.Namespace,
|
|
134
|
+
config: configparser.ConfigParser,
|
|
135
|
+
arg_name: str,
|
|
136
|
+
section: str,
|
|
137
|
+
fallback_value: str,
|
|
138
|
+
) -> str:
|
|
139
|
+
"""
|
|
140
|
+
Retrieves the configuration value, either from the command-line
|
|
141
|
+
arguments or from the config file.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
args (argparse.Namespace): The parsed command-line arguments object.
|
|
145
|
+
config (configparser.ConfigParser): The parsed configuration object.
|
|
146
|
+
arg_name (str): The name of the command-line argument.
|
|
147
|
+
section (str): The section in the config file where the value is located.
|
|
148
|
+
fallback_value (str): The fallback value to return if neither
|
|
149
|
+
argument nor config has a value.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
str: The final value, either from command-line arguments, config file, or fallback.
|
|
153
|
+
"""
|
|
154
|
+
arg_value = getattr(args, arg_name, None)
|
|
155
|
+
if arg_value:
|
|
156
|
+
return arg_value
|
|
157
|
+
|
|
158
|
+
env_var_name = f"PYPROXY_{arg_name.upper().replace('-', '_')}"
|
|
159
|
+
env_value = os.getenv(env_var_name)
|
|
160
|
+
if env_value:
|
|
161
|
+
return env_value
|
|
162
|
+
|
|
163
|
+
return config.get(section, arg_name, fallback=fallback_value)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def str_to_bool(value: str) -> bool:
|
|
167
|
+
"""
|
|
168
|
+
Converts a string representation of truth to a boolean value.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
value (str): The value to convert (e.g., "true", "1", "yes").
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
bool: True if the string represents a true value, False otherwise.
|
|
175
|
+
"""
|
|
176
|
+
return str(value).lower() in ("yes", "true", "t", "1")
|
pyproxy/utils/config.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyproxy.utils.config.py
|
|
3
|
+
|
|
4
|
+
This module defines configuration classes used by the HTTP/HTTPS proxy.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ProxyConfigLogger:
|
|
9
|
+
"""
|
|
10
|
+
Handles logging configuration for the proxy.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, access_log, block_log, no_logging_access, no_logging_block):
|
|
14
|
+
self.access_log = access_log
|
|
15
|
+
self.block_log = block_log
|
|
16
|
+
self.access_logger = None
|
|
17
|
+
self.block_logger = None
|
|
18
|
+
self.no_logging_access = no_logging_access
|
|
19
|
+
self.no_logging_block = no_logging_block
|
|
20
|
+
|
|
21
|
+
def __repr__(self):
|
|
22
|
+
return (
|
|
23
|
+
f"ProxyConfigLogger(access_log={self.access_log}, "
|
|
24
|
+
f"block_log={self.block_log}, "
|
|
25
|
+
f"no_logging_access={self.no_logging_access}, "
|
|
26
|
+
f"no_logging_block={self.no_logging_block})"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def to_dict(self):
|
|
30
|
+
"""
|
|
31
|
+
Converts the ProxyConfigLogger instance into a dictionary.
|
|
32
|
+
"""
|
|
33
|
+
return {
|
|
34
|
+
"access_log": self.access_log,
|
|
35
|
+
"block_log": self.block_log,
|
|
36
|
+
"no_logging_access": self.no_logging_access,
|
|
37
|
+
"no_logging_block": self.no_logging_block,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ProxyConfigFilter:
|
|
42
|
+
"""
|
|
43
|
+
Manages filtering configuration for the proxy.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, no_filter, filter_mode, blocked_sites, blocked_url):
|
|
47
|
+
self.no_filter = no_filter
|
|
48
|
+
self.filter_mode = filter_mode
|
|
49
|
+
self.blocked_sites = blocked_sites
|
|
50
|
+
self.blocked_url = blocked_url
|
|
51
|
+
|
|
52
|
+
def __repr__(self):
|
|
53
|
+
return (
|
|
54
|
+
f"ProxyConfigFilter(no_filter={self.no_filter}, "
|
|
55
|
+
f"filter_mode='{self.filter_mode}', "
|
|
56
|
+
f"blocked_sites={self.blocked_sites}, "
|
|
57
|
+
f"blocked_url={self.blocked_url})"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def to_dict(self):
|
|
61
|
+
"""
|
|
62
|
+
Converts the ProxyConfigFilter instance into a dictionary.
|
|
63
|
+
"""
|
|
64
|
+
return {
|
|
65
|
+
"no_filter": self.no_filter,
|
|
66
|
+
"filter_mode": self.filter_mode,
|
|
67
|
+
"blocked_sites": self.blocked_sites,
|
|
68
|
+
"blocked_url": self.blocked_url,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ProxyConfigSSL:
|
|
73
|
+
"""
|
|
74
|
+
Handles SSL/TLS inspection configuration.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
ssl_inspect,
|
|
80
|
+
inspect_ca_cert,
|
|
81
|
+
inspect_ca_key,
|
|
82
|
+
inspect_certs_folder,
|
|
83
|
+
cancel_inspect,
|
|
84
|
+
):
|
|
85
|
+
self.ssl_inspect = ssl_inspect
|
|
86
|
+
self.inspect_ca_cert = inspect_ca_cert
|
|
87
|
+
self.inspect_ca_key = inspect_ca_key
|
|
88
|
+
self.inspect_certs_folder = inspect_certs_folder
|
|
89
|
+
self.cancel_inspect = cancel_inspect
|
|
90
|
+
|
|
91
|
+
def __repr__(self):
|
|
92
|
+
return (
|
|
93
|
+
f"ProxyConfigSSL(ssl_inspect={self.ssl_inspect}, "
|
|
94
|
+
f"inspect_ca_cert='{self.inspect_ca_cert}', "
|
|
95
|
+
f"inspect_ca_key='{self.inspect_ca_key}', "
|
|
96
|
+
f"inspect_certs_folder='{self.inspect_certs_folder}', "
|
|
97
|
+
f"cancel_inspect={self.cancel_inspect})"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def to_dict(self):
|
|
101
|
+
"""
|
|
102
|
+
Converts the ProxyConfigSSL instance into a dictionary.
|
|
103
|
+
"""
|
|
104
|
+
return {
|
|
105
|
+
"ssl_inspect": self.ssl_inspect,
|
|
106
|
+
"inspect_ca_cert": self.inspect_ca_cert,
|
|
107
|
+
"inspect_ca_key": self.inspect_ca_key,
|
|
108
|
+
"inspect_certs_folder": self.inspect_certs_folder,
|
|
109
|
+
"cancel_inspect": self.cancel_inspect,
|
|
110
|
+
}
|
pyproxy/utils/crypto.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
pyproxy.utils.crypto.py
|
|
3
|
+
|
|
4
|
+
Certificate generation utilities for SSL inspection in pyproxy.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from OpenSSL import crypto
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def generate_certificate(domain, certs_folder, ca_cert, ca_key):
|
|
12
|
+
"""
|
|
13
|
+
Generates a self-signed SSL certificate for the given domain.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
domain (str): The domain name for which the certificate is generated.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
tuple: Paths to the generated certificate and private key files.
|
|
20
|
+
"""
|
|
21
|
+
cert_path = f"{certs_folder}{domain}.pem"
|
|
22
|
+
key_path = f"{certs_folder}{domain}.key"
|
|
23
|
+
|
|
24
|
+
if not os.path.exists(cert_path):
|
|
25
|
+
key = crypto.PKey()
|
|
26
|
+
key.generate_key(crypto.TYPE_RSA, 2048)
|
|
27
|
+
|
|
28
|
+
with open(ca_cert, "r", encoding="utf-8") as f:
|
|
29
|
+
ca_cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
|
|
30
|
+
with open(ca_key, "r", encoding="utf-8") as f:
|
|
31
|
+
ca_key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
|
|
32
|
+
|
|
33
|
+
cert = crypto.X509()
|
|
34
|
+
cert.set_serial_number(int.from_bytes(os.urandom(16), "big"))
|
|
35
|
+
cert.get_subject().CN = domain
|
|
36
|
+
cert.gmtime_adj_notBefore(0)
|
|
37
|
+
cert.gmtime_adj_notAfter(365 * 24 * 60 * 60)
|
|
38
|
+
cert.set_issuer(ca_cert.get_subject())
|
|
39
|
+
cert.set_pubkey(key)
|
|
40
|
+
san = f"DNS:{domain}"
|
|
41
|
+
cert.add_extensions(
|
|
42
|
+
[crypto.X509Extension(b"subjectAltName", False, san.encode())]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
cert.sign(ca_key, "sha256")
|
|
46
|
+
|
|
47
|
+
with open(cert_path, "wb") as f:
|
|
48
|
+
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
|
49
|
+
with open(key_path, "wb") as f:
|
|
50
|
+
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
|
|
51
|
+
|
|
52
|
+
return cert_path, key_path
|