Last active
May 29, 2025 12:21
-
-
Save jcoffland/3d2c68f1c819ff6a75f2c31469dad967 to your computer and use it in GitHub Desktop.
This script implements a very basic HTTP/HTTPS proxy.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# | |
# This script implements a very basic HTTP/HTTPS 1.1 proxy. | |
# | |
# Author: Joseph Coffland | |
# Copyright: 2025, Cauldron Development Oy | |
# License: GPL 3+ | |
import socket, _thread, select, argparse | |
class ConnectionHandler: | |
def __init__(self, connection, address, timeout, verbose): | |
self.client = connection | |
self.timeout = timeout | |
self.verbose = verbose | |
self.buffer = b'' | |
self.method, self.path, self.protocol = self._read_header() | |
self.log(f'New connection {self.method}, {self.path}, {self.protocol}') | |
if self.method == b'CONNECT': | |
self._connect(self.path) | |
self.client.sendall(b'HTTP/1.1 200 Connection established\n\n') | |
self._transfer() | |
else: | |
self.path = self.path[7:] | |
i = self.path.find(b'/') | |
host, path = self.path[:i], self.path[i:] | |
self._connect(host) | |
self.target.sendall( | |
b'%s %s %s\n' % (self.method, path, self.protocol) + self.buffer) | |
self._transfer() | |
self.client.close() | |
def log(self, *args): | |
if self.verbose: print(*args) | |
def _read_header(self): | |
while True: | |
self.buffer += self.client.recv(8192) | |
end = self.buffer.find(b'\n') | |
if end != -1: | |
header = self.buffer[:end + 1].split() | |
self.buffer = self.buffer[end + 1:] | |
return header | |
def _connect(self, path): | |
i = path.find(b':') | |
if i != -1: host, port = path[:i], int(path[i + 1:]) | |
else: host, port = path, 80 | |
(family, _, _, _, addr) = socket.getaddrinfo(host, port)[0] | |
self.target = socket.socket(family) | |
self.target.connect(addr) | |
def _transfer(self): | |
fds = [self.client, self.target] | |
while True: | |
sent = False | |
recv, _, err = select.select(fds, [], fds, self.timeout) | |
if not err and recv: | |
for fd in recv: | |
data = fd.recv(65536) | |
if data: | |
out = self.target if fd is self.client else self.client | |
out.sendall(data) | |
sent = True | |
if not sent: break | |
def main(args): | |
sock = socket.socket(socket.AF_INET6 if args.ipv6 else socket.AF_INET) | |
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
sock.bind((args.bind, args.port)) | |
print(f'Serving HTTP proxy on {args.bind}:{args.port}') | |
sock.listen(0) | |
while True: | |
_thread.start_new_thread( | |
ConnectionHandler, sock.accept() + (args.timeout, args.verbose)) | |
parser = argparse.ArgumentParser(description = 'A very basic HTTP(S) 1.1 proxy server') | |
parser.add_argument('--verbose', '-v', action = 'store_true', | |
help = 'verbose output') | |
parser.add_argument('--bind', '-b', default = '127.0.0.1', | |
help = 'address to bind') | |
parser.add_argument('--port', '-p', default = 12347, type = int, | |
help = 'port to bind') | |
parser.add_argument('--ipv6', '-6', action = 'store_true', help = 'use IPv6') | |
parser.add_argument('--timeout', '-t', default = 60, type = int, | |
help = 'timeout in seconds') | |
main(parser.parse_args()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment