把散落在代码里的真实部署值统一收进 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 未被误建)。
145 lines
6.5 KiB
TypeScript
145 lines
6.5 KiB
TypeScript
/**
|
||
* 覆盖网络 **管理面 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 > 本机配置 > 缺省)`
|
||
: '已生效(重启节点服务后按本文件生效)',
|
||
})
|
||
})
|
||
}
|