fix: allow proxied Pascal scene creation

This commit is contained in:
134 2026-07-12 08:48:25 +08:00
parent 98dddbcc2e
commit 4c1b97f210
4 changed files with 49 additions and 1 deletions

View File

@ -98,6 +98,9 @@ sudo env PASCAL_SMOKE_CONFIRM=1 pnpm pascal:smoke
| `PASCAL_BUN_VERSION` | 固定 Bun 版本 |
| `PASCAL_BACKUP_DIR` | 环境与 Nginx 配置备份目录 |
`PASCAL_PUBLIC_ORIGIN` 会同时写入 Pascal 的场景 API 允许来源,用于正确
识别 Nginx 后的 HTTPS 同域请求,不应配置为 `*`。
## 运行维护
```bash

View File

@ -231,6 +231,9 @@ write_pascal_env() {
printf 'NEXT_PUBLIC_PASCAL_BASE_PATH=%s\n' "$PASCAL_BASE_PATH"
printf 'PASCAL_REQUIRE_HOST_USER=true\n'
printf 'PASCAL_DATA_DIR=%s\n' "$PASCAL_DATA_DIR"
if [ -n "$PASCAL_PUBLIC_ORIGIN" ]; then
printf 'PASCAL_SCENE_API_ORIGINS=%s\n' "${PASCAL_PUBLIC_ORIGIN%/}"
fi
printf 'PASCAL_SCENE_API_TOKEN=%s\n' "$secret"
printf 'PASCAL_AGENT_PROXY_SECRET=%s\n' "$secret"
printf 'PASCAL_AGENT_API_KEY=%s\n' "$secret"

View File

@ -47,6 +47,38 @@ test('accepts bearer token auth when configured', () => {
expect(guardSceneApiRequest(request)).toBeNull()
})
test('accepts the public HTTPS origin behind a trusted reverse proxy', () => {
process.env.PASCAL_SCENE_API_TOKEN = 'secret'
const request = new Request('http://bbyy.ai/pascal-service/api/scenes', {
headers: {
host: 'bbyy.ai',
origin: 'https://bbyy.ai',
'x-forwarded-host': 'bbyy.ai',
'x-forwarded-proto': 'https',
'x-pascal-scene-token': 'secret',
},
})
expect(guardSceneApiRequest(request)).toBeNull()
})
test('rejects an unrelated origin behind the reverse proxy', async () => {
process.env.PASCAL_SCENE_API_TOKEN = 'secret'
const request = new Request('http://bbyy.ai/pascal-service/api/scenes', {
headers: {
host: 'bbyy.ai',
origin: 'https://evil.example',
'x-forwarded-host': 'bbyy.ai',
'x-forwarded-proto': 'https',
'x-pascal-scene-token': 'secret',
},
})
const response = guardSceneApiRequest(request)
expect(response?.status).toBe(403)
expect(await response?.json()).toEqual({ error: 'origin_not_allowed' })
})
test('applies configured CORS origins for preflight', () => {
process.env.PASCAL_SCENE_API_ORIGINS = 'https://app.example'
const request = new Request('https://editor.example/api/scenes', {

View File

@ -145,10 +145,20 @@ function configuredOrigins(): Set<string> {
function isSameOrigin(request: Request, origin: string): boolean {
const parsedOrigin = parseUrl(origin)
if (!parsedOrigin) return false
const requestUrl = new URL(request.url)
const forwardedProto = firstForwardedValue(request.headers.get('x-forwarded-proto'))
const forwardedHost = firstForwardedValue(request.headers.get('x-forwarded-host'))
const forwardedUrl =
forwardedProto && forwardedHost && /^(?:http|https)$/.test(forwardedProto)
? parseUrl(`${forwardedProto}://${forwardedHost}`)
: null
const requestUrl = forwardedUrl ?? new URL(request.url)
return normalizeOrigin(parsedOrigin) === normalizeOrigin(requestUrl)
}
function firstForwardedValue(value: string | null): string | null {
return value?.split(',')[0]?.trim().toLowerCase() || null
}
function isLoopbackRequest(request: Request): boolean {
const host = request.headers.get('host') ?? new URL(request.url).host
return isLoopbackHostname(stripPort(host))