seminar-vibecoding/http2socks.py
2026-08-06 23:08:41 +03:00

180 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""HTTP (CONNECT + plain) proxy that forwards everything to a SOCKS5 upstream.
Why: a lot of tools/extensions only speak HTTP(S) proxies, not SOCKS. Run
this once and point them at it -- it bridges to your local SSH dynamic SOCKS
tunnel, doing remote DNS (socks5h) on the upstream side.
+-----------+ HTTP/HTTPS +-----------+ SOCKS5h +-----------+
| tools | --> 127.0.0.1:18080 --> | this | --> 127.0.0.1:12026 --> | ssh -D |
+-----------+ | bridge | +-----------+
+-----------+
Usage:
.venv/bin/python http2socks.py # default 127.0.0.1:18080
.venv/bin/python http2socks.py --port 8080 # custom port
BIND=0.0.0.0 .venv/bin/python http2socks.py # bind all interfaces
UPSTREAM=socks5h://127.0.0.1:18080 .venv/bin/python http2socks.py
Env vars (override defaults):
BIND listen address (default 127.0.0.1)
PORT listen port (default 18080)
UPSTREAM socks5h://host:port (default socks5h://127.0.0.1:12026)
Then set for your tools:
HTTP_PROXY=http://127.0.0.1:18080
HTTPS_PROXY=http://127.0.0.1:18080
ALL_PROXY=http://127.0.0.1:18080
"""
import argparse
import os
import socket
import socketserver
import threading
import urllib.parse
import socks # PySocks
# --- upstream config --------------------------------------------------------
def parse_upstream(s: str):
u = urllib.parse.urlparse(s)
scheme = (u.scheme or "socks5h").lower()
if scheme not in ("socks5h", "socks5"):
raise SystemExit(f"unsupported upstream scheme: {scheme!r} (use socks5h://)")
# socks5h => PROXY_TYPE_SOCKS5 with remote DNS (rdns=True)
host = u.hostname or "127.0.0.1"
port = u.port or 12026
return host, port
UPSTREAM_DEFAULT = os.environ.get("UPSTREAM", "socks5h://127.0.0.1:12026")
UP_HOST, UP_PORT = parse_upstream(UPSTREAM_DEFAULT)
def make_upstream_socket() -> socks.socksocket:
s = socks.socksocket()
# PROXY_TYPE_SOCKS5 + rdns=True => socks5h (DNS resolved by the SOCKS server)
s.set_proxy(socks.SOCKS5, UP_HOST, UP_PORT, rdns=True)
return s
# --- proxy server -----------------------------------------------------------
def pipe(src: socket.socket, dst: socket.socket):
try:
while True:
data = src.recv(65536)
if not data:
break
dst.sendall(data)
except OSError:
pass
finally:
for s in (src, dst):
try:
s.shutdown(socket.SHUT_RDWR)
except OSError:
pass
try:
s.close()
except OSError:
pass
class ProxyHandler(socketserver.BaseRequestHandler):
timeout = 120
def handle(self):
req = self.request
# read the request line + headers
buf = b""
while b"\r\n\r\n" not in buf:
chunk = req.recv(4096)
if not chunk:
return
buf += chunk
if len(buf) > 1 << 16: # 64k guard against malformed clients
req.sendall(b"HTTP/1.1 400 Bad Request\r\n\r\n")
req.close()
return
head, _, rest = buf.partition(b"\r\n\r\n")
lines = head.split(b"\r\n")
request_line = lines[0].decode("latin-1")
try:
method, target, _ver = request_line.split(" ", 2)
except ValueError:
req.sendall(b"HTTP/1.1 400 Bad Request\r\n\r\n")
req.close()
return
upstream = make_upstream_socket()
if method == "CONNECT":
# target is host:port
host, _, port = target.rpartition(":")
if not host or not port:
req.sendall(b"HTTP/1.1 400 Bad Request\r\n\r\n")
req.close()
return
try:
upstream.connect((host, int(port)))
except Exception:
req.sendall(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
req.close()
upstream.close()
return
req.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
# any bytes already read past the CONNECT line go upstream
if rest:
upstream.sendall(rest)
threading.Thread(target=pipe, args=(req, upstream), daemon=True).start()
pipe(upstream, req)
else:
# plain HTTP request; target is a full URL
parsed = urllib.parse.urlparse(target)
host = parsed.hostname
port = parsed.port or (443 if parsed.scheme == "https" else 80)
if not host:
req.sendall(b"HTTP/1.1 400 Bad Request\r\n\r\n")
req.close()
upstream.close()
return
try:
upstream.connect((host, port))
except Exception:
req.sendall(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
req.close()
upstream.close()
return
# forward original request verbatim
upstream.sendall(buf)
threading.Thread(target=pipe, args=(req, upstream), daemon=True).start()
pipe(upstream, req)
class ThreadingTCPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
def main():
ap = argparse.ArgumentParser(description="HTTP proxy bridging to a SOCKS5h upstream")
ap.add_argument("--bind", default=os.environ.get("BIND", "127.0.0.1"))
ap.add_argument("--port", type=int, default=int(os.environ.get("PORT", "18080")))
args = ap.parse_args()
print(f"http2socks: listening on http://{args.bind}:{args.port} -> "
f"socks5h://{UP_HOST}:{UP_PORT} (remote DNS)", flush=True)
with ThreadingTCPServer((args.bind, args.port), ProxyHandler) as srv:
try:
srv.serve_forever()
except KeyboardInterrupt:
print("\nhttp2socks: shutting down", flush=True)
if __name__ == "__main__":
main()