Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 7x 7x 4x 3x 1x 2x 2x 2x 2x 1x 1x 1x 1x 2x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 2x 2x 1x 1x 2x 1x 1x 2x 2x 2x 2x 2x 2x 2x 2x 2x 4x 4x 4x 4x 4x 4x 3x 3x 3x 3x 3x 3x 3x 3x 6x 6x 3x 3x 3x 3x 3x 3x 3x 3x 3x 4x 4x 4x 4x 4x 4x 4x | import dns from "node:dns/promises";
import { isIP } from "node:net";
import { config } from "../../config";
import {
examplesManifestSchema,
type ExampleRecord,
validateManifestReferences,
} from "./examples-schema";
type CachedRemoteSnapshot = {
expiresAt: number;
examples: ExampleRecord[];
};
export class HttpProvider {
private cached: CachedRemoteSnapshot | null = null;
private loading: Promise<ExampleRecord[]> | null = null;
async getExamples(): Promise<{ examples: ExampleRecord[]; status: "remote" | "cache"; stale: boolean } | null> {
if (!config.examples.source || !config.examples.ref) return null;
if (this.cached && this.cached.expiresAt > Date.now()) {
return { examples: this.cached.examples, status: "cache", stale: false };
}
this.loading ??= this.loadRemote().finally(() => {
this.loading = null;
});
try {
const examples = await this.loading;
this.cached = { examples, expiresAt: Date.now() + config.examples.refreshMs };
return { examples, status: "remote", stale: false };
} catch {
Iif (this.cached) {
this.cached.expiresAt = Date.now() + config.examples.refreshMs;
return { examples: this.cached.examples, status: "cache", stale: true };
}
return null;
}
}
private async loadRemote(): Promise<ExampleRecord[]> {
const source = validateSourceUrl(config.examples.source);
const ref = validateRef(config.examples.ref);
const base = new URL(`${encodeURIComponent(ref)}/`, source);
const manifestUrl = new URL("manifest.json", base);
const manifestText = await fetchText(manifestUrl, config.examples.maxManifestBytes);
const parsed = examplesManifestSchema.safeParse(JSON.parse(manifestText) as unknown);
Iif (!parsed.success) throw new Error("Invalid external examples manifest");
const manifest = parsed.data;
validateManifestReferences(manifest);
Iif (manifest.ref && manifest.ref !== ref) throw new Error("External manifest ref does not match configured ref");
const fileCount = manifest.examples.reduce((total, example) => total + example.files.length, 0);
Iif (fileCount > config.examples.maxFiles) throw new Error("External examples exceed file count limit");
const examples = await Promise.all(manifest.examples.map(async (example) => {
const files = await Promise.all(example.files.map(async (file) => {
const fileUrl = new URL(file.path.split("/").map(encodeURIComponent).join("/"), base);
Iif (fileUrl.origin !== base.origin) throw new Error("Example path changed origin");
const content = await fetchText(fileUrl, config.examples.maxFileBytes);
return { ...file, content };
}));
return { ...example, files, source: "external" as const };
}));
const totalBytes = examples.reduce(
(total, example) => total + example.files.reduce((sum, file) => sum + Buffer.byteLength(file.content, "utf8"), 0),
0,
);
Iif (totalBytes > config.examples.maxTotalBytes) throw new Error("External examples exceed total size limit");
return examples;
}
}
function validateSourceUrl(value: string): URL {
const url = new URL(value.endsWith("/") ? value : `${value}/`);
Iif (url.username || url.password || url.search || url.hash) throw new Error("Invalid examples source URL");
Iif (url.protocol !== "https:" && !(config.examples.allowHttp && config.nodeEnv !== "production")) {
throw new Error("External examples source must use HTTPS");
}
Iif (!config.examples.allowedHosts.includes(url.hostname.toLowerCase())) {
throw new Error("External examples source host is not allowlisted");
}
Iif (isIP(url.hostname) !== 0) throw new Error("IP literal examples sources are not allowed");
return url;
}
function validateRef(value: string): string {
Iif (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)) throw new Error("Invalid external examples ref");
Iif (config.nodeEnv === "production" && value === "main") throw new Error("Floating refs are not allowed in production");
return value;
}
async function fetchText(url: URL, maxBytes: number): Promise<string> {
await assertPublicHost(url.hostname);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), config.examples.timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal, redirect: "manual" });
if (response.status >= 300 && response.status < 400) throw new Error("Redirects are not allowed for examples");
Iif (!response.ok) throw new Error(`Examples source returned ${response.status}`);
const contentLength = response.headers.get("content-length");
Iif (contentLength && Number(contentLength) > maxBytes) throw new Error("Examples response exceeds size limit");
Iif (!response.body) throw new Error("Examples response has no body");
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let total = 0;
while (true) {
const next = await reader.read();
if (next.done) break;
total += next.value.byteLength;
Iif (total > maxBytes) {
await reader.cancel();
throw new Error("Examples response exceeds size limit");
}
chunks.push(next.value);
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
} finally {
clearTimeout(timeout);
}
}
async function assertPublicHost(hostname: string): Promise<void> {
const addresses = await dns.lookup(hostname, { all: true });
Iif (addresses.length === 0 || addresses.some(({ address }) => isPrivateAddress(address))) {
throw new Error("Examples source resolves to a private or reserved address");
}
}
function isPrivateAddress(address: string): boolean {
Eif (isIP(address) === 4) {
const octets = address.split(".").map(Number);
const [a, b] = octets;
return a === 10 || a === 127 || a === 0 || (a === 169 && b === 254) || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 100 && b >= 64 && b <= 127);
}
const normalized = address.toLowerCase();
if (normalized.startsWith("::ffff:")) return isPrivateAddress(normalized.slice("::ffff:".length));
return normalized === "::" || normalized === "::1" || normalized.startsWith("fc") || normalized.startsWith("fd") || normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb");
}
|