aboutsummaryrefslogtreecommitdiff
path: root/test/haketilo_test/proxy_core.py
diff options
context:
space:
mode:
Diffstat (limited to 'test/haketilo_test/proxy_core.py')
-rw-r--r--test/haketilo_test/proxy_core.py141
1 files changed, 141 insertions, 0 deletions
diff --git a/test/haketilo_test/proxy_core.py b/test/haketilo_test/proxy_core.py
new file mode 100644
index 0000000..f6bb820
--- /dev/null
+++ b/test/haketilo_test/proxy_core.py
@@ -0,0 +1,141 @@
+# SPDX-License-Identifier: BSD-3-Clause
+
+"""
+The core for a "virtual network" proxy.
+"""
+
+# This file is part of Haketilo.
+#
+# Copyright (c) 2015, inaz2
+# Copyright (C) 2021 jahoti <jahoti@tilde.team>
+# Copyright (C) 2021 Wojtek Kosior <koszko@koszko.org>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# * Neither the name of proxy2 nor the names of its contributors may be used to
+# endorse or promote products derived from this software without specific
+# prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+#
+# I, Wojtek Kosior, thereby promise not to sue for violation of this file's
+# license. Although I request that you do not make use of this code in a way
+# incompliant with the license, I am not going to enforce this in court.
+
+from pathlib import Path
+import socket, ssl, subprocess, sys, threading
+from http.server import HTTPServer, BaseHTTPRequestHandler
+from socketserver import ThreadingMixIn
+
+lock = threading.Lock()
+
+class ProxyRequestHandler(BaseHTTPRequestHandler):
+ """
+ Handles a network request made to the proxy. Configures SSL encryption when
+ needed.
+ """
+ def __init__(self, *args, **kwargs):
+ """
+ Initialize self. Uses the same arguments as
+ http.server.BaseHTTPRequestHandler's constructor but also expect a
+ `certdir` keyword argument with appropriate path.
+ """
+ self.certdir = Path(kwargs.pop('certdir')).resolve()
+ super().__init__(*args, **kwargs)
+
+ def log_error(self, *args, **kwargs):
+ """
+ Like log_error in http.server.BaseHTTPRequestHandler but suppresses
+ "Request timed out: timeout('timed out',)".
+ """
+ if not isinstance(args[0], socket.timeout):
+ super().log_error(*args, **kwargs)
+
+ def get_cert(self, hostname):
+ """
+ If needed, generate a signed x509 certificate for `hostname`. Return
+ paths to certificate's key file and to certificate itself in a tuple.
+ """
+ root_keyfile = self.certdir / 'rootCA.key'
+ root_certfile = self.certdir / 'rootCA.pem'
+ keyfile = self.certdir / 'site.key'
+ certfile = self.certdir / f'{hostname}.crt'
+
+ with lock:
+ requestfile = self.certdir / f'{hostname}.csr'
+ if not certfile.exists():
+ subprocess.run([
+ 'openssl', 'req', '-new', '-key', str(keyfile),
+ '-subj', f'/CN={hostname}', '-out', str(requestfile)
+ ], check=True)
+ subprocess.run([
+ 'openssl', 'x509', '-req', '-in', str(requestfile),
+ '-CA', str(root_certfile), '-CAkey', str(root_keyfile),
+ '-CAcreateserial', '-out', str(certfile), '-days', '1024'
+ ], check=True)
+
+ return keyfile, certfile
+
+ def do_CONNECT(self):
+ """Wrap the connection with SSL using on-demand signed certificate."""
+ hostname = self.path.split(':')[0]
+ sslargs = {'server_side': True}
+ sslargs['keyfile'], sslargs['certfile'] = self.get_cert(hostname)
+
+ self.send_response(200)
+ self.end_headers()
+
+ self.connection = ssl.wrap_socket(self.connection, **sslargs)
+ self.rfile = self.connection.makefile('rb', self.rbufsize)
+ self.wfile = self.connection.makefile('wb', self.wbufsize)
+
+ connection_header = self.headers.get('Proxy-Connection', '').lower()
+ self.close_connection = int(connection_header == 'close')
+
+ def do_GET(self):
+ content_length = int(self.headers.get('Content-Length', 0))
+ req_body = self.rfile.read(content_length) if content_length else None
+
+ if self.path[0] == '/':
+ secure = 's' if isinstance(self.connection, ssl.SSLSocket) else ''
+ self.path = f'http{secure}://{self.headers["Host"]}{self.path}'
+
+ self.handle_request(req_body)
+
+ do_OPTIONS = do_DELETE = do_PUT = do_HEAD = do_POST = do_GET
+
+ def handle_request(self, req_body):
+ """Default handler that does nothing. Please override."""
+ pass
+
+
+class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
+ """The actual proxy server"""
+ address_family, daemon_threads = socket.AF_INET6, True
+
+ def handle_error(self, request, client_address):
+ """
+ Like handle_error in http.server.HTTPServer but suppresses socket/ssl
+ related errors.
+ """
+ cls, e = sys.exc_info()[:2]
+ if not (cls is socket.error or cls is ssl.SSLError):
+ return super().handle_error(request, client_address)