commit 4ba2045328c89533b8dbd753cbe865ceba17e597 Author: fuzhongyun <15339891972@163.com> Date: Wed Apr 1 10:04:01 2026 +0800 Initial commit (Clean history) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0f6f673 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.11-slim + +WORKDIR /app + +# 设置环境变量,防止 Python 写 .pyc 文件,并强制无缓冲输出 +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +COPY requirements.txt . + +# 1. 安装 Python 依赖 +# 2. 仅安装 Chromium 浏览器 +# 3. 仅安装 Chromium 所需的系统级依赖 +# 4. 清理 apt 缓存,极致压缩镜像体积 +RUN pip install --no-cache-dir -r requirements.txt \ + && playwright install chromium \ + && playwright install-deps chromium \ + && rm -rf /var/lib/apt/lists/* \ + && rm -rf /root/.cache/pip + +COPY main.py . + +EXPOSE 8000 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] + diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..88757bd --- /dev/null +++ b/deploy.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +set -e + +IMAGE_NAME="web-signature-service" +CONTAINER_NAME="web-signature-service" +PORT="20097" + +echo "=========================================" +echo "Web Header Signature Service - Deploy" +echo "=========================================" + +cd "$(dirname "$0")" + +# 检查 .env 文件,如果存在则加载 +ENV_FILE="" +if [ -f ".env" ]; then + echo "Found .env file, will use it for deployment." + ENV_FILE="--env-file .env" +else + echo "Warning: .env file not found. Service will use default target configurations." +fi + +case "${1:-up}" in + up) + echo "" + echo "[1/3] Building Docker image..." + docker build -t ${IMAGE_NAME} . + + echo "" + echo "[2/3] Stopping old container (if exists)..." + docker stop ${CONTAINER_NAME} 2>/dev/null || true + docker rm ${CONTAINER_NAME} 2>/dev/null || true + + echo "" + echo "[3/3] Starting service..." + # 注意: 传入环境变量配置 + docker run -d \ + --name ${CONTAINER_NAME} \ + -p ${PORT}:8000 \ + --restart unless-stopped \ + -v /dev/shm:/dev/shm \ + --security-opt seccomp=unconfined \ + -e TZ=Asia/Shanghai \ + ${ENV_FILE} \ + ${IMAGE_NAME} + + echo "" + echo "Waiting for service to start..." + sleep 3 + + echo "" + echo "=========================================" + echo "Deployment completed!" + echo "" + echo "Service URL: http://localhost:${PORT}" + echo "API Docs: http://localhost:${PORT}/docs" + echo "Health: http://localhost:${PORT}/health" + echo "Get Sign: http://localhost:${PORT}/fingerprint" + echo "" + echo "View Logs: docker logs -f ${CONTAINER_NAME}" + echo "Stop: docker stop ${CONTAINER_NAME}" + echo "=========================================" + ;; + + down) + echo "Stopping service..." + docker stop ${CONTAINER_NAME} 2>/dev/null || true + docker rm ${CONTAINER_NAME} 2>/dev/null || true + echo "Service stopped." + ;; + + logs) + docker logs -f ${CONTAINER_NAME} + ;; + + *) + echo "Usage: $0 [command]" + echo "Commands:" + echo " up - Build and start (default)" + echo " down - Stop and remove" + echo " logs - View logs" + ;; +esac + diff --git a/main.py b/main.py new file mode 100644 index 0000000..2d80b9c --- /dev/null +++ b/main.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import asyncio +import os +import logging +from typing import Optional +from contextlib import asynccontextmanager +from fastapi import FastAPI, HTTPException +from playwright.async_api import async_playwright + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger("fingerprint_service") + +TARGET_PAGE = os.getenv("TARGET_PAGE") +TARGET_API = os.getenv("TARGET_API") +FINGERPRINT_HEADER = os.getenv("FINGERPRINT_HEADER") + +playwright_instance = None +browser = None + +# [新增] 全局内存缓存池,存储 JS 脚本 +RESOURCE_CACHE = {} + +@asynccontextmanager +async def lifespan(app: FastAPI): + global playwright_instance, browser + logger.info("Starting Playwright instance...") + playwright_instance = await async_playwright().start() + browser = await playwright_instance.chromium.launch( + headless=True, + args=[ + "--no-sandbox", + "--disable-setuid-sandbox", + "--disable-dev-shm-usage", + "--disable-software-rasterizer", + ] + ) + logger.info("Playwright browser launched successfully.") + yield + logger.info("Closing Playwright browser...") + await browser.close() + await playwright_instance.stop() + logger.info("Playwright instance stopped.") + + +app = FastAPI( + title="Web Header Signature Service", + description="API service for dynamic header signature generation via headless browser", + version="1.0.0", + lifespan=lifespan +) + + +async def get_fingerprint(): + logger.info("Opening new browser page...") + page = await browser.new_page() + try: + # [新增] 拦截无用资源,极大提升加载速度 + async def route_intercept(route): + request = route.request + url = request.url + resource_type = request.resource_type + + # 1. 丢弃无用资源 + if resource_type in ["image", "stylesheet", "media", "font", "other"]: + await route.abort() + return + + # 2. 强缓存 JS 文件 + if resource_type == "script": + if url in RESOURCE_CACHE: + logger.debug(f"Cache hit for script: {url}") + await route.fulfill( + status=200, + headers=RESOURCE_CACHE[url]["headers"], + body=RESOURCE_CACHE[url]["body"] + ) + return + + # 缓存未命中,去真实抓取 + try: + logger.debug(f"Fetching script: {url}") + response = await route.fetch() + body = await response.body() + RESOURCE_CACHE[url] = { + "headers": response.headers, + "body": body + } + await route.fulfill(response=response, body=body) + return + except Exception as e: + logger.warning(f"Failed to fetch script {url}: {e}") + pass # 抓取失败,降级给底层处理 + + await route.continue_() + + await page.route("**/*", route_intercept) + + fingerprint_future = asyncio.Future() + + async def handle_request(request): + if TARGET_API in request.url and not fingerprint_future.done(): + headers = request.headers + if FINGERPRINT_HEADER in headers: + logger.info("Successfully intercepted target API request with required header.") + fingerprint_future.set_result(headers[FINGERPRINT_HEADER]) + + page.on("request", handle_request) + + # [修改] 并发执行 goto 和 wait_for。拿到结果就立刻返回,不再死等 goto 结束 + logger.info(f"Navigating to target page: {TARGET_PAGE}") + goto_task = asyncio.create_task(page.goto(TARGET_PAGE, wait_until="domcontentloaded")) + + logger.info("Waiting for header generation...") + fingerprint = await asyncio.wait_for(fingerprint_future, timeout=15) + logger.info("Header successfully generated and retrieved.") + return fingerprint + except asyncio.TimeoutError: + logger.error("Timeout while waiting for header generation.") + return None + except Exception as e: + logger.error(f"Unexpected error during generation: {str(e)}") + return None + finally: + logger.info("Closing browser page...") + await page.close() + + +@app.get("/") +async def root(): + return {"status": "ok", "message": "Web Header Signature Service is running"} + + +@app.get("/health") +async def health(): + return {"status": "healthy"} + + +@app.get("/fingerprint") +async def get_fingerprint_endpoint(): + logger.info("Received request for new header signature") + try: + fingerprint = await get_fingerprint() + if fingerprint: + logger.info("Successfully handled signature request") + return { + "status": "success", + "data": { + FINGERPRINT_HEADER: fingerprint + } + } + else: + logger.error("Failed to generate signature (returned None)") + raise HTTPException(status_code=500, detail="Failed to generate signature") + except Exception as e: + logger.error(f"Service error during request: {str(e)}") + raise HTTPException(status_code=500, detail=f"Service error: {str(e)}") + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..99f0351 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +playwright==1.58.0 +fastapi==0.115.0 +uvicorn==0.32.0