monitor.parse-access-log@1.0.0
impl/typescript.ts
4,989 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { isoToUnix } from "./time_iso_to_unix.ts"; ← from time.iso-to-unix ^1.0.0 · built alongside by fune
import { type AccessLogEntry } from "./monitor_parse_access_log_types.ts";
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
const isDigit = (c: string | undefined): boolean => c !== undefined && c >= "0" && c <= "9";
function digits(text: string, from: number, count: number): number | null {
let n = 0;
for (let i = from; i < from + count; i++) {
if (!isDigit(text[i])) return null;
n = n * 10 + (text.charCodeAt(i) - 48);
}
return n;
}
const dash = (field: string): string | null => (field === "-" ? null : field);
/** "[10/Oct/2000:13:55:36 -0700]" starting at `i` to Unix seconds, or null. */
function parseTime(t: string, i: number): number | null {
if (t[i] !== "[" || t[i + 3] !== "/" || t[i + 7] !== "/" || t[i + 12] !== ":" || t[i + 15] !== ":" || t[i + 18] !== ":" || t[i + 21] !== " " || t[i + 27] !== "]") return null;
const day = digits(t, i + 1, 2);
const month = MONTHS.indexOf(t.slice(i + 4, i + 7)) + 1;
const year = digits(t, i + 8, 4);
const hour = digits(t, i + 13, 2);
const minute = digits(t, i + 16, 2);
const second = digits(t, i + 19, 2);
const sign = t[i + 22];
const oh = digits(t, i + 23, 2);
const om = digits(t, i + 25, 2);
if (day === null || year === null || hour === null || minute === null || second === null || oh === null || om === null) return null;
if (month === 0 || year < 1 || hour > 23 || minute > 59 || second > 59 || oh > 23 || om > 59) return null;
if (sign !== "+" && sign !== "-") return null;
const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
const monthDays = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
if (day < 1 || day > monthDays) return null;
// Every field is checked above, so isoToUnix cannot throw here.
return isoToUnix(`${t.slice(i + 8, i + 12)}-${String(month).padStart(2, "0")}-${t.slice(i + 1, i + 3)}T${t.slice(i + 13, i + 21)}${sign}${t.slice(i + 23, i + 25)}:${t.slice(i + 25, i + 27)}`);
}
/** The end (index of the closing quote) of a quoted field opening at `i`, honouring \" escapes; -1 if none. */
function closingQuote(t: string, i: number): number {
if (t[i] !== '"') return -1;
for (let j = i + 1; j < t.length; j++) {
if (t[j] === "\\") j++;
else if (t[j] === '"') return j;
}
return -1;
}
const isMethod = (m: string): boolean => m.length > 0 && [...m].every((c) => c >= "A" && c <= "Z");
/**
* One Common or Combined Log Format line, or null when the line is in neither
* format: logs hold junk, and a reader should skip it rather than stop.
*/
export function parseAccessLog(line: string): AccessLogEntry | null {
let t = line;
while (t.endsWith("\n") || t.endsWith("\r")) t = t.slice(0, -1);
const hostEnd = t.indexOf(" ");
if (hostEnd < 1) return null;
const identEnd = t.indexOf(" ", hostEnd + 1);
if (identEnd < hostEnd + 2) return null;
const userEnd = t.indexOf(" [", identEnd + 1);
if (userEnd < identEnd + 2) return null;
const at = parseTime(t, userEnd + 1);
if (at === null) return null;
let i = userEnd + 29;
if (t[i] !== " ") return null;
const reqEnd = closingQuote(t, i + 1);
if (reqEnd < 0) return null;
const request = t.slice(i + 2, reqEnd);
i = reqEnd + 1;
// A three-digit status, then the byte count: both are required.
if (t[i] !== " " || t[i + 4] !== " ") return null;
const status = digits(t, i + 1, 3);
if (status === null || status < 100 || status > 599) return null;
i += 4;
let bytesEnd = t.indexOf(" ", i + 1);
if (bytesEnd < 0) bytesEnd = t.length;
const bytesText = t.slice(i + 1, bytesEnd);
let bytes: number | null = null;
if (bytesText !== "-") {
if (bytesText.length < 1 || bytesText.length > 15) return null;
bytes = digits(bytesText, 0, bytesText.length);
if (bytes === null) return null;
}
let referer: string | null = null;
let userAgent: string | null = null;
if (bytesEnd < t.length) {
const refEnd = closingQuote(t, bytesEnd + 1);
if (refEnd < 0 || t[refEnd + 1] !== " ") return null;
const uaEnd = closingQuote(t, refEnd + 2);
if (uaEnd < 0 || (uaEnd + 1 < t.length && t[uaEnd + 1] !== " ")) return null;
referer = dash(t.slice(bytesEnd + 2, refEnd));
userAgent = dash(t.slice(refEnd + 3, uaEnd));
}
let method: string | null = null;
let path: string | null = null;
let protocol: string | null = null;
const parts = request.split(" ");
if (parts.length === 3 && isMethod(parts[0]) && parts[1] !== "" && parts[2].startsWith("HTTP/")) {
[method, path, protocol] = parts;
} else if (parts.length === 2 && isMethod(parts[0]) && parts[1] !== "") {
[method, path] = parts;
}
return {
remoteHost: t.slice(0, hostEnd),
ident: dash(t.slice(hostEnd + 1, identEnd)),
user: dash(t.slice(identEnd + 1, userEnd)),
at,
method,
path,
protocol,
status,
bytes,
referer,
userAgent,
};
}