UNPKG

@ltonetwork/http-message-signatures

Version:

Implementation of the IETF HTTP Message Signatures draft standard

86 lines (78 loc) 3.55 kB
import { Component, Parameters, RequestLike, ResponseLike } from './types'; export function extractHeader({ headers }: RequestLike | ResponseLike, header: string): string { if (typeof headers.get === 'function') return headers.get(header) ?? ''; const lcHeader = header.toLowerCase(); const key = Object.keys(headers).find((name) => name.toLowerCase() === lcHeader); let val = key ? headers[key] ?? '' : ''; if (Array.isArray(val)) { val = val.join(', '); } return val.toString().replace(/\s+/g, ' '); } export function getUrl(message: RequestLike | ResponseLike, component: string): URL { if ('url' in message && 'protocol' in message) { const host = extractHeader(message, 'host'); const protocol = message.protocol || 'http'; const baseUrl = `${protocol}://${host}`; return new URL(message.url, baseUrl); } if (!(message as RequestLike).url) throw new Error(`${component} is only valid for requests`); return new URL((message as RequestLike).url); } // see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-06#section-2.3 export function extractComponent(message: RequestLike | ResponseLike, component: string): string { switch (component) { case '@method': if (!(message as RequestLike).method) throw new Error(`${component} is only valid for requests`); return (message as RequestLike).method.toUpperCase(); case '@target-uri': if (!(message as RequestLike).url) throw new Error(`${component} is only valid for requests`); return (message as RequestLike).url; case '@authority': { const url = getUrl(message, component); const port = url.port ? parseInt(url.port, 10) : null; return `${url.host}${port && ![80, 443].includes(port) ? `:${port}` : ''}`; } case '@scheme': return getUrl(message, component).protocol.slice(0, -1); case '@request-target': { const { pathname, search } = getUrl(message, component); return `${pathname}${search}`; } case '@path': return getUrl(message, component).pathname; case '@query': return getUrl(message, component).search; case '@status': if (!(message as ResponseLike).status) throw new Error(`${component} is only valid for responses`); return (message as ResponseLike).status.toString(); case '@query-params': case '@request-response': throw new Error(`${component} is not implemented yet`); default: throw new Error(`Unknown specialty component ${component}`); } } export function buildSignatureInputString(componentNames: Component[], parameters: Parameters): string { const components = componentNames.map((name) => `"${name.toLowerCase()}"`).join(' '); const values = Object.entries(parameters) .map(([parameter, value]) => { if (typeof value === 'number') return `;${parameter}=${value}`; if (value instanceof Date) return `;${parameter}=${Math.floor(value.getTime() / 1000)}`; return `;${parameter}="${value.toString()}"`; }) .join(''); return `(${components})${values}`; } export function buildSignedData( request: RequestLike | ResponseLike, components: Component[], signatureInputString: string, ): string { const parts = components.map((component) => { const value = component.startsWith('@') ? extractComponent(request, component) : extractHeader(request, component); return `"${component.toLowerCase()}": ${value}`; }); parts.push(`"@signature-params": ${signatureInputString}`); return parts.join('\n'); }