Files
dsh_shenxian/src/web/routes/overlay-nodes.ts
T
admin 452924d89c feat(config): 涉密内容外置到配置目录(档案 140)
把散落在代码里的真实部署值统一收进 config/,代码改为引用配置,
使仓库副本/开源导出不再带出生产域名、IP、内网路径与凭据。

新增 config/:platform.env.example(模板)· load.sh(shell 加载器)·
index.cjs(node 加载器)· README.md(键一览与优先级)。
真实值放 config/platform.env —— 已 .gitignore 排除,不入库、不进导出。

TS 侧新增 src/platform-paths.ts 作部署路径的唯一解析处(零副作用):
platformDir/stateDir/backupDir/artifactDir/installDir/scriptPath。
config.ts 接入这些字段;内置中继种子由生产 URL 改为空(改由
DSHS_OVERLAY_BOOTSTRAP_SEEDS 提供)。修掉 5 处硬编码绝对路径,
src/** 注释中性化 116 行/53 文件。

scripts/** 36 个内部运维脚本:真令牌/PG 口令/隧道目标/主机号/路径
一律改从配置取;web/wake.html 的注册域白名单改为运行时从
location.hostname 推导;test/** 夹具 119 行/13 文件改 RFC 2606/5737
保留值,并把「内置种子必须为空」固化为回归断言。

取证:tsc 0 错;npm test 373/375(唯一失败 lease 属既有);
全仓扫描(大小写不敏感)代码面涉密标识 = 0;已部署 47 并零回归
(/opt/dsh/* 未搬家,/var/lib/dshs/platform 未被误建)。
2026-09-19 15:12:19 +08:00

145 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 覆盖网络 **管理面 API**(序㊵ · P2/S5 一并做 —— 该文件是 P1 的出口项)。
*
* ## 它回答两个问题
* | 端点 | 作用 | 鉴权 |
* |---|---|---|
* | `GET /api/admin/overlay-nodes` | **有哪些网 / 每个节点什么状态**(只读汇总) | `requireAdmin` |
* | `GET /api/admin/overlay-nodes/direct` | 本机**直连开关**现状 + **三段提示** | `requireAdmin` |
* | `POST /api/admin/overlay-nodes/direct` | **用户可设置**(口径①)—— 只改本机配置的 `direct` 字段 | `requireAdmin` |
*
* ## 🔴 三条纪律
* ① **全部走 `requireAdmin`** —— ⛔ 不做第二个无鉴权端点:既有的 `GET /dshs-overlay/bootstrap`
* 之所以能无鉴权,是因为它**只服务还没有凭据的新节点**且内容被收窄到"去哪儿";
* "**有哪些节点**"是**拓扑信息**,放公网 = 扩大暴露面(命中 **R5**)。
* ② **只写"跟着这台机器走"的那一项** —— 直连开关放**本机配置**(`<NODE_CONFIG_FILE>.direct`),
* ⛔ 不写共享 drop-in / env:那属于控制面或运维的通道(见 `direct/index.ts#resolveDirectSwitch`
* 的优先级口径:env > 本机配置 > 缺省)。
* ③ **失败具名** —— 文件不存在 / 形状坏 / 写不进去,各自一个原因码,⛔ 不吞、⛔ 不静默创建。
*
* @module dshs/web/routes/overlay-nodes
*/
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
import type { FastifyPluginAsync } from 'fastify'
import {
DIRECT_ENV_KEY,
DEFAULT_DIRECT_ENABLED,
NODE_CONFIG_FILE_DEFAULT,
directFromNodeConfig,
directHintLines,
resolveDirectSwitch,
writeNodeConfigDirect,
} from '../../net/relay/direct/index.js'
import { loadRegistry, summarizeNetworks, listNodes } from '../../net/relay/registry.js'
import { requireAdmin } from '../middleware/authn.js'
import { join } from 'node:path'
import { dataRootDir } from '../../platform-paths.js'
/** 本机配置落点(`DSHS_OVERLAY_NODE_CONFIG` 可覆盖;缺省与 `join` 的 `--config` 一致)。 */
function nodeConfigFile(): string {
const v = process.env.DSHS_OVERLAY_NODE_CONFIG
return typeof v === 'string' && v.trim() !== '' ? v.trim() : NODE_CONFIG_FILE_DEFAULT
}
/** 注册表落点(`DSHS_OVERLAY_NODES_FILE` 可覆盖;缺省 = 参数表 `NODES_REGISTRY_FILE`)。 */
function registryFile(): string {
const v = process.env.DSHS_OVERLAY_NODES_FILE
return typeof v === 'string' && v.trim() !== '' ? v.trim() : join(dataRootDir(), 'overlay', 'nodes.json')
}
const fss = {
exists: (p: string) => existsSync(p),
read: (p: string) => readFileSync(p, 'utf8'),
write: (p: string, text: string, mode: number) => writeFileSync(p, text, { mode }),
rename: (from: string, to: string) => renameSync(from, to),
}
/** 读本机配置里的 `direct`(**缺文件 ⇒ `undefined`**,形状坏 ⇒ 抛给调用方具名化)。 */
function readLocalDirect(): { file: string; present: boolean; direct?: boolean } {
const file = nodeConfigFile()
if (!existsSync(file)) return { file, present: false }
const raw: unknown = JSON.parse(readFileSync(file, 'utf8'))
return { file, present: true, direct: directFromNodeConfig(raw) }
}
export const overlayNodeRoutes: FastifyPluginAsync = async (app) => {
/** 只读汇总:有哪些网 / 每张网几个 approved、几个 pending / 节点清单。 */
app.get('/api/admin/overlay-nodes', { preHandler: requireAdmin }, async (_request, reply) => {
const file = registryFile()
if (!existsSync(file)) {
// "这套准入还没开始用"是**合法状态** ⇒ 200 + 明确的 `present:false`(⛔ 不是 500,也不是空 200)
return reply.send({ registry: { file, present: false }, networks: [], nodes: [] })
}
let reg
try {
reg = loadRegistry(file)
} catch (err) {
return reply.code(500).send({ error: 'registry-unreadable', detail: err instanceof Error ? err.message : String(err) })
}
return reply.send({
registry: { file, present: true, version: reg.version },
networks: summarizeNetworks(reg),
nodes: listNodes(reg).map((n) => ({
network: n.network,
hostId: n.hostId,
status: n.status,
group: n.group,
appliedAt: n.appliedAt,
approvedAt: n.approvedAt,
})),
})
})
/** 直连开关现状(含**三段提示** —— 用户口径③的"设置面显示"落点)。 */
app.get('/api/admin/overlay-nodes/direct', { preHandler: requireAdmin }, async (_request, reply) => {
let local: { file: string; present: boolean; direct?: boolean }
try {
local = readLocalDirect()
} catch (err) {
return reply.code(500).send({ error: 'node-config-bad', detail: err instanceof Error ? err.message : String(err) })
}
const state = resolveDirectSwitch(process.env, { localDirect: local.direct })
return reply.send({
envKey: DIRECT_ENV_KEY,
defaultEnabled: DEFAULT_DIRECT_ENABLED,
effective: state.enabled,
source: state.source,
raw: state.raw,
invalid: state.invalid,
nodeConfig: { file: local.file, present: local.present, direct: local.direct ?? null },
hint: directHintLines(),
})
})
/**
* 设置直连开关(**用户可设置**)。
*
* ⛔ 只动本机配置的 `direct`;⚠️ 若 env 里显式设了 `DSHS_OVERLAY_DIRECT`,它会**压过**本项
* ⇒ 回执里把这件事**明说**(否则用户会以为"改了没生效 = 平台坏了")。
*/
app.post('/api/admin/overlay-nodes/direct', { preHandler: requireAdmin }, async (request, reply) => {
const body = request.body as { enabled?: unknown } | undefined
if (body === undefined || typeof body.enabled !== 'boolean') {
return reply.code(400).send({ error: 'bad-body', detail: 'body 须为 {enabled: boolean}' })
}
const outcome = writeNodeConfigDirect(nodeConfigFile(), body.enabled, fss)
if (!outcome.ok) return reply.code(400).send({ error: outcome.reason, detail: outcome.detail })
const state = resolveDirectSwitch(process.env, { localDirect: outcome.direct })
return reply.send({
ok: true,
file: outcome.file,
direct: outcome.direct,
effective: state.enabled,
source: state.source,
envOverrides: state.source === 'env',
note:
state.source === 'env'
? `⚠️ 本机 env 里已显式设了 ${DIRECT_ENV_KEY}=${state.raw} ⇒ 它**压过**本次设置(env > 本机配置 > 缺省)`
: '已生效(重启节点服务后按本文件生效)',
})
})
}