Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394import { dev } from "$app/env";
/* * Fetching where a record or a DID document points. Those hosts are whatever their author * wrote, so on the server they could aim our requests at our own network: the cloud * metadata address, a compose service, localhost. A target has to be https on a * public-looking name, and so does every redirect it answers with. A public name that * resolves to a private address still gets through; catching that needs a resolver check * this can't make portably across node and workers. Local dev talks to loopback services, * so it's let through there. */
const BLOCKED_SUFFIXES = [".localhost", ".local", ".internal", ".home.arpa"];const IPV4 = /^(?:\d{1,3}\.){3}\d{1,3}$/;const MAX_REDIRECTS = 3;
/** A name on the public internet: not an ip literal, a single-label service name, or an * internal-use suffix. A trailing dot (`localhost.`) is the same name. */export const isPublicHost = (hostname: string): boolean => { const host = hostname.toLowerCase().replace(/\.+$/, ""); // ipv6 literals keep their colons (and brackets) in hostname if (host === "" || host.includes(":") || IPV4.test(host)) return false; if (!host.includes(".")) return false; return !BLOCKED_SUFFIXES.some((suffix) => host.endsWith(suffix));};
/** https on a public name, or anything at all in local dev. */export const isPublicUrl = (url: URL, allowPrivate = dev): boolean => allowPrivate || (url.protocol === "https:" && isPublicHost(url.hostname));
export class UnsafeTargetError extends Error { constructor(url: URL) { super(`refusing to fetch ${url.origin}: not a public https host`); this.name = "UnsafeTargetError"; }}
/** * `fetch` to a public https target, following up to MAX_REDIRECTS redirects only while * each hop is public too. Rejects with UnsafeTargetError otherwise. Server-side only: a * browser hides a manual redirect's location. */export async function fetchPublic( fetch: typeof globalThis.fetch, target: string | URL, init: RequestInit = {}, allowPrivate = dev): Promise<Response> { let url = new URL(target); for (let hop = 0; ; hop++) { if (!isPublicUrl(url, allowPrivate)) throw new UnsafeTargetError(url); const res = await fetch(url, { ...init, redirect: "manual" }); const location = res.headers.get("location"); if (res.status < 300 || res.status >= 400 || !location) return res; if (hop >= MAX_REDIRECTS) throw new Error(`too many redirects from ${url.origin}`); await res.body?.cancel(); url = new URL(location, url); }}
/** A fetch that only reaches public https targets and follows no redirects: for xrpc * services named in records, which have no business redirecting. */export const publicOnly = (fetch: typeof globalThis.fetch, allowPrivate = dev): typeof globalThis.fetch => (input, init) => { const url = new URL(input instanceof Request ? input.url : input); if (!isPublicUrl(url, allowPrivate)) return Promise.reject(new UnsafeTargetError(url)); return fetch(input, { ...init, redirect: "error" }); };
/** The body as text, failing once it passes `maxBytes` rather than buffering it whole. */export async function readTextCapped(res: Response, maxBytes: number): Promise<string> { if (Number(res.headers.get("content-length")) > maxBytes) { await res.body?.cancel(); throw new Error(`response over ${maxBytes} bytes`); } if (!res.body) return ""; const reader = res.body.getReader(); const decoder = new TextDecoder(); let total = 0; let text = ""; for (;;) { const { done, value } = await reader.read(); if (done) break; total += value.byteLength; if (total > maxBytes) { await reader.cancel(); throw new Error(`response over ${maxBytes} bytes`); } text += decoder.decode(value, { stream: true }); } return text + decoder.decode();}