dsh-webui-oauth 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/README.en.md +1 -0
- package/README.md +1 -0
- package/index.js +124 -18
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录每个版本的**用户可见**变更。破坏性变更与升级动作单独标注。
|
|
4
4
|
|
|
5
|
+
## 0.7.1
|
|
6
|
+
|
|
7
|
+
兼容 dsh **0.2.2-rc.1**:`webServer.fallback` 从函数改成了 `{ resource }`。
|
|
8
|
+
|
|
9
|
+
### 修复:访问 `/` 返回 500 空响应(nginx 日志 `500 0`)
|
|
10
|
+
|
|
11
|
+
- **现象**:升级到 dsh 0.2.2-rc.1 后,访问 `/` 在完成核心 token 交换后返回
|
|
12
|
+
`500`,body 为空;nginx 访问日志表现为 `500 0`。
|
|
13
|
+
- **原因**:0.2.2-rc.1 起 `webServer.fallback` 不再一定是函数——
|
|
14
|
+
`dsh-host-frontend-static` 注册的是 `{ resource }`(Fetch 风格
|
|
15
|
+
`resource(request, peer) -> Response`)。插件仍按 `await fallback(req, res)`
|
|
16
|
+
调用,抛 `TypeError: fallback is not a function`,被外层 `catch` 兜成
|
|
17
|
+
`500` + 空 body。
|
|
18
|
+
- **修复**:新增 `serveFallback()` 统一分派三种形状:
|
|
19
|
+
1. 函数(旧核心);
|
|
20
|
+
2. `{ resource }` + `webServer.serveResource`(0.2.2-rc.1+);
|
|
21
|
+
3. `{ handler }`(部分中间版本)。
|
|
22
|
+
没有 fallback 时返回 `false`,由调用方 404——不再把"形状不匹配"误报成 500。
|
|
23
|
+
|
|
24
|
+
### 安全:资源型 fallback 也必须上闸门
|
|
25
|
+
|
|
26
|
+
此前 `wrapExistingFallback` 只处理 `typeof === 'function'`,遇到 `{ resource }`
|
|
27
|
+
直接跳过,等于把兜底入口留给匿名请求。现在:
|
|
28
|
+
|
|
29
|
+
- `resource(request, peer)` 会被包装:从 Fetch Request 的 `cookie` 头取会话,
|
|
30
|
+
未登录返回 `401 Response`,已登录原样放行;
|
|
31
|
+
- `registerFallback` 注册时的三种形状都会在注册那一刻上闸门;
|
|
32
|
+
- `undo` 会把 `resource` / `handler` 原样还原。
|
|
33
|
+
|
|
34
|
+
### 测试
|
|
35
|
+
|
|
36
|
+
- 新增 `test/fallback-shape.test.mjs`(19 项):三种形状分派、无 fallback /
|
|
37
|
+
未知形状 / 缺 `serveResource` 均不抛异常、资源型 fallback 的匿名拒绝与
|
|
38
|
+
登录放行、`undo` 还原。
|
|
39
|
+
- 全量 18 个测试文件、488 项断言通过。
|
|
40
|
+
|
|
5
41
|
## 0.7.0
|
|
6
42
|
|
|
7
43
|
新增**「仅允许 SSO」**开关;同时修复一个会把 OIDC 绑定悄悄弄丢的老问题。
|
package/README.en.md
CHANGED
|
@@ -401,6 +401,7 @@ wired into `npm test`).
|
|
|
401
401
|
|
|
402
402
|
## Known limits
|
|
403
403
|
|
|
404
|
+
- **Fallback shape on dsh 0.2.2-rc.1**: since that version `webServer.fallback` may be `{ resource }` (that is how `dsh-host-frontend-static` registers it) instead of a function. The plugin now handles function / `{ resource }` / `{ handler }`; a resource fallback is gated as well. If `/` returns an empty `500` after upgrading (nginx logs `500 0`), make sure the plugin is ≥ 0.7.1.
|
|
404
405
|
- **Inherent runtime-wrapping window**: between a route-object replacement (service hot-reload) and the next rescan (≤10s) there is an unprotected window; the fail-closed check on enabling covers the "initially exposed" case, so this window only affects hot-reload during runtime.
|
|
405
406
|
- **WebSocket and `trustedHosts`**: in reverse-proxy / LAN deployments (non-loopback Host), WS downlinks need the public hostname added to `client-connection.trustedHosts` in the DSH config (see "Architecture").
|
|
406
407
|
- **Proxy on a different host**: if the reverse proxy is not on the same machine as DSH (non-loopback peer), the proxy headers are not trusted and rate limiting aggregates per proxy IP (degrades to a global bucket).
|
package/README.md
CHANGED
|
@@ -411,6 +411,7 @@ IdP 侧日志确认完整往返:`discovery → jwks → authorize(issued code)
|
|
|
411
411
|
|
|
412
412
|
## 已知边界
|
|
413
413
|
|
|
414
|
+
- **dsh 0.2.2-rc.1 的 fallback 形状**:该版本起 `webServer.fallback` 可能是 `{ resource }`(`dsh-host-frontend-static` 就是这样注册的),而不再是函数。插件已兼容函数 / `{ resource }` / `{ handler }` 三种形状;资源型 fallback 同样会被闸门包装。若升级后 `/` 返回 `500` 且 body 为空(nginx 日志 `500 0`),请确认插件版本 ≥ 0.7.1。
|
|
414
415
|
- **运行时包装的固有窗口**:路由对象被替换(服务热重载)到下一次重扫之间(≤10s)存在未保护窗口;启用认证时的 fail-closed 已挡住「初始裸奔」,此窗口仅影响运行中的热重载场景。
|
|
415
416
|
- **WS 与 trustedHosts**:反代/局域网(非回环 Host)下,WS 下行需在 dsh 配置 `client-connection.trustedHosts` 中加入对外域名(见「架构」节)。
|
|
416
417
|
- **反代不同机**:若反代与 DSH 不在同一台机器(对端非回环),代理头不被信任,限流将按代理 IP 聚合(退化为全局桶)。
|
package/index.js
CHANGED
|
@@ -1154,6 +1154,41 @@ function wantsHtml(req) {
|
|
|
1154
1154
|
}
|
|
1155
1155
|
}
|
|
1156
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* 把请求交给 webServer 的 fallback。
|
|
1159
|
+
*
|
|
1160
|
+
* 0.2.2-rc.1 起 fallback 不再一定是函数:dsh-host-frontend-static 注册的是
|
|
1161
|
+
* `{ resource }`(Fetch 风格的 `resource(request, peer) -> Response`)。
|
|
1162
|
+
* 旧代码直接 `await fallback(req, res)` 会抛 TypeError(fallback is not a function),
|
|
1163
|
+
* 被外层 catch 兜成 `500` + 空 body——nginx 访问日志里就是 `500 0`。
|
|
1164
|
+
*
|
|
1165
|
+
* 这里统一三种形状:
|
|
1166
|
+
* 1. 函数(旧核心 / 第三方 handler);
|
|
1167
|
+
* 2. `{ resource }` + webServer.serveResource(0.2.2-rc.1+);
|
|
1168
|
+
* 3. `{ handler }`(部分中间版本)。
|
|
1169
|
+
*
|
|
1170
|
+
* @returns {Promise<boolean>} 是否已由 fallback 处理;false 表示没有 fallback,
|
|
1171
|
+
* 调用方应自行返回 404(不要把"没有 fallback"误报成 500)。
|
|
1172
|
+
*/
|
|
1173
|
+
export async function serveFallback(ctx, req, res) {
|
|
1174
|
+
const ws = ctx && ctx.webServer
|
|
1175
|
+
if (!ws) return false
|
|
1176
|
+
const fallback = ws.fallback
|
|
1177
|
+
if (typeof fallback === 'function') {
|
|
1178
|
+
await fallback(req, res)
|
|
1179
|
+
return true
|
|
1180
|
+
}
|
|
1181
|
+
if (fallback && typeof fallback.resource === 'function' && typeof ws.serveResource === 'function') {
|
|
1182
|
+
await ws.serveResource(req, res, new URL(req.url || '/', 'http://x'), fallback.resource)
|
|
1183
|
+
return true
|
|
1184
|
+
}
|
|
1185
|
+
if (fallback && typeof fallback.handler === 'function') {
|
|
1186
|
+
await fallback.handler(req, res)
|
|
1187
|
+
return true
|
|
1188
|
+
}
|
|
1189
|
+
return false
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1157
1192
|
function rejectUpgrade401(socket) {
|
|
1158
1193
|
try {
|
|
1159
1194
|
socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 13\r\n\r\nunauthorized\n')
|
|
@@ -1373,7 +1408,12 @@ function installRouteGate(ctx, checkRequest, log, options) {
|
|
|
1373
1408
|
// ---------------- fallback 也要包 ----------------
|
|
1374
1409
|
// fallback 是"未被任何具名路由认领"的兜底处理器。它同样可能被第三方插件占据,
|
|
1375
1410
|
// 若不管,攻击者只要找到一个不存在的路径就可能落到未鉴权的 fallback 上。
|
|
1376
|
-
|
|
1411
|
+
//
|
|
1412
|
+
// 0.2.2-rc.1 起 fallback 还可能是 { resource }(Fetch 风格):既不能当函数调用
|
|
1413
|
+
// (TypeError),也不能放任它绕过闸门。两种形状分别包装。
|
|
1414
|
+
const wrappedFallbacks = new WeakSet() // 原始 handler 函数
|
|
1415
|
+
const wrappedResources = new WeakSet() // 原始 resource 函数
|
|
1416
|
+
|
|
1377
1417
|
const gateFallback = (handler) => {
|
|
1378
1418
|
if (typeof handler !== 'function' || wrappedFallbacks.has(handler)) return handler
|
|
1379
1419
|
const original = handler
|
|
@@ -1385,23 +1425,71 @@ function installRouteGate(ctx, checkRequest, log, options) {
|
|
|
1385
1425
|
}
|
|
1386
1426
|
return original(req, res)
|
|
1387
1427
|
}
|
|
1388
|
-
wrappedFallbacks.add(
|
|
1428
|
+
wrappedFallbacks.add(original)
|
|
1389
1429
|
fallbackWrapped = true
|
|
1390
1430
|
log('wrapped fallback handler')
|
|
1391
1431
|
return gated
|
|
1392
1432
|
}
|
|
1393
1433
|
|
|
1394
|
-
|
|
1434
|
+
const gateResource = (resource) => {
|
|
1435
|
+
if (typeof resource !== 'function' || wrappedResources.has(resource)) return resource
|
|
1436
|
+
const original = resource
|
|
1437
|
+
const gated = async (request, peer) => {
|
|
1438
|
+
// resource 收到的是 Fetch Request;checkRequest 只依赖 cookie 头。
|
|
1439
|
+
const cookie = request && request.headers && typeof request.headers.get === 'function'
|
|
1440
|
+
? (request.headers.get('cookie') || '')
|
|
1441
|
+
: ''
|
|
1442
|
+
const headers = { cookie }
|
|
1443
|
+
if (!checkRequest({ method: request && request.method, url: request && request.url, headers })) {
|
|
1444
|
+
return new Response('unauthorized', {
|
|
1445
|
+
status: 401,
|
|
1446
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
1447
|
+
})
|
|
1448
|
+
}
|
|
1449
|
+
return original(request, peer)
|
|
1450
|
+
}
|
|
1451
|
+
wrappedResources.add(original)
|
|
1452
|
+
fallbackWrapped = true
|
|
1453
|
+
log('wrapped fallback resource')
|
|
1454
|
+
return gated
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// 已经就位的 fallback:按形状替换(保留原值以便 undo)。
|
|
1395
1458
|
const wrapExistingFallback = () => {
|
|
1396
1459
|
const current = ws.fallback
|
|
1397
|
-
if (typeof current
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1460
|
+
if (typeof current === 'function') {
|
|
1461
|
+
if (wrappedFallbacks.has(current)) return
|
|
1462
|
+
if (current.__dshWuaGated === true) return
|
|
1463
|
+
const gated = gateFallback(current)
|
|
1464
|
+
if (gated === current) return
|
|
1465
|
+
gated.__dshWuaGated = true
|
|
1466
|
+
ws.fallback = gated
|
|
1467
|
+
undos.push(() => { if (ws.fallback === gated) ws.fallback = current })
|
|
1468
|
+
return
|
|
1469
|
+
}
|
|
1470
|
+
if (current && typeof current === 'object') {
|
|
1471
|
+
if (typeof current.resource === 'function') {
|
|
1472
|
+
if (wrappedResources.has(current.resource)) { fallbackWrapped = true; return }
|
|
1473
|
+
if (current.resource.__dshWuaGated === true) return
|
|
1474
|
+
const original = current.resource
|
|
1475
|
+
const gated = gateResource(original)
|
|
1476
|
+
if (gated === original) return
|
|
1477
|
+
gated.__dshWuaGated = true
|
|
1478
|
+
current.resource = gated
|
|
1479
|
+
undos.push(() => { if (current.resource === gated) current.resource = original })
|
|
1480
|
+
return
|
|
1481
|
+
}
|
|
1482
|
+
if (typeof current.handler === 'function') {
|
|
1483
|
+
if (wrappedFallbacks.has(current.handler)) { fallbackWrapped = true; return }
|
|
1484
|
+
if (current.handler.__dshWuaGated === true) return
|
|
1485
|
+
const original = current.handler
|
|
1486
|
+
const gated = gateFallback(original)
|
|
1487
|
+
if (gated === original) return
|
|
1488
|
+
gated.__dshWuaGated = true
|
|
1489
|
+
current.handler = gated
|
|
1490
|
+
undos.push(() => { if (current.handler === gated) current.handler = original })
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1405
1493
|
}
|
|
1406
1494
|
|
|
1407
1495
|
// ---------------- 注册时即拦截(消除重扫窗口) ----------------
|
|
@@ -1432,8 +1520,28 @@ function installRouteGate(ctx, checkRequest, log, options) {
|
|
|
1432
1520
|
// registerFallback 走的是属性赋值而非 Map,单独接管。
|
|
1433
1521
|
if (typeof ws.registerFallback === 'function') {
|
|
1434
1522
|
const originalRegisterFallback = ws.registerFallback
|
|
1435
|
-
ws.registerFallback = function patchedRegisterFallback (
|
|
1436
|
-
|
|
1523
|
+
ws.registerFallback = function patchedRegisterFallback (fallback) {
|
|
1524
|
+
// 三种形状都要上闸门:函数 / { resource } / { handler }。
|
|
1525
|
+
try {
|
|
1526
|
+
if (typeof fallback === 'function') {
|
|
1527
|
+
fallback = gateFallback(fallback)
|
|
1528
|
+
} else if (fallback && typeof fallback === 'object') {
|
|
1529
|
+
if (typeof fallback.resource === 'function' && !wrappedResources.has(fallback.resource)) {
|
|
1530
|
+
const gated = gateResource(fallback.resource)
|
|
1531
|
+
if (gated !== fallback.resource) {
|
|
1532
|
+
gated.__dshWuaGated = true
|
|
1533
|
+
fallback.resource = gated
|
|
1534
|
+
}
|
|
1535
|
+
} else if (typeof fallback.handler === 'function' && !wrappedFallbacks.has(fallback.handler)) {
|
|
1536
|
+
const gated = gateFallback(fallback.handler)
|
|
1537
|
+
if (gated !== fallback.handler) {
|
|
1538
|
+
gated.__dshWuaGated = true
|
|
1539
|
+
fallback.handler = gated
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
} catch (e) { /* 包装失败不阻断注册,靠重扫兜底 */ }
|
|
1544
|
+
return originalRegisterFallback.call(this, fallback)
|
|
1437
1545
|
}
|
|
1438
1546
|
undos.push(() => { ws.registerFallback = originalRegisterFallback })
|
|
1439
1547
|
}
|
|
@@ -3399,11 +3507,9 @@ export async function apply(ctx) {
|
|
|
3399
3507
|
return
|
|
3400
3508
|
}
|
|
3401
3509
|
}
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
return
|
|
3406
|
-
}
|
|
3510
|
+
// fallback 可能是函数,也可能是 { resource }(0.2.2-rc.1+)。
|
|
3511
|
+
// 交给 serveFallback 统一分派,避免 TypeError → 500 空响应。
|
|
3512
|
+
if (await serveFallback(ctx, req, res)) return
|
|
3407
3513
|
res.writeHead(404)
|
|
3408
3514
|
res.end()
|
|
3409
3515
|
return
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-webui-oauth",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node test/redirect-scheme.test.mjs && node test/proxy-host.test.mjs && node test/trusted-origin.test.mjs && node test/oidc.test.mjs && node test/oidc-state-ttl.test.mjs && node test/set-cookie-multi.test.mjs && node test/oidc-integration.test.mjs && node test/oidc-config-store.test.mjs && node test/adopt-data-dir.test.mjs && node test/oidc-ec.test.mjs && node test/jwks-refresh.test.mjs && node test/oidc-bind.test.mjs && node test/client-bundle-id.test.mjs && node test/client-prefill.test.mjs && node test/displace-original.test.mjs && node test/oidc-redirect-matrix.test.mjs && node test/gate-default-deny.test.mjs"
|
|
7
|
+
"test": "node test/redirect-scheme.test.mjs && node test/proxy-host.test.mjs && node test/trusted-origin.test.mjs && node test/oidc.test.mjs && node test/oidc-state-ttl.test.mjs && node test/set-cookie-multi.test.mjs && node test/oidc-integration.test.mjs && node test/oidc-config-store.test.mjs && node test/adopt-data-dir.test.mjs && node test/oidc-ec.test.mjs && node test/jwks-refresh.test.mjs && node test/oidc-bind.test.mjs && node test/client-bundle-id.test.mjs && node test/client-prefill.test.mjs && node test/displace-original.test.mjs && node test/oidc-redirect-matrix.test.mjs && node test/gate-default-deny.test.mjs && node test/fallback-shape.test.mjs"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": "./index.js",
|