From 40b52f6391dba1c2b8d8647e6c6863b43cc76e6b Mon Sep 17 00:00:00 2001 From: tech Date: Thu, 28 May 2026 17:11:46 +0800 Subject: [PATCH] Initial commit --- README.md | 44 +++++++++++++++++++++++++++ lfs-proxy.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 README.md create mode 100644 lfs-proxy.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..76b19b6 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# jianda-proxy + +本地 HTTP 代理,解决腾讯云对未备案域名拦截导致的 Git LFS 下载失败问题。 + +## 背景 + +内网 Gitea 通过反向隧道暴露到腾讯云,客户端通过 `hosts` 文件将域名指向云服务器 IP。SSH clone 正常,但 LFS 文件下载走 HTTP 协议,腾讯云检测到域名未备案,基于 Host 头拦截请求返回 302。 + +本代理将所有发往 `git.zz.com` 的请求的 Host 头改写为 IP 地址,绕过备案拦截。 + +## 使用方法 + +### 1. 启动代理 + +```bash +python3 lfs-proxy.py [port] +``` + +默认监听 `127.0.0.1:13000`,可指定其他端口。 + +### 2. 配置 Git 走代理(只需一次) + +```bash +git config --global http.http://git.zz.com:3000/.proxy http://127.0.0.1:13000 +``` + +### 3. 正常 clone + +```bash +git clone ssh://git@git.zz.com:2222/Mustapha/lfs-test.git +``` + +## 原理 + +代理同时支持两种模式: + +- **正向代理**:接收 `GET http://git.zz.com:3000/...` 形式的请求,改写 Host 头转发到上游 +- **反向代理**:接收 `GET /...` 形式的请求,直接转发到上游并附带 IP Host 头 + +上游地址硬编码为 `62.234.191.215:3000`,可在脚本顶部修改。 + +## 根治方案 + +在 Gitea 服务端 `app.ini` 中将 `ROOT_URL` 从 `http://git.zz.com:3000` 改为 `http://62.234.191.215:3000`,可从根源解决,无需代理。 diff --git a/lfs-proxy.py b/lfs-proxy.py new file mode 100644 index 0000000..b399de6 --- /dev/null +++ b/lfs-proxy.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""HTTP proxy for Gitea LFS - rewrites Host header to bypass ICP domain block. + +Acts as both: + - Forward proxy: receives "GET http://git.zz.com:3000/..." and rewrites Host to IP + - Reverse proxy: receives "GET /..." directly and forwards to upstream with IP Host + +Usage: python3 lfs-proxy.py [port] +""" +import http.client +import http.server +import sys +from urllib.parse import urlparse + +LOCAL_PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 13000 +REMOTE_DOMAIN = "git.zz.com" +UPSTREAM_IP = "62.234.191.215" +UPSTREAM_PORT = 3000 + + +class ProxyHandler(http.server.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + + def _proxy_request(self, target_host, target_port, target_path, body=None): + body_len = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(body_len) if body_len else None + + # Always rewrite Host to IP for upstream + conn = http.client.HTTPConnection(target_host, target_port, timeout=120) + try: + headers = {} + skip = {"host", "transfer-encoding", "connection"} + for k in self.headers: + if k.lower() not in skip: + headers[k] = self.headers[k] + headers["Host"] = f"{UPSTREAM_IP}:{UPSTREAM_PORT}" + if body_len: + headers["Content-Length"] = str(body_len) + + conn.request(self.command, target_path, body=body, headers=headers) + resp = conn.getresponse() + resp_body = resp.read() + + self.send_response(resp.status) + sent_cl = False + for k, v in resp.getheaders(): + kl = k.lower() + if kl in ("transfer-encoding", "connection"): + continue + if kl == "content-length": + self.send_header(k, len(resp_body)) + sent_cl = True + continue + self.send_header(k, v) + if not sent_cl: + self.send_header("Content-Length", len(resp_body)) + self.end_headers() + self.wfile.write(resp_body) + finally: + conn.close() + + def _do(self): + raw_path = self.path + parsed = urlparse(raw_path) + + if parsed.hostname and parsed.hostname != "127.0.0.1" and parsed.hostname != "localhost": + # Forward proxy mode: full URL like "http://git.zz.com:3000/path" + target_host = UPSTREAM_IP if parsed.hostname == REMOTE_DOMAIN else parsed.hostname + target_port = parsed.port or 80 + target_path = parsed.path + ("?" + parsed.query if parsed.query else "") + self._proxy_request(target_host, target_port, target_path) + else: + # Direct/reverse proxy mode: path like "/Mustapha/lfs-test.git/..." + # This happens when LFS downloads from URLs rewritten to point at us + self._proxy_request(UPSTREAM_IP, UPSTREAM_PORT, raw_path) + + do_GET = do_POST = do_PUT = do_DELETE = do_HEAD = do_PATCH = _do + + def log_message(self, fmt, *args): + print(f"[proxy] {self.address_string()} - {fmt % args}", file=sys.stderr, flush=True) + + +if __name__ == "__main__": + with http.server.ThreadingHTTPServer(("127.0.0.1", LOCAL_PORT), ProxyHandler) as s: + print(f"Proxy: 127.0.0.1:{LOCAL_PORT} -> {UPSTREAM_IP}:{UPSTREAM_PORT}", file=sys.stderr, flush=True) + s.serve_forever()