mirror of
https://github.com/baoweise-bot/aimili-vpngate.git
synced 2026-09-04 23:26:47 +08:00
feat: initial open-source release with cleaned codebase
This commit is contained in:
+165
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
import select
|
||||
import socket
|
||||
import threading
|
||||
import urllib.parse
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
def parse_int(value: Any) -> int:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
def recv_exact(sock: socket.socket, size: int) -> bytes:
|
||||
data = b""
|
||||
while len(data) < size:
|
||||
chunk = sock.recv(size - len(data))
|
||||
if not chunk:
|
||||
raise ConnectionError("Unexpected disconnect.")
|
||||
data += chunk
|
||||
return data
|
||||
|
||||
def create_connection(address: tuple[str, int], timeout: float = 20) -> socket.socket:
|
||||
host, port = address
|
||||
err = None
|
||||
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
sock.settimeout(timeout)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, b"tun0")
|
||||
sock.connect(sa)
|
||||
return sock
|
||||
except OSError as e:
|
||||
err = e
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
if err is not None:
|
||||
raise err
|
||||
else:
|
||||
raise OSError("getaddrinfo returns empty list")
|
||||
|
||||
def relay(left: socket.socket, right: socket.socket) -> None:
|
||||
sockets = [left, right]
|
||||
while True:
|
||||
readable, _, errored = select.select(sockets, [], sockets, 120)
|
||||
if errored:
|
||||
return
|
||||
for source in readable:
|
||||
target = right if source is left else left
|
||||
data = source.recv(65536)
|
||||
if not data:
|
||||
return
|
||||
target.sendall(data)
|
||||
|
||||
def socks5_client(client: socket.socket, first_byte: bytes) -> None:
|
||||
upstream = None
|
||||
try:
|
||||
methods_count = recv_exact(client, 1)[0]
|
||||
recv_exact(client, methods_count)
|
||||
client.sendall(b"\x05\x00")
|
||||
version, command, _, address_type = recv_exact(client, 4)
|
||||
if version != 5 or command != 1:
|
||||
client.sendall(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||
return
|
||||
if address_type == 1:
|
||||
host = socket.inet_ntoa(recv_exact(client, 4))
|
||||
elif address_type == 3:
|
||||
host = recv_exact(client, recv_exact(client, 1)[0]).decode("idna")
|
||||
elif address_type == 4:
|
||||
host = socket.inet_ntop(socket.AF_INET6, recv_exact(client, 16))
|
||||
else:
|
||||
client.sendall(b"\x05\x08\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||
return
|
||||
port = int.from_bytes(recv_exact(client, 2), "big")
|
||||
upstream = create_connection((host, port), timeout=20)
|
||||
client.sendall(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00")
|
||||
relay(client, upstream)
|
||||
finally:
|
||||
client.close()
|
||||
if upstream:
|
||||
upstream.close()
|
||||
|
||||
def read_http_header(client: socket.socket, first_byte: bytes) -> bytes:
|
||||
data = first_byte
|
||||
while b"\r\n\r\n" not in data and len(data) < 65536:
|
||||
chunk = client.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
return data
|
||||
|
||||
def http_client(client: socket.socket, first_byte: bytes) -> None:
|
||||
upstream = None
|
||||
try:
|
||||
header = read_http_header(client, first_byte)
|
||||
head, rest = header.split(b"\r\n\r\n", 1)
|
||||
lines = head.decode("iso-8859-1", errors="replace").split("\r\n")
|
||||
method, target, version = lines[0].split(" ", 2)
|
||||
if method.upper() == "CONNECT":
|
||||
host, _, port_text = target.partition(":")
|
||||
port = parse_int(port_text) or 443
|
||||
upstream = create_connection((host, port), timeout=20)
|
||||
client.sendall(b"HTTP/1.1 200 Connection Established\r\n\r\n")
|
||||
if rest:
|
||||
upstream.sendall(rest)
|
||||
relay(client, upstream)
|
||||
return
|
||||
|
||||
parsed = urllib.parse.urlsplit(target)
|
||||
if not parsed.hostname:
|
||||
client.sendall(b"HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n")
|
||||
return
|
||||
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
||||
path = urllib.parse.urlunsplit(("", "", parsed.path or "/", parsed.query, ""))
|
||||
headers = [line for line in lines[1:] if not line.lower().startswith(("proxy-connection:", "connection:"))]
|
||||
request = f"{method} {path} {version}\r\n" + "\r\n".join(headers) + "\r\nConnection: close\r\n\r\n"
|
||||
upstream = create_connection((parsed.hostname, port), timeout=20)
|
||||
upstream.sendall(request.encode("iso-8859-1") + rest)
|
||||
relay(client, upstream)
|
||||
except Exception:
|
||||
try:
|
||||
client.sendall(b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\n\r\n")
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
client.close()
|
||||
if upstream:
|
||||
upstream.close()
|
||||
|
||||
def proxy_client(client: socket.socket, address: tuple[str, int]) -> None:
|
||||
try:
|
||||
client.settimeout(30)
|
||||
first = recv_exact(client, 1)
|
||||
if first == b"\x05":
|
||||
socks5_client(client, first)
|
||||
else:
|
||||
http_client(client, first)
|
||||
except Exception:
|
||||
try:
|
||||
client.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def start_proxy_server(host: str, port: int) -> None:
|
||||
try:
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind((host, port))
|
||||
server.listen(256)
|
||||
print(f"HTTP/SOCKS5 proxy listening on {host}:{port}", flush=True)
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to start HTTP/SOCKS5 proxy on {host}:{port}: {e}", flush=True)
|
||||
return
|
||||
|
||||
while True:
|
||||
try:
|
||||
client, address = server.accept()
|
||||
threading.Thread(target=proxy_client, args=(client, address), daemon=True).start()
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Proxy accept failed: {e}", flush=True)
|
||||
time.sleep(0.5)
|
||||
Reference in New Issue
Block a user