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.
Files changed (47) hide show
  1. {pyproxytools-0.4.2/pyproxytools.egg-info → pyproxytools-0.4.3}/PKG-INFO +7 -1
  2. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/README.md +6 -0
  3. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/__init__.py +1 -1
  4. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/handlers/http.py +40 -16
  5. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/handlers/https.py +81 -22
  6. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/pyproxy.py +40 -3
  7. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/server.py +8 -2
  8. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/config.py +2 -0
  9. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/logger.py +24 -2
  10. {pyproxytools-0.4.2 → pyproxytools-0.4.3/pyproxytools.egg-info}/PKG-INFO +7 -1
  11. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/utils/test_logger.py +15 -6
  12. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/LICENSE +0 -0
  13. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/benchmark/benchmark.py +0 -0
  14. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/benchmark/utils/__init__.py +0 -0
  15. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/benchmark/utils/html.py +0 -0
  16. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/benchmark/utils/req.py +0 -0
  17. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproject.toml +0 -0
  18. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/__main__.py +0 -0
  19. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/handlers/__init__.py +0 -0
  20. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/handlers/client.py +0 -0
  21. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/__init__.py +0 -0
  22. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/cancel_inspect.py +0 -0
  23. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/custom_header.py +0 -0
  24. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/filter.py +0 -0
  25. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/modules/shortcuts.py +0 -0
  26. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/monitoring/__init__.py +0 -0
  27. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/monitoring/web.py +0 -0
  28. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/__init__.py +0 -0
  29. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/args.py +0 -0
  30. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/crypto.py +0 -0
  31. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxy/utils/http_req.py +0 -0
  32. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/SOURCES.txt +0 -0
  33. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/dependency_links.txt +0 -0
  34. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/entry_points.txt +0 -0
  35. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/requires.txt +0 -0
  36. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/pyproxytools.egg-info/top_level.txt +0 -0
  37. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/requirements.txt +0 -0
  38. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/setup.cfg +0 -0
  39. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/setup.py +0 -0
  40. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/__init__.py +0 -0
  41. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/test_cancel_inspect.py +0 -0
  42. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/test_custom_header.py +0 -0
  43. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/test_filter.py +0 -0
  44. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/modules/test_shortcuts.py +0 -0
  45. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/utils/__init__.py +0 -0
  46. {pyproxytools-0.4.2 → pyproxytools-0.4.3}/tests/utils/test_crypto.py +0 -0
  47. {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.2
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
@@ -5,7 +5,7 @@ that holds the current version number of the application.
5
5
 
6
6
  import os
7
7
 
8
- __version__ = "0.4.2"
8
+ __version__ = "0.4.3"
9
9
 
10
10
  if os.path.isdir("pyproxy/monitoring"):
11
11
  __slim__ = False
@@ -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
- "%s - %s - %s", client_socket.getpeername()[0], url, first_line
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(client_socket, modified_request, url)
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]["target_ip"] = server_host
206
- self.active_connections[thread_id]["target_port"] = server_port
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
- "%s - %s - %s", client_socket.getpeername()[0], url, first_line
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
- ssl_client_socket.do_handshake()
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.transfer_data_between_sockets(ssl_client_socket, ssl_server_socket)
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 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
271
- server_socket.connect((server_host, server_port))
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
- "%s - %s - %s",
276
- client_socket.getpeername()[0],
277
- f"https://{server_host}",
278
- first_line,
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 "%(asctime)s - %(levelname)s - %(message)s"
78
- ),
79
- datefmt=datefmt if datefmt is not None else "%d/%m/%Y %H:%M:%S",
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, "AccessLogger"
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, "BlockLogger"
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
@@ -63,6 +63,8 @@ class ProxyConfigLogger:
63
63
  no_logging_access: bool
64
64
  no_logging_block: bool
65
65
  console_format: str
66
+ access_log_format: str
67
+ block_log_format: str
66
68
  datefmt: str
67
69
 
68
70
  def to_dict(self):
@@ -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(log_path: str, name: str) -> logging.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(logging.Formatter("%(asctime)s - %(message)s"))
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.2
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
- console_format or "%(log_color)s%(asctime)s - %(levelname)s - %(message)s"
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/%m/%Y %H:%M:%S"
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
- logger = configure_file_logger(log_path, "TestLogger")
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