1
|
# Copyright (c) 2015, inaz2
|
2
|
# Copyright (C) 2021 jahoti <jahoti@tilde.team>
|
3
|
# Licensing information is collated in the `copyright` file
|
4
|
|
5
|
"""
|
6
|
The core for a "virtual network" proxy
|
7
|
|
8
|
Be sure to set certdir to your intended certificates directory before running.
|
9
|
"""
|
10
|
|
11
|
import os, socket, ssl, subprocess, sys, threading, time
|
12
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
13
|
from socketserver import ThreadingMixIn
|
14
|
|
15
|
gen_cert_req, lock = 'openssl req -new -key %scert.key -subj /CN=%s', threading.Lock()
|
16
|
sign_cert_req = 'openssl x509 -req -days 3650 -CA %sca.crt -CAkey %sca.key -set_serial %d -out %s'
|
17
|
|
18
|
def popen(command, *args, **kwargs):
|
19
|
return subprocess.Popen((command % args).split(' '), **kwargs)
|
20
|
|
21
|
class ProxyRequestHandler(BaseHTTPRequestHandler):
|
22
|
"""Handles a network request made to the proxy"""
|
23
|
def log_error(self, format, *args):
|
24
|
# suppress "Request timed out: timeout('timed out',)"
|
25
|
if isinstance(args[0], socket.timeout):
|
26
|
return
|
27
|
|
28
|
self.log_message(format, *args)
|
29
|
|
30
|
def do_CONNECT(self):
|
31
|
hostname = self.path.split(':')[0]
|
32
|
certpath = '%s%s.crt' % (certdir, hostname if hostname != 'ca' else 'CA')
|
33
|
|
34
|
with lock:
|
35
|
if not os.path.isfile(certpath):
|
36
|
p1 = popen(gen_cert_req, certdir, hostname, stdout=subprocess.PIPE).stdout
|
37
|
popen(sign_cert_req, certdir, certdir, time.time() * 1000, certpath, stdin=p1, stderr=subprocess.PIPE).communicate()
|
38
|
|
39
|
self.send_response(200)
|
40
|
self.end_headers()
|
41
|
|
42
|
self.connection = ssl.wrap_socket(self.connection, keyfile=certdir+'cert.key', certfile=certpath, server_side=True)
|
43
|
self.rfile = self.connection.makefile('rb', self.rbufsize)
|
44
|
self.wfile = self.connection.makefile('wb', self.wbufsize)
|
45
|
|
46
|
self.close_connection = int(self.headers.get('Proxy-Connection', '').lower() == 'close')
|
47
|
|
48
|
def proxy(self):
|
49
|
content_length = int(self.headers.get('Content-Length', 0))
|
50
|
req_body = self.rfile.read(content_length) if content_length else None
|
51
|
|
52
|
if self.path[0] == '/':
|
53
|
if isinstance(self.connection, ssl.SSLSocket):
|
54
|
self.path = 'https://%s%s' % (self.headers['Host'], self.path)
|
55
|
else:
|
56
|
self.path = 'http://%s%s' % (self.headers['Host'], self.path)
|
57
|
|
58
|
self.handle_request(req_body)
|
59
|
|
60
|
do_OPTIONS = do_DELETE = do_PUT = do_HEAD = do_POST = do_GET = proxy
|
61
|
|
62
|
def handle_request(self, req_body):
|
63
|
pass
|
64
|
|
65
|
|
66
|
class ThreadingHTTPServer(ThreadingMixIn, HTTPServer):
|
67
|
"""The actual proxy server"""
|
68
|
address_family, daemon_threads = socket.AF_INET6, True
|
69
|
|
70
|
def handle_error(self, request, client_address):
|
71
|
# suppress socket/ssl related errors
|
72
|
cls, e = sys.exc_info()[:2]
|
73
|
if not (cls is socket.error or cls is ssl.SSLError):
|
74
|
return HTTPServer.handle_error(self, request, client_address)
|