Update features and dependencies
This commit is contained in:
@@ -1,7 +1,64 @@
|
||||
import { generateObject } from "ai";
|
||||
import dns from "node:dns/promises";
|
||||
import { z } from "zod";
|
||||
import { resolveModel, type AiConfig } from "../factory";
|
||||
|
||||
/**
|
||||
* Resolves the hostname in `rawUrl` and returns an error string if the
|
||||
* URL targets a private/reserved address range (SSRF guard), or null if safe.
|
||||
*/
|
||||
async function validateImportUrl(rawUrl: string): Promise<string | null> {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(rawUrl);
|
||||
} catch {
|
||||
return "Invalid URL";
|
||||
}
|
||||
|
||||
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
||||
return "URL must use http or https";
|
||||
}
|
||||
|
||||
let addresses: string[];
|
||||
try {
|
||||
const results = await dns.lookup(url.hostname, { all: true, family: 0 });
|
||||
addresses = results.map((r) => r.address);
|
||||
} catch {
|
||||
return "Unable to resolve hostname";
|
||||
}
|
||||
|
||||
for (const addr of addresses) {
|
||||
if (isPrivateAddress(addr)) {
|
||||
return "URL must not point to a private or reserved address";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isPrivateAddress(ip: string): boolean {
|
||||
const v4 = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (v4) {
|
||||
const [, a, b, c] = v4.map(Number) as [number, number, number, number, number];
|
||||
if (a === 127) return true;
|
||||
if (a === 10) return true;
|
||||
if (a === 172 && b >= 16 && b <= 31) return true;
|
||||
if (a === 192 && b === 168) return true;
|
||||
if (a === 169 && b === 254) return true;
|
||||
if (a >= 224) return true;
|
||||
return false;
|
||||
}
|
||||
const lower = ip.toLowerCase();
|
||||
if (lower === "::1") return true;
|
||||
if (lower === "::") return true;
|
||||
if (lower.startsWith("fe80:")) return true;
|
||||
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
||||
const v4mapped = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
||||
if (v4mapped) return isPrivateAddress(v4mapped[1]!);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const ImportedRecipeSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
@@ -33,6 +90,9 @@ const ImportedRecipeSchema = z.object({
|
||||
export type ImportedRecipe = z.infer<typeof ImportedRecipeSchema>;
|
||||
|
||||
export async function importFromUrl(url: string, config?: AiConfig): Promise<ImportedRecipe> {
|
||||
const ssrfError = await validateImportUrl(url);
|
||||
if (ssrfError) throw new Error(ssrfError);
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: { "User-Agent": "Mozilla/5.0 (compatible; Epicure/1.0; recipe importer)" },
|
||||
signal: AbortSignal.timeout(10000),
|
||||
|
||||
Reference in New Issue
Block a user