import { sshLogger } from "../../utils/logger.js"; function escapeHtml(value: string): string { return value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // Replicates openpubkey's client/choosers/web_chooser.go IssuerToName(). // OPKSSH's /select handler keys its providerMap by this derived name, NOT by the // `alias` field in config.yml. We need the same mapping so we can normalize any // `op=` query param we receive (which can be alias, issuer with protocol, or // issuer without protocol depending on client version) to what OPKSSH expects. function opksshIssuerToName(issuer: string): string | null { if (!issuer) return null; const withScheme = issuer.startsWith("http://") || issuer.startsWith("https://") ? issuer : `https://${issuer}`; if (withScheme.startsWith("https://accounts.google.com")) return "google"; if (withScheme.startsWith("https://login.microsoftonline.com")) return "azure"; if (withScheme.startsWith("https://gitlab.com")) return "gitlab"; if (withScheme.startsWith("https://issuer.hello.coop")) return "hello"; if (withScheme.startsWith("https://")) { const host = withScheme.slice("https://".length).split("/")[0]; return host || null; } return null; } export function normalizeSelectOpParam( rawOp: string, providers: Array<{ alias: string; issuer: string }>, ): string { if (!rawOp) return rawOp; const knownNames = new Set( providers .map((p) => opksshIssuerToName(p.issuer)) .filter((n): n is string => typeof n === "string" && n.length > 0), ); if (knownNames.has(rawOp)) return rawOp; const derivedFromRaw = opksshIssuerToName(rawOp); if (derivedFromRaw && knownNames.has(derivedFromRaw)) return derivedFromRaw; const matchByAlias = providers.find((p) => p.alias === rawOp); if (matchByAlias) { const name = opksshIssuerToName(matchByAlias.issuer); if (name) return name; } return rawOp; } type OpksshErrorPageOptions = { title: string; heading: string; message: string; details?: string; requestId?: string; statusCode?: number; }; export function renderOpksshErrorPage(opts: OpksshErrorPageOptions): string { const title = escapeHtml(opts.title); const heading = escapeHtml(opts.heading); const message = escapeHtml(opts.message); const detailsBlock = opts.details ? `
${escapeHtml(opts.details)}
` : ""; const requestIdBlock = opts.requestId ? `

Request ID: ${escapeHtml(opts.requestId)}

` : ""; return ` ${title}

${heading}

${message}

${detailsBlock} ${requestIdBlock}
`; } export function rewriteOPKSSHHtml( html: string, requestId: string, routePrefix: "opkssh-chooser" | "opkssh-callback", ): string { const basePath = `/host/${routePrefix}/${requestId}`; const localHostPattern = "(?:localhost|127\\.0\\.0\\.1)"; const attrPatterns = ["action", "href", "src", "formaction"]; for (const attr of attrPatterns) { html = html.replace( new RegExp(`${attr}="(/[^"]*)`, "g"), `${attr}="${basePath}$1`, ); html = html.replace( new RegExp(`${attr}='(/[^']*)`, "g"), `${attr}='${basePath}$1`, ); } for (const attr of ["href", "action", "src", "formaction"]) { html = html.replace( new RegExp( `${attr}=["']?http:\\/\\/${localHostPattern}:\\d+\\/([^"'\\s]*)`, "g", ), `${attr}="${basePath}/$1`, ); } html = html.replace( new RegExp( `(window\\.location\\.href\\s*=\\s*["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, "g", ), `$1${basePath}/$2$3`, ); html = html.replace( new RegExp( `(window\\.location\\s*=\\s*["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, "g", ), `$1${basePath}/$2$3`, ); html = html.replace( new RegExp( `(fetch\\(["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, "g", ), `$1${basePath}/$2$3`, ); html = html.replace( new RegExp( `(location\\.assign\\(["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["']\\))`, "g", ), `$1${basePath}/$2$3`, ); html = html.replace( new RegExp( `(location\\.replace\\(["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["']\\))`, "g", ), `$1${basePath}/$2$3`, ); // XMLHttpRequest.open("GET", "http://localhost:PORT/path", ...) html = html.replace( new RegExp( `(\\.open\\(["']\\w+["']\\s*,\\s*["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, "g", ), `$1${basePath}/$2$3`, ); html = html.replace( new RegExp( `(]+http-equiv=["']refresh["'][^>]+content=["'][^;]+;\\s*url=)http:\\/\\/${localHostPattern}:\\d+\\/([^"']+)(["'][^>]*>)`, "gi", ), `$1${basePath}/$2$3`, ); html = html.replace( new RegExp( `(data-[\\w-]+=["'])http:\\/\\/${localHostPattern}:\\d+\\/([^"']*)(["'])`, "g", ), `$1${basePath}/$2$3`, ); const baseTag = ``; if (html.includes("]*>/i, baseTag); } else if (html.includes("")) { sshLogger.info("Inserting base tag into head", { operation: "opkssh_html_rewrite_base_tag_insert", requestId, basePath, }); html = html.replace(//i, `${baseTag}`); } else { sshLogger.warn("No tag found, wrapping HTML", { operation: "opkssh_html_rewrite_no_head", requestId, htmlLength: html.length, htmlPreview: html.substring(0, 200), }); html = `${baseTag}${html}`; } sshLogger.info("HTML rewrite complete", { operation: "opkssh_html_rewrite_complete", requestId, routePrefix, hasBaseTag: html.includes("