feat(gateway): 为52号接口增加客户端静态Token校验

请求头X-API-Token默认yonghe1999,可通过interface52_token覆盖;缺少或不匹配返回401/code1009,不进入派发队列。客户端Token不转发到worker,其他接口保持原样。

同步Token输入框、配置示例与调用文档。57项网关测试及临时网关连接真实worker的浏览器验证通过。
This commit is contained in:
xsl
2026-11-04 11:35:02 +08:00
parent 544c2a83d6
commit 14ec4964a0
10 changed files with 120 additions and 6 deletions
+1
View File
@@ -99,6 +99,7 @@
- **怎么实现**:复用接口2 的 502 点渲染管线,三档分别用 `hairline_texture[/_high|/_low]` 同名贴图渲染叠图;生发同接口2(ComfyUI inpaint),**黑模板固定取 `hairline_texture_black/`(middle)**,每发型 1 张生发图。`best_hairline_center_point`=眉心 x × 首个选中发型 middle 档发际线 y。 - **怎么实现**:复用接口2 的 502 点渲染管线,三档分别用 `hairline_texture[/_high|/_low]` 同名贴图渲染叠图;生发同接口2(ComfyUI inpaint),**黑模板固定取 `hairline_texture_black/`(middle)**,每发型 1 张生发图。`best_hairline_center_point`=眉心 x × 首个选中发型 middle 档发际线 y。
### 接口52 发际线新规则 `/api/v1/hairline/generate-52`(网关透传) ### 接口52 发际线新规则 `/api/v1/hairline/generate-52`(网关透传)
- **客户端鉴权**:请求头`X-API-Token`,默认`yonghe1999`,可由`gateway/config.json`的`interface52_token`覆盖;不匹配返回HTTP401/code1009且不进入队列。客户端Token不传给worker,内部Token仍由网关添加。
- `main`新增网关路由和`static/test_interface52.html`。GPU worker负责中下庭均值定位、扶正裁切及生发;主备worker均需部署52号实现,网关只透传表单并附加内部Token。 - `main`新增网关路由和`static/test_interface52.html`。GPU worker负责中下庭均值定位、扶正裁切及生发;主备worker均需部署52号实现,网关只透传表单并附加内部Token。
- 非空图片使用原有`*_base64`→`*_url`落盘改写;空图片保留`*_base64: null`。metadata和坐标原样返回,middle叠加到`input_image_url`。旧接口、5号队列优先级及网关IP策略保持原样。 - 非空图片使用原有`*_base64`→`*_url`落盘改写;空图片保留`*_base64: null`。metadata和坐标原样返回,middle叠加到`input_image_url`。旧接口、5号队列优先级及网关IP策略保持原样。
- 参数、空字段与部署步骤见[接口52网关接入](接口52网关接入.md)。worker分支的IP限制与算法不合入本次网关改动。 - 参数、空字段与部署步骤见[接口52网关接入](接口52网关接入.md)。worker分支的IP限制与算法不合入本次网关改动。
+12
View File
@@ -7,12 +7,17 @@
客户端 → `POST /api/v1/hairline/generate-52` → 网关现有队列和worker池 → worker同路径 → 网关落盘非空图片并返回公网URL。 客户端 → `POST /api/v1/hairline/generate-52` → 网关现有队列和worker池 → worker同路径 → 网关落盘非空图片并返回公网URL。
- 请求体原样转发,网关附加配置中的`X-Internal-Token`;客户端和公开测试页不携带内部密码。 - 请求体原样转发,网关附加配置中的`X-Internal-Token`;客户端和公开测试页不携带内部密码。
- 客户端须通过`X-API-Token`传入52号静态Token,默认`yonghe1999`。网关校验后才进入队列;该Token不转发给worker,与内部密码独立。
- 沿用现有主备派发、隔离期、重试、日志和全局并发限制。原有5号插队优先级保持原样,52号走默认队列。 - 沿用现有主备派发、隔离期、重试、日志和全局并发限制。原有5号插队优先级保持原样,52号走默认队列。
- 5号`/api/v1/hairline/generate`和其他接口保持原样;网关不新增IP限制。 - 5号`/api/v1/hairline/generate`和其他接口保持原样;网关不新增IP限制。
- worker的中下庭规则、扶正、3:4裁切和生发结果由同一次worker响应提供,网关不重新计算坐标。 - worker的中下庭规则、扶正、3:4裁切和生发结果由同一次worker响应提供,网关不重新计算坐标。
## 请求 ## 请求
请求头必填:`X-API-Token: yonghe1999`。缺少、为空或不匹配时返回HTTP401,响应为`{"code":1009,"message":"未授权:缺少或错误的 X-API-Token","request_id":"gw-…","data":null}`,不读取转发表单、不占用GPU队列。其他接口不增加这项校验。
可在`gateway/config.json`设置`"interface52_token": "自定义值"`;旧配置省略该项时自动使用`yonghe1999`。显式配置空值或非字符串会导致启动校验失败,不能用来关闭鉴权。配置修改后重启网关生效,测试页输入框改为相同值。
使用`multipart/form-data`;图片三选一:`image_file`、`image_url`、`image_base64`。必填`gender=male/female`和`hair_style=1..7`(支持逗号分隔多选)。 使用`multipart/form-data`;图片三选一:`image_file`、`image_url`、`image_base64`。必填`gender=male/female`和`hair_style=1..7`(支持逗号分隔多选)。
可选参数全部透传: 可选参数全部透传:
@@ -51,6 +56,7 @@ worker始终返回`*_base64`。网关沿用原有公共改写,不添加52号
```bash ```bash
curl -X POST https://hair.xiangsilian.com/api/v1/hairline/generate-52 \ curl -X POST https://hair.xiangsilian.com/api/v1/hairline/generate-52 \
-H 'X-API-Token: yonghe1999' \
-F 'image_file=@/path/to/photo.jpg' \ -F 'image_file=@/path/to/photo.jpg' \
-F 'gender=male' -F 'hair_style=1' \ -F 'gender=male' -F 'hair_style=1' \
-F 'normalize_image=true' -F 'generate_grow_image=false' -F 'normalize_image=true' -F 'generate_grow_image=false'
@@ -58,8 +64,14 @@ curl -X POST https://hair.xiangsilian.com/api/v1/hairline/generate-52 \
成功时`code=0`,检查`input_image_url`和`hairline_images[].image_middle_url`可打开、尺寸一致;high/low仍为`*_base64: null`。访问测试页时无需输入内部Token。 成功时`code=0`,检查`input_image_url`和`hairline_images[].image_middle_url`可打开、尺寸一致;high/low仍为`*_base64: null`。访问测试页时无需输入内部Token。
测试页新增“客户端Token”输入框,默认`yonghe1999`,只作为`X-API-Token`请求头发送;自定义后端Token时应同步填写该值。
## 验证 ## 验证
`tests/test_gateway_interface52.py`覆盖5/52路由分离、完整表单透传、由网关注入内部Token、真实公共转发函数的图片落盘与URL可访问、空字段保持原样和队列槽释放。现有pool、serialization、monitor、reqlog测试用于防止回归。 `tests/test_gateway_interface52.py`覆盖5/52路由分离、完整表单透传、由网关注入内部Token、真实公共转发函数的图片落盘与URL可访问、空字段保持原样和队列槽释放。现有pool、serialization、monitor、reqlog测试用于防止回归。
静态Token测试覆盖默认值、自定义覆盖、缺失/错误/空Token、内部Token不能代替客户端Token、未通过时不派发、无效配置启动失败以及客户端Token不透传至worker。
加入客户端Token后的验证:57项网关测试通过;临时网关实测缺失/错误Token返回401,测试页携带默认Token成功调用真实worker并显示7型图片,旧5号页面正常。测试关闭生发,未改动正在运行的GPU worker。
2026-11-04验证结果:上述网关测试共42项通过。另起本机临时网关连接当前真实GPU worker,浏览器验证旧5号仍返回三档,52号女性7型均返回可访问的图片URL,长图输出1062×1416,矩形叠加与空字段正常,页面请求不携带内部Token,无脚本错误。本轮关闭生发以验证网关链路,未重新运行ComfyUI生发;临时网关验证后关闭。 2026-11-04验证结果:上述网关测试共42项通过。另起本机临时网关连接当前真实GPU worker,浏览器验证旧5号仍返回三档,52号女性7型均返回可访问的图片URL,长图输出1062×1416,矩形叠加与空字段正常,页面请求不携带内部Token,无脚本错误。本轮关闭生发以验证网关链路,未重新运行ComfyUI生发;临时网关验证后关闭。
+3 -1
View File
@@ -87,7 +87,7 @@
| 1006 | 文件超出大小限制 | 单文件超过 1 MB | | 1006 | 文件超出大小限制 | 单文件超过 1 MB |
| 1007 | 图片参数错误 | file / url / base64 未传,或同时传了多个(三者严格互斥) | | 1007 | 图片参数错误 | file / url / base64 未传,或同时传了多个(三者严格互斥) |
| 1008 | 图片格式不支持 | 非 JPG / PNG | | 1008 | 图片格式不支持 | 非 JPG / PNG |
| 1009 | 未授权 | 缺少或错误的 `X-Internal-Token`(`/api/*` 路径鉴权) | | 1009 | 未授权 | worker内部`X-Internal-Token`错误;网关52号缺少或错误的客户端`X-API-Token`。HTTP401 |
--- ---
@@ -526,6 +526,8 @@
**请求**:`POST /api/v1/hairline/generate-52`(网关透传至同路径GPU worker) **请求**:`POST /api/v1/hairline/generate-52`(网关透传至同路径GPU worker)
**客户端鉴权**:请求头必填`X-API-Token: yonghe1999`,缺少、为空或不匹配返回HTTP401/code1009,不进入worker队列。默认值可通过网关`gateway/config.json`的`interface52_token`覆盖,修改后重启网关;内部`X-Internal-Token`仍由网关注入,客户端无需提供。该校验仅用于网关52号。
**职责**:GPU worker返回base64并完成算法处理,网关负责透传参数、附加内部Token、保存非空图片及返回公网URL。网关不增加IP限制;网关出口IP须已在worker白名单内。部署与完整字段约定见[接口52网关接入](接口52网关接入.md)。 **职责**:GPU worker返回base64并完成算法处理,网关负责透传参数、附加内部Token、保存非空图片及返回公网URL。网关不增加IP限制;网关出口IP须已在worker白名单内。部署与完整字段约定见[接口52网关接入](接口52网关接入.md)。
### 图片扶正与标准构图 ### 图片扶正与标准构图
+2
View File
@@ -7,6 +7,8 @@
## 接口52(2026-11-04 已接入main) ## 接口52(2026-11-04 已接入main)
客户端新增请求头`X-API-Token`校验,默认`yonghe1999`,可在网关配置`interface52_token`覆盖;缺少或不匹配返回HTTP401/code1009,不进入队列。其他接口不增加该校验。
新增`POST /api/v1/hairline/generate-52`,由网关转发到GPU worker同路径。5号保持原样,52号的`normalize_image`等参数原样透传;非空图片沿用公共base64→URL改写,空图片仍保留`*_base64: null`。不在网关增加IP限制,不合入worker算法。 新增`POST /api/v1/hairline/generate-52`,由网关转发到GPU worker同路径。5号保持原样,52号的`normalize_image`等参数原样透传;非空图片沿用公共base64→URL改写,空图片仍保留`*_base64: null`。不在网关增加IP限制,不合入worker算法。
测试页`/static/test_interface52.html`不包含内部密码。部署前确认主/备worker均支持52号,并检查生发请求的上游超时。详见[接口52网关接入](接口52网关接入.md)。 测试页`/static/test_interface52.html`不包含内部密码。部署前确认主/备worker均支持52号,并检查生发请求的上游超时。详见[接口52网关接入](接口52网关接入.md)。
+18 -2
View File
@@ -6,13 +6,14 @@
import asyncio import asyncio
import base64 import base64
import hmac
import json import json
import logging import logging
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from fastapi import FastAPI, File, Form, Request, UploadFile from fastapi import FastAPI, File, Form, Header, Request, UploadFile
from fastapi.responses import HTMLResponse, JSONResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -674,12 +675,27 @@ async def hairline_generate(request: Request):
@app.post("/api/v1/hairline/generate-52", tags=["人脸分析"], @app.post("/api/v1/hairline/generate-52", tags=["人脸分析"],
summary="接口52:发际线新规则与人像裁切") summary="接口52:发际线新规则与人像裁切")
async def hairline_generate_52(request: Request): async def hairline_generate_52(
request: Request,
api_token: Optional[str] = Header(default=None, alias="X-API-Token",
description="52号接口客户端Token,默认yonghe1999"),
):
"""透传图片三选一、gender、hair_style及normalize_image等参数至GPU worker。 """透传图片三选一、gender、hair_style及normalize_image等参数至GPU worker。
worker负责中下庭均值定位、扶正裁切和生发;网关沿用原有派发、鉴权与图片URL改写。 worker负责中下庭均值定位、扶正裁切和生发;网关沿用原有派发、鉴权与图片URL改写。
空图片保持原有 *_base64: null 字段。接口5保持原样。 空图片保持原有 *_base64: null 字段。接口5保持原样。
客户端须传X-API-Token;缺少或不匹配时返回HTTP401/code1009,不进入派发队列。
""" """
from uuid import uuid4
from gateway.config import DEFAULTS, get_config
expected = get_config().get("interface52_token", DEFAULTS["interface52_token"])
if (not isinstance(expected, str) or not expected or not api_token
or not hmac.compare_digest(api_token.encode("utf-8"), expected.encode("utf-8"))):
return JSONResponse(status_code=401, content={
"code": 1009, "message": "未授权:缺少或错误的 X-API-Token",
"request_id": f"gw-{uuid4().hex[:8]}", "data": None,
})
return await _proxy(request, "/api/v1/hairline/generate-52") return await _proxy(request, "/api/v1/hairline/generate-52")
+1
View File
@@ -1,4 +1,5 @@
{ {
"interface52_token": "yonghe1999",
"workers": [ "workers": [
"http://127.0.0.1:28187", "http://127.0.0.1:28187",
"http://127.0.0.1:28188" "http://127.0.0.1:28188"
+5
View File
@@ -13,6 +13,7 @@ logger = logging.getLogger("gateway.config")
CONFIG_PATH = Path(__file__).resolve().parent / "config.json" CONFIG_PATH = Path(__file__).resolve().parent / "config.json"
DEFAULTS = { DEFAULTS = {
"interface52_token": "yonghe1999",
"public_base_url": "https://hair.xiangsilian.com", "public_base_url": "https://hair.xiangsilian.com",
"static_dir": "static/annotations", "static_dir": "static/annotations",
"primary_workers": [], # 主 worker 地址列表(须为 workers 的子集);空 = 全部视为主 worker "primary_workers": [], # 主 worker 地址列表(须为 workers 的子集);空 = 全部视为主 worker
@@ -76,6 +77,10 @@ def load_config() -> dict:
cfg = _deep_merge(DEFAULTS, raw) cfg = _deep_merge(DEFAULTS, raw)
# --- 校验 --- # --- 校验 ---
client_token = cfg["interface52_token"]
if not isinstance(client_token, str) or not client_token:
raise ValueError("配置错误: interface52_token 必须是非空字符串")
workers: List[str] = cfg.get("workers", []) workers: List[str] = cfg.get("workers", [])
if not workers: if not workers:
raise ValueError("配置错误: workers 列表不能为空,至少需要一个 worker 地址") raise ValueError("配置错误: workers 列表不能为空,至少需要一个 worker 地址")
+1
View File
@@ -267,6 +267,7 @@ console.log(features.gene_style); // "自然型"</pre>
<div class="card" id="if52"> <div class="card" id="if52">
<h2>52. 发际线新规则与裁切 &nbsp;<span class="badge post">POST</span> &nbsp;<code>/api/v1/hairline/generate-52</code></h2> <h2>52. 发际线新规则与裁切 &nbsp;<span class="badge post">POST</span> &nbsp;<code>/api/v1/hairline/generate-52</code></h2>
<div class="card-body"> <div class="card-body">
<p>客户端请求头必填 <code>X-API-Token: yonghe1999</code>。缺少或不匹配返回HTTP401、code=1009。可在网关配置<code>interface52_token</code>覆盖默认值;内部Token由网关添加。</p>
<p class="desc">网关透传至GPU worker执行。中下庭均值定位 middle,支持9→152扶正、人像裁切和矩形发际线预览。接口5保留原有三档实现;新功能使用此独立接口。</p> <p class="desc">网关透传至GPU worker执行。中下庭均值定位 middle,支持9→152扶正、人像裁切和矩形发际线预览。接口5保留原有三档实现;新功能使用此独立接口。</p>
<p>基础图片参数、<code>gender</code>、<code>hair_style</code>、<code>use_mask</code>、<code>prompt</code>、<code>generate_grow_image</code>沿用接口5。新增可选<code>normalize_image</code>,默认false,传true开启扶正与裁切;测试页显式开启。</p> <p>基础图片参数、<code>gender</code>、<code>hair_style</code>、<code>use_mask</code>、<code>prompt</code>、<code>generate_grow_image</code>沿用接口5。新增可选<code>normalize_image</code>,默认false,传true开启扶正与裁切;测试页显式开启。</p>
<p style="margin-top:12px">非空图片返回<code>*_url</code>,空图片沿用<code>*_base64: null</code>。另有<code>height_rule</code>、<code>framing</code>、<code>guide_landmarks</code>、原图及处理后底图。middle和坐标使用当前底图,high/low字段为null。标准裁切高约3.3333L、宽2.5L;长图在保留人脸的前提下裁为3:4。</p> <p style="margin-top:12px">非空图片返回<code>*_url</code>,空图片沿用<code>*_base64: null</code>。另有<code>height_rule</code>、<code>framing</code>、<code>guide_landmarks</code>、原图及处理后底图。middle和坐标使用当前底图,high/low字段为null。标准裁切高约3.3333L、宽2.5L;长图在保留人脸的前提下裁为3:4。</p>
+2 -1
View File
@@ -106,6 +106,7 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="upload-row"> <div class="upload-row">
<div class="form-group"><label for="apiToken">客户端 Token</label><input id="apiToken" type="password" value="yonghe1999" autocomplete="off"></div>
<div class="file-input"><input type="file" id="imageFile" accept="image/jpeg,image/png,.jpg,.jpeg,.png"></div> <div class="file-input"><input type="file" id="imageFile" accept="image/jpeg,image/png,.jpg,.jpeg,.png"></div>
<div class="form-group"> <div class="form-group">
<label>性别</label> <label>性别</label>
@@ -230,7 +231,7 @@ async function submitTest() {
const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value); fd.append('hair_style', checked.join(',')); fd.append('generate_grow_image', $('genGrowImg').checked ? 'true' : 'false'); fd.append('normalize_image', 'true'); const fd = new FormData(); fd.append('image_file', f); fd.append('gender', $('gender').value); fd.append('hair_style', checked.join(',')); fd.append('generate_grow_image', $('genGrowImg').checked ? 'true' : 'false'); fd.append('normalize_image', 'true');
const _reqStart = performance.now(); const _reqStart = performance.now();
try { try {
const r = await fetch(API_BASE + '/api/v1/hairline/generate-52', { method:'POST', body:fd }); const r = await fetch(API_BASE + '/api/v1/hairline/generate-52', { method:'POST', headers: { 'X-API-Token': $('apiToken').value }, body:fd });
const json = await r.json(); const json = await r.json();
const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2); const _elapsed = ((performance.now() - _reqStart) / 1000).toFixed(2);
$('jsonContent').textContent = JSON.stringify(json, null, 2); $('jsonContent').textContent = JSON.stringify(json, null, 2);
+75 -2
View File
@@ -15,6 +15,75 @@ from gateway import config, forward
gateway = importlib.import_module("gateway.app") gateway = importlib.import_module("gateway.app")
PATH = "/api/v1/hairline/generate-52" PATH = "/api/v1/hairline/generate-52"
CLIENT_HEADERS = {"X-API-Token": "yonghe1999"}
@pytest.mark.parametrize("headers", [
{}, {"X-API-Token": ""}, {"X-API-Token": "wrong"},
{"X-API-Token": "yonghe1999 "}, {"X-Internal-Token": "yonghe1999"},
])
def test_invalid_client_token_is_rejected_before_dispatch(monkeypatch, headers):
monkeypatch.setattr(config, "get_config", lambda: {})
proxy = AsyncMock()
monkeypatch.setattr(gateway, "_proxy", proxy)
client = TestClient(gateway.app)
try:
response = client.post(PATH, headers=headers)
assert response.status_code == 401
body = response.json()
assert body["code"] == 1009 and body["data"] is None
assert body["request_id"].startswith("gw-")
finally:
client.close()
proxy.assert_not_awaited()
def test_custom_token_replaces_default_only_on_interface52(monkeypatch):
monkeypatch.setattr(config, "get_config", lambda: {"interface52_token": "custom-client-token"})
proxy = AsyncMock(return_value={"code": 0, "data": None})
monkeypatch.setattr(gateway, "_proxy", proxy)
client = TestClient(gateway.app)
try:
assert client.post(PATH, headers=CLIENT_HEADERS).status_code == 401
assert client.post(PATH, headers={"X-API-Token": "custom-client-token"}).status_code == 200
assert client.post("/api/v1/hairline/generate").status_code == 200
finally:
client.close()
assert proxy.await_count == 2
@pytest.mark.parametrize("token", ["", None, 123, []])
def test_invalid_configured_token_cannot_disable_auth(monkeypatch, token):
monkeypatch.setattr(config, "get_config", lambda: {"interface52_token": token})
proxy = AsyncMock()
monkeypatch.setattr(gateway, "_proxy", proxy)
client = TestClient(gateway.app)
try:
assert client.post(PATH, headers=CLIENT_HEADERS).status_code == 401
finally:
client.close()
proxy.assert_not_awaited()
@pytest.mark.parametrize("token", ["", None, 123, []])
def test_startup_rejects_invalid_client_token_configuration(tmp_path, monkeypatch, token):
import json
path = tmp_path / "config.json"
path.write_text(json.dumps({"interface52_token": token}))
monkeypatch.setattr(config, "CONFIG_PATH", path)
monkeypatch.setattr(config, "_config_cache", None)
with pytest.raises(ValueError, match="interface52_token"):
config.load_config()
def test_existing_config_gets_default_client_token(tmp_path, monkeypatch):
import json
path = tmp_path / "config.json"
path.write_text(json.dumps({"workers": ["http://worker"], "shared_password": "test-internal-token",
"static_dir": str(tmp_path)}))
monkeypatch.setattr(config, "CONFIG_PATH", path)
monkeypatch.setattr(config, "_config_cache", None)
assert config.load_config()["interface52_token"] == "yonghe1999"
def image_bytes(fmt): def image_bytes(fmt):
@@ -46,11 +115,12 @@ def proxy_setup(tmp_path, monkeypatch):
@pytest.mark.parametrize("path", ["/api/v1/hairline/generate", PATH]) @pytest.mark.parametrize("path", ["/api/v1/hairline/generate", PATH])
def test_hairline_routes_forward_to_their_own_worker_path(monkeypatch, path): def test_hairline_routes_forward_to_their_own_worker_path(monkeypatch, path):
monkeypatch.setattr(config, "get_config", lambda: {})
proxy = AsyncMock(return_value={"code": 0, "data": None}) proxy = AsyncMock(return_value={"code": 0, "data": None})
monkeypatch.setattr(gateway, "_proxy", proxy) monkeypatch.setattr(gateway, "_proxy", proxy)
client = TestClient(gateway.app, client=("203.0.113.9", 50000)) client = TestClient(gateway.app, client=("203.0.113.9", 50000))
try: try:
assert client.post(path).json() == {"code": 0, "data": None} assert client.post(path, headers=CLIENT_HEADERS if path == PATH else {}).json() == {"code": 0, "data": None}
finally: finally:
client.close() client.close()
proxy.assert_awaited_once() proxy.assert_awaited_once()
@@ -93,6 +163,7 @@ def test_interface52_raw_forwarding_and_served_image_urls(proxy_setup, monkeypat
client = TestClient(gateway.app, client=("203.0.113.9", 50000)) client = TestClient(gateway.app, client=("203.0.113.9", 50000))
try: try:
result = client.post(PATH, content=raw, headers={ result = client.post(PATH, content=raw, headers={
**CLIENT_HEADERS,
"Content-Type": source.headers["content-type"], "X-Internal-Token": "untrusted-client-token"}) "Content-Type": source.headers["content-type"], "X-Internal-Token": "untrusted-client-token"})
assert result.status_code == 200 assert result.status_code == 200
data = result.json()["data"] data = result.json()["data"]
@@ -119,6 +190,7 @@ def test_interface52_raw_forwarding_and_served_image_urls(proxy_setup, monkeypat
assert calls[0].headers["content-type"] == source.headers["content-type"] assert calls[0].headers["content-type"] == source.headers["content-type"]
assert calls[0].headers["x-internal-token"] == cfg["shared_password"] assert calls[0].headers["x-internal-token"] == cfg["shared_password"]
assert calls[0].headers.get("x-forwarded-for") is None assert calls[0].headers.get("x-forwarded-for") is None
assert calls[0].headers.get("x-api-token") is None
acquire.assert_awaited_once_with(cfg, 0) acquire.assert_awaited_once_with(cfg, 0)
release.assert_called_once() release.assert_called_once()
release_worker.assert_awaited_once() release_worker.assert_awaited_once()
@@ -132,7 +204,7 @@ def test_worker_ip_rejection_is_preserved(proxy_setup, monkeypatch):
monkeypatch.setattr(forward, "get_client", lambda: upstream) monkeypatch.setattr(forward, "get_client", lambda: upstream)
client = TestClient(gateway.app) client = TestClient(gateway.app)
try: try:
response = client.post(PATH, data={"gender": "male", "hair_style": "1"}) response = client.post(PATH, headers=CLIENT_HEADERS, data={"gender": "male", "hair_style": "1"})
assert response.status_code == 403 and response.json() == body assert response.status_code == 403 and response.json() == body
finally: finally:
client.close() client.close()
@@ -148,6 +220,7 @@ def test_public_interface52_page_has_no_internal_password():
page = client.get("/static/test_interface52.html") page = client.get("/static/test_interface52.html")
assert page.status_code == 200 assert page.status_code == 200
assert PATH in page.text and "X-Internal-Token" not in page.text assert PATH in page.text and "X-Internal-Token" not in page.text
assert 'id="apiToken"' in page.text and "X-API-Token" in page.text
assert "localhost:8187" not in page.text assert "localhost:8187" not in page.text
assert "test_interface52_compare.html" not in page.text assert "test_interface52_compare.html" not in page.text
assert client.get("/static/hairline_reference_box.js").status_code == 200 assert client.get("/static/hairline_reference_box.js").status_code == 200