refactor: modularize backend security and observability

This commit is contained in:
237899745
2026-07-27 13:06:54 +08:00
parent f90311e68c
commit dce1e3622a
25 changed files with 1633 additions and 849 deletions

View File

@@ -0,0 +1,115 @@
const MONTHS = Object.freeze({
Jan: 0,
Feb: 1,
Mar: 2,
Apr: 3,
May: 4,
Jun: 5,
Jul: 6,
Aug: 7,
Sep: 8,
Oct: 9,
Nov: 10,
Dec: 11
});
function parseDownloadTrafficLogTime(line) {
if (!line || typeof line !== 'string') return null;
const match = line.match(/\[(\d{2})\/([A-Za-z]{3})\/(\d{4}):(\d{2}):(\d{2}):(\d{2})\s*([+-]\d{4})?\]/);
if (!match) return null;
const month = MONTHS[match[2]];
if (month === undefined) return null;
const year = Number(match[3]);
const day = Number(match[1]);
const hour = Number(match[4]);
const minute = Number(match[5]);
const second = Number(match[6]);
if ([year, day, hour, minute, second].some((value) => !Number.isFinite(value))) return null;
let utcMillis = Date.UTC(year, month, day, hour, minute, second);
const timezone = match[7];
if (timezone && /^[+-]\d{4}$/.test(timezone)) {
const sign = timezone[0] === '+' ? 1 : -1;
const timezoneHours = Number(timezone.slice(1, 3));
const timezoneMinutes = Number(timezone.slice(3, 5));
if (timezoneHours > 23 || timezoneMinutes > 59) return null;
utcMillis -= sign * ((timezoneHours * 60) + timezoneMinutes) * 60 * 1000;
}
const parsed = new Date(utcMillis);
if (Number.isNaN(parsed.getTime())) return null;
// Date.UTC normalizes impossible dates; reject them instead of charging the wrong day.
const offsetMinutes = timezone
? (timezone[0] === '+' ? 1 : -1) * ((Number(timezone.slice(1, 3)) * 60) + Number(timezone.slice(3, 5)))
: 0;
const localDate = new Date(parsed.getTime() + (offsetMinutes * 60 * 1000));
if (
localDate.getUTCFullYear() !== year
|| localDate.getUTCMonth() !== month
|| localDate.getUTCDate() !== day
|| localDate.getUTCHours() !== hour
|| localDate.getUTCMinutes() !== minute
|| localDate.getUTCSeconds() !== second
) {
return null;
}
return parsed;
}
function parseDownloadTrafficLine(line, fallbackDate = new Date()) {
if (!line || typeof line !== 'string') return null;
const trimmed = line.trim();
if (!trimmed || !/\bGET\b/i.test(trimmed)) return null;
const statusMatch = trimmed.match(/"\s*(\d{3})\s+(\d+|-)\b/);
if (!statusMatch) return null;
const statusCode = Number(statusMatch[1]);
const bytesSent = statusMatch[2] === '-' ? 0 : Number(statusMatch[2]);
if (![200, 206].includes(statusCode) || !Number.isFinite(bytesSent) || bytesSent <= 0) return null;
let objectKey = null;
const requestMatch = trimmed.match(/"(?:GET|HEAD)\s+([^" ]+)\s+HTTP\//i);
if (requestMatch?.[1]) {
let requestPath = requestMatch[1];
const queryIndex = requestPath.indexOf('?');
if (queryIndex >= 0) requestPath = requestPath.slice(0, queryIndex);
requestPath = requestPath.replace(/^https?:\/\/[^/]+/i, '').replace(/^\/+/, '');
try {
requestPath = decodeURIComponent(requestPath);
} catch {
// Keep the raw path when the log contains malformed percent encoding.
}
objectKey = requestPath || null;
}
if (!objectKey) {
const keyMatch = trimmed.match(/\buser_(\d+)\/[^\s"]+/);
objectKey = keyMatch?.[0] || null;
}
if (!objectKey) return null;
const userMatch = objectKey.match(/(?:^|\/)user_(\d+)\//);
if (!userMatch) return null;
const userId = Number(userMatch[1]);
if (!Number.isSafeInteger(userId) || userId <= 0) return null;
return {
userId,
bytes: Math.floor(bytesSent),
objectKey,
eventAt: parseDownloadTrafficLogTime(trimmed) || fallbackDate
};
}
module.exports = {
parseDownloadTrafficLine,
parseDownloadTrafficLogTime
};