1
|
# SPDX-License-Identifier: BSD-3-Clause
|
2
|
|
3
|
"""
|
4
|
The core for a "virtual network" proxy.
|
5
|
"""
|
6
|
|
7
|
# This file is part of Haketilo.
|
8
|
#
|
9
|
# Copyright (c) 2015, inaz2
|
10
|
# Copyright (C) 2021 jahoti <jahoti@tilde.team>
|
11
|
# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
|
12
|
#
|
13
|
# Redistribution and use in source and binary forms, with or without
|
14
|
# modification, are permitted provided that the following conditions are met:
|
15
|
#
|
16
|
# * Redistributions of source code must retain the above copyright notice, this
|
17
|
# list of conditions and the following disclaimer.
|
18
|
#
|
19
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
20
|
# this list of conditions and the following disclaimer in the documentation
|
21
|
# and/or other materials provided with the distribution.
|
22
|
#
|
23
|
# * Neither the name of proxy2 nor the names of its contributors may be used to
|
24
|
# endorse or promote products derived from this software without specific
|
25
|
# prior written permission.
|
26
|
#
|
27
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
28
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
29
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
30
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
31
|
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
32
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
33
|
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
34
|
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
35
|
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
36
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
37
|
#
|
38
|
#
|
39
|
# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
|
40
|
# license. Although I request that you do not make use this code in a way
|
41
|
# incompliant with the license, I am not going to enforce this in court.
|
42
|
|
43
|
from pathlib import Path
|
44
|
import socket, ssl, subprocess, sys, threading
|
45
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
46
|
from socketserver import ThreadingMixIn
|
47
|
|
48
|
lock = threading.Lock()
|
49
|
|
50
|
class ProxyRequestHandler(BaseHTTPRequestHandler):
|
51
|
"""
|
52
|
Handles a network request made to the proxy. Configures SSL encryption when
|
53
|
needed.
|
54
|
"""
|
55
|
def __init__(self, *args, **kwargs):
|
56
|
"""
|
57
|
Initialize self. Uses the same arguments as
|
58
|
http.server.BaseHTTPRequestHandler's constructor but also expect a
|
59
|
`certdir` keyword argument with appropriate path.
|
60
|
"""
|
61
|
self.certdir = Path(kwargs.pop('certdir')).resolve()
|
62
|
super().__init__(*args, **kwargs)
|
63
|
|
64
|
def log_error(self, *args, **kwargs):
|
65
|
"""
|
66
|
Like log_error in http.server.BaseHTTPRequestHandler but suppresses
|
67
|
"Request timed out: timeout('timed out',)".
|
68
|
"""
|
69
|
if not isinstance(args[0], socket.timeout):
|
70
|
super().log_error(*args, **kwargs)
|
71
|
|
72
|
def get_cert(self, hostname):
|
73
|
"""
|
74
|
If needed, generate a signed x509 certificate for `hostname`. Return
|
75
|
paths to certificate's key file and to certificate itself in a tuple.
|
76
|
"""
|
77
|
root_keyfile = self.certdir / 'rootCA.key'
|
78
|
root_certfile = self.certdir / 'rootCA.pem'
|
79
|
keyfile = self.certdir / 'site.key'
|
80
|
certfile = self.certdir / f'{hostname}.crt'
|
81
|
|
82
|
with lock:
|
83
|
requestfile = self.certdir / f'{hostname}.csr'
|
84
|
if not certfile.exists():
|
85
|
subprocess.run([
|
86
|
'openssl', 'req', '-new', '-key', str(keyfile),
|
87
|
'-subj', f'/CN={hostname}', '-out', str(requestfile)
|
88
|
], check=True)
|
89
|
subprocess.run([
|
90
|
'openssl', 'x509', '-req', '-in', str(requestfile),
|
91
|
'-CA', str(root_certfile), '-CAkey', str(root_keyfile),
|
92
|
'-CAcreateserial', '-out', str(certfile), '-days', '1024'
|
93
|
], check=True)
|
94
|
|
95
|
return keyfile, certfile
|
96
|
|
97
|
def do_CONNECT(self):
|
98
|
"""Wrap the connection with SSL using on-demand signed certificate."""
|
99
|
hostname = self.path.split(':')[0]
|
100
|
sslargs = {'server_side': True}
|
101
|
sslargs['keyfile'], sslargs['certfile'] = self.get_cert(hostname)
|
102
|
|
103
|
self.send_response(200)
|
104
|
self.end_headers()
|
105
|
|
106
|
self.connection = ssl.wrap_socket(self.connection, **sslargs)
|
107
|
self.rfile = self.connection.makefile('rb', self.rbufsize)
|
108
|
self.wfile = self.connection.makefile('wb', self.wbufsize)
|
109
|
|
110
|
connection_header = self.headers.get('Proxy-Connection', '').lower()
|
111
|
self.close_connection = int(connection_header == 'close')
|
112
|
|
113
|
def do_GET(self):
|
114
|
content_length = int(self.headers.get('Content-Length', 0))
|
115
|
req_body = self.rfile.read(content_length) if content_length else None
|
116
|
|
117
|
if self.path[0] == '/':
|
118
|
secure = 's' if isinstance(self.connection, ssl.SSLSocket) else ''
|
119
|
self.path = f'http{secure}://{self.headers["Host"]}{self.path}'
|
120
|
|
121
|
self.handle_request(req_body)
|
122
|
|
123
|
do_OPTIONS = do_DELETE = do_PUT = do_HEAD = do_POST = do_GET
|
124
|
|
125
|
def handle_request(self, req_body):
|
126
|
"""Default handler that does nothing. Please override."""
|
127
|
pass
|
128
|
|
129
|
|
130
|
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
131
|
"""The actual proxy server"""
|
132
|
address_family, daemon_threads = socket.AF_INET6, True
|
133
|
|
134
|
def handle_error(self, request, client_address):
|
135
|
"""
|
136
|
Like handle_error in http.server.HTTPServer but suppresses socket/ssl
|
137
|
related errors.
|
138
|
"""
|
139
|
cls, e = sys.exc_info()[:2]
|
140
|
if not (cls is socket.error or cls is ssl.SSLError):
|
141
|
return super().handle_error(request, client_address)
|