180 lines
6.1 KiB
Python
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() |