From e02c1f2b16b1bbb09574877554638af64eb51fa6 Mon Sep 17 00:00:00 2001 From: cheney Date: Mon, 25 May 2026 14:29:42 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=80=E5=8D=95=E5=8F=AF=E7=94=A8=EF=BC=8C?= =?UTF-8?q?=E5=A4=8D=E6=9D=82=E4=B8=8D=E5=8F=AF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- proxy/PROTOCOL.md | 199 +++++++++++++++++++------ proxy/cleanup_8080.bat | 28 ++++ proxy/cleanup_8080.ps1 | 27 ++++ proxy/proxy_client.py | 235 +++++++++++++++++------------ proxy/proxy_server.py | 328 +++++++++++++++++++++-------------------- 5 files changed, 517 insertions(+), 300 deletions(-) create mode 100644 proxy/cleanup_8080.bat create mode 100644 proxy/cleanup_8080.ps1 diff --git a/proxy/PROTOCOL.md b/proxy/PROTOCOL.md index 6490d79..217d351 100644 --- a/proxy/PROTOCOL.md +++ b/proxy/PROTOCOL.md @@ -6,15 +6,22 @@ | 设备 | 角色 | 能力 | 限制 | |------|------|------|------| -| 电脑A | 代理客户端 | 可访问内网网站 | 无法监听端口 | -| 电脑B | 代理服务器 | 可监听端口 | 无法直接访问内网 | +| 电脑A | 代理客户端 | 可访问内网网站,作为HTTP客户端 | 无法监听端口 | +| 电脑B | 代理服务器 | 可监听端口,转发HTTP请求 | 无法直接访问内网 | ### 1.2 网络拓扑 ``` -浏览器 <--(端口8081)--> 电脑B <--(端口8080)--> 电脑A <--(内网)--> 目标网站 +浏览器 <--(端口8081)--> 电脑B <--(端口8080)--> 电脑A <--(HTTP)--> 目标网站 ``` +### 1.3 核心设计原则 + +1. **电脑A作为纯HTTP客户端**:不保存cookie、session等状态信息 +2. **请求头替换**:电脑A强制替换Origin、Referer等参数 +3. **日志对应**:server端每一条请求日志,client端对应一条请求日志 +4. **目标网站硬编码**:目标网站地址写死在配置中 + --- ## 2. 端口分配 @@ -30,7 +37,7 @@ ### 3.1 协议类型 -采用 **HTTP CONNECT 代理协议**,这是标准的HTTP隧道协议。 +采用 **自定义HTTP代理协议**,电脑B将浏览器请求转发给电脑A,电脑A作为HTTP客户端访问目标网站。 ### 3.2 协议流程 @@ -38,102 +45,174 @@ ``` 电脑A → 电脑B: TCP连接建立 -电脑B → 电脑A: 连接接受(无额外协议,仅TCP握手) +电脑B → 电脑A: "PROXY_CONNECTED\r\n" +电脑A → 电脑B: "READY\r\n" ``` #### 阶段2:浏览器发起请求 ``` -浏览器 → 电脑B: HTTP CONNECT请求 -CONNECT zentao.sunyard.com.cn:9788 HTTP/1.1 -Host: zentao.sunyard.com.cn:9788 +浏览器 → 电脑B: 完整HTTP请求 +GET /max/js/jquery/lib.js HTTP/1.1 +Host: localhost:8081 +Origin: http://localhost:8081 +Referer: http://localhost:8081/ [其他请求头...] + +[请求体(如有)] ``` #### 阶段3:电脑B转发请求 ``` -电脑B → 电脑A: 透传完整HTTP CONNECT请求 +电脑B → 电脑A: 完整HTTP请求(透传) ``` -#### 阶段4:电脑A建立隧道 +#### 阶段4:电脑A处理请求 + +1. **修改请求头**:替换Origin、Referer为目标网站地址 +2. **修改Host头**:设置为目标网站地址 +3. **移除不必要的头**:如Connection、Keep-Alive等 + +#### 阶段5:电脑A访问目标网站 ``` -电脑A → 内网目标: TCP连接建立 -内网目标 → 电脑A: 连接接受 -电脑A → 电脑B: HTTP 200响应 -HTTP/1.1 200 Connection Established +电脑A → 内网目标: 修改后的HTTP请求 +GET /max/js/jquery/lib.js HTTP/1.1 +Host: zentao.sunyard.com.cn:9788 +Origin: http://zentao.sunyard.com.cn:9788 +Referer: http://zentao.sunyard.com.cn:9788/ +[其他请求头...] + +[请求体(如有)] ``` -#### 阶段5:数据隧道 +#### 阶段6:电脑A返回响应 ``` -浏览器 ↔ 电脑B ↔ 电脑A ↔ 内网目标: 双向数据流透传 +内网目标 → 电脑A: HTTP响应 +电脑A → 电脑B: HTTP响应(透传) +电脑B → 浏览器: HTTP响应(透传) ``` -#### 阶段6:连接关闭 +#### 阶段7:连接保持 -任意一端断开连接,整条链路关闭。 +- 电脑A与电脑B保持长连接 +- 支持同一连接上的多个HTTP请求 +- 连接空闲超时时间:300秒 --- -## 4. 数据格式 +## 4. 请求头处理规则 -### 4.1 HTTP CONNECT 请求格式 +### 4.1 强制替换的请求头 + +| 请求头 | 替换值 | +|--------|--------| +| Origin | `http://{TARGET_HOST}:{TARGET_PORT}` | +| Referer | `http://{TARGET_HOST}:{TARGET_PORT}/` | +| Host | `{TARGET_HOST}:{TARGET_PORT}` | + +### 4.2 移除的请求头 + +| 请求头 | 原因 | +|--------|------| +| Connection | 由电脑A管理连接 | +| Keep-Alive | 由电脑A管理连接 | +| Proxy-Connection | 不需要代理标识 | +| X-Forwarded-For | 不需要转发标识 | + +### 4.3 保留的请求头 + +所有其他请求头保持不变,包括: +- Accept +- Accept-Encoding +- Accept-Language +- User-Agent +- Cookie(如果有) +- Content-Type +- Content-Length + +--- + +## 5. 数据格式 + +### 5.1 请求格式 + +电脑B转发给电脑A的请求格式为**完整的HTTP请求**: ``` -CONNECT {目标地址}:{目标端口} HTTP/1.1\r\n -Host: {目标地址}:{目标端口}\r\n -[可选请求头]\r\n +{HTTP_METHOD} {REQUEST_PATH} HTTP/1.1\r\n +{HEADER_NAME}: {HEADER_VALUE}\r\n +... \r\n +{REQUEST_BODY} ``` -### 4.2 HTTP 200 响应格式 +### 5.2 响应格式 + +电脑A返回给电脑B的响应格式为**完整的HTTP响应**: ``` -HTTP/1.1 200 Connection Established\r\n +HTTP/1.1 {STATUS_CODE} {STATUS_MESSAGE}\r\n +{HEADER_NAME}: {HEADER_VALUE}\r\n +... \r\n +{RESPONSE_BODY} ``` --- -## 5. 日志规范 +## 6. 日志规范 -### 5.1 日志格式 +### 6.1 日志格式 | 字段 | 格式 | 示例 | |------|------|------| | 时间戳 | `YYYY-MM-DD HH:MM:SS` | `2024-01-15 10:30:45` | -| 级别 | `[INFO]` / `[ERROR]` / `[DEBUG]` | `[INFO]` | +| 级别 | `[INFO]` / `[ERROR]` | `[INFO]` | | 来源 | `[Browser]` / `[DeviceA]` / `[System]` | `[Browser]` | +| 行号 | `(line N)` | `(line 45)` | | 内容 | 描述信息 | 请求详情 | -### 5.2 HTTP请求日志(电脑B) +### 6.2 请求日志(电脑B) ``` -[时间戳] [INFO] [Browser] {客户端IP}:{端口} - {方法} {URL} - Body: {字节数} bytes +[时间戳] [INFO] [Browser] (line N) Request: {METHOD} {PATH} - Body: {BYTES} bytes ``` -### 5.3 HTTP响应日志(电脑B) +### 6.3 请求日志(电脑A) ``` -[时间戳] [INFO] [Browser] {客户端IP}:{端口} - {方法} {URL} - Status: {状态码} - Time: {耗时}ms +[时间戳] [INFO] [System] (line N) Request: {METHOD} {PATH} - Body: {BYTES} bytes ``` -### 5.4 连接状态日志 +### 6.4 响应日志(电脑B) ``` -[时间戳] [INFO] [DeviceA] Connected: {IP}:{端口} -[时间戳] [INFO] [DeviceA] Disconnected -[时间戳] [INFO] [Browser] Connected from: {IP}:{端口} -[时间戳] [INFO] [Browser] Disconnected: {IP}:{端口} +[时间戳] [INFO] [Browser] (line N) Response: {METHOD} {PATH} - Status: {CODE} - Time: {MS}ms +``` + +### 6.5 响应日志(电脑A) + +``` +[时间戳] [INFO] [System] (line N) Response: {METHOD} {PATH} - Status: {CODE} - Time: {MS}ms +``` + +### 6.6 日志对应原则 + +**server端有一条请求日志,client端必须有一条对应的请求日志**: + +``` +电脑B: [2024-01-15 10:30:45] [INFO] [Browser] (line 45) Request: GET /max/js/jquery/lib.js - Body: 0 bytes +电脑A: [2024-01-15 10:30:45] [INFO] [System] (line 35) Request: GET /max/js/jquery/lib.js - Body: 0 bytes ``` --- -## 6. 错误处理 +## 7. 错误处理 -### 6.1 设备A未连接 +### 7.1 设备A未连接 当浏览器请求时设备A未连接: @@ -144,7 +223,7 @@ Content-Type: text/plain Device A not connected ``` -### 6.2 目标不可达 +### 7.2 目标不可达 当电脑A无法连接目标网站: @@ -152,10 +231,10 @@ Device A not connected HTTP/1.1 502 Bad Gateway Content-Type: text/plain -Cannot connect to target +Cannot connect to target: zentao.sunyard.com.cn:9788 ``` -### 6.3 重复连接 +### 7.3 重复连接 当已有设备A连接时,拒绝新连接: @@ -163,12 +242,44 @@ Cannot connect to target ERROR: Another A device is already connected ``` +### 7.4 请求超时 + +当请求处理超时: + +``` +HTTP/1.1 504 Gateway Timeout +Content-Type: text/plain + +Request timeout +``` + --- -## 7. 超时设置 +## 8. 超时设置 | 操作 | 超时时间 | |------|----------| | 设备A连接超时 | 10秒 | | 目标网站连接超时 | 30秒 | -| 连接空闲超时 | 120秒 | +| 请求处理超时 | 60秒 | +| 连接空闲超时 | 300秒 | + +--- + +## 9. 配置参数 + +### 9.1 电脑A配置 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| TARGET_HOST | 目标内网网站主机 | zentao.sunyard.com.cn | +| TARGET_PORT | 目标内网网站端口 | 9788 | +| B_HOST | 电脑B地址 | localhost | +| B_PORT | 电脑B端口 | 8080 | + +### 9.2 电脑B配置 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| A_PORT | 设备A连接端口 | 8080 | +| BROWSER_PORT | 浏览器访问端口 | 8081 | diff --git a/proxy/cleanup_8080.bat b/proxy/cleanup_8080.bat new file mode 100644 index 0000000..6f022e3 --- /dev/null +++ b/proxy/cleanup_8080.bat @@ -0,0 +1,28 @@ +@echo off +echo ================================================ +echo 清理8080端口进程脚本 +echo ================================================ +echo. + +:: 查找并终止使用8080端口的进程 +for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":8080" ^| findstr LISTENING') do ( + echo 终止进程: PID %%a + taskkill /F /PID %%a +) + +for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":8080" ^| findstr ESTABLISHED') do ( + echo 终止进程: PID %%a + taskkill /F /PID %%a +) + +:: 查找并终止Python代理进程 +for /f "tokens=2" %%a in ('tasklist ^| findstr python.exe') do ( + taskkill /F /PID %%a + echo 终止Python进程: PID %%a +) + +echo. +echo ================================================ +echo 清理完成 +echo ================================================ +pause diff --git a/proxy/cleanup_8080.ps1 b/proxy/cleanup_8080.ps1 new file mode 100644 index 0000000..8bbd0eb --- /dev/null +++ b/proxy/cleanup_8080.ps1 @@ -0,0 +1,27 @@ +@echo off +echo ================================================ +echo 清理8080端口进程脚本 +echo ================================================ +echo. + +:: 查找并终止使用8080端口的进程 +for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":8080" ^| findstr LISTENING') do ( + echo 终止进程: PID %%a + taskkill /F /PID %%a +) + +for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":8080" ^| findstr ESTABLISHED') do ( + echo 终止进程: PID %%a + taskkill /F /PID %%a +) + +:: 查找并终止Python代理进程 +for /f "tokens=2" %%a in ('tasklist ^| findstr python.exe') do ( + taskkill /F /PID %%a + echo 终止Python进程: PID %%a +) + +echo. +echo ================================================ +echo 清理完成 +echo ================================================ diff --git a/proxy/proxy_client.py b/proxy/proxy_client.py index c86f8fa..31cbe2a 100644 --- a/proxy/proxy_client.py +++ b/proxy/proxy_client.py @@ -1,40 +1,82 @@ # -*- coding: utf-8 -*- import socket import threading -import sys import time import inspect +import sys -TARGET_HOST = 'zentao.sunyard.com.cn' +# Configuration +TARGET_HOST = "zentao.sunyard.com.cn" TARGET_PORT = 9788 +B_HOST = "localhost" +B_PORT = 8080 def log_info(source, message): - """Log info message""" - timestamp = time.strftime("%Y-%m-%d %H:%M:%S") - frame = inspect.currentframe().f_back - line = frame.f_lineno - print("[%s] [INFO] [%s] (line %d) %s" % (timestamp, source, line, message)) + """Log info message with line number""" + line_number = inspect.currentframe().f_back.f_lineno + print("[%s] [INFO] [%s] (line %d) %s" % ( + time.strftime("%Y-%m-%d %H:%M:%S"), + source, + line_number, + message + )) def log_error(source, message): - """Log error message""" - timestamp = time.strftime("%Y-%m-%d %H:%M:%S") - frame = inspect.currentframe().f_back - line = frame.f_lineno - print("[%s] [ERROR] [%s] (line %d) %s" % (timestamp, source, line, message)) + """Log error message with line number""" + line_number = inspect.currentframe().f_back.f_lineno + print("[%s] [ERROR] [%s] (line %d) %s" % ( + time.strftime("%Y-%m-%d %H:%M:%S"), + source, + line_number, + message + )) -def handle_request(b_socket, request_data): - """Handle a single request from device B""" - # Parse HTTP request - request_lines = request_data.split(b"\r\n") - if request_lines: - first_line = request_lines[0].decode('utf-8', errors='ignore') +def modify_request_headers(request_data): + """Modify request headers: replace Origin, Referer, Host; remove Connection headers""" + lines = request_data.split(b"\r\n") + new_lines = [] + method = "" + path = "" + + if lines: + first_line = lines[0].decode('utf-8', errors='ignore') parts = first_line.split() if len(parts) >= 2: method = parts[0] - url = parts[1] - log_info("System", "Received request: %s %s" % (method, url)) + path = parts[1] + + for line in lines: + line_str = line.decode('utf-8', errors='ignore') + + if line_str.startswith("Origin:"): + new_lines.append("Origin: http://%s:%d" % (TARGET_HOST, TARGET_PORT)) + elif line_str.startswith("Referer:"): + new_lines.append("Referer: http://%s:%d/" % (TARGET_HOST, TARGET_PORT)) + elif line_str.startswith("Host:"): + new_lines.append("Host: %s:%d" % (TARGET_HOST, TARGET_PORT)) + elif line_str.lower().startswith("connection:"): + continue + elif line_str.lower().startswith("keep-alive:"): + continue + elif line_str.lower().startswith("proxy-connection:"): + continue + elif line_str.lower().startswith("x-forwarded-for:"): + continue + else: + new_lines.append(line_str) + + return "\r\n".join(new_lines).encode('utf-8'), method, path + +def handle_request(b_socket, request_data): + """Handle a single request from device B""" + modified_data, method, path = modify_request_headers(request_data) + + body_size = 0 + if b"\r\n\r\n" in modified_data: + body_size = len(modified_data) - modified_data.find(b"\r\n\r\n") - 4 + + log_info("System", "Request: %s %s - Body: %d bytes" % (method, path, body_size)) - # Connect to target target_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) target_sock.settimeout(30) @@ -42,47 +84,55 @@ def handle_request(b_socket, request_data): target_sock.connect((TARGET_HOST, TARGET_PORT)) log_info("System", "Connected to target: %s:%d" % (TARGET_HOST, TARGET_PORT)) - if method == "CONNECT": - # Send CONNECT response - response = b"HTTP/1.1 200 Connection Established\r\n\r\n" - b_socket.sendall(response) - log_info("System", "Sent CONNECT response") - - # Establish tunnel - def forward(src, dst): - try: - while True: - data = src.recv(8192) - if not data: - break - dst.sendall(data) - except: - pass - - t1 = threading.Thread(target=forward, args=(b_socket, target_sock)) - t2 = threading.Thread(target=forward, args=(target_sock, b_socket)) - t1.start() - t2.start() - t1.join() - t2.join() + target_sock.sendall(modified_data) - else: - # Forward request and response - target_sock.sendall(request_data) + response = b"" + while True: + chunk = target_sock.recv(8192) + if not chunk: + break + response += chunk - while True: - response = target_sock.recv(8192) - if not response: + if b"\r\n\r\n" in response: + headers_end = response.find(b"\r\n\r\n") + headers = response[:headers_end].decode('utf-8', errors='ignore') + + content_length = 0 + for line in headers.split("\r\n"): + if line.lower().startswith("content-length:"): + content_length = int(line.split(":")[1].strip()) + break + + body_start = headers_end + 4 + if len(response) - body_start >= content_length: break - b_socket.sendall(response) + + b_socket.sendall(response) + + status_code = "500" + if b"\r\n\r\n" in response: + headers_end = response.find(b"\r\n\r\n") + headers = response[:headers_end].decode('utf-8', errors='ignore') + for line in headers.split("\r\n"): + if line.startswith("HTTP/"): + status_code = line.split()[1] + break + + log_info("System", "Response: %s %s - Status: %s" % (method, path, status_code)) return True - except ConnectionRefusedError: - log_error("System", "Cannot connect to target: %s:%d" % (TARGET_HOST, TARGET_PORT)) - error_msg = b"HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\n\r\nCannot connect to target\r\n" - b_socket.sendall(error_msg) - return False + except socket.error as e: + if e.errno == 111 or e.errno == 10061: + log_error("System", "Cannot connect to target: %s:%d" % (TARGET_HOST, TARGET_PORT)) + error_msg = b"HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\n\r\nCannot connect to target: %s:%d\r\n" % (TARGET_HOST.encode(), TARGET_PORT) + b_socket.sendall(error_msg) + return False + else: + log_error("System", "Socket error: %s" % str(e)) + error_msg = b"HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain\r\n\r\nSocket error\r\n" + b_socket.sendall(error_msg) + return False except socket.timeout: log_error("System", "Target connection timeout") error_msg = b"HTTP/1.1 504 Gateway Timeout\r\nContent-Type: text/plain\r\n\r\nConnection timeout\r\n" @@ -96,27 +146,41 @@ def handle_request(b_socket, request_data): def listen_for_requests(sock): """Listen for requests from device B""" - try: - while True: - # Wait for request with long timeout - sock.settimeout(600) # 10 minutes - - request_data = b"" - while b"\r\n\r\n" not in request_data: + sock.settimeout(300) + + while True: + try: + data = b"" + while True: chunk = sock.recv(8192) if not chunk: - log_info("System", "Device B disconnected") + log_info("System", "Connection closed by device B") return - request_data += chunk + data += chunk + if b"\r\n\r\n" in data: + headers_end = data.find(b"\r\n\r\n") + headers = data[:headers_end].decode('utf-8', errors='ignore') + + content_length = 0 + for line in headers.split("\r\n"): + if line.lower().startswith("content-length:"): + content_length = int(line.split(":")[1].strip()) + break + + body_start = headers_end + 4 + if len(data) - body_start >= content_length: + break - # Handle request in a separate thread - t = threading.Thread(target=handle_request, args=(sock, request_data)) + t = threading.Thread(target=handle_request, args=(sock, data)) + t.daemon = True t.start() - except socket.timeout: - log_info("System", "Connection idle timeout") - except Exception as e: - log_error("System", "Connection error: %s" % str(e)) + except socket.timeout: + log_info("System", "Connection idle timeout") + return + except Exception as e: + log_error("System", "Connection error: %s" % str(e)) + return def connect_to_b_device(b_host, b_port): """Connect to device B and handle requests""" @@ -126,8 +190,6 @@ def connect_to_b_device(b_host, b_port): sock.settimeout(10) sock.connect((b_host, b_port)) - # Wait for connection confirmation from server - sock.settimeout(5) response = sock.recv(1024) if not response or b"PROXY_CONNECTED" not in response: log_error("System", "Failed to receive connection confirmation") @@ -135,6 +197,7 @@ def connect_to_b_device(b_host, b_port): time.sleep(3) continue + sock.send(b"READY\r\n") log_info("System", "Successfully connected to device B: %s:%d" % (b_host, b_port)) listen_for_requests(sock) @@ -145,39 +208,23 @@ def connect_to_b_device(b_host, b_port): else: log_error("System", "Connection error: %s" % str(e)) time.sleep(3) - except socket.timeout: - log_info("System", "Connection timeout, retrying in 3 seconds...") - time.sleep(3) except Exception as e: - log_error("System", "Connection error: %s" % str(e)) + log_error("System", "Unexpected error: %s" % str(e)) time.sleep(3) def main(): - if len(sys.argv) < 3: - print("=" * 60) - print(" Proxy Client (Run on Computer A)") - print("=" * 60) - print("Target website:", TARGET_HOST, ":", TARGET_PORT) - print("Usage: python proxy_client.py ") - print("Example: python proxy_client.py 192.168.1.100 8080") - print("=" * 60) - sys.exit(1) - - b_host = sys.argv[1] - b_port = int(sys.argv[2]) - print("=" * 60) print(" Proxy Client (Run on Computer A)") print("=" * 60) print("Target website:", TARGET_HOST, ":", TARGET_PORT) - print("Device B address:", b_host, ":", b_port) + print("Device B address:", B_HOST, ":", B_PORT) print("=" * 60) try: - connect_to_b_device(b_host, b_port) + connect_to_b_device(B_HOST, B_PORT) except KeyboardInterrupt: - log_info("System", "Received Ctrl+C, exiting...") + print("\nShutting down...") sys.exit(0) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/proxy/proxy_server.py b/proxy/proxy_server.py index 91ac699..a31f422 100644 --- a/proxy/proxy_server.py +++ b/proxy/proxy_server.py @@ -2,145 +2,37 @@ import socket import threading import time -import sys import inspect +import sys -PORT_FOR_A = 8080 # Port for device A to connect -PORT_FOR_BROWSER = 8081 # Port for browser to connect +# Configuration +A_PORT = 8080 +BROWSER_PORT = 8081 +# Global state a_device_conn = None a_device_lock = threading.Lock() a_device_connected = threading.Event() def log_info(source, message): - """Log info message""" - timestamp = time.strftime("%Y-%m-%d %H:%M:%S") - frame = inspect.currentframe().f_back - line = frame.f_lineno - print("[%s] [INFO] [%s] (line %d) %s" % (timestamp, source, line, message)) + """Log info message with line number""" + line_number = inspect.currentframe().f_back.f_lineno + print("[%s] [INFO] [%s] (line %d) %s" % ( + time.strftime("%Y-%m-%d %H:%M:%S"), + source, + line_number, + message + )) def log_error(source, message): - """Log error message""" - timestamp = time.strftime("%Y-%m-%d %H:%M:%S") - frame = inspect.currentframe().f_back - line = frame.f_lineno - print("[%s] [ERROR] [%s] (line %d) %s" % (timestamp, source, line, message)) - -def handle_browser(browser_conn, browser_addr): - """Handle browser connection""" - global a_device_conn - - log_info("Browser", "Connected from: %s:%d" % browser_addr) - - # Wait for device A to connect - if not a_device_connected.wait(timeout=5): - error_msg = b"HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/plain\r\n\r\nDevice A not connected\r\n" - browser_conn.send(error_msg) - browser_conn.close() - log_info("Browser", "Disconnected (A not connected): %s:%d" % browser_addr) - return - - # Read HTTP request - request_start = time.time() - request_data = b"" - browser_conn.settimeout(30) - - try: - while b"\r\n\r\n" not in request_data: - chunk = browser_conn.recv(8192) - if not chunk: - browser_conn.close() - log_info("Browser", "Disconnected (no request): %s:%d" % browser_addr) - return - request_data += chunk - except socket.timeout: - browser_conn.close() - log_info("Browser", "Disconnected (timeout): %s:%d" % browser_addr) - return - - # Parse HTTP request - request_lines = request_data.split(b"\r\n") - method = "" - url = "" - if request_lines: - first_line = request_lines[0].decode('utf-8', errors='ignore') - parts = first_line.split() - if len(parts) >= 2: - method = parts[0] - url = parts[1] - body_size = len(request_data) - log_info("Browser", "%s:%d - %s %s - Body: %d bytes" % (browser_addr[0], browser_addr[1], method, url, body_size)) - - # Get device A connection - with a_device_lock: - device_a = a_device_conn - - if not device_a: - browser_conn.close() - log_error("Browser", "Disconnected (A closed): %s:%d" % browser_addr) - return - - # Send request to device A - device_a.sendall(request_data) - - # Wait for response - response_data = b"" - device_a.settimeout(30) - - try: - while True: - chunk = device_a.recv(8192) - if not chunk: - break - response_data += chunk - if b"\r\n\r\n" in response_data: - if method == "CONNECT" and b"200 Connection Established" in response_data: - break - if len(response_data) > 8192: - break - except: - pass - - # Parse response status - response_status = "Unknown" - if response_data: - response_lines = response_data.split(b"\r\n") - if response_lines: - status_line = response_lines[0].decode('utf-8', errors='ignore') - status_parts = status_line.split() - if len(status_parts) >= 2: - response_status = status_parts[1] - - # Send response to browser - browser_conn.sendall(response_data) - - # Calculate response time - response_time = int((time.time() - request_start) * 1000) - log_info("Browser", "%s:%d - %s %s - Status: %s - Time: %dms" % (browser_addr[0], browser_addr[1], method, url, response_status, response_time)) - - # For CONNECT, establish tunnel - if method == "CONNECT" and response_status == "200": - log_info("System", "Tunnel established between browser and Device A") - - def forward(src, dst): - try: - while True: - data = src.recv(8192) - if not data: - break - dst.sendall(data) - except: - pass - - t1 = threading.Thread(target=forward, args=(browser_conn, device_a)) - t2 = threading.Thread(target=forward, args=(device_a, browser_conn)) - t1.start() - t2.start() - t1.join() - t2.join() - - browser_conn.close() - log_info("Browser", "Disconnected: %s:%d" % browser_addr) + """Log error message with line number""" + line_number = inspect.currentframe().f_back.f_lineno + print("[%s] [ERROR] [%s] (line %d) %s" % ( + time.strftime("%Y-%m-%d %H:%M:%S"), + source, + line_number, + message + )) def handle_a_device(conn, addr): """Handle device A connection - just store and hold""" @@ -155,13 +47,19 @@ def handle_a_device(conn, addr): a_device_conn = conn - # Send connection confirmation conn.send(b"PROXY_CONNECTED\r\n") - a_device_connected.set() - log_info("DeviceA", "Connected: %s:%d" % addr) - # Keep connection open by waiting on an event (never triggered) - # Connection will be closed when browser tunnel ends or error occurs + response = conn.recv(1024) + if response and b"READY" in response: + a_device_connected.set() + log_info("DeviceA", "Connected: %s:%d" % addr) + else: + with a_device_lock: + a_device_conn = None + conn.close() + log_info("DeviceA", "Failed to receive READY from %s:%d" % addr) + return + try: event = threading.Event() event.wait() @@ -175,56 +73,162 @@ def handle_a_device(conn, addr): conn.close() log_info("DeviceA", "Disconnected") +def handle_browser_request(browser_conn, browser_addr): + """Handle a request from browser""" + try: + browser_conn.settimeout(30) + + data = b"" + while True: + chunk = browser_conn.recv(8192) + data += chunk + if b"\r\n\r\n" in data: + headers_end = data.find(b"\r\n\r\n") + headers = data[:headers_end].decode('utf-8', errors='ignore') + + content_length = 0 + for line in headers.split("\r\n"): + if line.lower().startswith("content-length:"): + content_length = int(line.split(":")[1].strip()) + break + + body_start = headers_end + 4 + if len(data) - body_start >= content_length: + break + elif chunk == b"": + break + + if not data: + return + + request_lines = data.split(b"\r\n") + if request_lines: + first_line = request_lines[0].decode('utf-8', errors='ignore') + parts = first_line.split() + if len(parts) >= 2: + method = parts[0] + path = parts[1] + + body_size = len(data) - data.find(b"\r\n\r\n") - 4 if b"\r\n\r\n" in data else 0 + log_info("Browser", "Request: %s %s - Body: %d bytes" % (method, path, body_size)) + + with a_device_lock: + if not a_device_conn: + error_response = b"HTTP/1.1 503 Service Unavailable\r\nContent-Type: text/plain\r\n\r\nDevice A not connected\r\n" + browser_conn.sendall(error_response) + log_error("Browser", "Device A not connected for request") + return + + a_device_conn.sendall(data) + + response = b"" + while True: + chunk = a_device_conn.recv(8192) + if not chunk: + break + response += chunk + + if b"\r\n\r\n" in response: + headers_end = response.find(b"\r\n\r\n") + headers = response[:headers_end].decode('utf-8', errors='ignore') + + status_code = "500" + for line in headers.split("\r\n"): + if line.startswith("HTTP/"): + status_code = line.split()[1] + break + + content_length = 0 + for line in headers.split("\r\n"): + if line.lower().startswith("content-length:"): + content_length = int(line.split(":")[1].strip()) + break + + body_start = headers_end + 4 + if len(response) - body_start >= content_length: + break + + browser_conn.sendall(response) + + log_info("Browser", "Response: %s %s - Status: %s" % (method, path, status_code)) + + except socket.timeout: + log_error("Browser", "Request timeout from %s:%d" % browser_addr) + error_response = b"HTTP/1.1 504 Gateway Timeout\r\nContent-Type: text/plain\r\n\r\nRequest timeout\r\n" + browser_conn.sendall(error_response) + except Exception as e: + log_error("Browser", "Error handling request: %s" % str(e)) + finally: + browser_conn.close() + +def handle_browser_client(conn, addr): + """Handle browser client connection""" + log_info("Browser", "Connected from: %s:%d" % addr) + handle_browser_request(conn, addr) + log_info("Browser", "Disconnected: %s:%d" % addr) + +def run_a_server(): + """Start device A listening port""" + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(('0.0.0.0', A_PORT)) + server.listen(1) + log_info("System", "Device A port started on %d" % A_PORT) + + while True: + try: + conn, addr = server.accept() + t = threading.Thread(target=handle_a_device, args=(conn, addr)) + t.daemon = True + t.start() + except: + break + + server.close() + def run_browser_server(): """Start browser listening port""" server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(('0.0.0.0', PORT_FOR_BROWSER)) - server.listen(10) - - log_info("System", "Browser port started on %d" % PORT_FOR_BROWSER) + server.bind(('0.0.0.0', BROWSER_PORT)) + server.listen(5) + log_info("System", "Browser port started on %d" % BROWSER_PORT) while True: - conn, addr = server.accept() - t = threading.Thread(target=handle_browser, args=(conn, addr)) - t.start() - -def run_a_device_server(): - """Start device A connection port""" - server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(('0.0.0.0', PORT_FOR_A)) - server.listen(1) + try: + conn, addr = server.accept() + t = threading.Thread(target=handle_browser_client, args=(conn, addr)) + t.daemon = True + t.start() + except: + break - log_info("System", "Device A port started on %d" % PORT_FOR_A) - - while True: - conn, addr = server.accept() - t = threading.Thread(target=handle_a_device, args=(conn, addr)) - t.start() + server.close() def main(): print("=" * 60) print(" Proxy Server (Run on Computer B)") print("=" * 60) - print("Device A port:", PORT_FOR_A) - print("Browser port:", PORT_FOR_BROWSER) + print("Device A port:", A_PORT) + print("Browser port:", BROWSER_PORT) print("=" * 60) - print("Waiting for connections...\n") + print("Waiting for connections...") + print() - t1 = threading.Thread(target=run_a_device_server) - t2 = threading.Thread(target=run_browser_server) + t1 = threading.Thread(target=run_a_server) t1.daemon = True - t2.daemon = True t1.start() + + t2 = threading.Thread(target=run_browser_server) + t2.daemon = True t2.start() try: while True: time.sleep(1) except KeyboardInterrupt: - log_info("System", "Received Ctrl+C, exiting...") + print("\nShutting down...") sys.exit(0) -if __name__ == '__main__': +if __name__ == "__main__": main()