refactor: modularize backend security and observability
This commit is contained in:
115
backend/utils/oss-log-parser.js
Normal file
115
backend/utils/oss-log-parser.js
Normal 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
|
||||
};
|
||||
Reference in New Issue
Block a user