#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
ChatBox Lite 通用 API 代理 —— 解决浏览器直连 AI 接口的跨域(CORS)问题
=====================================================================
作用：把浏览器发来的请求原样转发到目标 AI 接口，并把响应（含流式）原样回传，
     同时补上 CORS 头。只用到 Python 标准库，无需安装任何依赖。

用法
----
1) 本机运行（只给自己用，最安全）：
     python3 api-proxy.py
   → 监听 http://127.0.0.1:8099
   然后打开 chatbox-lite.html → 设置 → 通用设置 → 代理地址填 http://127.0.0.1:8099

2) 放在服务器上运行（手机/多设备共用）：
     PROXY_TOKEN=换成一个只有你知道的口令 python3 api-proxy.py 0.0.0.0 8099
   代理地址填 http://你的服务器IP:8099，代理口令填上面那个 PROXY_TOKEN
   ⚠️ 公网暴露时必须设置 PROXY_TOKEN，否则任何人都能拿它当免费代理。

3) 后台常驻（Linux）：
     nohup env PROXY_TOKEN=口令 python3 api-proxy.py 0.0.0.0 8099 > proxy.log 2>&1 &

说明
----
- 只做转发，不记录、不保存任何内容；API Key 仍只存在浏览器里。
- 支持 GET/POST/PUT/DELETE 与流式(SSE)响应，AI 聊天常用场景全覆盖。
- 若报 SSL 证书错误（公司证书代理等特殊情况），可加 PROXY_INSECURE=1 启动跳过校验。
"""
import os
import ssl
import sys
import json
import urllib.request
import urllib.error
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs

TOKEN = os.environ.get("PROXY_TOKEN", "").strip()
TIMEOUT = 600  # 秒
# 个别网络（如公司装了证书代理）可能报 SSL 证书错误，此时可设 PROXY_INSECURE=1 跳过校验
SSL_CTX = ssl._create_unverified_context() if os.environ.get("PROXY_INSECURE") == "1" else None
# 不转发给目标服务器的请求头（避免误导目标、避免压缩影响流式）
DROP_HEADERS = {
    "host", "origin", "referer", "content-length", "connection",
    "accept-encoding", "accept-language", "cookie",
    "sec-fetch-mode", "sec-fetch-site", "sec-fetch-dest",
}


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"

    # ---------- 工具 ----------
    def cors(self):
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Headers", "*")
        self.send_header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")
        self.send_header("Access-Control-Max-Age", "86400")

    def reply_json(self, code, obj):
        body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
        self.send_response(code)
        self.cors()
        self.send_header("Content-Type", "application/json; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):
        sys.stderr.write("[proxy] %s - %s\n" % (self.address_string(), fmt % args))

    # ---------- 预检 ----------
    def do_OPTIONS(self):
        self.send_response(204)
        self.cors()
        self.send_header("Content-Length", "0")
        self.end_headers()

    # ---------- 转发 ----------
    def handle_forward(self):
        q = parse_qs(urlparse(self.path).query)
        target = (q.get("url") or [""])[0]
        if TOKEN and (q.get("token") or [""])[0] != TOKEN:
            self.reply_json(401, {"error": {"message": "proxy: 口令不正确（PROXY_TOKEN）"}})
            return
        if not target.lower().startswith(("http://", "https://")):
            self.reply_json(400, {"error": {"message": "proxy: 缺少合法的 ?url= 参数"}})
            return

        n = int(self.headers.get("Content-Length") or 0)
        body = self.rfile.read(n) if n else None
        headers = {k: v for k, v in self.headers.items() if k.lower() not in DROP_HEADERS}
        headers.setdefault("User-Agent", "chatbox-lite-proxy/1.0")

        req = urllib.request.Request(target, data=body, headers=headers, method=self.command)
        try:
            with urllib.request.urlopen(req, timeout=TIMEOUT, context=SSL_CTX) as resp:
                self.send_response(resp.status)
                self.cors()
                for k in ("Content-Type", "Cache-Control", "Request-Id"):
                    v = resp.headers.get(k)
                    if v:
                        self.send_header(k, v)
                # 不定长 + 关闭连接：保证流式数据边到边发
                self.send_header("Connection", "close")
                self.close_connection = True
                self.end_headers()
                while True:
                    chunk = resp.read(1024)
                    if not chunk:
                        break
                    self.wfile.write(chunk)
                    self.wfile.flush()
        except urllib.error.HTTPError as e:      # 4xx/5xx 也原样带 CORS 回传
            data = e.read()
            self.send_response(e.code)
            self.cors()
            self.send_header("Content-Type", e.headers.get("Content-Type", "application/json"))
            self.send_header("Content-Length", str(len(data)))
            self.send_header("Connection", "close")
            self.close_connection = True
            self.end_headers()
            self.wfile.write(data)
        except BrokenPipeError:
            pass
        except Exception as e:                   # 网络错误等
            try:
                self.reply_json(502, {"error": {"message": "proxy: 转发失败 - %s" % e}})
            except Exception:
                pass

    do_GET = handle_forward
    do_POST = handle_forward
    do_PUT = handle_forward
    do_DELETE = handle_forward


def main():
    host = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
    port = int(sys.argv[2]) if len(sys.argv) > 2 else 8099
    srv = ThreadingHTTPServer((host, port), Handler)
    if host != "127.0.0.1" and not TOKEN:
        print("⚠️  警告：正在对外网监听但未设置 PROXY_TOKEN，建议加口令后再用于公网！")
    print("✅ 代理已启动：http://%s:%d" % (host, port))
    print("   在 chatbox-lite.html → 设置 → 通用设置 → 代理地址 填这个地址即可。")
    if TOKEN:
        print("   已启用口令校验（代理口令 = PROXY_TOKEN）")
    try:
        srv.serve_forever()
    except KeyboardInterrupt:
        print("\n已停止。")


if __name__ == "__main__":
    main()
