Skip to content

Commit c458b3b

Browse files
committed
add feature shortcuts
1 parent 676cf7d commit c458b3b

9 files changed

Lines changed: 203 additions & 30 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ cython_debug/
172172

173173
# Config file
174174
config/*.txt
175+
!config/*.example.txt
175176

176177
# Certs folder
177178
certs/*.key

config.ini.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ filter_mode = local
1818
blocked_sites = config/blocked_sites.txt
1919
blocked_url = config/blocked_url.txt
2020

21+
[Options]
22+
shortcuts = config/shortcuts.txt
23+
2124
[Security]
2225
ssl_inspect = false
2326
inspect_ca_cert = certs/ca/cert.pem

config/.gitkeep

Whitespace-only changes.

config/blocked_sites.example.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
example.com

config/blocked_url.example.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
example2.com/about

config/shortcuts.example.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
f=https://facebook.com
2+
gh=https://github.com
3+
yt=https://youtube.com

pyproxy.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@
4343
type=str,
4444
help="Path to the block log file"
4545
)
46+
parser.add_argument(
47+
"--shortcuts",
48+
type=str,
49+
help="Path to the shortcuts file"
50+
)
4651
parser.add_argument(
4752
"--html-403",
4853
type=str,
@@ -118,6 +123,11 @@
118123
if args.blocked_url
119124
else config.get('Filtering', 'blocked_url', fallback="config/blocked_url.txt")
120125
)
126+
shortcuts = (
127+
args.blocked_url
128+
if args.blocked_url
129+
else config.get('Options', 'shortcuts', fallback="config/shortcuts.txt")
130+
)
121131
no_logging_access = (
122132
args.no_logging_access
123133
if args.no_logging_access
@@ -163,6 +173,7 @@
163173
ssl_inspect=ssl_inspect,
164174
blocked_sites=blocked_sites,
165175
blocked_url=blocked_url,
176+
shortcuts=shortcuts,
166177
inspect_ca_cert=inspect_ca_cert,
167178
inspect_ca_key=inspect_ca_key,
168179
inspect_certs_folder=inspect_certs_folder

utils/proxy.py

Lines changed: 91 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from OpenSSL import crypto
2222

2323
from utils.filter import filter_process
24+
from utils.shortcuts import shortcuts_process
2425
from utils.logger import configure_file_logger, configure_console_logger
2526

2627
class ProxyServer:
@@ -33,7 +34,7 @@ class ProxyServer:
3334
# pylint: disable=too-many-locals
3435
def __init__(self, host, port, debug, access_log, block_log,
3536
html_403, no_filter, filter_mode, no_logging_access, no_logging_block, ssl_inspect,
36-
blocked_sites, blocked_url, inspect_ca_cert, inspect_ca_key, inspect_certs_folder):
37+
blocked_sites, blocked_url, shortcuts, inspect_ca_cert, inspect_ca_key, inspect_certs_folder):
3738
"""
3839
Initializes the ProxyServer instance with the provided configurations.
3940
"""
@@ -46,11 +47,15 @@ def __init__(self, host, port, debug, access_log, block_log,
4647
self.no_logging_block = no_logging_block
4748
self.ssl_inspect = ssl_inspect
4849
self.filter_proc = None
49-
self.queue = multiprocessing.Queue()
50-
self.result_queue = multiprocessing.Queue()
50+
self.filter_queue = multiprocessing.Queue()
51+
self.filter_result_queue = multiprocessing.Queue()
52+
self.shortcuts_proc = None
53+
self.shortcuts_queue = multiprocessing.Queue()
54+
self.shortcuts_result_queue = multiprocessing.Queue()
5155
self.console_logger = configure_console_logger()
5256
self.config_blocked_sites = blocked_sites
5357
self.config_blocked_url = blocked_url
58+
self.config_shortcuts = shortcuts
5459
self.config_inspect_cert = inspect_ca_cert
5560
self.config_inspect_key = inspect_ca_key
5661
self.config_inspect_certs_folder = inspect_certs_folder
@@ -79,6 +84,7 @@ def start(self):
7984
self.console_logger.debug("[*] ssl_inspect = %s", self.ssl_inspect)
8085
self.console_logger.debug("[*] blocked_sites = %s", self.config_blocked_sites)
8186
self.console_logger.debug("[*] blocked_url = %s", self.config_blocked_url)
87+
self.console_logger.debug("[*] shortcuts = %s", self.config_shortcuts)
8288
self.console_logger.debug("[*] inspect_ca_cert = %s", self.config_inspect_cert)
8389
self.console_logger.debug("[*] inspect_ca_key = %s", self.config_inspect_key)
8490
self.console_logger.debug(
@@ -116,14 +122,27 @@ def start(self):
116122
self.filter_proc = multiprocessing.Process(
117123
target=filter_process,
118124
args=(
119-
self.queue,
120-
self.result_queue,
125+
self.filter_queue,
126+
self.filter_result_queue,
121127
self.filter_mode,
122128
self.config_blocked_sites,
123129
self.config_blocked_url
124130
)
125131
)
126132
self.filter_proc.start()
133+
self.console_logger.debug("[*] Starting the filter process...")
134+
135+
if self.config_shortcuts and os.path.isfile(self.config_shortcuts):
136+
self.shortcuts_proc = multiprocessing.Process(
137+
target=shortcuts_process,
138+
args=(
139+
self.shortcuts_queue,
140+
self.shortcuts_result_queue,
141+
self.config_shortcuts
142+
)
143+
)
144+
self.shortcuts_proc.start()
145+
self.console_logger.debug("[*] Starting the shortcuts process...")
127146

128147
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
129148
server.bind(self.host_port)
@@ -176,9 +195,28 @@ def handle_http_request(self, client_socket, request):
176195
first_line = request.decode(errors='ignore').split("\n")[0]
177196
url = first_line.split(" ")[1]
178197

198+
if self.config_shortcuts:
199+
domain, _ = self.parse_url(url)
200+
print(url)
201+
print(domain)
202+
self.shortcuts_queue.put(domain)
203+
shortcut_url = self.shortcuts_result_queue.get()
204+
print(shortcut_url)
205+
if shortcut_url:
206+
response = (
207+
"HTTP/1.1 302 Found\r\n"
208+
"Location: {shortcut_url}\r\n"
209+
"Content-Length: 0\r\n"
210+
"\r\n"
211+
).format(shortcut_url=shortcut_url)
212+
213+
client_socket.sendall(response.encode())
214+
client_socket.close()
215+
return
216+
179217
if not self.no_filter:
180-
self.queue.put(url)
181-
result = self.result_queue.get()
218+
self.filter_queue.put(url)
219+
result = self.filter_result_queue.get()
182220
if result[1] == "Blocked":
183221
if not self.no_logging_block:
184222
self.block_logger.info(
@@ -218,16 +256,28 @@ def forward_request_to_server(self, client_socket, request, url):
218256
url (str): The target URL from the HTTP request.
219257
"""
220258
server_host, server_port = self.parse_url(url)
221-
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
222-
server_socket.connect((server_host, server_port))
223-
server_socket.sendall(request)
224259

225-
while True:
226-
response = server_socket.recv(4096)
227-
if len(response) > 0:
228-
client_socket.send(response)
229-
else:
230-
break
260+
try:
261+
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
262+
server_socket.connect((server_host, server_port))
263+
server_socket.sendall(request)
264+
265+
while True:
266+
response = server_socket.recv(4096)
267+
if len(response) > 0:
268+
client_socket.send(response)
269+
else:
270+
break
271+
except Exception as e:
272+
self.console_logger.error(f"Error connecting to the server {server_host}: {e}")
273+
response = (
274+
f"HTTP/1.1 502 Bad Gateway\r\n"
275+
f"Content-Length: {len('Bad Gateway')} \r\n"
276+
f"\r\n"
277+
f"Bad Gateway"
278+
)
279+
client_socket.sendall(response.encode())
280+
client_socket.close()
231281

232282
def parse_url(self, url):
233283
"""
@@ -270,8 +320,8 @@ def handle_https_connection(self, client_socket, first_line):
270320
server_port = int(server_port)
271321

272322
if not self.no_filter:
273-
self.queue.put(target)
274-
result = self.result_queue.get()
323+
self.filter_queue.put(target)
324+
result = self.filter_result_queue.get()
275325
if result[1] == "Blocked":
276326
if not self.no_logging_block:
277327
self.block_logger.info(
@@ -330,8 +380,8 @@ def handle_https_connection(self, client_socket, first_line):
330380
full_url = f"https://{server_host}{path}"
331381

332382
if not self.no_filter:
333-
self.queue.put(f"{server_host}{path}")
334-
result = self.result_queue.get()
383+
self.filter_queue.put(f"{server_host}{path}")
384+
result = self.filter_result_queue.get()
335385
if result[1] == "Blocked":
336386
if not self.no_logging_block:
337387
self.block_logger.info(
@@ -381,17 +431,28 @@ def handle_https_connection(self, client_socket, first_line):
381431
client_socket.close()
382432

383433
else:
384-
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
385-
server_socket.connect((server_host, server_port))
386-
client_socket.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
387-
if not self.no_logging_access:
388-
self.access_logger.info(
389-
"%s - %s - %s",
390-
client_socket.getpeername()[0],
391-
f"https://{server_host}",
392-
first_line
434+
try:
435+
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
436+
server_socket.connect((server_host, server_port))
437+
client_socket.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
438+
if not self.no_logging_access:
439+
self.access_logger.info(
440+
"%s - %s - %s",
441+
client_socket.getpeername()[0],
442+
f"https://{server_host}",
443+
first_line
444+
)
445+
self.transfer_data_between_sockets(client_socket, server_socket)
446+
except Exception as e:
447+
self.console_logger.error(f"Error connecting to the server {server_host}: {e}")
448+
response = (
449+
f"HTTP/1.1 502 Bad Gateway\r\n"
450+
f"Content-Length: {len('Bad Gateway')} \r\n"
451+
f"\r\n"
452+
f"Bad Gateway"
393453
)
394-
self.transfer_data_between_sockets(client_socket, server_socket)
454+
client_socket.sendall(response.encode())
455+
client_socket.close()
395456

396457
def transfer_data_between_sockets(self, client_socket, server_socket):
397458
"""

utils/shortcuts.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
"""
2+
filter.py
3+
4+
This module contains functions and a process to filter and block domains and URLs.
5+
It loads blocked domain names and URLs from specified files, then listens for
6+
incoming requests to check if the domain or URL should be blocked.
7+
8+
Functions:
9+
- load_blacklist: Loads blocked FQDNs and URLs from files into sets for fast lookup.
10+
- filter_process: The process that checks whether a domain or URL is blocked.
11+
"""
12+
13+
import multiprocessing
14+
import time
15+
import sys
16+
import threading
17+
import requests
18+
19+
def load_shortcuts(shortcuts_path: str) -> dict:
20+
"""
21+
Loads blocked FQDNs or URLs from a file or URL into a set for fast lookup.
22+
23+
Args:
24+
blocked_sites_path (str): The path or URL to the file containing blocked FQDNs.
25+
blocked_url_path (str): The path or URL to the file containing blocked URLs.
26+
filter_mode (str): Mode to determine if we load from local file or HTTP URL.
27+
28+
Returns:
29+
set: A set of blocked domains/URLs.
30+
"""
31+
shortcuts = {}
32+
33+
with open(shortcuts_path, 'r', encoding='utf-8') as f:
34+
for line in f:
35+
line = line.strip()
36+
if "=" in line:
37+
alias, url = line.split("=", 1)
38+
shortcuts[alias.strip()] = url.strip()
39+
40+
return shortcuts
41+
42+
# pylint: disable=too-many-locals
43+
def shortcuts_process(
44+
queue: multiprocessing.Queue,
45+
result_queue: multiprocessing.Queue,
46+
shortcuts_path: str
47+
) -> None:
48+
"""
49+
Process that listens for requests and checks if the domain/URL should be blocked.
50+
51+
Args:
52+
queue (multiprocessing.Queue): A queue to receive URL/domain for checking.
53+
result_queue (multiprocessing.Queue): A queue to send back the result of
54+
the filtering (blocked or allowed).
55+
filter_mode (str): Filter list mode (local or http).
56+
blocked_sites_path (str): The path to the file containing blocked FQDNs.
57+
blocked_url_path (str): The path to the file containing blocked URLs.
58+
"""
59+
manager = multiprocessing.Manager()
60+
shortcuts_data = manager.dict({
61+
"shortcuts": load_shortcuts(shortcuts_path)
62+
})
63+
64+
error_event = threading.Event()
65+
66+
def file_monitor() -> None:
67+
try:
68+
while True:
69+
new_shortcuts = load_shortcuts(shortcuts_path)
70+
71+
shortcuts_data["shortcuts"] = new_shortcuts
72+
73+
time.sleep(5)
74+
except (IOError, ValueError) as e:
75+
print(f"File monitor error: {e}")
76+
error_event.set()
77+
78+
monitor_thread = threading.Thread(target=file_monitor, daemon=True)
79+
monitor_thread.start()
80+
81+
while True:
82+
if error_event.is_set():
83+
print("Error detected in file monitor thread, terminating process.")
84+
sys.exit(1)
85+
86+
try:
87+
alias = queue.get()
88+
url = shortcuts_data["shortcuts"].get(alias)
89+
result_queue.put(url)
90+
91+
except KeyboardInterrupt:
92+
break

0 commit comments

Comments
 (0)