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.
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")
@@ -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
+ }
@@ -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