hugging-claw / local-proxy.py
abc1181's picture
Add CF Worker proxy (local-proxy.py) for clean egress
89dc462
Raw
History Blame Contribute Delete
6.68 kB
#!/usr/bin/env python3
import os, sys, socket, ssl, urllib.request, urllib.parse, struct, base64, select, threading, time
WORKER_HOST = os.environ.get("WORKER_HOST", "egress-proxy.arjav-3003jain.workers.dev")
PROXY_PORT = int(os.environ.get("LOCAL_PROXY_PORT", "8087"))
BUF_SIZE = 65536
def log(msg):
print(f"[proxy] {time.strftime('%H:%M:%S')} {msg}", flush=True)
def http_proxy_req(data):
try:
lines = data.split(b"\r\n")
parts = lines[0].decode("utf-8", errors="replace").split(" ")
if len(parts) < 3:
return None
method, target_url = parts[0], parts[1]
worker_url = f"https://{WORKER_HOST}/http?url={urllib.parse.quote(target_url, safe='')}"
req = urllib.request.Request(worker_url, method=method)
for line in lines[1:]:
if b":" in line:
k, v = line.split(b":", 1)
k = k.strip().decode("utf-8", errors="replace").lower()
if k in ("host", "proxy-connection", "proxy-authorization", "transfer-encoding"):
continue
req.add_header(k, v.strip().decode("utf-8", errors="replace"))
if method in ("POST", "PUT", "PATCH"):
req.data = data.split(b"\r\n\r\n", 1)[1] if b"\r\n\r\n" in data else b""
resp = urllib.request.urlopen(req, timeout=30)
hdrs = f"HTTP/1.1 {resp.status} OK\r\n".encode()
for k, v in resp.headers.items():
if k.lower() not in ("transfer-encoding", "content-encoding"):
hdrs += f"{k}: {v}\r\n".encode()
return hdrs + b"\r\n" + resp.read()
except urllib.error.HTTPError as e:
return f"HTTP/1.1 {e.code} Error\r\n\r\n{e.read().decode(errors='replace')}".encode()
except Exception as e:
return f"HTTP/1.1 502 Bad Gateway\r\n\r\nProxy error: {e}".encode()
def recv_exact(sock, n):
data = b""
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
return None
data += chunk
return data
def ws_send_frame(sock, data):
mask_key = os.urandom(4)
masked = bytes(b ^ mask_key[i % 4] for i, b in enumerate(data))
length = len(data)
hdr = bytearray([0x82])
if length < 126:
hdr.append(0x80 | length)
elif length < 65536:
hdr += bytearray([0x80 | 126]) + bytearray(struct.pack(">H", length))
else:
hdr += bytearray([0x80 | 127]) + bytearray(struct.pack(">Q", length))
sock.sendall(bytes(hdr) + mask_key + masked)
def ws_recv_frame(sock):
while True:
b1 = sock.recv(1)
if not b1:
return None
op = b1[0] & 0x0F
if op == 0x08:
return None
if op in (0x01, 0x02):
break
if op == 0x09:
ws_send_frame(sock, b"")
continue
if op == 0x0A:
continue
b2 = sock.recv(1)[0]
length = b2 & 0x7F
if length == 126:
raw = recv_exact(sock, 2)
if raw is None: return None
length = struct.unpack(">H", raw)[0]
elif length == 127:
raw = recv_exact(sock, 8)
if raw is None: return None
length = struct.unpack(">Q", raw)[0]
return recv_exact(sock, length)
def ws_connect_raw(host, port):
ctx = ssl.create_default_context()
sock = socket.create_connection((WORKER_HOST, 443), timeout=15)
ssock = ctx.wrap_socket(sock, server_hostname=WORKER_HOST)
key = base64.b64encode(os.urandom(16)).decode()
req = (
f"GET /ws-tunnel?host={host}&port={port} HTTP/1.1\r\n"
f"Host: {WORKER_HOST}\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: {key}\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"\r\n"
)
ssock.sendall(req.encode())
resp = b""
while b"\r\n\r\n" not in resp:
chunk = ssock.recv(BUF_SIZE)
if not chunk:
raise ConnectionError("Worker hung up")
resp += chunk
if b"101" not in resp:
raise ConnectionError(f"Upgrade failed: {resp.split(b'\r\n')[0].decode()}")
return key, ssock
def handle_connect(client_sock, addr, host, port):
try:
key, ws_sock = ws_connect_raw(host, port)
except Exception as e:
log(f"CONNECT {host}:{port} tunnel error: {e}")
try:
client_sock.sendall(b"HTTP/1.1 502 Bad Gateway\r\n\r\n")
except:
pass
return
try:
client_sock.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
except:
try:
ws_sock.close()
except:
pass
return
stop = threading.Event()
def client_to_ws():
try:
while not stop.is_set():
ready, _, _ = select.select([client_sock], [], [], 1)
if not ready:
continue
data = client_sock.recv(BUF_SIZE)
if not data:
break
ws_send_frame(ws_sock, data)
except:
pass
finally:
stop.set()
def ws_to_client():
try:
while not stop.is_set():
data = ws_recv_frame(ws_sock)
if data is None:
break
client_sock.sendall(data)
except:
pass
finally:
stop.set()
t1 = threading.Thread(target=client_to_ws, daemon=True)
t2 = threading.Thread(target=ws_to_client, daemon=True)
t1.start()
t2.start()
t1.join()
t2.join()
try:
ws_sock.close()
except:
pass
def handle_client(client_sock, addr):
try:
data = client_sock.recv(BUF_SIZE)
if not data:
return
parts = data.split(b"\r\n")[0].decode("utf-8", errors="replace").split(" ")
if parts[0] == "CONNECT":
target = parts[1]
host, port = (target.split(":") + ["443"])[:2]
handle_connect(client_sock, addr, host, int(port))
else:
response = http_proxy_req(data)
if response:
client_sock.sendall(response)
except:
pass
finally:
try:
client_sock.close()
except:
pass
def main():
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", PROXY_PORT))
srv.listen(128)
log(f"Listening 127.0.0.1:{PROXY_PORT}{WORKER_HOST}")
while True:
c, a = srv.accept()
threading.Thread(target=handle_client, args=(c, a), daemon=True).start()
if __name__ == "__main__":
main()