Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions danmu_api/configs/envs.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export class Envs {
static originalEnvVars = new Map();
static accessedEnvVars = new Map();

// Node 本地部署时由 server.js 注入:启动前的真实系统环境变量快照(最高优先级判定依据)与 .env 原始解析结果
static systemEnvBackup = null;
static rawEnvValues = null;

// 允许在值中写入 # 等 dotenv 视为注释字符的文本类变量;读取时绕过 dotenv 截断以保留完整内容。仅纳入 encrypt=false 变量(带令牌/密码 URL 若入此集合会绕过加密返回明文,故禁止纳入)。
static RAW_ENV_KEYS = new Set(['AI_MATCH_PROMPT', 'ANIME_TITLE_FILTER', 'AUTO_MATCH_MAPPING_TABLE', 'BLOCKED_WORDS', 'COLOR_POOL', 'CUSTOM_MERGE_RULES', 'DANMU_OFFSET', 'DANMU_PUSH_URL', 'EPISODE_TITLE_FILTER', 'IP_BLACKLIST', 'OTHER_SERVER', 'TITLE_MAPPING_TABLE', 'TITLE_NOISE_FILTER', 'VOD_SERVERS']);

static VOD_ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq', 'migu', 'sohu', 'leshi', 'xigua', 'maiduidui', 'aiyifan']; // vod允许的播放平台
static ALLOWED_PLATFORMS = ['qiyi', 'bilibili1', 'imgo', 'youku', 'qq', 'migu', 'renren', 'hanjutv', 'sohu', 'leshi', 'xigua', 'maiduidui', 'aiyifan', 'hongguo', 'dandan', 'bahamut', 'animeko', 'custom']; // 全部源允许的播放平台
static ALLOWED_SOURCES = ['360', 'vod', 'tmdb', 'douban', 'tencent', 'youku', 'iqiyi', 'imgo', 'bilibili', 'migu', 'renren', 'hanjutv', 'sohu', 'leshi', 'xigua', 'maiduidui', 'aiyifan', 'hongguo', 'dandan', 'bahamut', 'animeko', 'custom']; // 允许的源
Expand Down Expand Up @@ -63,6 +70,10 @@ export class Envs {
* @returns {any} 转换后的值
*/
static get(key, defaultValue, type = 'string', encrypt = false) {
// 文本类且未加密的自定义变量绕过 dotenv 注释截断,保留 # 等字符;加密变量不在此路径,避免绕过加密返回明文
if (type === 'string' && !encrypt && Envs.RAW_ENV_KEYS.has(key)) {
return this.getRawEnv(key, defaultValue);
}
let value;
if (typeof this.env !== 'undefined' && this.env[key]) {
value = this.env[key];
Expand Down Expand Up @@ -119,6 +130,64 @@ export class Envs {
return '*'.repeat(str.length);
}

/**
* 解析 .env 原始内容:跳过整行 # 注释、保留行内 #,并剥除整体双引号包裹(与 node-handler 引号写入一致)。
* @param {string} text .env 文件原始内容
* @returns {Object} 键值映射
*/
static parseRawEnvText(text) {
const result = {};
if (typeof text !== 'string') return result;
const lines = text.split(/\r?\n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq === -1) continue;
const key = trimmed.slice(0, eq).trim();
if (!key) continue;
let value = trimmed.slice(eq + 1).trim();
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1);
}
result[key] = value;
}
return result;
}

/**
* 读取自定义文本类变量,绕过 dotenv 截断保留 #:系统环境变量 > .env 原始值 > 默认值;非 Node 部署退化为普通取值。
* @param {string} key 环境变量键
* @param {string} defaultValue 默认值
* @returns {string} 原始值(含 #)
*/
static getRawEnv(key, defaultValue = '') {
const finalize = (v) => {
this.originalEnvVars.set(key, v);
this.accessedEnvVars.set(key, v);
return v;
};

// 非 Node 运行时(测试 / Workers):不做文件读取,等价于普通取值,保证测试隔离与平台兼容
if (!Envs.systemEnvBackup) {
if (this.env && this.env[key]) return finalize(this.env[key]);
if (typeof process !== 'undefined' && process.env?.[key]) return finalize(process.env[key]);
return finalize(defaultValue);
}

// Node 运行时:系统环境变量始终最高优先级
if (Object.prototype.hasOwnProperty.call(Envs.systemEnvBackup, key)) {
return finalize(Envs.systemEnvBackup[key]);
}

// 否则读取 .env 原始行(保留 #),未配置该键时回退 process.env 或默认值
if (Envs.rawEnvValues && Object.prototype.hasOwnProperty.call(Envs.rawEnvValues, key)) {
return finalize(Envs.rawEnvValues[key]);
}
if (typeof process !== 'undefined' && process.env?.[key]) return finalize(process.env[key]);
return finalize(defaultValue);
}

/**
* 解析 VOD 服务器配置
* @returns {Array} 服务器列表
Expand Down Expand Up @@ -321,8 +390,7 @@ export class Envs {
console.warn(`[Envs] 解析合并映射表规则失败: ${rStr}`, e);
}
}

this.accessedEnvVars.set('CUSTOM_MERGE_RULES', raw);

return rules;
}

Expand Down
13 changes: 13 additions & 0 deletions danmu_api/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import dotenv from 'dotenv';
import { Request as NodeFetchRequest } from 'node-fetch';
import { handleRequest } from './worker.js';
import { Globals } from './configs/globals.js';
import { Envs } from './configs/envs.js';
import { clearBangumiDataCache, initBangumiData } from './utils/bangumi-data-util.js';
import { getLocalCaches, judgeLocalCacheValid } from './utils/cache-util.js';
import { getRedisCaches, judgeRedisValid } from './utils/redis-util.js';
Expand Down Expand Up @@ -42,6 +43,9 @@ const envPath = path.join(configDir, '.env');
// 保存系统环境变量的副本,确保它们具有最高优先级
const systemEnvBackup = { ...process.env };

// 注入到 Envs,供自定义规则变量读取时判定系统环境变量优先级(绕过 dotenv 注释截断)
Envs.systemEnvBackup = systemEnvBackup;


// 引入 zlib 模块,用于响应数据的 GZIP 压缩
// (注:zlib 已在顶部 import,此处保留原版注释意图说明)
Expand Down Expand Up @@ -138,6 +142,15 @@ function loadEnv() {
process.env[key] = value;
}

// 解析 .env 原始内容,供支持自定义规则的变量绕过 dotenv 注释截断(保留 # 等字符)
try {
if (fs.existsSync(envPath)) {
Envs.rawEnvValues = Envs.parseRawEnvText(fs.readFileSync(envPath, 'utf8'));
}
} catch (e) {
// 原始解析失败不影响启动,相关变量回退到普通取值语义
}

console.log('[server] .env file loaded successfully');
} catch (e) {
console.log('[server] dotenv not available or .env file not found, using system environment variables');
Expand Down
32 changes: 32 additions & 0 deletions danmu_api/worker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2850,6 +2850,38 @@ test('worker.js API endpoints', async (t) => {
// });
// });

// // 测试自定义文本类变量绕过 dotenv 注释截断(保留 # 等字符),对应 envs.js RAW_ENV_KEYS 修复
// import { Envs } from './configs/envs.js';
//
// test('envs RAW_ENV_KEYS 保留 # 不被 dotenv 截断', async (t) => {
// const reset = () => { Envs.systemEnvBackup = null; Envs.rawEnvValues = null; Envs.env = undefined; };
//
// await t.test('parseRawEnvText 保留行内 # 与剥除外层双引号', () => {
// const parsed = Envs.parseRawEnvText('K1=v1\nK2=v with # hash\nK3="q # v"');
// assert.strictEqual(parsed.K2, 'v with # hash');
// assert.strictEqual(parsed.K3, 'q # v');
// });
//
// await t.test('CUSTOM_MERGE_RULES / COLOR_POOL / URL 类变量含 # 完整保留', () => {
// reset();
// Envs.systemEnvBackup = {};
// Envs.rawEnvValues = {
// CUSTOM_MERGE_RULES: 'A #1 revival@bili',
// COLOR_POOL: '#FF0000,#00FF00',
// DANMU_PUSH_URL: 'http://h.com/cb#frag',
// };
// assert.strictEqual(Envs.get('CUSTOM_MERGE_RULES', '', 'string'), 'A #1 revival@bili');
// assert.strictEqual(Envs.get('COLOR_POOL', '', 'string'), '#FF0000,#00FF00');
// assert.strictEqual(Envs.get('DANMU_PUSH_URL', '', 'string'), 'http://h.com/cb#frag');
// });
//
// await t.test('!encrypt 守卫:加密变量不走原始解析,防止绕过加密', () => {
// reset();
// Envs.systemEnvBackup = {};
// Envs.rawEnvValues = { DANMU_PUSH_URL: 'http://x.com/cb#frag' };
// assert.strictEqual(Envs.get('DANMU_PUSH_URL', 'DEF', 'string', true), 'DEF');
// });

// test('nipaplay 弹弹302关联工具函数', async (t) => {
//
// // generateNipaplaySignature:相同入参确定性产出,输出为 sha256 的 base64(44 字符)
Expand Down
Loading