mirror of
https://github.com/masterking32/MasterHttpRelayVPN.git
synced 2026-05-17 21:24:37 +03:00
Refactor documentation and code for Apps Script relay mode; remove WebSocket support.
This commit is contained in:
@@ -35,7 +35,7 @@ This PR does not claim full compatibility for all websites. It focuses on making
|
|||||||
|
|
||||||
Local verification:
|
Local verification:
|
||||||
|
|
||||||
- `python3 -m py_compile main.py proxy_server.py domain_fronter.py mitm.py h2_transport.py ws.py cert_installer.py`
|
- `python3 -m py_compile main.py proxy_server.py domain_fronter.py mitm.py h2_transport.py cert_installer.py`
|
||||||
|
|
||||||
Observed behavior during manual testing:
|
Observed behavior during manual testing:
|
||||||
|
|
||||||
|
|||||||
@@ -176,14 +176,7 @@ Firefox uses its own certificate store, so even after OS-level install you need
|
|||||||
|
|
||||||
## Modes Overview
|
## Modes Overview
|
||||||
|
|
||||||
| Mode | What You Need | Description |
|
This project focuses entirely on the **Apps Script** relay — a free Google account is all you need, no server, no VPS, no Cloudflare setup. Everything is configured out of the box for this mode.
|
||||||
|------|--------------|-------------|
|
|
||||||
| `apps_script` | Free Google account | **Easiest.** Uses Google Apps Script as relay. No server needed. |
|
|
||||||
| `google_fronting` | Google Cloud Run service | Uses your own Cloud Run service behind Google's CDN. |
|
|
||||||
| `domain_fronting` | Cloudflare Worker | Uses a Cloudflare Worker as relay. |
|
|
||||||
| `custom_domain` | Custom domain on Cloudflare | Connects directly to your domain on Cloudflare. |
|
|
||||||
|
|
||||||
Most users should use **`apps_script`** mode — it's free and requires no server.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -193,7 +186,6 @@ Most users should use **`apps_script`** mode — it's free and requires no serve
|
|||||||
|
|
||||||
| Setting | What It Does |
|
| Setting | What It Does |
|
||||||
|---------|-------------|
|
|---------|-------------|
|
||||||
| `mode` | Which relay type to use (see table above) |
|
|
||||||
| `auth_key` | Password shared between your computer and the relay |
|
| `auth_key` | Password shared between your computer and the relay |
|
||||||
| `script_id` | Your Google Apps Script Deployment ID |
|
| `script_id` | Your Google Apps Script Deployment ID |
|
||||||
| `listen_host` | Where to listen (`127.0.0.1` = only this computer) |
|
| `listen_host` | Where to listen (`127.0.0.1` = only this computer) |
|
||||||
@@ -207,8 +199,6 @@ Most users should use **`apps_script`** mode — it's free and requires no serve
|
|||||||
| `google_ip` | `216.239.38.120` | Google IP address to connect through |
|
| `google_ip` | `216.239.38.120` | Google IP address to connect through |
|
||||||
| `front_domain` | `www.google.com` | Domain shown to the firewall/filter |
|
| `front_domain` | `www.google.com` | Domain shown to the firewall/filter |
|
||||||
| `verify_ssl` | `true` | Verify TLS certificates |
|
| `verify_ssl` | `true` | Verify TLS certificates |
|
||||||
| `worker_host` | — | Hostname for Cloudflare/Cloud Run modes |
|
|
||||||
| `custom_domain` | — | Your custom domain on Cloudflare |
|
|
||||||
| `script_ids` | — | Multiple Script IDs for load balancing (array) |
|
| `script_ids` | — | Multiple Script IDs for load balancing (array) |
|
||||||
|
|
||||||
### Load Balancing
|
### Load Balancing
|
||||||
@@ -269,11 +259,10 @@ python3 main.py --no-cert-check # Skip automatic CA install check on st
|
|||||||
|------|-------------|
|
|------|-------------|
|
||||||
| `main.py` | Starts the proxy |
|
| `main.py` | Starts the proxy |
|
||||||
| `proxy_server.py` | Handles browser connections |
|
| `proxy_server.py` | Handles browser connections |
|
||||||
| `domain_fronter.py` | Disguises traffic through CDN/Google |
|
| `domain_fronter.py` | Apps Script relay client (fronted through Google) |
|
||||||
| `h2_transport.py` | Faster connections using HTTP/2 (optional) |
|
| `h2_transport.py` | Faster connections using HTTP/2 (optional) |
|
||||||
| `mitm.py` | Handles HTTPS certificate generation |
|
| `mitm.py` | Handles HTTPS certificate generation |
|
||||||
| `cert_installer.py` | Cross-platform CA certificate installer (Windows/macOS/Linux + Firefox) |
|
| `cert_installer.py` | Cross-platform CA certificate installer (Windows/macOS/Linux + Firefox) |
|
||||||
| `ws.py` | WebSocket support |
|
|
||||||
| `Code.gs` | The relay script you deploy to Google Apps Script |
|
| `Code.gs` | The relay script you deploy to Google Apps Script |
|
||||||
| `config.example.json` | Example config — copy to `config.json` |
|
| `config.example.json` | Example config — copy to `config.json` |
|
||||||
|
|
||||||
|
|||||||
+2
-13
@@ -166,14 +166,7 @@ Firefox معمولا certificate store جداگانه دارد:
|
|||||||
|
|
||||||
## حالتهای موجود
|
## حالتهای موجود
|
||||||
|
|
||||||
| حالت | نیازمندی | توضیح |
|
این پروژه کاملاً روی حالت **Apps Script** تمرکز دارد. فقط به یک اکانت رایگان Google نیاز دارید — بدون VPS، بدون سرور، بدون Cloudflare. همهچیز برای همین حالت تنظیم شده است.
|
||||||
|------|----------|-------|
|
|
||||||
| `apps_script` | اکانت رایگان Google | سادهترین حالت، بدون نیاز به سرور |
|
|
||||||
| `google_fronting` | Google Cloud Run | استفاده از سرویس Cloud Run خودتان |
|
|
||||||
| `domain_fronting` | Cloudflare Worker | استفاده از Worker روی Cloudflare |
|
|
||||||
| `custom_domain` | دامنه شخصی روی Cloudflare | اتصال مستقیم به دامنه خودتان |
|
|
||||||
|
|
||||||
برای اکثر کاربران، `apps_script` بهترین انتخاب است.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -181,7 +174,6 @@ Firefox معمولا certificate store جداگانه دارد:
|
|||||||
|
|
||||||
| تنظیم | توضیح |
|
| تنظیم | توضیح |
|
||||||
|------|-------|
|
|------|-------|
|
||||||
| `mode` | نوع رله |
|
|
||||||
| `auth_key` | رمز مشترک بین برنامه و رله |
|
| `auth_key` | رمز مشترک بین برنامه و رله |
|
||||||
| `script_id` | Deployment ID مربوط به Apps Script |
|
| `script_id` | Deployment ID مربوط به Apps Script |
|
||||||
| `listen_host` | آدرس محلی برای اجرا |
|
| `listen_host` | آدرس محلی برای اجرا |
|
||||||
@@ -195,8 +187,6 @@ Firefox معمولا certificate store جداگانه دارد:
|
|||||||
| `google_ip` | `216.239.38.120` | IP مورد استفاده برای مسیر Google |
|
| `google_ip` | `216.239.38.120` | IP مورد استفاده برای مسیر Google |
|
||||||
| `front_domain` | `www.google.com` | دامنهای که فیلتر میبیند |
|
| `front_domain` | `www.google.com` | دامنهای که فیلتر میبیند |
|
||||||
| `verify_ssl` | `true` | بررسی اعتبار TLS |
|
| `verify_ssl` | `true` | بررسی اعتبار TLS |
|
||||||
| `worker_host` | - | برای حالتهای Cloudflare/Cloud Run |
|
|
||||||
| `custom_domain` | - | دامنه شخصی شما |
|
|
||||||
| `script_ids` | - | چند Deployment ID برای load balancing |
|
| `script_ids` | - | چند Deployment ID برای load balancing |
|
||||||
|
|
||||||
### استفاده از چند Script ID
|
### استفاده از چند Script ID
|
||||||
@@ -255,11 +245,10 @@ python3 main.py --no-cert-check # رد شدن از بررسی خودکار
|
|||||||
|------|--------|
|
|------|--------|
|
||||||
| `main.py` | اجرای برنامه |
|
| `main.py` | اجرای برنامه |
|
||||||
| `proxy_server.py` | مدیریت اتصال مرورگر |
|
| `proxy_server.py` | مدیریت اتصال مرورگر |
|
||||||
| `domain_fronter.py` | انجام domain fronting |
|
| `domain_fronter.py` | کلاینت رله Apps Script (با عبور از Google) |
|
||||||
| `h2_transport.py` | ارتباط سریعتر با HTTP/2 |
|
| `h2_transport.py` | ارتباط سریعتر با HTTP/2 |
|
||||||
| `mitm.py` | ساخت و مدیریت certificate |
|
| `mitm.py` | ساخت و مدیریت certificate |
|
||||||
| `cert_installer.py` | نصب خودکار گواهی CA در ویندوز، مک، لینوکس و Firefox |
|
| `cert_installer.py` | نصب خودکار گواهی CA در ویندوز، مک، لینوکس و Firefox |
|
||||||
| `ws.py` | پشتیبانی WebSocket |
|
|
||||||
| `Code.gs` | رله Apps Script |
|
| `Code.gs` | رله Apps Script |
|
||||||
| `config.example.json` | فایل نمونه تنظیمات |
|
| `config.example.json` | فایل نمونه تنظیمات |
|
||||||
|
|
||||||
|
|||||||
+23
-188
@@ -1,19 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
CDN Relay engine.
|
Apps Script relay engine.
|
||||||
|
|
||||||
Modes:
|
Domain fronting via Google Apps Script: POST JSON to script.google.com
|
||||||
1. custom_domain — SNI and Host both point to your custom domain on CF.
|
(fronted through www.google.com). Apps Script fetches the target URL and
|
||||||
2. domain_fronting — SNI = front_domain (allowed), Host = worker_host.
|
returns the response.
|
||||||
3. google_fronting — Connect to Google IP, SNI=google, Host=Cloud Run.
|
|
||||||
4. apps_script — Domain fronting via Google Apps Script relay.
|
|
||||||
POST JSON to script.google.com (fronted through www.google.com).
|
|
||||||
Apps Script fetches the target URL and returns the response.
|
|
||||||
|
|
||||||
Modes 1-3:
|
|
||||||
tunnel() — WebSocket-based TCP tunnel (HTTPS / any TCP)
|
|
||||||
forward() — HTTP request forwarding (plain HTTP)
|
|
||||||
|
|
||||||
Mode 4 (apps_script):
|
|
||||||
relay() — JSON-based HTTP relay through Apps Script
|
relay() — JSON-based HTTP relay through Apps Script
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -23,14 +14,11 @@ import hashlib
|
|||||||
import gzip
|
import gzip
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
import time
|
import time
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from ws import ws_encode, ws_decode
|
|
||||||
|
|
||||||
log = logging.getLogger("Fronter")
|
log = logging.getLogger("Fronter")
|
||||||
|
|
||||||
|
|
||||||
@@ -42,34 +30,16 @@ class DomainFronter:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, config: dict):
|
def __init__(self, config: dict):
|
||||||
mode = config.get("mode", "domain_fronting")
|
self.connect_host = config.get("google_ip", "216.239.38.120")
|
||||||
|
self.sni_host = config.get("front_domain", "www.google.com")
|
||||||
|
self.http_host = "script.google.com"
|
||||||
|
# Multi-script round-robin for higher throughput
|
||||||
|
script = config.get("script_ids") or config.get("script_id")
|
||||||
|
self._script_ids = script if isinstance(script, list) else [script]
|
||||||
|
self._script_idx = 0
|
||||||
|
self.script_id = self._script_ids[0] # backward compat / logging
|
||||||
|
self._dev_available = False # True if /dev endpoint works (no redirect, ~400ms faster)
|
||||||
|
|
||||||
if mode == "custom_domain":
|
|
||||||
domain = config["custom_domain"]
|
|
||||||
self.connect_host = domain
|
|
||||||
self.sni_host = domain
|
|
||||||
self.http_host = domain
|
|
||||||
elif mode == "google_fronting":
|
|
||||||
self.connect_host = config.get("google_ip", "216.239.38.120")
|
|
||||||
self.sni_host = config.get("front_domain", "www.google.com")
|
|
||||||
self.http_host = config["worker_host"]
|
|
||||||
elif mode == "apps_script":
|
|
||||||
self.connect_host = config.get("google_ip", "216.239.38.120")
|
|
||||||
self.sni_host = config.get("front_domain", "www.google.com")
|
|
||||||
self.http_host = "script.google.com"
|
|
||||||
# Multi-script round-robin for higher throughput
|
|
||||||
script = config.get("script_ids") or config.get("script_id")
|
|
||||||
self._script_ids = script if isinstance(script, list) else [script]
|
|
||||||
self._script_idx = 0
|
|
||||||
self.script_id = self._script_ids[0] # backward compat / logging
|
|
||||||
self._dev_available = False # True if /dev endpoint works (no redirect, ~400ms faster)
|
|
||||||
else:
|
|
||||||
self.connect_host = config["front_domain"]
|
|
||||||
self.sni_host = config["front_domain"]
|
|
||||||
self.http_host = config["worker_host"]
|
|
||||||
|
|
||||||
self.mode = mode
|
|
||||||
self.worker_path = config.get("worker_path", "")
|
|
||||||
self.auth_key = config.get("auth_key", "")
|
self.auth_key = config.get("auth_key", "")
|
||||||
self.verify_ssl = config.get("verify_ssl", True)
|
self.verify_ssl = config.get("verify_ssl", True)
|
||||||
|
|
||||||
@@ -98,17 +68,16 @@ class DomainFronter:
|
|||||||
|
|
||||||
# HTTP/2 multiplexing — one connection handles all requests
|
# HTTP/2 multiplexing — one connection handles all requests
|
||||||
self._h2 = None
|
self._h2 = None
|
||||||
if mode == "apps_script":
|
try:
|
||||||
try:
|
from h2_transport import H2Transport, H2_AVAILABLE
|
||||||
from h2_transport import H2Transport, H2_AVAILABLE
|
if H2_AVAILABLE:
|
||||||
if H2_AVAILABLE:
|
self._h2 = H2Transport(
|
||||||
self._h2 = H2Transport(
|
self.connect_host, self.sni_host, self.verify_ssl
|
||||||
self.connect_host, self.sni_host, self.verify_ssl
|
)
|
||||||
)
|
log.info("HTTP/2 multiplexing available — "
|
||||||
log.info("HTTP/2 multiplexing available — "
|
"all requests will share one connection")
|
||||||
"all requests will share one connection")
|
except ImportError:
|
||||||
except ImportError:
|
pass
|
||||||
pass
|
|
||||||
|
|
||||||
# ── helpers ───────────────────────────────────────────────────
|
# ── helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -392,140 +361,6 @@ class DomainFronter:
|
|||||||
def _auth_header(self) -> str:
|
def _auth_header(self) -> str:
|
||||||
return f"X-Auth-Key: {self.auth_key}\r\n" if self.auth_key else ""
|
return f"X-Auth-Key: {self.auth_key}\r\n" if self.auth_key else ""
|
||||||
|
|
||||||
# ── WebSocket tunnel (CONNECT / HTTPS) ────────────────────────
|
|
||||||
|
|
||||||
async def tunnel(self, target_host: str, target_port: int,
|
|
||||||
client_r: asyncio.StreamReader,
|
|
||||||
client_w: asyncio.StreamWriter):
|
|
||||||
"""Tunnel raw TCP bytes through a domain-fronted WebSocket."""
|
|
||||||
try:
|
|
||||||
remote_r, remote_w = await self._open()
|
|
||||||
except Exception as e:
|
|
||||||
log.error("TLS connect to %s failed: %s", self.connect_host, e)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
# ---- WebSocket upgrade ----
|
|
||||||
ws_key = base64.b64encode(os.urandom(16)).decode()
|
|
||||||
path = f"{self.worker_path}/tunnel?host={target_host}&port={target_port}"
|
|
||||||
handshake = (
|
|
||||||
f"GET {path} HTTP/1.1\r\n"
|
|
||||||
f"Host: {self.http_host}\r\n"
|
|
||||||
f"Upgrade: websocket\r\n"
|
|
||||||
f"Connection: Upgrade\r\n"
|
|
||||||
f"Sec-WebSocket-Key: {ws_key}\r\n"
|
|
||||||
f"Sec-WebSocket-Version: 13\r\n"
|
|
||||||
f"{self._auth_header()}"
|
|
||||||
f"\r\n"
|
|
||||||
)
|
|
||||||
remote_w.write(handshake.encode())
|
|
||||||
await remote_w.drain()
|
|
||||||
|
|
||||||
# Read the 101 Switching Protocols response
|
|
||||||
resp = b""
|
|
||||||
while b"\r\n\r\n" not in resp:
|
|
||||||
chunk = await asyncio.wait_for(remote_r.read(4096), timeout=15)
|
|
||||||
if not chunk:
|
|
||||||
raise ConnectionError("No WebSocket handshake response")
|
|
||||||
resp += chunk
|
|
||||||
|
|
||||||
status_line = resp.split(b"\r\n")[0]
|
|
||||||
if b"101" not in status_line:
|
|
||||||
raise ConnectionError(
|
|
||||||
f"WebSocket upgrade rejected: {status_line.decode(errors='replace')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
log.info("Tunnel ready → %s:%d", target_host, target_port)
|
|
||||||
|
|
||||||
# ---- bidirectional relay ----
|
|
||||||
await asyncio.gather(
|
|
||||||
self._client_to_ws(client_r, remote_w),
|
|
||||||
self._ws_to_client(remote_r, client_w),
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.error("Tunnel error (%s:%d): %s", target_host, target_port, e)
|
|
||||||
finally:
|
|
||||||
try:
|
|
||||||
remote_w.close()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def _client_to_ws(self, src: asyncio.StreamReader,
|
|
||||||
dst: asyncio.StreamWriter):
|
|
||||||
"""Read plaintext from the browser, wrap in WS frames, send to CDN."""
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
data = await src.read(16384)
|
|
||||||
if not data:
|
|
||||||
# Send a WS close frame
|
|
||||||
dst.write(ws_encode(b"", opcode=0x08))
|
|
||||||
await dst.drain()
|
|
||||||
break
|
|
||||||
dst.write(ws_encode(data))
|
|
||||||
await dst.drain()
|
|
||||||
except (ConnectionError, asyncio.CancelledError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def _ws_to_client(self, src: asyncio.StreamReader,
|
|
||||||
dst: asyncio.StreamWriter):
|
|
||||||
"""Read WS frames from CDN, unwrap, write plaintext to browser."""
|
|
||||||
buf = b""
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
chunk = await src.read(16384)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
buf += chunk
|
|
||||||
while buf:
|
|
||||||
result = ws_decode(buf)
|
|
||||||
if result is None:
|
|
||||||
break # need more data
|
|
||||||
opcode, payload, consumed = result
|
|
||||||
buf = buf[consumed:]
|
|
||||||
if opcode == 0x08: # close
|
|
||||||
return
|
|
||||||
if payload:
|
|
||||||
dst.write(payload)
|
|
||||||
await dst.drain()
|
|
||||||
except (ConnectionError, asyncio.CancelledError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# ── HTTP forwarding ───────────────────────────────────────────
|
|
||||||
|
|
||||||
async def forward(self, raw_request: bytes) -> bytes:
|
|
||||||
"""Forward a plain HTTP request through the domain-fronted channel.
|
|
||||||
|
|
||||||
Uses keep-alive connections from the pool for efficiency.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
reader, writer, created = await self._acquire()
|
|
||||||
|
|
||||||
# Wrap the original HTTP request inside a POST to the worker.
|
|
||||||
request = (
|
|
||||||
f"POST {self.worker_path}/forward HTTP/1.1\r\n"
|
|
||||||
f"Host: {self.http_host}\r\n"
|
|
||||||
f"Content-Type: application/octet-stream\r\n"
|
|
||||||
f"Content-Length: {len(raw_request)}\r\n"
|
|
||||||
f"Connection: keep-alive\r\n"
|
|
||||||
f"{self._auth_header()}"
|
|
||||||
f"\r\n"
|
|
||||||
)
|
|
||||||
writer.write(request.encode() + raw_request)
|
|
||||||
await writer.drain()
|
|
||||||
|
|
||||||
status, resp_headers, resp_body = await self._read_http_response(reader)
|
|
||||||
|
|
||||||
await self._release(reader, writer, created)
|
|
||||||
|
|
||||||
# The worker wraps the target's response in its own HTTP
|
|
||||||
# envelope. The body IS the raw HTTP response from the target.
|
|
||||||
return resp_body
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.error("Forward failed: %s", e)
|
|
||||||
return b"HTTP/1.1 502 Bad Gateway\r\n\r\nDomain fronting request failed\r\n"
|
|
||||||
|
|
||||||
# ── Apps Script relay (apps_script mode) ──────────────────────
|
# ── Apps Script relay (apps_script mode) ──────────────────────
|
||||||
|
|
||||||
async def relay(self, method: str, url: str,
|
async def relay(self, method: str, url: str,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
DomainFront Tunnel — Bypass DPI censorship via Domain Fronting.
|
DomainFront Tunnel — Bypass DPI censorship via Google Apps Script.
|
||||||
|
|
||||||
Run a local HTTP proxy that tunnels all traffic through a CDN using
|
Run a local HTTP proxy that tunnels all traffic through a Google Apps
|
||||||
domain fronting: the TLS SNI shows an allowed domain while the encrypted
|
Script relay fronted by www.google.com (TLS SNI shows www.google.com
|
||||||
HTTP Host header routes to your Cloudflare Worker relay.
|
while the encrypted Host header points at script.google.com).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -33,7 +33,7 @@ def setup_logging(level_name: str):
|
|||||||
def parse_args():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="domainfront-tunnel",
|
prog="domainfront-tunnel",
|
||||||
description="Local HTTP proxy that tunnels traffic through domain fronting.",
|
description="Local HTTP proxy that relays traffic through Google Apps Script.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-c", "--config",
|
"-c", "--config",
|
||||||
@@ -136,25 +136,13 @@ def main():
|
|||||||
print(f"Missing required config key: {key}")
|
print(f"Missing required config key: {key}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
mode = config.get("mode", "domain_fronting")
|
# Always Apps Script mode — force-set for backward-compat configs.
|
||||||
if mode == "custom_domain" and "custom_domain" not in config:
|
config["mode"] = "apps_script"
|
||||||
print("Mode 'custom_domain' requires 'custom_domain' in config")
|
sid = config.get("script_ids") or config.get("script_id")
|
||||||
|
if not sid or (isinstance(sid, str) and sid == "YOUR_APPS_SCRIPT_DEPLOYMENT_ID"):
|
||||||
|
print("Missing 'script_id' in config.")
|
||||||
|
print("Deploy the Apps Script from Code.gs and paste the Deployment ID.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
if mode == "domain_fronting":
|
|
||||||
for key in ("front_domain", "worker_host"):
|
|
||||||
if key not in config:
|
|
||||||
print(f"Mode 'domain_fronting' requires '{key}' in config")
|
|
||||||
sys.exit(1)
|
|
||||||
if mode == "google_fronting":
|
|
||||||
if "worker_host" not in config:
|
|
||||||
print("Mode 'google_fronting' requires 'worker_host' in config (your Cloud Run URL)")
|
|
||||||
sys.exit(1)
|
|
||||||
if mode == "apps_script":
|
|
||||||
sid = config.get("script_ids") or config.get("script_id")
|
|
||||||
if not sid or (isinstance(sid, str) and sid == "YOUR_APPS_SCRIPT_DEPLOYMENT_ID"):
|
|
||||||
print("Mode 'apps_script' requires 'script_id' in config.")
|
|
||||||
print("Deploy the Apps Script from appsscript/Code.gs and paste the Deployment ID.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# ── Certificate installation ──────────────────────────────────────────
|
# ── Certificate installation ──────────────────────────────────────────
|
||||||
if args.install_cert:
|
if args.install_cert:
|
||||||
@@ -167,49 +155,38 @@ def main():
|
|||||||
setup_logging(config.get("log_level", "INFO"))
|
setup_logging(config.get("log_level", "INFO"))
|
||||||
log = logging.getLogger("Main")
|
log = logging.getLogger("Main")
|
||||||
|
|
||||||
mode = config.get("mode", "domain_fronting")
|
log.info("DomainFront Tunnel starting (Apps Script relay)")
|
||||||
log.info("DomainFront Tunnel starting (mode: %s)", mode)
|
|
||||||
|
|
||||||
if mode == "custom_domain":
|
log.info("Apps Script relay : SNI=%s → script.google.com",
|
||||||
log.info("Custom domain : %s", config["custom_domain"])
|
config.get("front_domain", "www.google.com"))
|
||||||
elif mode == "google_fronting":
|
script_ids = config.get("script_ids") or config.get("script_id")
|
||||||
log.info("Google fronting : SNI=%s → Host=%s",
|
if isinstance(script_ids, list):
|
||||||
config.get("front_domain", "www.google.com"), config["worker_host"])
|
log.info("Script IDs : %d scripts (sticky per-host)", len(script_ids))
|
||||||
log.info("Google IP : %s", config.get("google_ip", "216.239.38.120"))
|
for i, sid in enumerate(script_ids):
|
||||||
elif mode == "apps_script":
|
log.info(" [%d] %s", i + 1, sid)
|
||||||
log.info("Apps Script relay : SNI=%s → script.google.com",
|
|
||||||
config.get("front_domain", "www.google.com"))
|
|
||||||
script_ids = config.get("script_ids") or config.get("script_id")
|
|
||||||
if isinstance(script_ids, list):
|
|
||||||
log.info("Script IDs : %d scripts (sticky per-host)", len(script_ids))
|
|
||||||
for i, sid in enumerate(script_ids):
|
|
||||||
log.info(" [%d] %s", i + 1, sid)
|
|
||||||
else:
|
|
||||||
log.info("Script ID : %s", script_ids)
|
|
||||||
|
|
||||||
# Ensure CA file exists before checking / installing it.
|
|
||||||
# MITMCertManager generates ca/ca.crt on first instantiation.
|
|
||||||
if not os.path.exists(CA_CERT_FILE):
|
|
||||||
from mitm import MITMCertManager
|
|
||||||
MITMCertManager() # side-effect: creates ca/ca.crt + ca/ca.key
|
|
||||||
|
|
||||||
# Auto-install MITM CA if not already trusted
|
|
||||||
if not args.no_cert_check:
|
|
||||||
if not is_ca_trusted(CA_CERT_FILE):
|
|
||||||
log.warning("MITM CA is not trusted — attempting automatic installation…")
|
|
||||||
ok = install_ca(CA_CERT_FILE)
|
|
||||||
if ok:
|
|
||||||
log.info("CA certificate installed. You may need to restart your browser.")
|
|
||||||
else:
|
|
||||||
log.error(
|
|
||||||
"Auto-install failed. Run with --install-cert (may need admin/sudo) "
|
|
||||||
"or manually install ca/ca.crt as a trusted root CA."
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
log.info("MITM CA is already trusted.")
|
|
||||||
else:
|
else:
|
||||||
log.info("Front domain (SNI) : %s", config.get("front_domain", "?"))
|
log.info("Script ID : %s", script_ids)
|
||||||
log.info("Worker host (Host) : %s", config.get("worker_host", "?"))
|
|
||||||
|
# Ensure CA file exists before checking / installing it.
|
||||||
|
# MITMCertManager generates ca/ca.crt on first instantiation.
|
||||||
|
if not os.path.exists(CA_CERT_FILE):
|
||||||
|
from mitm import MITMCertManager
|
||||||
|
MITMCertManager() # side-effect: creates ca/ca.crt + ca/ca.key
|
||||||
|
|
||||||
|
# Auto-install MITM CA if not already trusted
|
||||||
|
if not args.no_cert_check:
|
||||||
|
if not is_ca_trusted(CA_CERT_FILE):
|
||||||
|
log.warning("MITM CA is not trusted — attempting automatic installation…")
|
||||||
|
ok = install_ca(CA_CERT_FILE)
|
||||||
|
if ok:
|
||||||
|
log.info("CA certificate installed. You may need to restart your browser.")
|
||||||
|
else:
|
||||||
|
log.error(
|
||||||
|
"Auto-install failed. Run with --install-cert (may need admin/sudo) "
|
||||||
|
"or manually install ca/ca.crt as a trusted root CA."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
log.info("MITM CA is already trusted.")
|
||||||
|
|
||||||
log.info("HTTP proxy : %s:%d",
|
log.info("HTTP proxy : %s:%d",
|
||||||
config.get("listen_host", "127.0.0.1"),
|
config.get("listen_host", "127.0.0.1"),
|
||||||
|
|||||||
+78
-154
@@ -2,11 +2,8 @@
|
|||||||
Local HTTP proxy server.
|
Local HTTP proxy server.
|
||||||
|
|
||||||
Intercepts the user's browser traffic and forwards everything through
|
Intercepts the user's browser traffic and forwards everything through
|
||||||
a domain-fronted connection to a CDN worker or Apps Script relay.
|
the Apps Script relay (MITM-decrypts HTTPS locally, forwards requests
|
||||||
|
as JSON to script.google.com fronted through www.google.com).
|
||||||
Supports:
|
|
||||||
- CONNECT method → WebSocket tunnel (modes 1-3) or MITM relay (apps_script)
|
|
||||||
- GET / POST etc. → HTTP forwarding (modes 1-3) or JSON relay (apps_script)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -150,17 +147,11 @@ class ProxyServer:
|
|||||||
self.socks_enabled = config.get("socks5_enabled", True)
|
self.socks_enabled = config.get("socks5_enabled", True)
|
||||||
self.socks_host = config.get("socks5_host", self.host)
|
self.socks_host = config.get("socks5_host", self.host)
|
||||||
self.socks_port = config.get("socks5_port", 1080)
|
self.socks_port = config.get("socks5_port", 1080)
|
||||||
self.mode = config.get("mode", "domain_fronting")
|
|
||||||
self.fronter = DomainFronter(config)
|
self.fronter = DomainFronter(config)
|
||||||
self.mitm = None
|
self.mitm = None
|
||||||
self._cache = ResponseCache(max_mb=50)
|
self._cache = ResponseCache(max_mb=50)
|
||||||
self._direct_fail_until: dict[str, float] = {}
|
self._direct_fail_until: dict[str, float] = {}
|
||||||
|
|
||||||
# Persistent HTTP tunnel cache for google_fronting mode
|
|
||||||
# Key: "host:port" → (tunnel_reader, tunnel_writer, lock)
|
|
||||||
self._http_tunnels: dict = {}
|
|
||||||
self._tunnel_lock = asyncio.Lock()
|
|
||||||
|
|
||||||
# hosts override — DNS fake-map: domain/suffix → IP
|
# hosts override — DNS fake-map: domain/suffix → IP
|
||||||
# Checked before any real DNS lookup; supports exact and suffix matching.
|
# Checked before any real DNS lookup; supports exact and suffix matching.
|
||||||
self._hosts: dict[str, str] = config.get("hosts", {})
|
self._hosts: dict[str, str] = config.get("hosts", {})
|
||||||
@@ -181,14 +172,13 @@ class ProxyServer:
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.mode == "apps_script":
|
try:
|
||||||
try:
|
from mitm import MITMCertManager
|
||||||
from mitm import MITMCertManager
|
self.mitm = MITMCertManager()
|
||||||
self.mitm = MITMCertManager()
|
except ImportError:
|
||||||
except ImportError:
|
log.error("Apps Script relay requires the 'cryptography' package.")
|
||||||
log.error("apps_script mode requires 'cryptography' package.")
|
log.error("Run: pip install cryptography")
|
||||||
log.error("Run: pip install cryptography")
|
raise SystemExit(1)
|
||||||
raise SystemExit(1)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _header_value(headers: dict | None, name: str) -> str:
|
def _header_value(headers: dict | None, name: str) -> str:
|
||||||
@@ -406,44 +396,41 @@ class ProxyServer:
|
|||||||
async def _handle_target_tunnel(self, host: str, port: int,
|
async def _handle_target_tunnel(self, host: str, port: int,
|
||||||
reader: asyncio.StreamReader,
|
reader: asyncio.StreamReader,
|
||||||
writer: asyncio.StreamWriter):
|
writer: asyncio.StreamWriter):
|
||||||
"""Route a target connection through the active relay mode."""
|
"""Route a target connection through the Apps Script relay."""
|
||||||
|
|
||||||
if self.mode == "apps_script":
|
override_ip = self._sni_rewrite_ip(host)
|
||||||
override_ip = self._sni_rewrite_ip(host)
|
if override_ip:
|
||||||
if override_ip:
|
# SNI-blocked domain: MITM-decrypt from browser, then
|
||||||
# SNI-blocked domain: MITM-decrypt from browser, then
|
# re-connect to the override IP with SNI=front_domain so
|
||||||
# re-connect to the override IP with SNI=front_domain so
|
# the ISP never sees the blocked hostname in the TLS handshake.
|
||||||
# the ISP never sees the blocked hostname in the TLS handshake.
|
log.info("SNI-rewrite tunnel → %s via %s (SNI: %s)",
|
||||||
log.info("SNI-rewrite tunnel → %s via %s (SNI: %s)",
|
host, override_ip, self.fronter.sni_host)
|
||||||
host, override_ip, self.fronter.sni_host)
|
await self._do_sni_rewrite_tunnel(host, port, reader, writer,
|
||||||
await self._do_sni_rewrite_tunnel(host, port, reader, writer,
|
connect_ip=override_ip)
|
||||||
connect_ip=override_ip)
|
elif self._is_google_domain(host):
|
||||||
elif self._is_google_domain(host):
|
if self._direct_temporarily_disabled(host):
|
||||||
if self._direct_temporarily_disabled(host):
|
log.info("Relay fallback → %s (direct tunnel temporarily disabled)", host)
|
||||||
log.info("Relay fallback → %s (direct tunnel temporarily disabled)", host)
|
|
||||||
if port == 443:
|
|
||||||
await self._do_mitm_connect(host, port, reader, writer)
|
|
||||||
else:
|
|
||||||
await self._do_plain_http_tunnel(host, port, reader, writer)
|
|
||||||
return
|
|
||||||
|
|
||||||
log.info("Direct tunnel → %s (Google domain, skipping relay)", host)
|
|
||||||
ok = await self._do_direct_tunnel(host, port, reader, writer)
|
|
||||||
if ok:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._remember_direct_failure(host)
|
|
||||||
log.warning("Direct tunnel fallback → %s (switching to relay)", host)
|
|
||||||
if port == 443:
|
if port == 443:
|
||||||
await self._do_mitm_connect(host, port, reader, writer)
|
await self._do_mitm_connect(host, port, reader, writer)
|
||||||
else:
|
else:
|
||||||
await self._do_plain_http_tunnel(host, port, reader, writer)
|
await self._do_plain_http_tunnel(host, port, reader, writer)
|
||||||
elif port == 443:
|
return
|
||||||
|
|
||||||
|
log.info("Direct tunnel → %s (Google domain, skipping relay)", host)
|
||||||
|
ok = await self._do_direct_tunnel(host, port, reader, writer)
|
||||||
|
if ok:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._remember_direct_failure(host)
|
||||||
|
log.warning("Direct tunnel fallback → %s (switching to relay)", host)
|
||||||
|
if port == 443:
|
||||||
await self._do_mitm_connect(host, port, reader, writer)
|
await self._do_mitm_connect(host, port, reader, writer)
|
||||||
else:
|
else:
|
||||||
await self._do_plain_http_tunnel(host, port, reader, writer)
|
await self._do_plain_http_tunnel(host, port, reader, writer)
|
||||||
|
elif port == 443:
|
||||||
|
await self._do_mitm_connect(host, port, reader, writer)
|
||||||
else:
|
else:
|
||||||
await self.fronter.tunnel(host, port, reader, writer)
|
await self._do_plain_http_tunnel(host, port, reader, writer)
|
||||||
|
|
||||||
# ── Hosts override (fake DNS) ─────────────────────────────────
|
# ── Hosts override (fake DNS) ─────────────────────────────────
|
||||||
|
|
||||||
@@ -1028,117 +1015,54 @@ class ProxyServer:
|
|||||||
first_line = header_block.split(b"\r\n")[0].decode(errors="replace")
|
first_line = header_block.split(b"\r\n")[0].decode(errors="replace")
|
||||||
log.info("HTTP → %s", first_line)
|
log.info("HTTP → %s", first_line)
|
||||||
|
|
||||||
if self.mode == "apps_script":
|
# Parse request and relay through Apps Script
|
||||||
# Parse request and relay through Apps Script
|
parts = first_line.strip().split(" ", 2)
|
||||||
parts = first_line.strip().split(" ", 2)
|
method = parts[0] if parts else "GET"
|
||||||
method = parts[0] if parts else "GET"
|
url = parts[1] if len(parts) > 1 else "/"
|
||||||
url = parts[1] if len(parts) > 1 else "/"
|
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
for raw_line in header_block.split(b"\r\n")[1:]:
|
for raw_line in header_block.split(b"\r\n")[1:]:
|
||||||
if b":" in raw_line:
|
if b":" in raw_line:
|
||||||
k, v = raw_line.decode(errors="replace").split(":", 1)
|
k, v = raw_line.decode(errors="replace").split(":", 1)
|
||||||
headers[k.strip()] = v.strip()
|
headers[k.strip()] = v.strip()
|
||||||
|
|
||||||
# ── CORS preflight over plain HTTP ────────────────────────────
|
# ── CORS preflight over plain HTTP ────────────────────────────
|
||||||
origin = next(
|
origin = next(
|
||||||
(v for k, v in headers.items() if k.lower() == "origin"), ""
|
(v for k, v in headers.items() if k.lower() == "origin"), ""
|
||||||
)
|
)
|
||||||
acr_method = next(
|
acr_method = next(
|
||||||
(v for k, v in headers.items()
|
(v for k, v in headers.items()
|
||||||
if k.lower() == "access-control-request-method"), ""
|
if k.lower() == "access-control-request-method"), ""
|
||||||
)
|
)
|
||||||
acr_headers_val = next(
|
acr_headers_val = next(
|
||||||
(v for k, v in headers.items()
|
(v for k, v in headers.items()
|
||||||
if k.lower() == "access-control-request-headers"), ""
|
if k.lower() == "access-control-request-headers"), ""
|
||||||
)
|
)
|
||||||
if method.upper() == "OPTIONS" and acr_method:
|
if method.upper() == "OPTIONS" and acr_method:
|
||||||
log.debug("CORS preflight (HTTP) → %s (responding locally)", url[:60])
|
log.debug("CORS preflight (HTTP) → %s (responding locally)", url[:60])
|
||||||
writer.write(self._cors_preflight_response(origin, acr_method, acr_headers_val))
|
writer.write(self._cors_preflight_response(origin, acr_method, acr_headers_val))
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Cache check for GET
|
# Cache check for GET
|
||||||
response = None
|
response = None
|
||||||
if self._cache_allowed(method, url, headers, body):
|
if self._cache_allowed(method, url, headers, body):
|
||||||
response = self._cache.get(url)
|
response = self._cache.get(url)
|
||||||
if response:
|
if response:
|
||||||
log.debug("Cache HIT (HTTP): %s", url[:60])
|
log.debug("Cache HIT (HTTP): %s", url[:60])
|
||||||
|
|
||||||
if response is None:
|
if response is None:
|
||||||
response = await self._relay_smart(method, url, headers, body)
|
response = await self._relay_smart(method, url, headers, body)
|
||||||
# Cache successful GET
|
# Cache successful GET
|
||||||
if self._cache_allowed(method, url, headers, body) and response:
|
if self._cache_allowed(method, url, headers, body) and response:
|
||||||
ttl = ResponseCache.parse_ttl(response, url)
|
ttl = ResponseCache.parse_ttl(response, url)
|
||||||
if ttl > 0:
|
if ttl > 0:
|
||||||
self._cache.put(url, response, ttl)
|
self._cache.put(url, response, ttl)
|
||||||
|
|
||||||
# Inject CORS headers for cross-origin requests
|
# Inject CORS headers for cross-origin requests
|
||||||
if origin and response:
|
if origin and response:
|
||||||
response = self._inject_cors_headers(response, origin)
|
response = self._inject_cors_headers(response, origin)
|
||||||
self._log_response_summary(url, response)
|
self._log_response_summary(url, response)
|
||||||
elif self.mode in ("google_fronting", "custom_domain", "domain_fronting"):
|
|
||||||
# Use WebSocket tunnel for ALL traffic (much faster than forward())
|
|
||||||
response = await self._tunnel_http(header_block, body)
|
|
||||||
else:
|
|
||||||
response = await self.fronter.forward(header_block + body)
|
|
||||||
|
|
||||||
writer.write(response)
|
writer.write(response)
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
|
|
||||||
async def _tunnel_http(self, header_block: bytes, body: bytes) -> bytes:
|
|
||||||
"""Forward plain HTTP via a persistent WebSocket tunnel.
|
|
||||||
|
|
||||||
Instead of opening a new TLS+HTTP connection for each request
|
|
||||||
(the old forward() path), this keeps a WebSocket tunnel open
|
|
||||||
to the target host and pipes raw HTTP through it.
|
|
||||||
Much faster for rapid-fire requests (e.g., Telegram API).
|
|
||||||
"""
|
|
||||||
# Parse target host:port from the raw HTTP request
|
|
||||||
host = ""
|
|
||||||
port = 80
|
|
||||||
for line in header_block.split(b"\r\n")[1:]:
|
|
||||||
if not line:
|
|
||||||
break
|
|
||||||
if line.lower().startswith(b"host:"):
|
|
||||||
host_val = line.split(b":", 1)[1].strip().decode(errors="replace")
|
|
||||||
if ":" in host_val:
|
|
||||||
h, p = host_val.rsplit(":", 1)
|
|
||||||
try:
|
|
||||||
host, port = h, int(p)
|
|
||||||
except ValueError:
|
|
||||||
host = host_val
|
|
||||||
else:
|
|
||||||
host = host_val
|
|
||||||
break
|
|
||||||
|
|
||||||
if not host:
|
|
||||||
return b"HTTP/1.1 400 Bad Request\r\n\r\nNo Host header\r\n"
|
|
||||||
|
|
||||||
# Rewrite the request line: browser sends absolute URL
|
|
||||||
# (e.g., "GET http://host/path HTTP/1.1") but the target
|
|
||||||
# server expects a relative path ("GET /path HTTP/1.1")
|
|
||||||
first_line = header_block.split(b"\r\n")[0]
|
|
||||||
first_str = first_line.decode(errors="replace")
|
|
||||||
parts = first_str.split(" ", 2)
|
|
||||||
if len(parts) >= 2 and parts[1].startswith("http://"):
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
parsed = urlparse(parts[1])
|
|
||||||
rel_path = parsed.path or "/"
|
|
||||||
if parsed.query:
|
|
||||||
rel_path += "?" + parsed.query
|
|
||||||
new_first = f"{parts[0]} {rel_path}"
|
|
||||||
if len(parts) == 3:
|
|
||||||
new_first += f" {parts[2]}"
|
|
||||||
header_block = new_first.encode() + b"\r\n" + b"\r\n".join(header_block.split(b"\r\n")[1:])
|
|
||||||
|
|
||||||
raw_request = header_block + body
|
|
||||||
|
|
||||||
# Send through tunnel
|
|
||||||
try:
|
|
||||||
return await asyncio.wait_for(
|
|
||||||
self.fronter.forward(raw_request), timeout=30
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
log.error("Tunnel HTTP failed (%s:%d): %s", host, port, e)
|
|
||||||
return b"HTTP/1.1 502 Bad Gateway\r\n\r\nTunnel forward failed\r\n"
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
"""
|
|
||||||
Minimal WebSocket frame encoder / decoder (RFC 6455).
|
|
||||||
|
|
||||||
Only handles binary (opcode 0x02) and close (opcode 0x08) frames.
|
|
||||||
Client-to-server frames are always masked as required by the spec.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import struct
|
|
||||||
|
|
||||||
|
|
||||||
def ws_encode(data: bytes, opcode: int = 0x02) -> bytes:
|
|
||||||
"""Encode *data* into a masked binary WebSocket frame."""
|
|
||||||
head = bytearray([0x80 | opcode]) # FIN + opcode
|
|
||||||
|
|
||||||
length = len(data)
|
|
||||||
if length < 126:
|
|
||||||
head.append(0x80 | length)
|
|
||||||
elif length < 0x10000:
|
|
||||||
head.append(0x80 | 126)
|
|
||||||
head += struct.pack("!H", length)
|
|
||||||
else:
|
|
||||||
head.append(0x80 | 127)
|
|
||||||
head += struct.pack("!Q", length)
|
|
||||||
|
|
||||||
mask = os.urandom(4)
|
|
||||||
head += mask
|
|
||||||
|
|
||||||
masked = bytearray(data)
|
|
||||||
for i in range(len(masked)):
|
|
||||||
masked[i] ^= mask[i & 3]
|
|
||||||
|
|
||||||
return bytes(head) + bytes(masked)
|
|
||||||
|
|
||||||
|
|
||||||
def ws_decode(buf: bytes):
|
|
||||||
"""Try to decode one frame from *buf*.
|
|
||||||
|
|
||||||
Returns ``(opcode, payload, consumed_bytes)`` or ``None`` if the
|
|
||||||
buffer does not yet contain a complete frame.
|
|
||||||
"""
|
|
||||||
if len(buf) < 2:
|
|
||||||
return None
|
|
||||||
|
|
||||||
opcode = buf[0] & 0x0F
|
|
||||||
is_masked = buf[1] & 0x80
|
|
||||||
length = buf[1] & 0x7F
|
|
||||||
pos = 2
|
|
||||||
|
|
||||||
if length == 126:
|
|
||||||
if len(buf) < 4:
|
|
||||||
return None
|
|
||||||
length = struct.unpack("!H", buf[2:4])[0]
|
|
||||||
pos = 4
|
|
||||||
elif length == 127:
|
|
||||||
if len(buf) < 10:
|
|
||||||
return None
|
|
||||||
length = struct.unpack("!Q", buf[2:10])[0]
|
|
||||||
pos = 10
|
|
||||||
|
|
||||||
mask = None
|
|
||||||
if is_masked:
|
|
||||||
if len(buf) < pos + 4:
|
|
||||||
return None
|
|
||||||
mask = buf[pos : pos + 4]
|
|
||||||
pos += 4
|
|
||||||
|
|
||||||
if len(buf) < pos + length:
|
|
||||||
return None
|
|
||||||
|
|
||||||
payload = bytearray(buf[pos : pos + length])
|
|
||||||
if mask:
|
|
||||||
for i in range(len(payload)):
|
|
||||||
payload[i] ^= mask[i & 3]
|
|
||||||
|
|
||||||
return opcode, bytes(payload), pos + length
|
|
||||||
Reference in New Issue
Block a user