import type { IncomingMessage, ServerResponse } from "node:http"; type Photo = { id: string; name: string; latitude: number | null; longitude: number | null; capturedAt: string | null; thumbUrl: string; fullUrl: string; source: "demo" | "nextcloud"; }; type ShareListingEntry = { href: string; name: string; lastModified: string | null; contentType: string; isCollection: boolean; }; function parseHttpUrl(value: string, errorMessage: string): URL { const url = new URL(value.trim()); if (url.protocol !== "https:" && url.protocol !== "http:") { throw new Error(errorMessage); } if (url.username || url.password) { throw new Error("URL credentials are not allowed."); } return url; } function resolveDavUrlFromShareUrl(shareUrl: string): string { const url = parseHttpUrl(shareUrl, "Please enter a public Nextcloud share link."); const publicShare = url.pathname.match(/\/s\/([^/?#]+)/); if (publicShare) { return `${url.origin}/public.php/dav/files/${publicShare[1]}/`; } const davShare = url.pathname.match(/\/public\.php\/dav\/files\/([^/?#]+)\/?/); if (davShare) { return `${url.origin}/public.php/dav/files/${davShare[1]}/`; } throw new Error("Please enter a public Nextcloud share link."); } function resolveValidatedBlobUrl(targetUrl: string, shareUrl: string): string { const target = parseHttpUrl(targetUrl, "Invalid image URL."); const shareDavUrl = new URL(resolveDavUrlFromShareUrl(shareUrl)); if (target.origin !== shareDavUrl.origin || !target.pathname.startsWith(shareDavUrl.pathname)) { throw new Error("Image URL is outside the requested Nextcloud share."); } return target.toString(); } async function proxyUpstream(url: string, init?: RequestInit) { const upstream = await fetch(url, init); const body = await upstream.arrayBuffer(); return { body: Buffer.from(body), contentType: upstream.headers.get("content-type") ?? "application/octet-stream", status: upstream.status }; } function htmlPage(): string { return ` mapix
No photos imported yet.
`; } export function createRequestHandler() { return async function handleRequest(req: IncomingMessage, res: ServerResponse) { const url = new URL(req.url ?? "/", "http://localhost"); if (url.pathname === "/health") { res.statusCode = 200; res.setHeader("content-type", "application/json; charset=utf-8"); res.end(JSON.stringify({ status: "ok" })); return; } if (url.pathname === "/api/nextcloud/list") { const share = url.searchParams.get("share"); if (!share) { res.statusCode = 400; res.setHeader("content-type", "application/json; charset=utf-8"); res.end(JSON.stringify({ error: "missing `share` parameter" })); return; } try { const davUrl = resolveDavUrlFromShareUrl(share); const xml = '' + '' + "" + "" + "" + "" + "" + "" + "" + ""; const upstream = await proxyUpstream(davUrl, { method: "PROPFIND", headers: { Depth: "1", "Content-Type": "application/xml; charset=utf-8", Accept: "application/xml, text/xml" }, body: xml }); res.statusCode = upstream.status; res.setHeader("content-type", upstream.contentType); res.end(upstream.body); return; } catch (error) { res.statusCode = 502; res.setHeader("content-type", "application/json; charset=utf-8"); res.end( JSON.stringify({ error: error instanceof Error ? error.message : "upstream request failed" }) ); return; } } if (url.pathname === "/api/nextcloud/blob") { const target = url.searchParams.get("url"); const share = url.searchParams.get("share"); if (!target || !share) { res.statusCode = 400; res.setHeader("content-type", "application/json; charset=utf-8"); res.end(JSON.stringify({ error: "missing `share` or `url` parameter" })); return; } let validatedTarget: string; try { validatedTarget = resolveValidatedBlobUrl(target, share); } catch (error) { res.statusCode = 400; res.setHeader("content-type", "application/json; charset=utf-8"); res.end( JSON.stringify({ error: error instanceof Error ? error.message : "invalid image URL" }) ); return; } try { const upstream = await proxyUpstream(validatedTarget); res.statusCode = upstream.status; res.setHeader("content-type", upstream.contentType); res.end(upstream.body); return; } catch (error) { res.statusCode = 502; res.setHeader("content-type", "application/json; charset=utf-8"); res.end( JSON.stringify({ error: error instanceof Error ? error.message : "upstream request failed" }) ); return; } } res.statusCode = 200; res.setHeader("content-type", "text/html; charset=utf-8"); res.end(htmlPage()); }; }