Initial commit (Clean history)
This commit is contained in:
commit
4ba2045328
|
|
@ -0,0 +1 @@
|
|||
.env
|
||||
|
|
@ -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"]
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
playwright==1.58.0
|
||||
fastapi==0.115.0
|
||||
uvicorn==0.32.0
|
||||
Loading…
Reference in New Issue