// 会员系统服务器(零依赖纯 node http) // · 端口: HY_PORT → MEMBER_PORT → TRIM_SERVICE_PORT → 12701; // 绑定 MEMBER_HOST 或 0.0.0.0(供局域网内成套报价软件访问) // · 数据库存 huiyuan.json,目录解析(逐级回退,目录不可写自动降级): // HY_DB_DIR(向导/网页设置指定) → TRIM_DATA_SHARE_PATHS 第一项 → TRIM_PKGVAR/files → 本目录(本地开发) // · 项目云数据落 DB_DIR/项目云/<手机号安全化>/<编号安全化>/{项目.zip,meta.json} // · 配置持久化: TRIM_PKGETC/app.env(KEY=VALUE),与 cmd 脚本共用,存 HY_PORT/HY_DB_DIR // · 客户端 API(与原版完全兼容): // POST /api/login {手机号,密码} → {ok,会员类型,有效期} | {ok:false,msg} // POST /api/register {手机号,密码} → {ok} 注册为"试用会员",有效期=今天+30天 // GET /api/member?手机号=xxx → {ok,手机号,会员类型,有效期} // POST /api/changepwd {手机号,旧密码,新密码} → {ok} // GET /api/list → {ok,会员:[...]}(管理页用) // POST /api/save {会员:[...]} → {ok}(管理页整表保存) // · 项目云 API(2026-09-16): // POST /api/proj/upload body=JSON头 + \n\n + zip二进制 → {ok,编号,大小,时间}(覆盖式,上限 2GB) // GET /api/proj/list?手机号= → {ok,项目:[{编号,名称,时间,大小}]} // GET /api/proj/download?手机号=&编号= → zip 二进制流 // POST /api/proj/delete {手机号,编号} → {ok} // 上传前校验会员已注册(huiyuan.json);手机号/编号文件名安全化+防路径穿越; // 元数据存各项目目录 meta.json(项目名/上传时间/大小)。 // · 设置 API(网页设置页用): // GET /api/settings → 端口/数据目录/数据库与项目云路径/默认目录/已授权目录/会员数 // POST /api/settings {dbdir} → 校验可写→迁移 huiyuan.json+项目云→回写 app.env // 已授权目录经 Unix Socket 调开放 API trim.file.getSharedAccessibleFolders(失败优雅降级) // · 安全(v1.4.0 双层加密通讯): // 第1层 TLS:HTTPS 端口(自动=网页端口+1,可用 HY_API_PORT 覆盖)供客户端, // 自签证书首次启动由 openssl 自动生成(存配置目录,升级保留/重装重生成) // 第2层 信封加密(/api/enc,仅 HTTPS):客户端随机 AES-256 密钥加密业务 JSON, // AES 密钥用 RSA-2048 公钥封装(OAEP),服务端私钥永不出服务器; // HMAC-SHA256 验签防篡改;时间戳±5分钟+随机数缓存防重放 // 登录/注册/查会员/改密/项目云列表与删除走双层;项目 zip 上传下载走 TLS 单层(大文件) // 客户端接口已从 HTTP 端口撤下(返回 TLS_REQUIRED 提示);HTTP 端口仅剩网页管理(管理密码保护) // 管理接口(/api/list /api/save /api/settings /api/admin/changepwd)需管理密码登录: // POST /api/admin/login 换 token,后续请求带 X-Admin-Token 头(24h 过期;自定义头同时天然防 CSRF) // 登录/注册/管理登录失败按 IP 限速(1 分钟内失败≥10 次锁 60 秒) // 注册校验手机号 11 位/密码 4-64 位,HY_REG_OPEN=0 可关闭开放注册(设置页开关) // 上传配额每会员 1GB;整表保存与目录迁移前滚动备份 huiyuan.json.bak // · 页面: / 或 /admin.html(会员管理) /xiangmu-cloud.html(项目云) /lib-cloud.html(库备份) /settings.html(设置) var http = require('http'); var fs = require('fs'); var path = require('path'); var crypto = require('crypto'); var execFileSync = require('child_process').execFileSync; var VERSION = '1.5.1'; var APPNAME = 'fnnas.huiyuan'; var HOST = process.env.MEMBER_HOST || '0.0.0.0'; var DB_NAME = 'huiyuan.json'; var PROJ_DIR_NAME = '项目云'; var UPLOAD_MAX = 2 * 1024 * 1024 * 1024; // 单次上传上限 2GB,防磁盘打满 var USER_QUOTA = 1024 * 1024 * 1024; // 每会员项目云总配额 1GB // ---- 开放 API(应用共享授权):仅后端可通过 Unix Socket 调用 ---- var API_SOCKET = '/var/run/trim_open_gateway_apiscope.socket'; var API_PATH = '/api/v1/trimapp'; // ---- 端口 ---- var PORT = parseInt(process.env.HY_PORT || process.env.MEMBER_PORT || '', 10) || parseInt(process.env.TRIM_SERVICE_PORT || '', 10) || 12701; var WWW = path.join(__dirname, '..', 'www'); // ---- 配置文件(TRIM_PKGETC/app.env,cmd 脚本与本服务共用) ---- var CFG_FILE = process.env.HY_CFG || (process.env.TRIM_PKGETC ? path.join(process.env.TRIM_PKGETC, 'app.env') : null); function 读配置() { var cfg = {}; if (!CFG_FILE) return cfg; try { fs.readFileSync(CFG_FILE, 'utf8').split(/\r?\n/).forEach(function (line) { var m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/); if (m) cfg[m[1]] = m[2]; }); } catch (e) { /* 配置不存在用默认值 */ } return cfg; } function 写配置(cfg) { if (!CFG_FILE) return; // 本地开发无配置文件,仅内存生效 var tmp = CFG_FILE + '.tmp'; fs.writeFileSync(tmp, Object.keys(cfg).map(function (k) { return k + '=' + (cfg[k] || ''); }).join('\n') + '\n', 'utf8'); fs.renameSync(tmp, CFG_FILE); } // ---- 管理密码鉴权(sha256(盐+密码) 存 app.env;token 在内存,重启失效) ---- function sha256hex(s) { return crypto.createHash('sha256').update(s, 'utf8').digest('hex'); } function 管理密码已设置() { var cfg = 读配置(); return !!(cfg.ADMIN_PWD_SALT && cfg.ADMIN_PWD_SHA256); } function 管理密码正确(密码) { var cfg = 读配置(); if (!cfg.ADMIN_PWD_SALT || !cfg.ADMIN_PWD_SHA256) return false; var got = sha256hex(cfg.ADMIN_PWD_SALT + String(密码 || '')); var want = String(cfg.ADMIN_PWD_SHA256).toLowerCase(); if (got.length !== want.length) return false; try { return crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want)); } catch (e) { return false; } } function 设置管理密码(新密码) { var salt = crypto.randomBytes(8).toString('hex'); var cfg = 读配置(); cfg.ADMIN_PWD_SALT = salt; cfg.ADMIN_PWD_SHA256 = sha256hex(salt + 新密码); 写配置(cfg); } var TOKENS = new Map(); // token → 过期时间戳 var TOKEN_TTL = 24 * 3600 * 1000; // 24 小时 function 新token() { var t = crypto.randomBytes(24).toString('hex'); TOKENS.set(t, Date.now() + TOKEN_TTL); return t; } function token有效(t) { if (!t || !TOKENS.has(t)) return false; if (Date.now() > TOKENS.get(t)) { TOKENS.delete(t); return false; } return true; } function 是管理员(req) { return token有效(req.headers['x-admin-token']); } setInterval(function () { var now = Date.now(); TOKENS.forEach(function (exp, t) { if (now > exp) TOKENS.delete(t); }); 清理NONCES(); }, 3600 * 1000).unref(); // ---- 限速:按 key 记失败,1 分钟内失败≥10 次锁 60 秒(内存,重启清零) ---- var FAILS = new Map(); // key → {n, window, lock} function 被锁秒(key) { var f = FAILS.get(key); return f && f.lock && Date.now() < f.lock ? Math.ceil((f.lock - Date.now()) / 1000) : 0; } function 失败记录(key) { var now = Date.now(); var f = FAILS.get(key); if (!f || now > f.window) f = { n: 0, window: now + 60000, lock: 0 }; f.n++; if (f.n >= 10) f.lock = now + 60000; FAILS.set(key, f); } function 失败清零(key) { FAILS.delete(key); } // ---- 配置目录(TLS 证书/RSA 密钥存放处) ---- var CFG_DIR = CFG_FILE ? path.dirname(CFG_FILE) : __dirname; // ---- RSA-2048 密钥对(信封加密;私钥永不出服务器,首次启动生成并持久化) ---- var RSA_PRIV_FILE = path.join(CFG_DIR, 'huiyuan_rsa.pem'); var RSA_KEY = null; try { if (!fs.existsSync(RSA_PRIV_FILE)) { var kp = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); fs.writeFileSync(RSA_PRIV_FILE, kp.privateKey, { mode: 0o600 }); } RSA_KEY = crypto.createPrivateKey(fs.readFileSync(RSA_PRIV_FILE)); } catch (e) { console.warn('RSA 密钥初始化失败(加密通道不可用): ' + e.message); } function 公钥PEM() { return crypto.createPublicKey(RSA_KEY).export({ type: 'spki', format: 'pem' }).toString(); } function 公钥指纹() { // SHA256 前 16 位 hex,供管理员核验公钥身份 var der = crypto.createPublicKey(RSA_KEY).export({ type: 'spki', format: 'der' }); return crypto.createHash('sha256').update(der).digest('hex').slice(0, 16).toUpperCase(); } // ---- 信封加密(应用层第2层):RSA-OAEP 封装 AES-256 密钥 + AES-CBC 加密 + HMAC 验签 + 防重放 ---- var NONCES = new Map(); // nonce → 过期时间戳(防重放) function 清理NONCES() { var now = Date.now(); NONCES.forEach(function (exp, n) { if (now > exp) NONCES.delete(n); }); } function 解密信封(payload, ip) { try { if (!RSA_KEY) return { err: '服务端加密未初始化' }; if (!payload || !payload.k || !payload.iv || !payload.data || !payload.mac || !payload.ts || !payload.nonce) return { err: '信封字段缺失' }; var aesKey = crypto.privateDecrypt( { key: RSA_KEY, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha1' }, Buffer.from(payload.k, 'base64')); if (aesKey.length !== 32) return { err: '会话密钥长度非法' }; var ts = Number(payload.ts); if (!ts || Math.abs(Date.now() - ts) > 300000) return { err: '时间戳超出允许窗口(±5分钟)' }; if (NONCES.has(String(payload.nonce))) return { err: '重复的请求(疑似抓包重放),已拒绝' }; var want = crypto.createHmac('sha256', aesKey) .update(payload.ts + '.' + payload.nonce + '.' + payload.iv + '.' + payload.data).digest('hex'); if (want !== String(payload.mac)) return { err: '签名校验失败(数据可能被篡改)' }; var d = crypto.createDecipheriv('aes-256-cbc', aesKey, Buffer.from(payload.iv, 'hex')); var inner = JSON.parse(Buffer.concat([d.update(Buffer.from(payload.data, 'base64')), d.final()]).toString('utf8')); return { aesKey: aesKey, inner: inner, nonce: String(payload.nonce) }; } catch (e) { return { err: '信封解密失败: ' + e.message }; } } function 加密响应(aesKey, obj) { var ivb = crypto.randomBytes(16); var c = crypto.createCipheriv('aes-256-cbc', aesKey, ivb); var dataB = Buffer.concat([c.update(Buffer.from(JSON.stringify(obj), 'utf8')), c.final()]).toString('base64'); var iv = ivb.toString('hex'); var mac = crypto.createHmac('sha256', aesKey).update(iv + '.' + dataB).digest('hex'); return { ok: true, iv: iv, mac: mac, data: dataB }; } // 信封内允许的业务接口(凭据类小数据;zip 上传下载走 TLS 单层) var ENC_ROUTES = { '/api/login': function (b, req, ip) { return 登录(b || {}, ip); }, '/api/register': function (b, req, ip) { return 注册(b || {}, ip); }, '/api/member': function (b) { return 查会员(b || {}); }, '/api/changepwd': function (b) { return 改密(b || {}); }, '/api/proj/list': function (b, req) { if (!项目云鉴权(b['手机号'], b['密码'], req)) return { ok: false, msg: '手机号或密码错误' }; return 列项目(b['手机号']); }, '/api/proj/delete': function (b, req) { if (!项目云鉴权(b['手机号'], b['密码'], req)) return { ok: false, msg: '手机号或密码错误' }; var d3 = b['手机号'] && b['编号'] ? 项目目录(b['手机号'], b['编号']) : null; if (!d3) return { ok: false, msg: '参数错误' }; try { fs.rmSync(d3, { recursive: true, force: true }); return { ok: true }; } catch (e2) { return { ok: false, msg: '删除失败: ' + e2.message }; } } }; // ---- 数据库目录解析 ---- function 目录不可写原因(dir) { try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); var t = path.join(dir, '.huiyuan-write-test'); fs.writeFileSync(t, '1'); fs.unlinkSync(t); return null; // 可写 } catch (e) { return (e.code ? e.code + ' ' : '') + e.message; } } function 目录可写(dir) { return 目录不可写原因(dir) === null; } function 默认目录() { var share = (process.env.TRIM_DATA_SHARE_PATHS || '').split(':')[0]; if (share) return share; // 应用数据目录(文件管理→应用文件) if (process.env.TRIM_PKGVAR) return path.join(process.env.TRIM_PKGVAR, 'files'); return __dirname; // 本地开发回退 } var DEFAULT_DIR = 默认目录(); var DB_DIR; (function () { var cands = []; var cfgDbDir = 读配置().HY_DB_DIR || process.env.HY_DB_DIR || ''; if (cfgDbDir) cands.push(cfgDbDir); cands.push(DEFAULT_DIR); if (process.env.TRIM_PKGVAR) cands.push(path.join(process.env.TRIM_PKGVAR, 'files')); cands.push(__dirname); for (var i = 0; i < cands.length; i++) { if (目录可写(cands[i])) { DB_DIR = cands[i]; return; } } DB_DIR = __dirname; })(); var DB_FILE = path.join(DB_DIR, DB_NAME); // 启动时确保数据库文件存在:缺失则落一个空会员库(安装包不带任何会员数据) if (!fs.existsSync(DB_FILE)) { try { 写库({ '会员': [] }); console.log('数据库不存在,已创建空会员库: ' + DB_FILE); } catch (e) { console.warn('空数据库创建失败(首次保存时再写入): ' + e.message); } } // ---- 会员密码哈希(v1.3.0):存储格式 盐(32hex)$哈希(64hex),每会员独立随机盐 ---- // 协议不变:客户端仍传明文,服务端哈希后比对/存储 var 哈希格式 = /^[0-9a-f]{32}\$[0-9a-f]{64}$/; function 哈希密码(明文) { var salt = crypto.randomBytes(16).toString('hex'); return salt + '$' + sha256hex(salt + String(明文)); } function 校验密码(存储值, 输入) { var stored = String(存储值 || ''); var input = String(输入 === undefined || 输入 === null ? '' : 输入); if (!stored) return false; if (哈希格式.test(stored)) { var i = stored.indexOf('$'); return sha256hex(stored.slice(0, i) + input) === stored.slice(i + 1); } return stored === input; // 迁移前的明文兜底 } // 启动时把明文密码一次性升级为哈希(迁移前备份,成功后用哈希版刷新备份,磁盘不残留明文) (function 升级哈希() { try { var d = 读库(), changed = 0; d['会员'].forEach(function (m) { if (m['密码'] && !哈希格式.test(String(m['密码']))) { m['密码'] = 哈希密码(m['密码']); changed++; } }); if (changed) { 备份库(); // 迁移前备份(此刻 .bak 为明文版) 写库(d); 备份库(); // 用哈希版覆盖备份 console.log('已把 ' + changed + ' 个会员密码升级为哈希存储'); } } catch (e) { console.warn('密码哈希迁移失败(保持原样,下次启动重试): ' + e.message); } })(); // ---- 数据读写(原子写:临时文件+rename) ---- function 读库() { try { var d = JSON.parse(fs.readFileSync(DB_FILE, 'utf8')); if (!Array.isArray(d['会员'])) d['会员'] = []; return d; } catch (e) { return { '会员': [] }; } } function 备份库() { // 整表保存/迁移前滚动备份上一版,防误操作丢数据 try { if (fs.existsSync(DB_FILE)) { // 不能用 copyFileSync 直接覆盖:飞牛 ACL 下已有文件可能是 000 权限,打开即 EACCES; // 与 写库 一致走 tmp+rename(只需目录可写) var tmp = DB_FILE + '.bak.tmp'; fs.writeFileSync(tmp, fs.readFileSync(DB_FILE)); fs.renameSync(tmp, DB_FILE + '.bak'); } } catch (e) { console.warn('备份失败: ' + e.message); } } function 写库(data) { var tmp = DB_FILE + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf8'); fs.renameSync(tmp, DB_FILE); } function 今天加天数(n) { var d = new Date(Date.now() + n * 86400000); return d.getFullYear() + '-' + ('0' + (d.getMonth() + 1)).slice(-2) + '-' + ('0' + d.getDate()).slice(-2); } // ---- 业务 ---- function 登录(b, ip) { var lock = 被锁秒('login:' + ip); if (lock) return { ok: false, msg: '尝试过于频繁,请 ' + lock + ' 秒后再试' }; var db = 读库(); for (var i = 0; i < db['会员'].length; i++) { var m = db['会员'][i]; if (m['手机号'] === b['手机号']) { if (!校验密码(m['密码'], b['密码'])) { 失败记录('login:' + ip); return { ok: false, msg: '密码错误' }; } 失败清零('login:' + ip); return { ok: true, 会员类型: m['会员类型'], 有效期: m['有效期'] }; } } 失败记录('login:' + ip); return { ok: false, msg: '该手机号未注册' }; } function 注册(b, ip) { var lock = 被锁秒('reg:' + ip); if (lock) return { ok: false, msg: '尝试过于频繁,请 ' + lock + ' 秒后再试' }; if ((读配置().HY_REG_OPEN || '1') !== '1') return { ok: false, msg: '注册已关闭,请联系管理员开通' }; if (!/^1\d{10}$/.test(String(b['手机号'] || ''))) return { ok: false, msg: '手机号格式不正确(11 位数字)' }; var p = String(b['密码'] || ''); if (p.length < 4 || p.length > 64) return { ok: false, msg: '密码长度需 4-64 位' }; var db = 读库(); for (var i = 0; i < db['会员'].length; i++) if (db['会员'][i]['手机号'] === b['手机号']) { 失败记录('reg:' + ip); return { ok: false, msg: '该手机号已注册' }; } 失败清零('reg:' + ip); db['会员'].push({ '手机号': b['手机号'], '密码': 哈希密码(p), '会员类型': '试用会员', '有效期': 今天加天数(30), '注册时间': 今天加天数(0), '备注': '' }); 写库(db); return { ok: true, msg: '注册成功' }; } function 查会员(b) { var db = 读库(); for (var i = 0; i < db['会员'].length; i++) { var m = db['会员'][i]; if (m['手机号'] === b['手机号']) return { ok: true, 手机号: m['手机号'], 会员类型: m['会员类型'], 有效期: m['有效期'] }; } return { ok: false, msg: '该手机号未注册' }; } function 改密(b) { var db = 读库(); for (var i = 0; i < db['会员'].length; i++) { var m = db['会员'][i]; if (m['手机号'] === b['手机号']) { if (!校验密码(m['密码'], b['旧密码'])) return { ok: false, msg: '旧密码错误' }; if (!b['新密码']) return { ok: false, msg: '新密码不能为空' }; m['密码'] = 哈希密码(b['新密码']); 写库(db); return { ok: true }; } } return { ok: false, msg: '该手机号未注册' }; } // ---- 项目云(2026-09-16):数据落 DB_DIR/项目云/<手机号安全化>/<编号安全化>/{项目.zip,meta.json} ---- function 项目根() { return path.join(DB_DIR, PROJ_DIR_NAME); } // DB_DIR 可在设置页切换,须动态取 function 安全名(s) { // 文件名安全化:去掉路径分隔/非法字符,限长 80 var t = String(s || '').replace(/[\\\/:*?"<>|]/g, '_').replace(/\.\./g, '_').trim(); if (t.length > 80) t = t.slice(0, 80); return t; } function 会员存在(手机号) { var db = 读库(); for (var i = 0; i < db['会员'].length; i++) if (db['会员'][i]['手机号'] === 手机号) return true; return false; } function 会员密码正确(手机号, 密码) { if (!密码) return false; var db = 读库(); for (var i = 0; i < db['会员'].length; i++) if (db['会员'][i]['手机号'] === 手机号) return 校验密码(db['会员'][i]['密码'], 密码); return false; } // 项目云鉴权:管理 token 通过,或 会员手机号+密码 匹配(客户端需在各接口带上"密码") function 项目云鉴权(手机号, 密码, req) { return 是管理员(req) || 会员密码正确(手机号, 密码); } function 目录大小(dir) { // 递归估算目录总字节,读不了的部分按 0 var total = 0; try { fs.readdirSync(dir).forEach(function (name) { var p = path.join(dir, name); var st = fs.statSync(p); if (st.isDirectory()) total += 目录大小(p); else total += st.size; }); } catch (e) { } return total; } function 项目目录(手机号, 编号) { // 防穿越:拼好后必须仍在项目云根下 var root = 项目根(); var d = path.join(root, 安全名(手机号), 安全名(编号)); return d.indexOf(root) === 0 ? d : null; } function 列项目(手机号) { if (!手机号 || !会员存在(手机号)) return { ok: false, msg: '该手机号未注册' }; var dir = 项目目录(手机号, ''); var 出 = []; try { fs.readdirSync(dir).forEach(function (编号) { var p = path.join(dir, 编号); try { var meta = JSON.parse(fs.readFileSync(path.join(p, 'meta.json'), 'utf8')); 出.push({ 编号: meta['编号'] || 编号, 名称: meta['名称'] || '', 时间: meta['时间'] || '', 大小: meta['大小'] || 0 }); } catch (e) { } }); } catch (e) { } // 目录不存在=无项目 return { ok: true, '项目': 出 }; } // ---- 开放 API 调用(查管理员授权给本应用的目录) ---- function 查授权目录(cb) { var token = process.env.TRIM_API_TOKEN; if (!token) return cb({ ok: false, msg: '未获取到 TRIM_API_TOKEN(需从飞牛应用中心启动)' }); if (!fs.existsSync(API_SOCKET)) return cb({ ok: false, msg: '开放 API 接口不存在:需要 fnOS 1.2.0401 及以上、App 1.34.0 及以上' }); var body = JSON.stringify({ reqId: String(Date.now()), req: 'trim.file.getSharedAccessibleFolders', appName: APPNAME, data: {} }); var req = http.request({ socketPath: API_SOCKET, path: API_PATH, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'Content-Length': Buffer.byteLength(body) } }, function (res) { var chunks = []; res.on('data', function (c) { chunks.push(c); }); res.on('end', function () { try { var j = JSON.parse(Buffer.concat(chunks).toString('utf8')); if (j.code === 0) return cb({ ok: true, paths: ((j.data || {}).paths || []).sort() }); cb({ ok: false, msg: j.msg || ('开放 API 返回 code=' + j.code) }); } catch (e) { cb({ ok: false, msg: '开放 API 响应异常: ' + e.message }); } }); }); req.on('error', function (e) { cb({ ok: false, msg: '开放 API 调用失败: ' + e.message }); }); req.end(body); } // ---- 设置:查询 / 切换数据库目录 ---- function 设置信息(cb) { 查授权目录(function (auth) { var db = 读库(); var cfgDir = 读配置().HY_DB_DIR || ''; cb({ ok: true, version: VERSION, port: PORT, dbdir: DB_DIR, dbfile: DB_FILE, defaultdir: DEFAULT_DIR, isDefaultDir: DB_DIR === DEFAULT_DIR, configuredDir: cfgDir, projroot: 项目根(), authorized: auth.ok ? auth.paths : [], authorizedOk: auth.ok, authorizedMsg: auth.ok ? '' : auth.msg, count: db['会员'].length, regOpen: (读配置().HY_REG_OPEN || '1') === '1', adminConfigured: 管理密码已设置(), tlsfp: TLS指纹(), keyfp: RSA_KEY ? 公钥指纹() : '' }); }); } function 切换数据库目录(newDir, cb) { if (typeof newDir !== 'string' || !/^\/.+/.test(newDir.trim())) return cb({ ok: false, msg: '数据库目录必须是以 / 开头的完整路径' }); newDir = newDir.trim(); if (newDir === DB_DIR) return cb({ ok: true, dbdir: DB_DIR, dbfile: DB_FILE, msg: '目录未变化' }); var why = 目录不可写原因(newDir); if (why) return cb({ ok: false, msg: '目标目录不可写(' + why + ')。共享目录需先在应用设置或本页「授权新目录」完成目录授权' }); var 旧文件 = DB_FILE; var 旧项目根 = 项目根(); var 新文件 = path.join(newDir, DB_NAME); var warn = ''; 备份库(); // 迁移前备份 // 迁移顺序:先写新文件→回读校验→再删旧文件,任何一步失败都不丢数据 try { if (fs.existsSync(旧文件)) { var content = fs.readFileSync(旧文件); fs.writeFileSync(新文件, content); JSON.parse(fs.readFileSync(新文件, 'utf8')); // 新文件完整性校验 try { fs.unlinkSync(旧文件); } catch (e) { warn = '旧文件暂无法删除,已保留在 ' + 旧文件 + ' (不影响使用)'; } } try { fs.unlinkSync(旧文件 + '.tmp'); } catch (e) {} } catch (e) { console.error('数据库迁移失败: ' + ((e.code ? e.code + ' ' : '') + e.message)); return cb({ ok: false, msg: '迁移失败: ' + ((e.code ? e.code + ' ' : '') + e.message) + ',原数据未受影响' }); } DB_DIR = newDir; DB_FILE = 新文件; // 新目录无库文件时落一个空会员库 try { if (!fs.existsSync(DB_FILE)) 写库({ '会员': [] }); } catch (e) { console.warn('新目录空库初始化失败: ' + e.message); } // 项目云整目录一并迁移(尽力而为:失败不回滚,旧数据保留并提示) try { if (fs.existsSync(旧项目根)) { var 新项目根 = path.join(newDir, PROJ_DIR_NAME); try { fs.renameSync(旧项目根, 新项目根); } catch (e) { fs.cpSync(旧项目根, 新项目根, { recursive: true }); fs.rmSync(旧项目根, { recursive: true, force: true }); } } } catch (e) { warn = (warn ? warn + ';' : '') + '项目云目录未能迁移,仍保留在 ' + 旧项目根 + ' (' + e.message + ')'; } process.env.HY_DB_DIR = newDir; try { var cfg = 读配置(); cfg.HY_DB_DIR = newDir; 写配置(cfg); } catch (e) { console.error('配置回写失败: ' + e.message); return cb({ ok: false, msg: '已迁移到 ' + DB_DIR + ' 但配置回写失败(重启后可能回退): ' + e.message }); } console.log('数据库目录已切换: ' + 旧文件 + ' → ' + DB_FILE + (warn ? ' (' + warn + ')' : '')); cb({ ok: true, dbdir: DB_DIR, dbfile: DB_FILE, msg: warn }); } // ---- HTTP ---- function sendJson(res, obj) { res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(obj)); } function readBody(req, cb) { var chunks = []; req.on('data', function (c) { chunks.push(c); }); req.on('end', function () { try { cb(null, JSON.parse(Buffer.concat(chunks).toString('utf8'))); } catch (e) { cb(e, null); } }); } var MIME = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.json': 'application/json; charset=utf-8', '.png': 'image/png', '.ico': 'image/x-icon' }; var TLS_HINT = { ok: false, code: 'TLS_REQUIRED', msg: '该接口已迁移至加密通道,请升级客户端并使用 https://IP:端口 连接' }; function handler(req, res, isTLS) { // 全站 HTTPS 安全响应头 res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.setHeader('Strict-Transport-Security', 'max-age=31536000'); var urlPath = decodeURIComponent(req.url.split('?')[0]); var ip = (req.socket.remoteAddress || '').replace(/^::ffff:/, '') || 'unknown'; var query = {}; (req.url.split('?')[1] || '').split('&').forEach(function (kv) { var p = kv.split('='); if (p[0]) query[decodeURIComponent(p[0])] = decodeURIComponent((p[1] || '').replace(/\+/g, ' ')); }); // 公开:版本号(页面徽章用,不含敏感信息) if (urlPath === '/api/version') return sendJson(res, { ok: true, version: VERSION }); // 公开:TLS 证书下载(安装到系统"受信任的根证书"后,浏览器/客户端不再有证书警告) if (urlPath === '/api/cert') { try { var crt = fs.readFileSync(TLS_CRT_FILE); res.writeHead(200, { 'Content-Type': 'application/x-x509-ca-cert', 'Content-Disposition': 'attachment; filename="fnnas-huiyuan.crt"', 'Content-Length': crt.length }); return res.end(crt); } catch (e) { return sendJson(res, { ok: false, msg: '证书读取失败: ' + e.message }); } } // ---- 加密通道(仅 HTTPS) ---- if (urlPath === '/api/pubkey') { if (!isTLS) return sendJson(res, TLS_HINT); if (!RSA_KEY) return sendJson(res, { ok: false, msg: '服务端加密未初始化' }); return sendJson(res, { ok: true, pubkey: 公钥PEM(), keyfp: 公钥指纹() }); } if (urlPath === '/api/enc') { if (!isTLS) return sendJson(res, TLS_HINT); return readBody(req, function (e, payload) { if (e || !payload || !payload.k) return sendJson(res, { ok: false, msg: '信封格式错误' }); var r = 解密信封(payload, ip); if (r.err) return sendJson(res, { ok: false, msg: r.err }); NONCES.set(r.nonce, Date.now() + 600000); // 记录 nonce 防重放 var route = ENC_ROUTES[r.inner.path]; var result; if (!route) result = { ok: false, msg: '不支持的加密接口: ' + r.inner.path }; else { try { result = route(r.inner.body || {}, req, ip); } catch (e2) { result = { ok: false, msg: '处理失败: ' + e2.message }; } } sendJson(res, 加密响应(r.aesKey, result)); }); } // ---- 客户端会员接口:仅加密通道提供(HTTP 端口返回迁移提示) ---- var 仅TLS = (urlPath === '/api/login' || urlPath === '/api/register' || urlPath === '/api/member' || urlPath === '/api/changepwd'); if (仅TLS && !isTLS) return sendJson(res, TLS_HINT); // 管理密码登录/登录态/改管理密码 if (urlPath === '/api/admin/login') return readBody(req, function (e, b) { b = b || {}; var lock = 被锁秒('adm:' + ip); if (lock) return sendJson(res, { ok: false, msg: '尝试过于频繁,请 ' + lock + ' 秒后再试' }); if (!管理密码已设置()) return sendJson(res, { ok: false, msg: '管理密码未设置(请在安装向导或应用设置中设置)' }); if (!管理密码正确(b['密码'])) { 失败记录('adm:' + ip); return sendJson(res, { ok: false, msg: '管理密码错误' }); } 失败清零('adm:' + ip); return sendJson(res, { ok: true, token: 新token() }); }); if (urlPath === '/api/admin/check') return sendJson(res, { ok: 是管理员(req), adminConfigured: 管理密码已设置() }); if (urlPath === '/api/admin/addmember') return readBody(req, function (e, b) { if (!是管理员(req)) return sendJson(res, { ok: false, msg: '未登录或登录已过期' }); b = b || {}; if (!/^1\d{10}$/.test(String(b['手机号'] || ''))) return sendJson(res, { ok: false, msg: '手机号格式不正确(11 位数字)' }); var p = String(b['密码'] || ''); if (p.length < 4 || p.length > 64) return sendJson(res, { ok: false, msg: '密码长度需 4-64 位' }); var db = 读库(); for (var i = 0; i < db['会员'].length; i++) if (db['会员'][i]['手机号'] === b['手机号']) return sendJson(res, { ok: false, msg: '该手机号已注册' }); db['会员'].push({ '手机号': b['手机号'], '密码': 哈希密码(p), '会员类型': String(b['会员类型'] || '试用会员'), '有效期': String(b['有效期'] || 今天加天数(30)), '注册时间': 今天加天数(0), '备注': String(b['备注'] || '') }); 备份库(); 写库(db); return sendJson(res, { ok: true, msg: '创建成功' }); }); // TLS 证书导入/信息/重置(管理登录) if (urlPath === '/api/admin/tls/info') { if (!是管理员(req)) return sendJson(res, { ok: false, msg: '未登录或登录已过期' }); try { var crtText = fs.readFileSync(TLS_CRT_FILE, 'utf8'); var tlsInfo = 解析证书(crtText); tlsInfo.source = (tlsInfo.subject.indexOf('fnnas-huiyuan') >= 0 && tlsInfo.issuer.indexOf('fnnas-huiyuan') >= 0) ? 'selfsigned' : 'custom'; return sendJson(res, { ok: true, tls: tlsInfo }); } catch (e) { return sendJson(res, { ok: false, msg: '证书读取失败: ' + e.message }); } } if (urlPath === '/api/admin/tls/import') return readBody(req, function (e, b) { if (!是管理员(req)) return sendJson(res, { ok: false, msg: '未登录或登录已过期' }); b = b || {}; if (!b.cert || !b.key) return sendJson(res, { ok: false, msg: '请同时提供证书文件和私钥文件(PEM 格式)' }); var info; try { info = 解析证书(b.cert); } catch (e2) { return sendJson(res, { ok: false, msg: '证书文件无法解析(需 PEM 格式;证书链请用 fullchain 全链文件): ' + e2.message }); } if (!密钥证书匹配(b.key, b.cert)) return sendJson(res, { ok: false, msg: '私钥与证书不匹配,请确认两个文件是同一套' }); try { fs.writeFileSync(TLS_CRT_FILE, String(b.cert)); fs.writeFileSync(TLS_KEY_FILE, String(b.key), { mode: 0o600 }); console.log('已导入自定义 TLS 证书: ' + info.subject); // 先响应本请求,再热切换监听(切换会断开旧连接,不能打断响应本身) res.on('finish', function () { setTimeout(热重载证书, 200); }); return sendJson(res, { ok: true, tls: info, msg: '导入成功,已立即生效' }); } catch (e3) { return sendJson(res, { ok: false, msg: '写入证书失败: ' + e3.message }); } }); if (urlPath === '/api/admin/tls/reset') { if (!是管理员(req)) return sendJson(res, { ok: false, msg: '未登录或登录已过期' }); try { 生成自签证书(); res.on('finish', function () { setTimeout(热重载证书, 200); }); return sendJson(res, { ok: true, tls: 解析证书(fs.readFileSync(TLS_CRT_FILE, 'utf8')) }); } catch (e) { return sendJson(res, { ok: false, msg: '重置失败: ' + e.message }); } } if (urlPath === '/api/admin/changepwd') return readBody(req, function (e, b) { if (!是管理员(req)) return sendJson(res, { ok: false, msg: '未登录或登录已过期' }); b = b || {}; var p = String(b['新密码'] || ''); if (p.length < 6) return sendJson(res, { ok: false, msg: '新管理密码至少 6 位' }); try { 设置管理密码(p); return sendJson(res, { ok: true }); } catch (e2) { return sendJson(res, { ok: false, msg: '写入配置失败: ' + e2.message }); } }); // (客户端会员接口 login/register/member/changepwd 已迁至加密通道 /api/enc,见上) // 管理 API(需 X-Admin-Token;自定义头跨站请求带不了,同时天然防 CSRF) if (urlPath === '/api/list') { if (!是管理员(req)) return sendJson(res, { ok: false, msg: '未登录或登录已过期' }); var db = 读库(); return sendJson(res, { ok: true, '会员': db['会员'] }); } if (urlPath === '/api/save') return readBody(req, function (e, b) { if (!是管理员(req)) return sendJson(res, { ok: false, msg: '未登录或登录已过期' }); if (e || !b || !Array.isArray(b['会员'])) return sendJson(res, { ok: false, msg: '参数错误' }); // 密码语义:空=保持现有;明文=重置该会员密码(哈希入库);符合哈希格式=原样保留(管理页回传未改值) var 旧库 = 读库(); var 旧密码 = {}; 旧库['会员'].forEach(function (m) { if (m['手机号']) 旧密码[m['手机号']] = m['密码'] || ''; }); b['会员'].forEach(function (m, idx) { var p = m['密码']; if (!p) { m['密码'] = 旧密码[m['手机号']] !== undefined ? 旧密码[m['手机号']] : (旧库['会员'][idx] ? 旧库['会员'][idx]['密码'] || '' : ''); } else if (!哈希格式.test(String(p))) { m['密码'] = 哈希密码(p); } }); 备份库(); 写库(b); return sendJson(res, { ok: true }); }); // ---- 项目云路由(HTTPS:会员密码或管理 token;HTTP:仅管理 token,网页管理用) ---- // 上传:body = JSON 文本(项目编号/名称/手机号/密码) + "\n\n" + zip 二进制 if (urlPath === '/api/proj/upload') { if (!isTLS && !是管理员(req)) return sendJson(res, TLS_HINT); var 收到 = 0, 超限 = false; var chunks = []; req.on('data', function (c) { 收到 += c.length; if (收到 > UPLOAD_MAX) { 超限 = true; req.destroy(); return; } chunks.push(c); }); req.on('end', function () { if (超限) return sendJson(res, { ok: false, msg: '超出上传上限 2GB' }); var buf = Buffer.concat(chunks); var sep = buf.indexOf('\n\n'); if (sep < 0) return sendJson(res, { ok: false, msg: '缺少 JSON 头' }); var 头 = {}; try { 头 = JSON.parse(buf.slice(0, sep).toString('utf8')); } catch (e) { return sendJson(res, { ok: false, msg: 'JSON 头解析失败' }); } var 手机号 = 头['手机号'], 编号 = 头['编号']; if (!手机号 || !编号) return sendJson(res, { ok: false, msg: '缺少手机号或项目编号' }); if (!项目云鉴权(手机号, 头['密码'], req)) return sendJson(res, { ok: false, msg: '手机号或密码错误' }); var zip = buf.slice(sep + 2); if (zip.length < 100) return sendJson(res, { ok: false, msg: 'zip 数据为空或过小' }); var d = 项目目录(手机号, 编号); if (!d) return sendJson(res, { ok: false, msg: '路径非法' }); if (目录大小(项目目录(手机号, '')) + zip.length > USER_QUOTA) return sendJson(res, { ok: false, msg: '云空间已满(每会员上限 1GB),请联系管理员清理' }); try { fs.mkdirSync(d, { recursive: true }); var tmp = path.join(d, '项目.zip.tmp'); fs.writeFileSync(tmp, zip); var 目标 = path.join(d, '项目.zip'); try { fs.unlinkSync(目标); } catch (e) { } fs.renameSync(tmp, 目标); var meta = { 编号: 编号, 名称: 头['名称'] || '', 时间: 头['时间'] || 今天加天数(0), 大小: zip.length }; var mt = path.join(d, 'meta.json.tmp'); fs.writeFileSync(mt, JSON.stringify(meta), 'utf8'); fs.renameSync(mt, path.join(d, 'meta.json')); return sendJson(res, { ok: true, 编号: 编号, 大小: zip.length, 时间: meta['时间'] }); } catch (e) { return sendJson(res, { ok: false, msg: '写盘失败: ' + e.message }); } }); return; } if (urlPath === '/api/proj/list') { if (!isTLS && !是管理员(req)) return sendJson(res, TLS_HINT); if (!项目云鉴权(query['手机号'], query['密码'], req)) return sendJson(res, { ok: false, msg: '手机号或密码错误' }); return sendJson(res, 列项目(query['手机号'])); } if (urlPath === '/api/proj/download') { if (!isTLS && !是管理员(req)) return sendJson(res, TLS_HINT); var 手机号 = query['手机号'], 编号 = query['编号']; if (!项目云鉴权(手机号, query['密码'], req)) return sendJson(res, { ok: false, msg: '手机号或密码错误' }); var d2 = 手机号 && 编号 ? 项目目录(手机号, 编号) : null; var zp = d2 ? path.join(d2, '项目.zip') : ''; if (!d2 || !fs.existsSync(zp)) return sendJson(res, { ok: false, msg: '云上没有该项目' }); // 中文文件名只能放 filename*(UTF-8 百分号编码),ASCII fallback 防 latin-1 编码崩溃 var 下载名 = 安全名(编号) + '.zip'; var ascii名 = 下载名.replace(/[^\x20-\x7e]/g, '_'); res.writeHead(200, { 'Content-Type': 'application/zip', 'Content-Length': fs.statSync(zp).size, 'Content-Disposition': "attachment; filename=\"" + ascii名 + "\"; filename*=UTF-8''" + encodeURIComponent(下载名) }); fs.createReadStream(zp).pipe(res); return; } if (urlPath === '/api/proj/delete') return readBody(req, function (e, b) { b = b || {}; if (!isTLS && !是管理员(req)) return sendJson(res, TLS_HINT); if (!项目云鉴权(b['手机号'], b['密码'], req)) return sendJson(res, { ok: false, msg: '手机号或密码错误' }); var d3 = b['手机号'] && b['编号'] ? 项目目录(b['手机号'], b['编号']) : null; if (!d3) return sendJson(res, { ok: false, msg: '参数错误' }); try { fs.rmSync(d3, { recursive: true, force: true }); return sendJson(res, { ok: true }); } catch (e2) { return sendJson(res, { ok: false, msg: '删除失败: ' + e2.message }); } }); // 设置 API(需管理登录) if (urlPath === '/api/settings' && req.method === 'GET') { if (!是管理员(req)) return sendJson(res, { ok: false, msg: '未登录或登录已过期' }); return 设置信息(function (j) { sendJson(res, j); }); } if (urlPath === '/api/settings' && req.method === 'POST') return readBody(req, function (e, b) { if (!是管理员(req)) return sendJson(res, { ok: false, msg: '未登录或登录已过期' }); if (e || !b) return sendJson(res, { ok: false, msg: '参数错误' }); if (typeof b.regOpen === 'boolean') { try { var c = 读配置(); c.HY_REG_OPEN = b.regOpen ? '1' : '0'; 写配置(c); return sendJson(res, { ok: true, regOpen: b.regOpen }); } catch (e2) { return sendJson(res, { ok: false, msg: '写入配置失败: ' + e2.message }); } } 切换数据库目录(b.dbdir || '', function (j) { sendJson(res, j); }); }); // 静态文件(会员管理页/设置页) if (urlPath === '/' || urlPath === '/admin.html') urlPath = '/admin.html'; var file = path.join(WWW, path.normalize(urlPath).replace(/^([A-Za-z]:)?[\\\/]+/, '')); if (file.indexOf(WWW) !== 0) { res.writeHead(403); res.end('Forbidden'); return; } fs.readFile(file, function (err, data) { if (err) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Not Found: ' + urlPath); return; } var ext = path.extname(file).toLowerCase(); // 禁止缓存:应用升级后浏览器必须取新页面,避免跑旧版脚本 res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream', 'Cache-Control': 'no-cache' }); res.end(data); }); } // ---- 全站 HTTPS(唯一监听;HTTP 明文通道彻底停用) ---- var TLS_KEY_FILE = path.join(CFG_DIR, 'tls.key'); var TLS_CRT_FILE = path.join(CFG_DIR, 'tls.crt'); function TLS指纹() { // 证书 SHA256 前 16 位,供管理员核验 try { return crypto.createHash('sha256').update(fs.readFileSync(TLS_CRT_FILE)).digest('hex').slice(0, 16).toUpperCase(); } catch (e) { return ''; } } function 生成自签证书() { execFileSync('openssl', ['req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-keyout', TLS_KEY_FILE, '-out', TLS_CRT_FILE, '-days', '3650', '-subj', '/CN=fnnas-huiyuan'], { stdio: 'ignore' }); console.log('已生成自签 TLS 证书: ' + TLS_CRT_FILE); } // 用 openssl 解析证书文本 → 结构化信息(导入校验与设置页展示用) function 解析证书(pemCert) { var tmp = path.join(CFG_DIR, '.import-cert.pem'); fs.writeFileSync(tmp, String(pemCert)); try { var out = execFileSync('openssl', ['x509', '-in', tmp, '-noout', '-subject', '-issuer', '-startdate', '-enddate', '-ext', 'subjectAltName'], { encoding: 'utf8' }); var info = { subject: '', issuer: '', notBefore: '', notAfter: '', san: '' }; out.split(/\r?\n/).forEach(function (line) { if (line.indexOf('subject=') === 0) info.subject = line.slice(8).trim(); else if (line.indexOf('issuer=') === 0) info.issuer = line.slice(7).trim(); else if (line.indexOf('notBefore=') === 0) info.notBefore = line.slice(10).trim(); else if (line.indexOf('notAfter=') === 0) info.notAfter = line.slice(9).trim(); else if (line.indexOf('Subject Alternative Name') >= 0 && line.indexOf(':') >= 0) info.san = line.slice(line.indexOf(':') + 1).trim(); }); info.fp = crypto.createHash('sha256').update(pemCert).digest('hex').slice(0, 16).toUpperCase(); return info; } finally { try { fs.unlinkSync(tmp); } catch (e) { } } } // 校验私钥与证书匹配(公钥比对) function 密钥证书匹配(pemKey, pemCert) { var k = path.join(CFG_DIR, '.import-key.pem'), c = path.join(CFG_DIR, '.import-cert.pem'); fs.writeFileSync(k, String(pemKey)); fs.writeFileSync(c, String(pemCert)); try { var pub1 = execFileSync('openssl', ['pkey', '-in', k, '-pubout'], { encoding: 'utf8' }); var pub2 = execFileSync('openssl', ['x509', '-in', c, '-noout', '-pubkey'], { encoding: 'utf8' }); return pub1.replace(/\s+/g, '') === pub2.replace(/\s+/g, ''); } finally { try { fs.unlinkSync(k); } catch (e) { } try { fs.unlinkSync(c); } catch (e2) { } } } var HTTPS_SERVER = null; // 当前 HTTPS 监听(导入/重置证书时整体热切换) function TLS选项() { return { key: fs.readFileSync(TLS_KEY_FILE), cert: fs.readFileSync(TLS_CRT_FILE), SNICallback: function (servername, cb) { // 域名访问(SNI)握手期间动态取上下文,配合热切换 try { cb(null, require('tls').createSecureContext({ key: fs.readFileSync(TLS_KEY_FILE), cert: fs.readFileSync(TLS_CRT_FILE) })); } catch (e) { cb(e); } } }; } function 启动HTTPS监听(首次) { var https = require('https'); var srv = https.createServer(TLS选项(), function (req, res) { handler(req, res, true); }); srv.on('error', function (e) { var msg = 'HTTPS 监听失败: ' + e.message + (e.code === 'EADDRINUSE' ? '(端口被占用,可在应用设置中更换端口)' : ''); if (首次) { console.error(msg); process.exit(1); } console.error(msg + ',1 秒后重试'); setTimeout(function () { try { 启动HTTPS监听(false); } catch (e2) { console.error(e2.message); } }, 1000); }); HTTPS_SERVER = srv; srv.listen(PORT, HOST, function () { if (首次) { console.log('会员系统服务 v' + VERSION + '(全站 HTTPS) on https://' + HOST + ':' + PORT + ' 数据库=' + DB_FILE + (DB_DIR === DEFAULT_DIR ? '(默认目录)' : '(自定义目录)') + ' 项目云=' + 项目根() + ' 默认目录=' + DEFAULT_DIR + ' 证书指纹=' + TLS指纹() + ' RSA指纹=' + (RSA_KEY ? 公钥指纹() : '未初始化')); } else { console.log('HTTPS 证书已热切换并重新监听,当前指纹=' + TLS指纹()); } }); } function 热重载证书() { // 导入/重置证书后调用:断开旧连接,用新证书重建监听 try { if (HTTPS_SERVER) { HTTPS_SERVER.closeAllConnections(); HTTPS_SERVER.close(); } } catch (e) { } 启动HTTPS监听(false); } (function 启动HTTPS() { try { if (!fs.existsSync(TLS_KEY_FILE) || !fs.existsSync(TLS_CRT_FILE)) 生成自签证书(); } catch (e) { console.error('TLS 证书初始化失败(本应用强制全站 HTTPS,拒绝以明文启动): ' + e.message); process.exit(1); } 启动HTTPS监听(true); })();