feat: add configurable stealth download security policies
This commit is contained in:
@@ -1705,6 +1705,40 @@ function initDefaultSettings() {
|
|||||||
if (!SettingsDB.get('global_theme')) {
|
if (!SettingsDB.get('global_theme')) {
|
||||||
SettingsDB.set('global_theme', 'dark');
|
SettingsDB.set('global_theme', 'dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载安全策略默认值(同IP同文件限速:5分钟3次、1小时10次、1天20次)
|
||||||
|
if (!SettingsDB.get('download_security_enabled')) {
|
||||||
|
SettingsDB.set('download_security_enabled', 'true');
|
||||||
|
}
|
||||||
|
if (!SettingsDB.get('download_security_same_file_enabled')) {
|
||||||
|
SettingsDB.set('download_security_same_file_enabled', 'true');
|
||||||
|
}
|
||||||
|
if (!SettingsDB.get('download_security_same_file_limit_5m')) {
|
||||||
|
SettingsDB.set('download_security_same_file_limit_5m', '3');
|
||||||
|
}
|
||||||
|
if (!SettingsDB.get('download_security_same_file_limit_1h')) {
|
||||||
|
SettingsDB.set('download_security_same_file_limit_1h', '10');
|
||||||
|
}
|
||||||
|
if (!SettingsDB.get('download_security_same_file_limit_1d')) {
|
||||||
|
SettingsDB.set('download_security_same_file_limit_1d', '20');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扩展策略默认关闭(管理员可按需开启)
|
||||||
|
if (!SettingsDB.get('download_security_same_user_enabled')) {
|
||||||
|
SettingsDB.set('download_security_same_user_enabled', 'false');
|
||||||
|
}
|
||||||
|
if (!SettingsDB.get('download_security_same_user_limit_1h')) {
|
||||||
|
SettingsDB.set('download_security_same_user_limit_1h', '80');
|
||||||
|
}
|
||||||
|
if (!SettingsDB.get('download_security_same_user_limit_1d')) {
|
||||||
|
SettingsDB.set('download_security_same_user_limit_1d', '300');
|
||||||
|
}
|
||||||
|
if (!SettingsDB.get('download_security_same_file_min_interval_enabled')) {
|
||||||
|
SettingsDB.set('download_security_same_file_min_interval_enabled', 'false');
|
||||||
|
}
|
||||||
|
if (!SettingsDB.get('download_security_same_file_min_interval_seconds')) {
|
||||||
|
SettingsDB.set('download_security_same_file_min_interval_seconds', '2');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据库迁移 - 主题偏好字段
|
// 数据库迁移 - 主题偏好字段
|
||||||
|
|||||||
@@ -104,6 +104,24 @@ const GLOBAL_SEARCH_DEFAULT_LIMIT = Number(process.env.GLOBAL_SEARCH_DEFAULT_LIM
|
|||||||
const GLOBAL_SEARCH_MAX_LIMIT = Number(process.env.GLOBAL_SEARCH_MAX_LIMIT || 200);
|
const GLOBAL_SEARCH_MAX_LIMIT = Number(process.env.GLOBAL_SEARCH_MAX_LIMIT || 200);
|
||||||
const GLOBAL_SEARCH_MAX_SCANNED_NODES = Number(process.env.GLOBAL_SEARCH_MAX_SCANNED_NODES || 4000);
|
const GLOBAL_SEARCH_MAX_SCANNED_NODES = Number(process.env.GLOBAL_SEARCH_MAX_SCANNED_NODES || 4000);
|
||||||
const SHARE_CODE_REGEX = /^[A-Za-z0-9]{6,32}$/;
|
const SHARE_CODE_REGEX = /^[A-Za-z0-9]{6,32}$/;
|
||||||
|
const DOWNLOAD_SECURITY_DEFAULTS = Object.freeze({
|
||||||
|
enabled: true,
|
||||||
|
same_ip_same_file: {
|
||||||
|
enabled: true,
|
||||||
|
limit_5m: 3,
|
||||||
|
limit_1h: 10,
|
||||||
|
limit_1d: 20
|
||||||
|
},
|
||||||
|
same_ip_same_user: {
|
||||||
|
enabled: false,
|
||||||
|
limit_1h: 80,
|
||||||
|
limit_1d: 300
|
||||||
|
},
|
||||||
|
same_ip_same_file_min_interval: {
|
||||||
|
enabled: false,
|
||||||
|
seconds: 2
|
||||||
|
}
|
||||||
|
});
|
||||||
const COOKIE_SECURE_MODE = String(process.env.COOKIE_SECURE || '').toLowerCase();
|
const COOKIE_SECURE_MODE = String(process.env.COOKIE_SECURE || '').toLowerCase();
|
||||||
const SHOULD_USE_SECURE_COOKIES =
|
const SHOULD_USE_SECURE_COOKIES =
|
||||||
COOKIE_SECURE_MODE === 'true' ||
|
COOKIE_SECURE_MODE === 'true' ||
|
||||||
@@ -695,6 +713,482 @@ function getBusyDownloadMessage() {
|
|||||||
return '当前网络繁忙,请稍后再试';
|
return '当前网络繁忙,请稍后再试';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseBooleanLike(rawValue, fallback = false) {
|
||||||
|
if (rawValue === null || rawValue === undefined || rawValue === '') {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
if (typeof rawValue === 'boolean') {
|
||||||
|
return rawValue;
|
||||||
|
}
|
||||||
|
const normalized = String(rawValue).trim().toLowerCase();
|
||||||
|
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampIntegerSetting(rawValue, fallback, min, max) {
|
||||||
|
const value = Number(rawValue);
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return Math.min(max, Math.max(min, Math.floor(value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDownloadSecuritySettings() {
|
||||||
|
const defaults = DOWNLOAD_SECURITY_DEFAULTS;
|
||||||
|
return {
|
||||||
|
enabled: parseBooleanLike(SettingsDB.get('download_security_enabled'), defaults.enabled),
|
||||||
|
same_ip_same_file: {
|
||||||
|
enabled: parseBooleanLike(
|
||||||
|
SettingsDB.get('download_security_same_file_enabled'),
|
||||||
|
defaults.same_ip_same_file.enabled
|
||||||
|
),
|
||||||
|
limit_5m: clampIntegerSetting(
|
||||||
|
SettingsDB.get('download_security_same_file_limit_5m'),
|
||||||
|
defaults.same_ip_same_file.limit_5m,
|
||||||
|
1,
|
||||||
|
20000
|
||||||
|
),
|
||||||
|
limit_1h: clampIntegerSetting(
|
||||||
|
SettingsDB.get('download_security_same_file_limit_1h'),
|
||||||
|
defaults.same_ip_same_file.limit_1h,
|
||||||
|
1,
|
||||||
|
50000
|
||||||
|
),
|
||||||
|
limit_1d: clampIntegerSetting(
|
||||||
|
SettingsDB.get('download_security_same_file_limit_1d'),
|
||||||
|
defaults.same_ip_same_file.limit_1d,
|
||||||
|
1,
|
||||||
|
200000
|
||||||
|
)
|
||||||
|
},
|
||||||
|
same_ip_same_user: {
|
||||||
|
enabled: parseBooleanLike(
|
||||||
|
SettingsDB.get('download_security_same_user_enabled'),
|
||||||
|
defaults.same_ip_same_user.enabled
|
||||||
|
),
|
||||||
|
limit_1h: clampIntegerSetting(
|
||||||
|
SettingsDB.get('download_security_same_user_limit_1h'),
|
||||||
|
defaults.same_ip_same_user.limit_1h,
|
||||||
|
1,
|
||||||
|
50000
|
||||||
|
),
|
||||||
|
limit_1d: clampIntegerSetting(
|
||||||
|
SettingsDB.get('download_security_same_user_limit_1d'),
|
||||||
|
defaults.same_ip_same_user.limit_1d,
|
||||||
|
1,
|
||||||
|
200000
|
||||||
|
)
|
||||||
|
},
|
||||||
|
same_ip_same_file_min_interval: {
|
||||||
|
enabled: parseBooleanLike(
|
||||||
|
SettingsDB.get('download_security_same_file_min_interval_enabled'),
|
||||||
|
defaults.same_ip_same_file_min_interval.enabled
|
||||||
|
),
|
||||||
|
seconds: clampIntegerSetting(
|
||||||
|
SettingsDB.get('download_security_same_file_min_interval_seconds'),
|
||||||
|
defaults.same_ip_same_file_min_interval.seconds,
|
||||||
|
1,
|
||||||
|
3600
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDownloadSecuritySettingsPayload(rawPayload, fallbackSettings = DOWNLOAD_SECURITY_DEFAULTS) {
|
||||||
|
const source = rawPayload && typeof rawPayload === 'object' ? rawPayload : {};
|
||||||
|
const base = fallbackSettings && typeof fallbackSettings === 'object'
|
||||||
|
? fallbackSettings
|
||||||
|
: DOWNLOAD_SECURITY_DEFAULTS;
|
||||||
|
|
||||||
|
const sameFileRaw = source.same_ip_same_file && typeof source.same_ip_same_file === 'object'
|
||||||
|
? source.same_ip_same_file
|
||||||
|
: {};
|
||||||
|
const sameUserRaw = source.same_ip_same_user && typeof source.same_ip_same_user === 'object'
|
||||||
|
? source.same_ip_same_user
|
||||||
|
: {};
|
||||||
|
const minIntervalRaw = source.same_ip_same_file_min_interval && typeof source.same_ip_same_file_min_interval === 'object'
|
||||||
|
? source.same_ip_same_file_min_interval
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: parseBooleanLike(source.enabled, !!base.enabled),
|
||||||
|
same_ip_same_file: {
|
||||||
|
enabled: parseBooleanLike(sameFileRaw.enabled, !!base.same_ip_same_file?.enabled),
|
||||||
|
limit_5m: clampIntegerSetting(
|
||||||
|
sameFileRaw.limit_5m,
|
||||||
|
Number(base.same_ip_same_file?.limit_5m || DOWNLOAD_SECURITY_DEFAULTS.same_ip_same_file.limit_5m),
|
||||||
|
1,
|
||||||
|
20000
|
||||||
|
),
|
||||||
|
limit_1h: clampIntegerSetting(
|
||||||
|
sameFileRaw.limit_1h,
|
||||||
|
Number(base.same_ip_same_file?.limit_1h || DOWNLOAD_SECURITY_DEFAULTS.same_ip_same_file.limit_1h),
|
||||||
|
1,
|
||||||
|
50000
|
||||||
|
),
|
||||||
|
limit_1d: clampIntegerSetting(
|
||||||
|
sameFileRaw.limit_1d,
|
||||||
|
Number(base.same_ip_same_file?.limit_1d || DOWNLOAD_SECURITY_DEFAULTS.same_ip_same_file.limit_1d),
|
||||||
|
1,
|
||||||
|
200000
|
||||||
|
)
|
||||||
|
},
|
||||||
|
same_ip_same_user: {
|
||||||
|
enabled: parseBooleanLike(sameUserRaw.enabled, !!base.same_ip_same_user?.enabled),
|
||||||
|
limit_1h: clampIntegerSetting(
|
||||||
|
sameUserRaw.limit_1h,
|
||||||
|
Number(base.same_ip_same_user?.limit_1h || DOWNLOAD_SECURITY_DEFAULTS.same_ip_same_user.limit_1h),
|
||||||
|
1,
|
||||||
|
50000
|
||||||
|
),
|
||||||
|
limit_1d: clampIntegerSetting(
|
||||||
|
sameUserRaw.limit_1d,
|
||||||
|
Number(base.same_ip_same_user?.limit_1d || DOWNLOAD_SECURITY_DEFAULTS.same_ip_same_user.limit_1d),
|
||||||
|
1,
|
||||||
|
200000
|
||||||
|
)
|
||||||
|
},
|
||||||
|
same_ip_same_file_min_interval: {
|
||||||
|
enabled: parseBooleanLike(
|
||||||
|
minIntervalRaw.enabled,
|
||||||
|
!!base.same_ip_same_file_min_interval?.enabled
|
||||||
|
),
|
||||||
|
seconds: clampIntegerSetting(
|
||||||
|
minIntervalRaw.seconds,
|
||||||
|
Number(base.same_ip_same_file_min_interval?.seconds || DOWNLOAD_SECURITY_DEFAULTS.same_ip_same_file_min_interval.seconds),
|
||||||
|
1,
|
||||||
|
3600
|
||||||
|
)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDownloadSecuritySettings(settings) {
|
||||||
|
SettingsDB.set('download_security_enabled', settings.enabled ? 'true' : 'false');
|
||||||
|
SettingsDB.set('download_security_same_file_enabled', settings.same_ip_same_file.enabled ? 'true' : 'false');
|
||||||
|
SettingsDB.set('download_security_same_file_limit_5m', String(settings.same_ip_same_file.limit_5m));
|
||||||
|
SettingsDB.set('download_security_same_file_limit_1h', String(settings.same_ip_same_file.limit_1h));
|
||||||
|
SettingsDB.set('download_security_same_file_limit_1d', String(settings.same_ip_same_file.limit_1d));
|
||||||
|
SettingsDB.set('download_security_same_user_enabled', settings.same_ip_same_user.enabled ? 'true' : 'false');
|
||||||
|
SettingsDB.set('download_security_same_user_limit_1h', String(settings.same_ip_same_user.limit_1h));
|
||||||
|
SettingsDB.set('download_security_same_user_limit_1d', String(settings.same_ip_same_user.limit_1d));
|
||||||
|
SettingsDB.set(
|
||||||
|
'download_security_same_file_min_interval_enabled',
|
||||||
|
settings.same_ip_same_file_min_interval.enabled ? 'true' : 'false'
|
||||||
|
);
|
||||||
|
SettingsDB.set(
|
||||||
|
'download_security_same_file_min_interval_seconds',
|
||||||
|
String(settings.same_ip_same_file_min_interval.seconds)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadSecurityCounter {
|
||||||
|
constructor() {
|
||||||
|
this.windowEvents = new Map();
|
||||||
|
this.lastSeenAt = new Map();
|
||||||
|
this.cleanupInterval = setInterval(() => {
|
||||||
|
this.cleanup();
|
||||||
|
}, 10 * 60 * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRecentEvents(key, maxWindowMs, now = Date.now()) {
|
||||||
|
const safeWindow = Math.max(60 * 1000, Number(maxWindowMs) || (24 * 60 * 60 * 1000));
|
||||||
|
const cutoff = now - safeWindow;
|
||||||
|
const list = this.windowEvents.get(key) || [];
|
||||||
|
if (list.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstValidIndex = 0;
|
||||||
|
while (firstValidIndex < list.length && list[firstValidIndex] <= cutoff) {
|
||||||
|
firstValidIndex += 1;
|
||||||
|
}
|
||||||
|
if (firstValidIndex > 0) {
|
||||||
|
list.splice(0, firstValidIndex);
|
||||||
|
if (list.length === 0) {
|
||||||
|
this.windowEvents.delete(key);
|
||||||
|
} else {
|
||||||
|
this.windowEvents.set(key, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.windowEvents.get(key) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
wouldExceedWindowLimit(key, rules = [], now = Date.now()) {
|
||||||
|
if (!key || !Array.isArray(rules) || rules.length === 0) {
|
||||||
|
return { blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedRules = rules
|
||||||
|
.map((rule) => ({
|
||||||
|
code: String(rule?.code || 'window_limit'),
|
||||||
|
windowMs: Math.max(1000, Math.floor(Number(rule?.windowMs) || 0)),
|
||||||
|
limit: Math.max(1, Math.floor(Number(rule?.limit) || 0))
|
||||||
|
}))
|
||||||
|
.filter(rule => rule.windowMs > 0 && rule.limit > 0);
|
||||||
|
|
||||||
|
if (normalizedRules.length === 0) {
|
||||||
|
return { blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWindowMs = normalizedRules.reduce((max, item) => Math.max(max, item.windowMs), 0);
|
||||||
|
const events = this.getRecentEvents(key, maxWindowMs, now);
|
||||||
|
|
||||||
|
for (const rule of normalizedRules) {
|
||||||
|
const threshold = now - rule.windowMs;
|
||||||
|
let count = 0;
|
||||||
|
let limitHitTimestamp = null;
|
||||||
|
|
||||||
|
for (let i = events.length - 1; i >= 0; i -= 1) {
|
||||||
|
const ts = events[i];
|
||||||
|
if (ts <= threshold) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
count += 1;
|
||||||
|
if (count === rule.limit) {
|
||||||
|
limitHitTimestamp = ts;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count >= rule.limit) {
|
||||||
|
const retryAfterMs = limitHitTimestamp
|
||||||
|
? Math.max(1000, (limitHitTimestamp + rule.windowMs) - now + 120)
|
||||||
|
: rule.windowMs;
|
||||||
|
return {
|
||||||
|
blocked: true,
|
||||||
|
code: rule.code,
|
||||||
|
retryAfterSec: Math.ceil(retryAfterMs / 1000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
recordWindowEvent(key, now = Date.now()) {
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = this.windowEvents.get(key);
|
||||||
|
if (list) {
|
||||||
|
list.push(now);
|
||||||
|
this.windowEvents.set(key, list);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.windowEvents.set(key, [now]);
|
||||||
|
}
|
||||||
|
|
||||||
|
wouldViolateMinInterval(key, minIntervalMs, now = Date.now()) {
|
||||||
|
const safeIntervalMs = Math.max(1000, Math.floor(Number(minIntervalMs) || 0));
|
||||||
|
if (!key || safeIntervalMs <= 0) {
|
||||||
|
return { blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSeen = Number(this.lastSeenAt.get(key) || 0);
|
||||||
|
if (lastSeen > 0 && now - lastSeen < safeIntervalMs) {
|
||||||
|
return {
|
||||||
|
blocked: true,
|
||||||
|
code: 'same_ip_same_file_min_interval',
|
||||||
|
retryAfterSec: Math.ceil((safeIntervalMs - (now - lastSeen)) / 1000)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
recordMinIntervalEvent(key, now = Date.now()) {
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.lastSeenAt.set(key, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
const now = Date.now();
|
||||||
|
const keepWindowMs = 2 * 24 * 60 * 60 * 1000;
|
||||||
|
const staleWindowBefore = now - keepWindowMs;
|
||||||
|
|
||||||
|
for (const [key, list] of this.windowEvents.entries()) {
|
||||||
|
if (!Array.isArray(list) || list.length === 0) {
|
||||||
|
this.windowEvents.delete(key);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let firstValidIndex = 0;
|
||||||
|
while (firstValidIndex < list.length && list[firstValidIndex] <= staleWindowBefore) {
|
||||||
|
firstValidIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstValidIndex > 0) {
|
||||||
|
list.splice(0, firstValidIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.length === 0) {
|
||||||
|
this.windowEvents.delete(key);
|
||||||
|
} else {
|
||||||
|
this.windowEvents.set(key, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of this.lastSeenAt.entries()) {
|
||||||
|
if (!Number.isFinite(value) || value <= staleWindowBefore) {
|
||||||
|
this.lastSeenAt.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const downloadSecurityCounter = new DownloadSecurityCounter();
|
||||||
|
|
||||||
|
function evaluateDownloadSecurityPolicy(req, {
|
||||||
|
ownerUserId,
|
||||||
|
filePath,
|
||||||
|
source = 'download'
|
||||||
|
} = {}) {
|
||||||
|
const settings = getDownloadSecuritySettings();
|
||||||
|
if (!settings.enabled) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownerId = Math.floor(Number(ownerUserId) || 0);
|
||||||
|
const normalizedPath = normalizeVirtualPath(filePath || '');
|
||||||
|
if (ownerId <= 0 || !normalizedPath) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientIp = String(req?.ip || req?.socket?.remoteAddress || '').trim();
|
||||||
|
if (!clientIp) {
|
||||||
|
return { allowed: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
const eventKeys = [];
|
||||||
|
const minIntervalKeys = [];
|
||||||
|
|
||||||
|
if (settings.same_ip_same_file.enabled) {
|
||||||
|
const sameFileKey = `download_sec:file:${clientIp}:${ownerId}:${normalizedPath}`;
|
||||||
|
const sameFileRules = [
|
||||||
|
{
|
||||||
|
code: 'same_ip_same_file_5m',
|
||||||
|
windowMs: 5 * 60 * 1000,
|
||||||
|
limit: settings.same_ip_same_file.limit_5m
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'same_ip_same_file_1h',
|
||||||
|
windowMs: 60 * 60 * 1000,
|
||||||
|
limit: settings.same_ip_same_file.limit_1h
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'same_ip_same_file_1d',
|
||||||
|
windowMs: 24 * 60 * 60 * 1000,
|
||||||
|
limit: settings.same_ip_same_file.limit_1d
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const sameFileCheck = downloadSecurityCounter.wouldExceedWindowLimit(sameFileKey, sameFileRules, now);
|
||||||
|
if (sameFileCheck.blocked) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
code: sameFileCheck.code,
|
||||||
|
retryAfterSec: sameFileCheck.retryAfterSec
|
||||||
|
};
|
||||||
|
}
|
||||||
|
eventKeys.push(sameFileKey);
|
||||||
|
|
||||||
|
if (settings.same_ip_same_file_min_interval.enabled) {
|
||||||
|
const minIntervalKey = `download_sec:min:${clientIp}:${ownerId}:${normalizedPath}`;
|
||||||
|
const minIntervalCheck = downloadSecurityCounter.wouldViolateMinInterval(
|
||||||
|
minIntervalKey,
|
||||||
|
settings.same_ip_same_file_min_interval.seconds * 1000,
|
||||||
|
now
|
||||||
|
);
|
||||||
|
if (minIntervalCheck.blocked) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
code: minIntervalCheck.code,
|
||||||
|
retryAfterSec: minIntervalCheck.retryAfterSec
|
||||||
|
};
|
||||||
|
}
|
||||||
|
minIntervalKeys.push(minIntervalKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.same_ip_same_user.enabled) {
|
||||||
|
const sameUserKey = `download_sec:user:${clientIp}:${ownerId}`;
|
||||||
|
const sameUserRules = [
|
||||||
|
{
|
||||||
|
code: 'same_ip_same_user_1h',
|
||||||
|
windowMs: 60 * 60 * 1000,
|
||||||
|
limit: settings.same_ip_same_user.limit_1h
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'same_ip_same_user_1d',
|
||||||
|
windowMs: 24 * 60 * 60 * 1000,
|
||||||
|
limit: settings.same_ip_same_user.limit_1d
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const sameUserCheck = downloadSecurityCounter.wouldExceedWindowLimit(sameUserKey, sameUserRules, now);
|
||||||
|
if (sameUserCheck.blocked) {
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
code: sameUserCheck.code,
|
||||||
|
retryAfterSec: sameUserCheck.retryAfterSec
|
||||||
|
};
|
||||||
|
}
|
||||||
|
eventKeys.push(sameUserKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of eventKeys) {
|
||||||
|
downloadSecurityCounter.recordWindowEvent(key, now);
|
||||||
|
}
|
||||||
|
for (const key of minIntervalKeys) {
|
||||||
|
downloadSecurityCounter.recordMinIntervalEvent(key, now);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
source
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDownloadSecurityBlock(req, res, blockResult, options = {}) {
|
||||||
|
const statusCode = Number(options.statusCode || 503);
|
||||||
|
const plainText = options.plainText === true;
|
||||||
|
const ownerUserId = Number(options.ownerUserId || 0);
|
||||||
|
const filePath = typeof options.filePath === 'string' ? options.filePath : '';
|
||||||
|
const source = options.source || 'download';
|
||||||
|
|
||||||
|
logSecurity(
|
||||||
|
req,
|
||||||
|
'download_security_block',
|
||||||
|
`下载请求触发限速策略(${source})`,
|
||||||
|
{
|
||||||
|
source,
|
||||||
|
policy_code: blockResult?.code || 'download_security_blocked',
|
||||||
|
retry_after_sec: Number(blockResult?.retryAfterSec || 0),
|
||||||
|
owner_user_id: ownerUserId || null,
|
||||||
|
file_path: filePath || null
|
||||||
|
},
|
||||||
|
'warn'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (plainText) {
|
||||||
|
return sendPlainTextError(res, statusCode, getBusyDownloadMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(statusCode).json({
|
||||||
|
success: false,
|
||||||
|
message: getBusyDownloadMessage(),
|
||||||
|
security_blocked: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function sendPlainTextError(res, statusCode, message) {
|
function sendPlainTextError(res, statusCode, message) {
|
||||||
return res.status(statusCode).type('text/plain; charset=utf-8').send(message);
|
return res.status(statusCode).type('text/plain; charset=utf-8').send(message);
|
||||||
}
|
}
|
||||||
@@ -5770,6 +6264,22 @@ app.get('/api/files/download-url', authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isPreviewMode) {
|
||||||
|
const securityResult = evaluateDownloadSecurityPolicy(req, {
|
||||||
|
ownerUserId: latestUser.id,
|
||||||
|
filePath: normalizedPath,
|
||||||
|
source: 'user_download_url'
|
||||||
|
});
|
||||||
|
if (!securityResult.allowed) {
|
||||||
|
return handleDownloadSecurityBlock(req, res, securityResult, {
|
||||||
|
statusCode: 503,
|
||||||
|
ownerUserId: latestUser.id,
|
||||||
|
filePath: normalizedPath,
|
||||||
|
source: 'user_download_url'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const trafficState = getDownloadTrafficState(latestUser);
|
const trafficState = getDownloadTrafficState(latestUser);
|
||||||
|
|
||||||
// 检查用户是否配置了 OSS(包括个人配置和系统级统一配置)
|
// 检查用户是否配置了 OSS(包括个人配置和系统级统一配置)
|
||||||
@@ -6183,6 +6693,21 @@ app.get('/api/files/download', authMiddleware, async (req, res) => {
|
|||||||
message: '用户不存在'
|
message: '用户不存在'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const securityResult = evaluateDownloadSecurityPolicy(req, {
|
||||||
|
ownerUserId: latestUser.id,
|
||||||
|
filePath: normalizedPath,
|
||||||
|
source: 'user_download_stream'
|
||||||
|
});
|
||||||
|
if (!securityResult.allowed) {
|
||||||
|
return handleDownloadSecurityBlock(req, res, securityResult, {
|
||||||
|
statusCode: 503,
|
||||||
|
ownerUserId: latestUser.id,
|
||||||
|
filePath: normalizedPath,
|
||||||
|
source: 'user_download_stream'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const trafficState = getDownloadTrafficState(latestUser);
|
const trafficState = getDownloadTrafficState(latestUser);
|
||||||
|
|
||||||
// 使用统一存储接口
|
// 使用统一存储接口
|
||||||
@@ -7517,6 +8042,22 @@ app.post('/api/share/:code/download-url', shareRateLimitMiddleware, async (req,
|
|||||||
const storageType = share.storage_type || 'oss';
|
const storageType = share.storage_type || 'oss';
|
||||||
const ownerTrafficState = getDownloadTrafficState(shareOwner);
|
const ownerTrafficState = getDownloadTrafficState(shareOwner);
|
||||||
|
|
||||||
|
if (storageType === 'oss') {
|
||||||
|
const securityResult = evaluateDownloadSecurityPolicy(req, {
|
||||||
|
ownerUserId: shareOwner.id,
|
||||||
|
filePath: normalizedFilePath,
|
||||||
|
source: 'share_download_url_oss'
|
||||||
|
});
|
||||||
|
if (!securityResult.allowed) {
|
||||||
|
return handleDownloadSecurityBlock(req, res, securityResult, {
|
||||||
|
statusCode: 503,
|
||||||
|
ownerUserId: shareOwner.id,
|
||||||
|
filePath: normalizedFilePath,
|
||||||
|
source: 'share_download_url_oss'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 本地存储:继续走后端下载
|
// 本地存储:继续走后端下载
|
||||||
if (storageType !== 'oss') {
|
if (storageType !== 'oss') {
|
||||||
if (!ownerTrafficState.isUnlimited) {
|
if (!ownerTrafficState.isUnlimited) {
|
||||||
@@ -7808,6 +8349,21 @@ app.get('/api/share/:code/download-file', shareRateLimitMiddleware, async (req,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
shareOwnerId = shareOwner.id;
|
shareOwnerId = shareOwner.id;
|
||||||
|
|
||||||
|
const securityResult = evaluateDownloadSecurityPolicy(req, {
|
||||||
|
ownerUserId: shareOwner.id,
|
||||||
|
filePath,
|
||||||
|
source: 'share_download_stream'
|
||||||
|
});
|
||||||
|
if (!securityResult.allowed) {
|
||||||
|
return handleDownloadSecurityBlock(req, res, securityResult, {
|
||||||
|
statusCode: 503,
|
||||||
|
ownerUserId: shareOwner.id,
|
||||||
|
filePath,
|
||||||
|
source: 'share_download_stream'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const ownerTrafficState = getDownloadTrafficState(shareOwner);
|
const ownerTrafficState = getDownloadTrafficState(shareOwner);
|
||||||
|
|
||||||
// 使用统一存储接口,根据分享的storage_type选择存储后端
|
// 使用统一存储接口,根据分享的storage_type选择存储后端
|
||||||
@@ -7916,12 +8472,14 @@ app.get('/api/admin/settings', authMiddleware, adminMiddleware, (req, res) => {
|
|||||||
const smtpFrom = SettingsDB.get('smtp_from') || smtpUser;
|
const smtpFrom = SettingsDB.get('smtp_from') || smtpUser;
|
||||||
const smtpHasPassword = !!SettingsDB.get('smtp_password');
|
const smtpHasPassword = !!SettingsDB.get('smtp_password');
|
||||||
const globalTheme = SettingsDB.get('global_theme') || 'dark';
|
const globalTheme = SettingsDB.get('global_theme') || 'dark';
|
||||||
|
const downloadSecurity = getDownloadSecuritySettings();
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
settings: {
|
settings: {
|
||||||
max_upload_size: maxUploadSize,
|
max_upload_size: maxUploadSize,
|
||||||
global_theme: globalTheme,
|
global_theme: globalTheme,
|
||||||
|
download_security: downloadSecurity,
|
||||||
smtp: {
|
smtp: {
|
||||||
host: smtpHost || '',
|
host: smtpHost || '',
|
||||||
port: smtpPort ? parseInt(smtpPort, 10) : 465,
|
port: smtpPort ? parseInt(smtpPort, 10) : 465,
|
||||||
@@ -7948,7 +8506,12 @@ app.post('/api/admin/settings',
|
|||||||
adminMiddleware,
|
adminMiddleware,
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
try {
|
try {
|
||||||
const { max_upload_size, smtp, global_theme } = req.body;
|
const {
|
||||||
|
max_upload_size,
|
||||||
|
smtp,
|
||||||
|
global_theme,
|
||||||
|
download_security
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
if (max_upload_size !== undefined) {
|
if (max_upload_size !== undefined) {
|
||||||
const size = parseInt(max_upload_size);
|
const size = parseInt(max_upload_size);
|
||||||
@@ -7973,6 +8536,22 @@ app.post('/api/admin/settings',
|
|||||||
SettingsDB.set('global_theme', global_theme);
|
SettingsDB.set('global_theme', global_theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (download_security !== undefined) {
|
||||||
|
if (!download_security || typeof download_security !== 'object') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '下载安全策略配置格式错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSecuritySettings = getDownloadSecuritySettings();
|
||||||
|
const normalizedSecuritySettings = normalizeDownloadSecuritySettingsPayload(
|
||||||
|
download_security,
|
||||||
|
currentSecuritySettings
|
||||||
|
);
|
||||||
|
saveDownloadSecuritySettings(normalizedSecuritySettings);
|
||||||
|
}
|
||||||
|
|
||||||
if (smtp) {
|
if (smtp) {
|
||||||
if (!smtp.host || !smtp.port || !smtp.user) {
|
if (!smtp.host || !smtp.port || !smtp.user) {
|
||||||
return res.status(400).json({ success: false, message: 'SMTP配置不完整' });
|
return res.status(400).json({ success: false, message: 'SMTP配置不完整' });
|
||||||
@@ -9813,6 +10392,21 @@ app.get('/d/:code', async (req, res) => {
|
|||||||
}
|
}
|
||||||
linkOwnerId = linkOwner.id;
|
linkOwnerId = linkOwner.id;
|
||||||
|
|
||||||
|
const securityResult = evaluateDownloadSecurityPolicy(req, {
|
||||||
|
ownerUserId: linkOwner.id,
|
||||||
|
filePath: normalizedPath,
|
||||||
|
source: 'direct_link_download'
|
||||||
|
});
|
||||||
|
if (!securityResult.allowed) {
|
||||||
|
return handleDownloadSecurityBlock(req, res, securityResult, {
|
||||||
|
statusCode: 503,
|
||||||
|
plainText: true,
|
||||||
|
ownerUserId: linkOwner.id,
|
||||||
|
filePath: normalizedPath,
|
||||||
|
source: 'direct_link_download'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const ownerTrafficState = getDownloadTrafficState(linkOwner);
|
const ownerTrafficState = getDownloadTrafficState(linkOwner);
|
||||||
const storageType = directLink.storage_type || 'oss';
|
const storageType = directLink.storage_type || 'oss';
|
||||||
const directFileName = (directLink.file_name && String(directLink.file_name).trim())
|
const directFileName = (directLink.file_name && String(directLink.file_name).trim())
|
||||||
|
|||||||
@@ -3396,6 +3396,63 @@
|
|||||||
<span style="color: var(--text-secondary); font-size: 13px;">修改后需要重启服务才能生效</span>
|
<span style="color: var(--text-secondary); font-size: 13px;">修改后需要重启服务才能生效</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<hr style="margin: 20px 0;">
|
||||||
|
<h4 style="margin-bottom: 12px;"><i class="fas fa-shield-alt"></i> 下载安全策略(无感防刷)</h4>
|
||||||
|
<div class="alert alert-info" style="margin-bottom: 15px;">
|
||||||
|
触发策略时对用户返回“当前网络繁忙,请稍后再试”,避免暴露具体风控规则。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 14px;">
|
||||||
|
<div style="padding: 12px; border: 1px solid var(--glass-border); border-radius: 10px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||||||
|
<input type="checkbox" id="ds-enabled" v-model="systemSettings.downloadSecurity.enabled">
|
||||||
|
<label for="ds-enabled" style="margin: 0; font-weight: 600;">启用下载安全策略总开关</label>
|
||||||
|
</div>
|
||||||
|
<div style="font-size: 12px; color: var(--text-secondary);">
|
||||||
|
关闭后不做下载频率限制。
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 12px; border: 1px solid var(--glass-border); border-radius: 10px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||||||
|
<input type="checkbox" id="ds-same-file" v-model="systemSettings.downloadSecurity.same_ip_same_file.enabled">
|
||||||
|
<label for="ds-same-file" style="margin: 0; font-weight: 600;">同IP + 同文件限频</label>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
|
||||||
|
<span style="font-size: 12px; color: var(--text-secondary);">5分钟</span>
|
||||||
|
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_file.limit_5m" min="1" style="width: 80px;">
|
||||||
|
<span style="font-size: 12px; color: var(--text-secondary);">1小时</span>
|
||||||
|
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_file.limit_1h" min="1" style="width: 80px;">
|
||||||
|
<span style="font-size: 12px; color: var(--text-secondary);">1天</span>
|
||||||
|
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_file.limit_1d" min="1" style="width: 80px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 12px; border: 1px solid var(--glass-border); border-radius: 10px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||||||
|
<input type="checkbox" id="ds-same-user" v-model="systemSettings.downloadSecurity.same_ip_same_user.enabled">
|
||||||
|
<label for="ds-same-user" style="margin: 0; font-weight: 600;">扩展:同IP + 同用户总限频</label>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
|
||||||
|
<span style="font-size: 12px; color: var(--text-secondary);">1小时</span>
|
||||||
|
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_user.limit_1h" min="1" style="width: 90px;">
|
||||||
|
<span style="font-size: 12px; color: var(--text-secondary);">1天</span>
|
||||||
|
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_user.limit_1d" min="1" style="width: 90px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding: 12px; border: 1px solid var(--glass-border); border-radius: 10px;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px;">
|
||||||
|
<input type="checkbox" id="ds-min-interval" v-model="systemSettings.downloadSecurity.same_ip_same_file_min_interval.enabled">
|
||||||
|
<label for="ds-min-interval" style="margin: 0; font-weight: 600;">扩展:同IP + 同文件最小间隔</label>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 8px; align-items: center;">
|
||||||
|
<span style="font-size: 12px; color: var(--text-secondary);">间隔秒数</span>
|
||||||
|
<input type="number" class="form-input" v-model.number="systemSettings.downloadSecurity.same_ip_same_file_min_interval.seconds" min="1" style="width: 90px;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr style="margin: 20px 0;">
|
<hr style="margin: 20px 0;">
|
||||||
<h4 style="margin-bottom: 12px;"><i class="fas fa-palette"></i> 全局主题设置</h4>
|
<h4 style="margin-bottom: 12px;"><i class="fas fa-palette"></i> 全局主题设置</h4>
|
||||||
<div style="margin-bottom: 15px;">
|
<div style="margin-bottom: 15px;">
|
||||||
|
|||||||
@@ -228,6 +228,24 @@ createApp({
|
|||||||
// 系统设置
|
// 系统设置
|
||||||
systemSettings: {
|
systemSettings: {
|
||||||
maxUploadSizeMB: 100,
|
maxUploadSizeMB: 100,
|
||||||
|
downloadSecurity: {
|
||||||
|
enabled: true,
|
||||||
|
same_ip_same_file: {
|
||||||
|
enabled: true,
|
||||||
|
limit_5m: 3,
|
||||||
|
limit_1h: 10,
|
||||||
|
limit_1d: 20
|
||||||
|
},
|
||||||
|
same_ip_same_user: {
|
||||||
|
enabled: false,
|
||||||
|
limit_1h: 80,
|
||||||
|
limit_1d: 300
|
||||||
|
},
|
||||||
|
same_ip_same_file_min_interval: {
|
||||||
|
enabled: false,
|
||||||
|
seconds: 2
|
||||||
|
}
|
||||||
|
},
|
||||||
smtp: {
|
smtp: {
|
||||||
host: '',
|
host: '',
|
||||||
port: 465,
|
port: 465,
|
||||||
@@ -4216,6 +4234,19 @@ handleDragLeave(e) {
|
|||||||
this.globalTheme = settings.global_theme;
|
this.globalTheme = settings.global_theme;
|
||||||
console.log('[主题] globalTheme已设置为:', this.globalTheme);
|
console.log('[主题] globalTheme已设置为:', this.globalTheme);
|
||||||
}
|
}
|
||||||
|
if (settings.download_security && typeof settings.download_security === 'object') {
|
||||||
|
const ds = settings.download_security;
|
||||||
|
this.systemSettings.downloadSecurity.enabled = !!ds.enabled;
|
||||||
|
this.systemSettings.downloadSecurity.same_ip_same_file.enabled = !!ds.same_ip_same_file?.enabled;
|
||||||
|
this.systemSettings.downloadSecurity.same_ip_same_file.limit_5m = Math.max(1, Number(ds.same_ip_same_file?.limit_5m || 3));
|
||||||
|
this.systemSettings.downloadSecurity.same_ip_same_file.limit_1h = Math.max(1, Number(ds.same_ip_same_file?.limit_1h || 10));
|
||||||
|
this.systemSettings.downloadSecurity.same_ip_same_file.limit_1d = Math.max(1, Number(ds.same_ip_same_file?.limit_1d || 20));
|
||||||
|
this.systemSettings.downloadSecurity.same_ip_same_user.enabled = !!ds.same_ip_same_user?.enabled;
|
||||||
|
this.systemSettings.downloadSecurity.same_ip_same_user.limit_1h = Math.max(1, Number(ds.same_ip_same_user?.limit_1h || 80));
|
||||||
|
this.systemSettings.downloadSecurity.same_ip_same_user.limit_1d = Math.max(1, Number(ds.same_ip_same_user?.limit_1d || 300));
|
||||||
|
this.systemSettings.downloadSecurity.same_ip_same_file_min_interval.enabled = !!ds.same_ip_same_file_min_interval?.enabled;
|
||||||
|
this.systemSettings.downloadSecurity.same_ip_same_file_min_interval.seconds = Math.max(1, Number(ds.same_ip_same_file_min_interval?.seconds || 2));
|
||||||
|
}
|
||||||
if (settings.smtp) {
|
if (settings.smtp) {
|
||||||
this.systemSettings.smtp.host = settings.smtp.host || '';
|
this.systemSettings.smtp.host = settings.smtp.host || '';
|
||||||
this.systemSettings.smtp.port = settings.smtp.port || 465;
|
this.systemSettings.smtp.port = settings.smtp.port || 465;
|
||||||
@@ -4251,6 +4282,24 @@ handleDragLeave(e) {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
max_upload_size: maxUploadSize,
|
max_upload_size: maxUploadSize,
|
||||||
|
download_security: {
|
||||||
|
enabled: !!this.systemSettings.downloadSecurity.enabled,
|
||||||
|
same_ip_same_file: {
|
||||||
|
enabled: !!this.systemSettings.downloadSecurity.same_ip_same_file.enabled,
|
||||||
|
limit_5m: Math.max(1, Math.floor(Number(this.systemSettings.downloadSecurity.same_ip_same_file.limit_5m) || 3)),
|
||||||
|
limit_1h: Math.max(1, Math.floor(Number(this.systemSettings.downloadSecurity.same_ip_same_file.limit_1h) || 10)),
|
||||||
|
limit_1d: Math.max(1, Math.floor(Number(this.systemSettings.downloadSecurity.same_ip_same_file.limit_1d) || 20))
|
||||||
|
},
|
||||||
|
same_ip_same_user: {
|
||||||
|
enabled: !!this.systemSettings.downloadSecurity.same_ip_same_user.enabled,
|
||||||
|
limit_1h: Math.max(1, Math.floor(Number(this.systemSettings.downloadSecurity.same_ip_same_user.limit_1h) || 80)),
|
||||||
|
limit_1d: Math.max(1, Math.floor(Number(this.systemSettings.downloadSecurity.same_ip_same_user.limit_1d) || 300))
|
||||||
|
},
|
||||||
|
same_ip_same_file_min_interval: {
|
||||||
|
enabled: !!this.systemSettings.downloadSecurity.same_ip_same_file_min_interval.enabled,
|
||||||
|
seconds: Math.max(1, Math.floor(Number(this.systemSettings.downloadSecurity.same_ip_same_file_min_interval.seconds) || 2))
|
||||||
|
}
|
||||||
|
},
|
||||||
smtp: {
|
smtp: {
|
||||||
host: this.systemSettings.smtp.host,
|
host: this.systemSettings.smtp.host,
|
||||||
port: this.systemSettings.smtp.port,
|
port: this.systemSettings.smtp.port,
|
||||||
|
|||||||
Reference in New Issue
Block a user