Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/rotate-lighthouse-session-cookie.mjs
T

224 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
const DEFAULT_LOGIN_URL = "https://boiler.saas-ctrl.com/login";
const DEFAULT_COOKIE_NAME = "RJ_session";
function fail(message) {
console.error(`FAIL lighthouse cookie rotation: ${message}`);
process.exit(1);
}
function requiredEnv(name) {
const value = process.env[name];
if (typeof value !== "string" || value.trim().length === 0) {
fail(`missing required env var ${name}`);
}
return value;
}
function optionalEnv(name) {
const value = process.env[name];
return typeof value === "string" && value.trim().length > 0 ? value : null;
}
function assertSafeEnvValue(name, value) {
if (/[\r\n]/.test(value)) {
fail(`${name} must not contain newline characters`);
}
}
function splitSetCookieHeader(headerValue) {
if (!headerValue) return [];
const cookies = [];
let start = 0;
for (let index = 0; index < headerValue.length; index += 1) {
if (headerValue[index] !== ",") continue;
const rest = headerValue.slice(index + 1);
if (/^\s*[^=;,\s]+=/.test(rest)) {
cookies.push(headerValue.slice(start, index).trim());
start = index + 1;
}
}
cookies.push(headerValue.slice(start).trim());
return cookies.filter(Boolean);
}
function getSetCookieHeaders(headers) {
if (typeof headers.getSetCookie === "function") {
return headers.getSetCookie();
}
return splitSetCookieHeader(headers.get("set-cookie"));
}
function parseCookiePair(setCookieHeader) {
const [pair] = setCookieHeader.split(";");
const separator = pair.indexOf("=");
if (separator <= 0) return null;
return {
name: pair.slice(0, separator).trim(),
value: pair.slice(separator + 1).trim(),
};
}
function collectCookies(headers, jar) {
for (const header of getSetCookieHeaders(headers)) {
const parsed = parseCookiePair(header);
if (parsed) {
jar.set(parsed.name, parsed.value);
}
}
}
function cookieHeader(jar) {
return [...jar.entries()].map(([name, value]) => `${name}=${value}`).join("; ");
}
function decodeHtmlAttribute(value) {
return value
.replaceAll("&quot;", "\"")
.replaceAll("&#34;", "\"")
.replaceAll("&#x22;", "\"")
.replaceAll("&apos;", "'")
.replaceAll("&#39;", "'")
.replaceAll("&#x27;", "'")
.replaceAll("&amp;", "&");
}
function parseAttributes(tag) {
const attributes = new Map();
const attributePattern = /([A-Za-z_:][-A-Za-z0-9_:.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g;
let match;
while ((match = attributePattern.exec(tag))) {
attributes.set(match[1].toLowerCase(), decodeHtmlAttribute(match[2] ?? match[3] ?? match[4] ?? ""));
}
return attributes;
}
function extractCsrfToken(html) {
const inputPattern = /<input\b[^>]*>/gi;
let match;
while ((match = inputPattern.exec(html))) {
const attributes = parseAttributes(match[0]);
if (attributes.get("name") === "csrf" && attributes.has("value")) {
return attributes.get("value");
}
}
return null;
}
function appendGitHubEnv(values) {
const githubEnv = process.env.GITHUB_ENV;
if (!githubEnv) return;
const lines = [];
for (const [name, value] of Object.entries(values)) {
if (typeof value !== "string" || value.length === 0) continue;
assertSafeEnvValue(name, value);
lines.push(`${name}=${value}`);
}
if (lines.length > 0) {
fs.appendFileSync(githubEnv, `${lines.join("\n")}\n`, { mode: 0o600 });
}
}
function writeCookieOutput(cookieValue) {
const outputPath = process.env.LIGHTHOUSE_SESSION_COOKIE_OUTPUT;
if (!outputPath) return;
assertSafeEnvValue("LIGHTHOUSE_SESSION_COOKIE_OUTPUT", outputPath);
fs.writeFileSync(outputPath, `${cookieValue}\n`, { mode: 0o600 });
try {
fs.chmodSync(outputPath, 0o600);
} catch {
// Best-effort on filesystems that do not support chmod.
}
}
async function main() {
const loginUrl = process.env.LIGHTHOUSE_LOGIN_URL || DEFAULT_LOGIN_URL;
const email = optionalEnv("LIGHTHOUSE_LOGIN_EMAIL");
const password = optionalEnv("LIGHTHOUSE_LOGIN_PASSWORD");
const cookieName = process.env.LIGHTHOUSE_SESSION_COOKIE_NAME || DEFAULT_COOKIE_NAME;
const cookieDomain = process.env.LIGHTHOUSE_COOKIE_DOMAIN || new URL(loginUrl).hostname;
const fallbackCookie = optionalEnv("LIGHTHOUSE_SESSION_COOKIE");
if (!email || !password) {
if (fallbackCookie) {
appendGitHubEnv({
LIGHTHOUSE_SESSION_COOKIE: fallbackCookie,
LIGHTHOUSE_SESSION_COOKIE_NAME: cookieName,
LIGHTHOUSE_COOKIE_DOMAIN: cookieDomain,
});
writeCookieOutput(fallbackCookie);
console.log(`Using pre-provisioned Lighthouse session cookie ${cookieName} for ${new URL(loginUrl).origin}.`);
return;
}
requiredEnv(!email ? "LIGHTHOUSE_LOGIN_EMAIL" : "LIGHTHOUSE_LOGIN_PASSWORD");
}
const jar = new Map();
const getResponse = await fetch(loginUrl, {
redirect: "manual",
headers: {
accept: "text/html,application/xhtml+xml",
},
});
if (!getResponse.ok) {
fail(`GET ${new URL(loginUrl).origin}/login returned HTTP ${getResponse.status}`);
}
collectCookies(getResponse.headers, jar);
const html = await getResponse.text();
const csrf = extractCsrfToken(html);
if (!csrf) {
fail("login page did not contain an input[name=csrf] token");
}
const form = new URLSearchParams({
action: "login",
email,
password,
redirectTo: "/",
csrf,
});
const postResponse = await fetch(loginUrl, {
method: "POST",
redirect: "manual",
headers: {
accept: "text/html,application/xhtml+xml",
"content-type": "application/x-www-form-urlencoded",
cookie: cookieHeader(jar),
"x-csrf-token": csrf,
},
body: form,
});
collectCookies(postResponse.headers, jar);
if (postResponse.status < 200 || postResponse.status >= 400) {
fail(`POST ${new URL(loginUrl).origin}/login returned HTTP ${postResponse.status}`);
}
const sessionCookie = jar.get(cookieName);
if (!sessionCookie) {
fail(`login did not return the ${cookieName} session cookie`);
}
appendGitHubEnv({
LIGHTHOUSE_SESSION_COOKIE: sessionCookie,
LIGHTHOUSE_SESSION_COOKIE_NAME: cookieName,
LIGHTHOUSE_COOKIE_DOMAIN: cookieDomain,
});
writeCookieOutput(sessionCookie);
console.log(`Minted Lighthouse session cookie ${cookieName} for ${new URL(loginUrl).origin}.`);
}
await main();