Skip to content

Commit 70e7e49

Browse files
committed
add feature custom header
1 parent e5a9da5 commit 70e7e49

8 files changed

Lines changed: 168 additions & 8 deletions

File tree

.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,8 +171,8 @@ cython_debug/
171171
.pypirc
172172

173173
# Config file
174-
config/*.txt
175-
!config/*.example.txt
174+
config/*
175+
!config/*.example*
176176

177177
# Certs folder
178178
certs/*.key

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
- Support distant (http) blacklist
3737
- Shortcuts
3838
- Cancel inspection on bank site
39+
- Custom header
3940

4041
## 📦 **Installation**
4142

@@ -86,7 +87,6 @@ If you encounter any problems, or if you want to use the program in a particular
8687
- Benchmark
8788
- Admin mode, statistiques and real time request
8889
- Image slim wihtout admin interface
89-
- Custom header
9090
- Fix HSTS
9191
- Wiki ssl inspection (volume certs/ca...)
9292

config.ini.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ blocked_url = config/blocked_url.txt
2020

2121
[Options]
2222
shortcuts = config/shortcuts.txt
23+
custom_header = config/custom_header.json
2324

2425
[Security]
2526
ssl_inspect = false

config/custom_header.example.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"https://example.com": {
3+
"X-Custom-Header": "HeaderValue",
4+
"X-Another-Header": "AnotherValue"
5+
},
6+
"https://example2.com": {
7+
"X-Different-Header": "DifferentValue"
8+
}
9+
}

pyproxy.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@
7070
type=str,
7171
help="Path to the text file containing the list of shortcuts"
7272
)
73+
parser.add_argument(
74+
"--custom-header",
75+
type=str,
76+
help="Path to the json file containing the list of custom headers"
77+
)
7378
parser.add_argument("--no-logging-access", action="store_true", help="Disable access logging")
7479
parser.add_argument("--no-logging-block", action="store_true", help="Disable block logging")
7580
parser.add_argument("--ssl-inspect", action="store_true", help="Enable SSL inspection")
@@ -133,6 +138,11 @@
133138
if args.blocked_url
134139
else config.get('Options', 'shortcuts', fallback="config/shortcuts.txt")
135140
)
141+
custom_header = (
142+
args.blocked_url
143+
if args.blocked_url
144+
else config.get('Options', 'custom_header', fallback="config/custom_header.json")
145+
)
136146
no_logging_access = (
137147
args.no_logging_access
138148
if args.no_logging_access
@@ -184,6 +194,7 @@
184194
blocked_sites=blocked_sites,
185195
blocked_url=blocked_url,
186196
shortcuts=shortcuts,
197+
custom_header=custom_header,
187198
inspect_ca_cert=inspect_ca_cert,
188199
inspect_ca_key=inspect_ca_key,
189200
inspect_certs_folder=inspect_certs_folder,

utils/cancel_inspect.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ def file_monitor() -> None:
5959
try:
6060
while True:
6161
new_cancel_inspect = load_cancel_inspect(cancel_inspect_path)
62-
cancel_inspect_data.clear()
63-
cancel_inspect_data.extend(new_cancel_inspect)
64-
62+
cancel_inspect_data[:] = new_cancel_inspect
6563
time.sleep(5)
6664
except (IOError, ValueError) as e:
6765
print(f"File monitor error: {e}")

utils/custom_header.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
custom_header.py
3+
4+
This module contains functions and a process to load and monitor custom header entries.
5+
It reads a file containing custom header data and checks whether specific entries exist in that file.
6+
The file is monitored in a background thread for live updates.
7+
8+
Functions:
9+
- load_custom_header: Loads custom header entries from a file into a list.
10+
- custom_header_process: Process that listens for header-like entries and checks
11+
if they exist in the custom header list.
12+
"""
13+
14+
import multiprocessing
15+
import time
16+
import sys
17+
import threading
18+
import json
19+
20+
def load_custom_header(custom_header_path: str) -> dict:
21+
"""
22+
Loads custom header entries from a file into a list.
23+
24+
Args:
25+
custom_header_path (str): The path to the file containing the custom headers.
26+
27+
Returns:
28+
dict: A dictionary containing the custom header data loaded from the file.
29+
"""
30+
with open(custom_header_path, 'r', encoding='utf-8') as f:
31+
return json.load(f)
32+
33+
# pylint: disable=too-many-locals
34+
def custom_header_process(
35+
queue: multiprocessing.Queue,
36+
result_queue: multiprocessing.Queue,
37+
custom_header_path: str
38+
) -> None:
39+
"""
40+
Process that monitors the custom header file and checks if received entries exist in it.
41+
42+
Args:
43+
queue (multiprocessing.Queue): A queue to receive header-like entries to check.
44+
result_queue (multiprocessing.Queue): A queue to send back True/False depending on match.
45+
custom_header_path (str): Path to the file containing custom header entries.
46+
"""
47+
manager = multiprocessing.Manager()
48+
custom_header_data = manager.dict(
49+
load_custom_header(custom_header_path)
50+
)
51+
52+
error_event = threading.Event()
53+
54+
def file_monitor() -> None:
55+
try:
56+
while True:
57+
new_custom_header = load_custom_header(custom_header_path)
58+
custom_header_data.clear()
59+
custom_header_data.update(new_custom_header)
60+
time.sleep(5)
61+
except (IOError, ValueError) as e:
62+
print(f"File monitor error: {e}")
63+
error_event.set()
64+
65+
monitor_thread = threading.Thread(target=file_monitor, daemon=True)
66+
monitor_thread.start()
67+
68+
while True:
69+
if error_event.is_set():
70+
print("Error detected in file monitor thread, terminating process.")
71+
sys.exit(1)
72+
73+
try:
74+
url = queue.get()
75+
headers = custom_header_data.get(url, {})
76+
result_queue.put(headers)
77+
78+
except KeyboardInterrupt:
79+
break

utils/proxy.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from utils.filter import filter_process
2424
from utils.shortcuts import shortcuts_process
2525
from utils.cancel_inspect import cancel_inspect_process
26+
from utils.custom_header import custom_header_process
2627
from utils.logger import configure_file_logger, configure_console_logger
2728

2829
class ProxyServer:
@@ -35,7 +36,7 @@ class ProxyServer:
3536
# pylint: disable=too-many-locals
3637
def __init__(self, host, port, debug, access_log, block_log,
3738
html_403, no_filter, filter_mode, no_logging_access, no_logging_block, ssl_inspect,
38-
blocked_sites, blocked_url, shortcuts, inspect_ca_cert,
39+
blocked_sites, blocked_url, shortcuts, custom_header, inspect_ca_cert,
3940
inspect_ca_key, inspect_certs_folder, cancel_inspect):
4041
"""
4142
Initializes the ProxyServer instance with the provided configurations.
@@ -57,10 +58,14 @@ def __init__(self, host, port, debug, access_log, block_log,
5758
self.cancel_inspect_proc = None
5859
self.cancel_inspect_queue = multiprocessing.Queue()
5960
self.cancel_inspect_result_queue = multiprocessing.Queue()
61+
self.custom_header_proc = None
62+
self.custom_header_queue = multiprocessing.Queue()
63+
self.custom_header_result_queue = multiprocessing.Queue()
6064
self.console_logger = configure_console_logger()
6165
self.config_blocked_sites = blocked_sites
6266
self.config_blocked_url = blocked_url
6367
self.config_shortcuts = shortcuts
68+
self.config_custom_header = custom_header
6469
self.config_cancel_inspect = cancel_inspect
6570
self.config_inspect_cert = inspect_ca_cert
6671
self.config_inspect_key = inspect_ca_key
@@ -91,6 +96,7 @@ def start(self):
9196
self.console_logger.debug("[*] blocked_sites = %s", self.config_blocked_sites)
9297
self.console_logger.debug("[*] blocked_url = %s", self.config_blocked_url)
9398
self.console_logger.debug("[*] shortcuts = %s", self.config_shortcuts)
99+
self.console_logger.debug("[*] custom_header = %s", self.config_custom_header)
94100
self.console_logger.debug("[*] inspect_ca_cert = %s", self.config_inspect_cert)
95101
self.console_logger.debug("[*] inspect_ca_key = %s", self.config_inspect_key)
96102
self.console_logger.debug(
@@ -162,6 +168,18 @@ def start(self):
162168
self.cancel_inspect_proc.start()
163169
self.console_logger.debug("[*] Starting the cancel inspection process...")
164170

171+
if self.config_custom_header and os.path.isfile(self.config_custom_header):
172+
self.custom_header_proc = multiprocessing.Process(
173+
target=custom_header_process,
174+
args=(
175+
self.custom_header_queue,
176+
self.custom_header_result_queue,
177+
self.config_custom_header
178+
)
179+
)
180+
self.custom_header_proc.start()
181+
self.console_logger.debug("[*] Starting the custom header process...")
182+
165183
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
166184
server.bind(self.host_port)
167185
server.listen(10)
@@ -213,6 +231,13 @@ def handle_http_request(self, client_socket, request):
213231
first_line = request.decode(errors='ignore').split("\n")[0]
214232
url = first_line.split(" ")[1]
215233

234+
if self.config_custom_header and os.path.isfile(self.config_custom_header):
235+
headers = self.extract_headers(request.decode(errors='ignore'))
236+
self.custom_header_queue.put(url)
237+
new_headers = self.custom_header_result_queue.get()
238+
headers.update(new_headers)
239+
print("headers", headers)
240+
216241
if self.config_shortcuts:
217242
domain, _ = self.parse_url(url)
218243
self.shortcuts_queue.put(domain)
@@ -259,7 +284,26 @@ def handle_http_request(self, client_socket, request):
259284
f"http://{server_host}",
260285
first_line
261286
)
262-
self.forward_request_to_server(client_socket, request, url)
287+
288+
if self.config_custom_header and os.path.isfile(self.config_custom_header):
289+
request_lines = request.decode(errors='ignore').split("\r\n")
290+
request_line = request_lines[0] # GET / HTTP/1.1
291+
292+
header_lines = [f"{key}: {value}" for key, value in headers.items()]
293+
reconstructed_headers = "\r\n".join(header_lines)
294+
295+
if "\r\n\r\n" in request.decode(errors='ignore'):
296+
body = request.decode(errors='ignore').split("\r\n\r\n", 1)[1]
297+
else:
298+
body = ""
299+
300+
modified_request = f"{request_line}\r\n{reconstructed_headers}\r\n\r\n{body}".encode()
301+
302+
# 5. Envoyer au serveur
303+
self.forward_request_to_server(client_socket, modified_request, url)
304+
305+
else:
306+
self.forward_request_to_server(client_socket, request, url)
263307

264308
def forward_request_to_server(self, client_socket, request, url):
265309
"""
@@ -320,6 +364,24 @@ def parse_url(self, url):
320364

321365
return server_host, server_port
322366

367+
def extract_headers(self, request_str):
368+
"""
369+
Extracts the HTTP headers from a raw HTTP request string.
370+
371+
Args:
372+
request_str (str): The full HTTP request as a decoded string.
373+
374+
Returns:
375+
dict: A dictionary containing the HTTP header fields as key-value pairs.
376+
"""
377+
headers = {}
378+
lines = request_str.split("\n")[1:]
379+
for line in lines:
380+
if line.strip():
381+
key, value = line.split(":", 1)
382+
headers[key.strip()] = value.strip()
383+
return headers
384+
323385
# pylint: disable=too-many-locals,too-many-statements,too-many-branches,too-many-nested-blocks
324386
def handle_https_connection(self, client_socket, first_line):
325387
"""

0 commit comments

Comments
 (0)