← back to Rentv 826 Tracker

lib/parse.js

52 lines

'use strict';

// Parser for nginx "combined" log format:
//   $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent"
// e.g.  1.2.3.4 - boomer [10/Aug/2026:06:53:12 +0000] "GET /826/ HTTP/1.1" 200 1024 "https://ref" "Mozilla/5.0"

const LINE_RE = /^(\S+) \S+ (\S+) \[([^\]]+)\] "([^"]*)" (\d{3}) (\S+) "([^"]*)" "([^"]*)"/;

const MONTHS = { Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11 };

// "10/Aug/2026:06:53:12 +0000" -> epoch ms (UTC-correct via the offset)
function parseNginxTime(s) {
  const m = /^(\d{2})\/(\w{3})\/(\d{4}):(\d{2}):(\d{2}):(\d{2}) ([+-]\d{4})$/.exec(s);
  if (!m) return null;
  const [, dd, mon, yyyy, hh, mi, ss, tz] = m;
  const month = MONTHS[mon];
  if (month === undefined) return null;
  const offMin = (tz[0] === '-' ? -1 : 1) * (parseInt(tz.slice(1, 3), 10) * 60 + parseInt(tz.slice(3, 5), 10));
  const asUTC = Date.UTC(+yyyy, month, +dd, +hh, +mi, +ss);
  return asUTC - offMin * 60 * 1000;
}

// Returns a normalized hit object, or null if the line is unparseable.
function parseLine(line) {
  const m = LINE_RE.exec(line);
  if (!m) return null;
  const [, remote_addr, remote_user_raw, time_local, request, status, bytes_raw, referer, ua] = m;
  const ts = parseNginxTime(time_local);
  if (ts == null) return null;

  const reqParts = request.split(' ');
  const method = reqParts[0] || null;
  const path = reqParts[1] || null;

  const remote_user = remote_user_raw === '-' ? null : remote_user_raw;
  const bytes = bytes_raw === '-' ? 0 : parseInt(bytes_raw, 10) || 0;

  return {
    ip: remote_addr,
    remote_user,
    ts,
    method,
    path,
    status: parseInt(status, 10),
    bytes,
    referer: referer === '-' ? null : referer,
    ua: ua === '-' ? null : ua,
  };
}

module.exports = { parseLine, parseNginxTime };