pyproxytools 0.4.2__tar.gz → 0.4.3__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.
- {pyproxytools-0.4.2/pyproxytools.egg-info → pyproxytools-0.4.3}/PKG-INFO +7 -1
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/README.md +6 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/__init__.py +1 -1
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/handlers/http.py +40 -16
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/handlers/https.py +81 -22
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/pyproxy.py +40 -3
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/server.py +8 -2
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/config.py +2 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/logger.py +24 -2
- {pyproxytools-0.4.2 → pyproxytools-0.4.3/pyproxytools.egg-info}/PKG-INFO +7 -1
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/utils/test_logger.py +15 -6
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/LICENSE +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/benchmark/benchmark.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/benchmark/utils/__init__.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/benchmark/utils/html.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/benchmark/utils/req.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproject.toml +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/__main__.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/handlers/__init__.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/handlers/client.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/__init__.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/cancel_inspect.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/custom_header.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/filter.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/shortcuts.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/monitoring/__init__.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/monitoring/web.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/__init__.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/args.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/crypto.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/http_req.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/SOURCES.txt +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/dependency_links.txt +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/entry_points.txt +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/requires.txt +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/top_level.txt +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/requirements.txt +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/setup.cfg +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/setup.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/__init__.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/test_cancel_inspect.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/test_custom_header.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/test_filter.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/test_shortcuts.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/utils/__init__.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/utils/test_crypto.py +0 -0
- {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/utils/test_http_req.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyproxytools
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Lightweight and fast python web proxy
|
|
5
5
|
Author: 6C656C65
|
|
6
6
|
License-Expression: MIT
|
|
@@ -93,6 +93,12 @@ docker run -d ghcr.io/6c656c65/pyproxy:latest
|
|
|
93
93
|
```
|
|
94
94
|
You can use slim images by adding `-slim` to the end of the tags
|
|
95
95
|
|
|
96
|
+
### Install with Compose
|
|
97
|
+
```bash
|
|
98
|
+
wget https://raw.githubusercontent.com/6C656C65/pyproxy/main/docker-compose.yml
|
|
99
|
+
docker-compose up -d
|
|
100
|
+
```
|
|
101
|
+
|
|
96
102
|
## 🚀 **Usage**
|
|
97
103
|
|
|
98
104
|
### Start the proxy
|
|
@@ -60,6 +60,12 @@ docker run -d ghcr.io/6c656c65/pyproxy:latest
|
|
|
60
60
|
```
|
|
61
61
|
You can use slim images by adding `-slim` to the end of the tags
|
|
62
62
|
|
|
63
|
+
### Install with Compose
|
|
64
|
+
```bash
|
|
65
|
+
wget https://raw.githubusercontent.com/6C656C65/pyproxy/main/docker-compose.yml
|
|
66
|
+
docker-compose up -d
|
|
67
|
+
```
|
|
68
|
+
|
|
63
69
|
## 🚀 **Usage**
|
|
64
70
|
|
|
65
71
|
### Start the proxy
|
|
@@ -109,8 +109,18 @@ class HttpHandler:
|
|
|
109
109
|
Sends an HTTP 403 Forbidden response to the client.
|
|
110
110
|
"""
|
|
111
111
|
if not self.logger_config.no_logging_block:
|
|
112
|
+
method, domain_port, protocol = first_line.split(" ")
|
|
113
|
+
domain, port = domain_port.split(":")
|
|
112
114
|
self.logger_config.block_logger.info(
|
|
113
|
-
"
|
|
115
|
+
"",
|
|
116
|
+
extra={
|
|
117
|
+
"ip_src": client_socket.getpeername()[0],
|
|
118
|
+
"url": url,
|
|
119
|
+
"method": method,
|
|
120
|
+
"domain": domain,
|
|
121
|
+
"port": port,
|
|
122
|
+
"protocol": protocol,
|
|
123
|
+
},
|
|
114
124
|
)
|
|
115
125
|
with open(self.html_403, "r", encoding="utf-8") as f:
|
|
116
126
|
custom_403_page = f.read()
|
|
@@ -155,16 +165,6 @@ class HttpHandler:
|
|
|
155
165
|
self._send_403(client_socket, url, first_line)
|
|
156
166
|
return
|
|
157
167
|
|
|
158
|
-
parsed_url = urlparse(url)
|
|
159
|
-
server_host = parsed_url.hostname
|
|
160
|
-
if not self.logger_config.no_logging_access:
|
|
161
|
-
self.logger_config.access_logger.info(
|
|
162
|
-
"%s - %s - %s",
|
|
163
|
-
client_socket.getpeername()[0],
|
|
164
|
-
f"http://{server_host}",
|
|
165
|
-
first_line,
|
|
166
|
-
)
|
|
167
|
-
|
|
168
168
|
if self.config_custom_header and os.path.isfile(self.config_custom_header):
|
|
169
169
|
request_text = request.decode(errors="ignore")
|
|
170
170
|
request_lines = request_text.split("\r\n")
|
|
@@ -177,12 +177,14 @@ class HttpHandler:
|
|
|
177
177
|
)
|
|
178
178
|
modified_request = self._rebuild_http_request(request_line, headers, body)
|
|
179
179
|
|
|
180
|
-
self.forward_request_to_server(
|
|
180
|
+
self.forward_request_to_server(
|
|
181
|
+
client_socket, modified_request, url, first_line
|
|
182
|
+
)
|
|
181
183
|
|
|
182
184
|
else:
|
|
183
|
-
self.forward_request_to_server(client_socket, request, url)
|
|
185
|
+
self.forward_request_to_server(client_socket, request, url, first_line)
|
|
184
186
|
|
|
185
|
-
def forward_request_to_server(self, client_socket, request, url):
|
|
187
|
+
def forward_request_to_server(self, client_socket, request, url, first_line):
|
|
186
188
|
"""
|
|
187
189
|
Forwards the HTTP request to the target server and sends the response back to the client.
|
|
188
190
|
|
|
@@ -190,6 +192,7 @@ class HttpHandler:
|
|
|
190
192
|
client_socket (socket): The socket object for the client connection.
|
|
191
193
|
request (bytes): The raw HTTP request sent by the client.
|
|
192
194
|
url (str): The target URL from the HTTP request.
|
|
195
|
+
first_line (str): The first line of the HTTP request (e.g., "GET / HTTP/1.1").
|
|
193
196
|
"""
|
|
194
197
|
if self.proxy_config.enable:
|
|
195
198
|
server_host, server_port = self.proxy_config.host, self.proxy_config.port
|
|
@@ -202,8 +205,12 @@ class HttpHandler:
|
|
|
202
205
|
thread_id = threading.get_ident()
|
|
203
206
|
|
|
204
207
|
if thread_id in self.active_connections:
|
|
205
|
-
self.active_connections[thread_id]
|
|
206
|
-
|
|
208
|
+
self.active_connections[thread_id] = {
|
|
209
|
+
"target_ip": server_host,
|
|
210
|
+
"target_port": server_port,
|
|
211
|
+
"bytes_sent": 0,
|
|
212
|
+
"bytes_received": 0,
|
|
213
|
+
}
|
|
207
214
|
|
|
208
215
|
try:
|
|
209
216
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
@@ -238,6 +245,23 @@ class HttpHandler:
|
|
|
238
245
|
client_socket.close()
|
|
239
246
|
self.active_connections.pop(thread_id, None)
|
|
240
247
|
finally:
|
|
248
|
+
if not self.logger_config.no_logging_access:
|
|
249
|
+
method, url, protocol = first_line.split(" ")
|
|
250
|
+
|
|
251
|
+
conn_data = self.active_connections.get(thread_id, {})
|
|
252
|
+
self.logger_config.access_logger.info(
|
|
253
|
+
"",
|
|
254
|
+
extra={
|
|
255
|
+
"ip_src": client_socket.getpeername()[0],
|
|
256
|
+
"url": f"http://{server_host}",
|
|
257
|
+
"method": method,
|
|
258
|
+
"domain": parsed_url.hostname,
|
|
259
|
+
"port": parsed_url.port,
|
|
260
|
+
"protocol": protocol,
|
|
261
|
+
"bytes_sent": conn_data.get("bytes_sent", 0),
|
|
262
|
+
"bytes_received": conn_data.get("bytes_received", 0),
|
|
263
|
+
},
|
|
264
|
+
)
|
|
241
265
|
client_socket.close()
|
|
242
266
|
server_socket.close()
|
|
243
267
|
self.active_connections.pop(thread_id, None)
|
|
@@ -80,8 +80,18 @@ class HttpsHandler:
|
|
|
80
80
|
Sends an HTTP 403 Forbidden response to the client.
|
|
81
81
|
"""
|
|
82
82
|
if not self.logger_config.no_logging_block:
|
|
83
|
+
method, domain_port, protocol = first_line.split(" ")
|
|
84
|
+
domain, port = domain_port.split(":")
|
|
83
85
|
self.logger_config.block_logger.info(
|
|
84
|
-
"
|
|
86
|
+
"",
|
|
87
|
+
extra={
|
|
88
|
+
"ip_src": client_socket.getpeername()[0],
|
|
89
|
+
"url": url,
|
|
90
|
+
"method": method,
|
|
91
|
+
"domain": domain,
|
|
92
|
+
"port": port,
|
|
93
|
+
"protocol": protocol,
|
|
94
|
+
},
|
|
85
95
|
)
|
|
86
96
|
with open(self.html_403, "r", encoding="utf-8") as f:
|
|
87
97
|
custom_403_page = f.read()
|
|
@@ -150,7 +160,15 @@ class HttpsHandler:
|
|
|
150
160
|
ssl_client_socket = client_context.wrap_socket(
|
|
151
161
|
client_socket, server_side=True, do_handshake_on_connect=False
|
|
152
162
|
)
|
|
153
|
-
|
|
163
|
+
try:
|
|
164
|
+
ssl_client_socket.do_handshake()
|
|
165
|
+
except ssl.SSLError as e:
|
|
166
|
+
if "TLSV1_ALERT_UNKNOWN_CA" in str(e):
|
|
167
|
+
self.console_logger.debug("Client refused cert: %s", e)
|
|
168
|
+
ssl_client_socket.close()
|
|
169
|
+
raise ConnectionAbortedError("Client refused SSL cert")
|
|
170
|
+
else:
|
|
171
|
+
raise
|
|
154
172
|
return ssl_client_socket
|
|
155
173
|
|
|
156
174
|
def _wrap_server_socket_with_ssl(self, server_socket, server_host):
|
|
@@ -171,7 +189,7 @@ class HttpsHandler:
|
|
|
171
189
|
)
|
|
172
190
|
return ssl_server_socket
|
|
173
191
|
|
|
174
|
-
def _process_first_ssl_request(self, ssl_client_socket, server_host):
|
|
192
|
+
def _process_first_ssl_request(self, ssl_client_socket, server_host, first_line):
|
|
175
193
|
"""
|
|
176
194
|
Reads and processes the first SSL client request, extracts the method and full URL.
|
|
177
195
|
"""
|
|
@@ -188,15 +206,6 @@ class HttpsHandler:
|
|
|
188
206
|
if self._is_blocked(f"{server_host}{path}"):
|
|
189
207
|
return None, full_url, True
|
|
190
208
|
|
|
191
|
-
if not self.logger_config.no_logging_access:
|
|
192
|
-
self.logger_config.access_logger.info(
|
|
193
|
-
"%s - %s - %s %s",
|
|
194
|
-
ssl_client_socket.getpeername()[0],
|
|
195
|
-
f"https://{server_host}",
|
|
196
|
-
method,
|
|
197
|
-
full_url,
|
|
198
|
-
)
|
|
199
|
-
|
|
200
209
|
return first_request, full_url, False
|
|
201
210
|
except Exception as e:
|
|
202
211
|
self.logger_config.error_logger.error(f"SSL request processing error : {e}")
|
|
@@ -221,6 +230,12 @@ class HttpsHandler:
|
|
|
221
230
|
|
|
222
231
|
not_inspect = self._should_skip_inspection(server_host)
|
|
223
232
|
|
|
233
|
+
thread_id = threading.get_ident()
|
|
234
|
+
self.active_connections[thread_id] = {
|
|
235
|
+
"bytes_sent": 0,
|
|
236
|
+
"bytes_received": 0,
|
|
237
|
+
}
|
|
238
|
+
|
|
224
239
|
if self.ssl_config.ssl_inspect and not not_inspect:
|
|
225
240
|
try:
|
|
226
241
|
cert_path, key_path = generate_certificate(
|
|
@@ -236,6 +251,8 @@ class HttpsHandler:
|
|
|
236
251
|
client_socket, cert_path, key_path
|
|
237
252
|
)
|
|
238
253
|
|
|
254
|
+
tls_version = ssl_client_socket.version() or "unknown"
|
|
255
|
+
|
|
239
256
|
server_socket = self._establish_server_connection(
|
|
240
257
|
server_host, server_port
|
|
241
258
|
)
|
|
@@ -244,7 +261,7 @@ class HttpsHandler:
|
|
|
244
261
|
)
|
|
245
262
|
|
|
246
263
|
first_request, full_url, is_blocked = self._process_first_ssl_request(
|
|
247
|
-
ssl_client_socket, server_host
|
|
264
|
+
ssl_client_socket, server_host, first_line
|
|
248
265
|
)
|
|
249
266
|
if is_blocked:
|
|
250
267
|
self._send_403(ssl_client_socket, target, first_line)
|
|
@@ -254,9 +271,31 @@ class HttpsHandler:
|
|
|
254
271
|
return
|
|
255
272
|
|
|
256
273
|
ssl_server_socket.sendall(first_request.encode())
|
|
274
|
+
client_ip = ssl_client_socket.getpeername()[0]
|
|
275
|
+
bytes_sent, bytes_received = self.transfer_data_between_sockets(
|
|
276
|
+
ssl_client_socket, ssl_server_socket
|
|
277
|
+
)
|
|
257
278
|
|
|
258
|
-
self.
|
|
279
|
+
if not self.logger_config.no_logging_access:
|
|
280
|
+
method, _, protocol = first_line.split(" ")
|
|
281
|
+
self.logger_config.access_logger.info(
|
|
282
|
+
"",
|
|
283
|
+
extra={
|
|
284
|
+
"ip_src": client_ip,
|
|
285
|
+
"url": target,
|
|
286
|
+
"method": method,
|
|
287
|
+
"domain": server_host,
|
|
288
|
+
"port": server_port,
|
|
289
|
+
"protocol": protocol,
|
|
290
|
+
"bytes_sent": bytes_sent,
|
|
291
|
+
"bytes_received": bytes_received,
|
|
292
|
+
"tls_version": tls_version,
|
|
293
|
+
},
|
|
294
|
+
)
|
|
259
295
|
|
|
296
|
+
except ConnectionAbortedError:
|
|
297
|
+
self.active_connections.pop(threading.get_ident(), None)
|
|
298
|
+
return
|
|
260
299
|
except ssl.SSLError as e:
|
|
261
300
|
self.console_logger.error("SSL error: %s", str(e))
|
|
262
301
|
except socket.error as e:
|
|
@@ -267,17 +306,31 @@ class HttpsHandler:
|
|
|
267
306
|
|
|
268
307
|
else:
|
|
269
308
|
try:
|
|
270
|
-
server_socket =
|
|
271
|
-
|
|
309
|
+
server_socket = self._establish_server_connection(
|
|
310
|
+
server_host, server_port
|
|
311
|
+
)
|
|
272
312
|
client_socket.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
|
|
313
|
+
|
|
314
|
+
client_ip = client_socket.getpeername()[0]
|
|
315
|
+
bytes_sent, bytes_received = self.transfer_data_between_sockets(
|
|
316
|
+
client_socket, server_socket
|
|
317
|
+
)
|
|
318
|
+
|
|
273
319
|
if not self.logger_config.no_logging_access:
|
|
320
|
+
_, _, protocol = first_line.split(" ")
|
|
274
321
|
self.logger_config.access_logger.info(
|
|
275
|
-
"
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
322
|
+
"",
|
|
323
|
+
extra={
|
|
324
|
+
"ip_src": client_ip,
|
|
325
|
+
"url": target,
|
|
326
|
+
"method": "CONNECT",
|
|
327
|
+
"domain": server_host,
|
|
328
|
+
"port": server_port,
|
|
329
|
+
"protocol": protocol,
|
|
330
|
+
"bytes_sent": bytes_sent,
|
|
331
|
+
"bytes_received": bytes_received,
|
|
332
|
+
},
|
|
279
333
|
)
|
|
280
|
-
self.transfer_data_between_sockets(client_socket, server_socket)
|
|
281
334
|
except (
|
|
282
335
|
socket.timeout,
|
|
283
336
|
socket.gaierror,
|
|
@@ -295,6 +348,8 @@ class HttpsHandler:
|
|
|
295
348
|
)
|
|
296
349
|
client_socket.sendall(response.encode())
|
|
297
350
|
client_socket.close()
|
|
351
|
+
finally:
|
|
352
|
+
self.active_connections.pop(thread_id, None)
|
|
298
353
|
|
|
299
354
|
def transfer_data_between_sockets(self, client_socket, server_socket):
|
|
300
355
|
"""
|
|
@@ -327,8 +382,12 @@ class HttpsHandler:
|
|
|
327
382
|
self.console_logger.debug("Closing connection.")
|
|
328
383
|
client_socket.close()
|
|
329
384
|
server_socket.close()
|
|
385
|
+
bytes_sent = self.active_connections[thread_id]["bytes_sent"]
|
|
386
|
+
bytes_received = self.active_connections[thread_id][
|
|
387
|
+
"bytes_received"
|
|
388
|
+
]
|
|
330
389
|
self.active_connections.pop(threading.get_ident(), None)
|
|
331
|
-
return
|
|
390
|
+
return bytes_sent, bytes_received
|
|
332
391
|
if sock is client_socket:
|
|
333
392
|
server_socket.sendall(data)
|
|
334
393
|
self.active_connections[thread_id]["bytes_sent"] += len(data)
|
|
@@ -56,6 +56,8 @@ def main():
|
|
|
56
56
|
)
|
|
57
57
|
|
|
58
58
|
console_format = config.get("Logging", "console_format", fallback=None)
|
|
59
|
+
access_log_format = config.get("Logging", "access_log_format", fallback=None)
|
|
60
|
+
block_log_format = config.get("Logging", "block_log_format", fallback=None)
|
|
59
61
|
datefmt = config.get("Logging", "datefmt", fallback=None)
|
|
60
62
|
|
|
61
63
|
logger_config = ProxyConfigLogger(
|
|
@@ -74,9 +76,44 @@ def main():
|
|
|
74
76
|
console_format=(
|
|
75
77
|
console_format
|
|
76
78
|
if console_format is not None
|
|
77
|
-
else
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
else (
|
|
80
|
+
"date=%(asctime)s "
|
|
81
|
+
"level=%(levelname)s "
|
|
82
|
+
"file=%(filename)s "
|
|
83
|
+
"function=%(funcName)s "
|
|
84
|
+
"message=%(message)s"
|
|
85
|
+
)
|
|
86
|
+
),
|
|
87
|
+
access_log_format=(
|
|
88
|
+
access_log_format
|
|
89
|
+
if access_log_format is not None
|
|
90
|
+
else (
|
|
91
|
+
"date=%(asctime)s "
|
|
92
|
+
"ip_src=%(ip_src)s "
|
|
93
|
+
"url=%(url)s "
|
|
94
|
+
"method=%(method)s "
|
|
95
|
+
"domain=%(domain)s "
|
|
96
|
+
"port=%(port)s "
|
|
97
|
+
"protocol=%(protocol)s "
|
|
98
|
+
"bytes_sent=%(bytes_sent)s "
|
|
99
|
+
"bytes_received=%(bytes_received)s "
|
|
100
|
+
"tls_version=%(tls_version)s"
|
|
101
|
+
)
|
|
102
|
+
),
|
|
103
|
+
block_log_format=(
|
|
104
|
+
block_log_format
|
|
105
|
+
if block_log_format is not None
|
|
106
|
+
else (
|
|
107
|
+
"date=%(asctime)s "
|
|
108
|
+
"ip_src=%(ip_src)s "
|
|
109
|
+
"url=%(url)s "
|
|
110
|
+
"method=%(method)s "
|
|
111
|
+
"domain=%(domain)s "
|
|
112
|
+
"port=%(port)s "
|
|
113
|
+
"protocol=%(protocol)s"
|
|
114
|
+
)
|
|
115
|
+
),
|
|
116
|
+
datefmt=datefmt if datefmt is not None else "%Y-%m-%d %H:%M:%S",
|
|
80
117
|
)
|
|
81
118
|
|
|
82
119
|
filter_config = ProxyConfigFilter(
|
|
@@ -105,11 +105,17 @@ class ProxyServer:
|
|
|
105
105
|
self.console_logger = configure_console_logger(self.logger_config)
|
|
106
106
|
if not self.logger_config.no_logging_access:
|
|
107
107
|
self.logger_config.access_logger = configure_file_logger(
|
|
108
|
-
self.logger_config.access_log,
|
|
108
|
+
self.logger_config.access_log,
|
|
109
|
+
"AccessLogger",
|
|
110
|
+
self.logger_config.access_log_format,
|
|
111
|
+
self.logger_config.datefmt,
|
|
109
112
|
)
|
|
110
113
|
if not self.logger_config.no_logging_block:
|
|
111
114
|
self.logger_config.block_logger = configure_file_logger(
|
|
112
|
-
self.logger_config.block_log,
|
|
115
|
+
self.logger_config.block_log,
|
|
116
|
+
"BlockLogger",
|
|
117
|
+
self.logger_config.block_log_format,
|
|
118
|
+
self.logger_config.datefmt,
|
|
113
119
|
)
|
|
114
120
|
|
|
115
121
|
# Configuration files
|
|
@@ -9,6 +9,23 @@ import os
|
|
|
9
9
|
import colorlog
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
class SafeFormatter(logging.Formatter):
|
|
13
|
+
def format(self, record):
|
|
14
|
+
if self._fmt:
|
|
15
|
+
for key in self._fmt.split("%("):
|
|
16
|
+
if ")" in key:
|
|
17
|
+
var = key.split(")")[0]
|
|
18
|
+
if not hasattr(record, var):
|
|
19
|
+
setattr(record, var, "")
|
|
20
|
+
else:
|
|
21
|
+
val = getattr(record, var)
|
|
22
|
+
if isinstance(val, str):
|
|
23
|
+
setattr(
|
|
24
|
+
record, var, val.replace("\n", "").replace("\r", "")
|
|
25
|
+
)
|
|
26
|
+
return super().format(record)
|
|
27
|
+
|
|
28
|
+
|
|
12
29
|
def configure_console_logger(logger_config) -> logging.Logger:
|
|
13
30
|
"""
|
|
14
31
|
Configures and returns a logger that outputs log messages to the console.
|
|
@@ -41,13 +58,18 @@ def configure_console_logger(logger_config) -> logging.Logger:
|
|
|
41
58
|
return console_logger
|
|
42
59
|
|
|
43
60
|
|
|
44
|
-
def configure_file_logger(
|
|
61
|
+
def configure_file_logger(
|
|
62
|
+
log_path: str, name: str, log_format: str, datefmt: str
|
|
63
|
+
) -> logging.Logger:
|
|
45
64
|
"""
|
|
46
65
|
Configures and returns a logger that writes log messages to a specified file.
|
|
47
66
|
|
|
48
67
|
Args:
|
|
49
68
|
log_path (str): The path where the log file will be created or appended to.
|
|
50
69
|
name (str): Logger's name.
|
|
70
|
+
log_format (str): The format string for log messages
|
|
71
|
+
(e.g., with fields like %(ip_src)s, %(method)s).
|
|
72
|
+
datefmt (str): The format string for timestamps (e.g., "%d/%m/%Y %H:%M:%S").
|
|
51
73
|
|
|
52
74
|
Returns:
|
|
53
75
|
logging.Logger: A logger instance that writes to the specified log file.
|
|
@@ -56,6 +78,6 @@ def configure_file_logger(log_path: str, name: str) -> logging.Logger:
|
|
|
56
78
|
file_logger = logging.getLogger(name)
|
|
57
79
|
file_logger.setLevel(logging.INFO)
|
|
58
80
|
file_handler = logging.FileHandler(log_path)
|
|
59
|
-
file_handler.setFormatter(
|
|
81
|
+
file_handler.setFormatter(SafeFormatter(log_format, datefmt=datefmt))
|
|
60
82
|
file_logger.addHandler(file_handler)
|
|
61
83
|
return file_logger
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyproxytools
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Lightweight and fast python web proxy
|
|
5
5
|
Author: 6C656C65
|
|
6
6
|
License-Expression: MIT
|
|
@@ -93,6 +93,12 @@ docker run -d ghcr.io/6c656c65/pyproxy:latest
|
|
|
93
93
|
```
|
|
94
94
|
You can use slim images by adding `-slim` to the end of the tags
|
|
95
95
|
|
|
96
|
+
### Install with Compose
|
|
97
|
+
```bash
|
|
98
|
+
wget https://raw.githubusercontent.com/6C656C65/pyproxy/main/docker-compose.yml
|
|
99
|
+
docker-compose up -d
|
|
100
|
+
```
|
|
101
|
+
|
|
96
102
|
## 🚀 **Usage**
|
|
97
103
|
|
|
98
104
|
### Start the proxy
|
|
@@ -14,10 +14,15 @@ from pyproxy.utils.logger import configure_console_logger, configure_file_logger
|
|
|
14
14
|
|
|
15
15
|
class DummyLoggerConfig:
|
|
16
16
|
def __init__(self, console_format=None, datefmt=None):
|
|
17
|
-
self.console_format = (
|
|
18
|
-
|
|
17
|
+
self.console_format = console_format or (
|
|
18
|
+
"%(log_color)s"
|
|
19
|
+
"date=%(asctime)s "
|
|
20
|
+
"level=%(levelname)s "
|
|
21
|
+
"file=%(filename)s "
|
|
22
|
+
"function=%(funcName)s "
|
|
23
|
+
"message=%(message)s"
|
|
19
24
|
)
|
|
20
|
-
self.datefmt = datefmt or "%d
|
|
25
|
+
self.datefmt = datefmt or "%Y-%m-%d %H:%M:%S"
|
|
21
26
|
|
|
22
27
|
|
|
23
28
|
class TestLogger(unittest.TestCase):
|
|
@@ -56,11 +61,15 @@ class TestLogger(unittest.TestCase):
|
|
|
56
61
|
mock_file_handler.return_value = mock_handler_instance
|
|
57
62
|
|
|
58
63
|
log_path = "logs/test.log"
|
|
59
|
-
|
|
64
|
+
log_name = "TestLogger"
|
|
65
|
+
log_format = "%(asctime)s - %(levelname)s - %(message)s"
|
|
66
|
+
datefmt = "%Y-%m-%d %H:%M:%S"
|
|
67
|
+
logger = configure_file_logger(log_path, log_name, log_format, datefmt)
|
|
60
68
|
|
|
61
|
-
self.assertTrue(logger.hasHandlers())
|
|
62
|
-
self.assertEqual(logger.level, logging.INFO)
|
|
69
|
+
self.assertTrue(logger.hasHandlers(), "Logger should have handlers.")
|
|
70
|
+
self.assertEqual(logger.level, logging.INFO, "Logger level should be INFO.")
|
|
63
71
|
mock_file_handler.assert_called_once_with(log_path)
|
|
72
|
+
mock_handler_instance.setFormatter.assert_called_once()
|
|
64
73
|
|
|
65
74
|
def tearDown(self):
|
|
66
75
|
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|