fix: allow proxied Pascal scene creation
This commit is contained in:
parent
98dddbcc2e
commit
4c1b97f210
@ -98,6 +98,9 @@ sudo env PASCAL_SMOKE_CONFIRM=1 pnpm pascal:smoke
|
|||||||
| `PASCAL_BUN_VERSION` | 固定 Bun 版本 |
|
| `PASCAL_BUN_VERSION` | 固定 Bun 版本 |
|
||||||
| `PASCAL_BACKUP_DIR` | 环境与 Nginx 配置备份目录 |
|
| `PASCAL_BACKUP_DIR` | 环境与 Nginx 配置备份目录 |
|
||||||
|
|
||||||
|
`PASCAL_PUBLIC_ORIGIN` 会同时写入 Pascal 的场景 API 允许来源,用于正确
|
||||||
|
识别 Nginx 后的 HTTPS 同域请求,不应配置为 `*`。
|
||||||
|
|
||||||
## 运行维护
|
## 运行维护
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -231,6 +231,9 @@ write_pascal_env() {
|
|||||||
printf 'NEXT_PUBLIC_PASCAL_BASE_PATH=%s\n' "$PASCAL_BASE_PATH"
|
printf 'NEXT_PUBLIC_PASCAL_BASE_PATH=%s\n' "$PASCAL_BASE_PATH"
|
||||||
printf 'PASCAL_REQUIRE_HOST_USER=true\n'
|
printf 'PASCAL_REQUIRE_HOST_USER=true\n'
|
||||||
printf 'PASCAL_DATA_DIR=%s\n' "$PASCAL_DATA_DIR"
|
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_SCENE_API_TOKEN=%s\n' "$secret"
|
||||||
printf 'PASCAL_AGENT_PROXY_SECRET=%s\n' "$secret"
|
printf 'PASCAL_AGENT_PROXY_SECRET=%s\n' "$secret"
|
||||||
printf 'PASCAL_AGENT_API_KEY=%s\n' "$secret"
|
printf 'PASCAL_AGENT_API_KEY=%s\n' "$secret"
|
||||||
|
|||||||
@ -47,6 +47,38 @@ test('accepts bearer token auth when configured', () => {
|
|||||||
expect(guardSceneApiRequest(request)).toBeNull()
|
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', () => {
|
test('applies configured CORS origins for preflight', () => {
|
||||||
process.env.PASCAL_SCENE_API_ORIGINS = 'https://app.example'
|
process.env.PASCAL_SCENE_API_ORIGINS = 'https://app.example'
|
||||||
const request = new Request('https://editor.example/api/scenes', {
|
const request = new Request('https://editor.example/api/scenes', {
|
||||||
|
|||||||
@ -145,10 +145,20 @@ function configuredOrigins(): Set<string> {
|
|||||||
function isSameOrigin(request: Request, origin: string): boolean {
|
function isSameOrigin(request: Request, origin: string): boolean {
|
||||||
const parsedOrigin = parseUrl(origin)
|
const parsedOrigin = parseUrl(origin)
|
||||||
if (!parsedOrigin) return false
|
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)
|
return normalizeOrigin(parsedOrigin) === normalizeOrigin(requestUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function firstForwardedValue(value: string | null): string | null {
|
||||||
|
return value?.split(',')[0]?.trim().toLowerCase() || null
|
||||||
|
}
|
||||||
|
|
||||||
function isLoopbackRequest(request: Request): boolean {
|
function isLoopbackRequest(request: Request): boolean {
|
||||||
const host = request.headers.get('host') ?? new URL(request.url).host
|
const host = request.headers.get('host') ?? new URL(request.url).host
|
||||||
return isLoopbackHostname(stripPort(host))
|
return isLoopbackHostname(stripPort(host))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user