diff --git a/src/pages/api/upload-image.ts b/src/pages/api/upload-image.ts index d5a1e62..fd84f84 100644 --- a/src/pages/api/upload-image.ts +++ b/src/pages/api/upload-image.ts @@ -2,8 +2,17 @@ import type { APIRoute } from "astro"; import { checkOrigin, checkAuth, createPdsSession } from "../../lib/api"; import { blobUrl } from "../../lib/pds"; import { PDS_URL, DID, MAX_IMAGE_SIZE } from "../../lib/constants"; + const ALLOWED_TYPES = new Set(["image/png", "image/jpeg", "image/webp"]); +const MAGIC: Record = { + "image/png": [0x89, 0x50, 0x4e, 0x47], + "image/jpeg": [0xff, 0xd8, 0xff], + "image/webp": [0x52, 0x49, 0x46, 0x46], +}; + +const WEBP_SIG = [0x57, 0x45, 0x42, 0x50]; // "WEBP" at offset 8 + export const POST: APIRoute = async ({ request, cookies }) => { const originErr = checkOrigin(request); if (originErr) return originErr; @@ -11,40 +20,52 @@ export const POST: APIRoute = async ({ request, cookies }) => { const authErr = checkAuth(cookies); if (authErr) return authErr; - let formData: FormData; + let body: { data?: string; mimeType?: string }; try { - formData = await request.formData(); + body = await request.json(); } catch { return new Response( - JSON.stringify({ error: "Invalid form data" }), + JSON.stringify({ error: "Invalid JSON body" }), { status: 400 } ); } - const file = formData.get("file"); + const { data, mimeType } = body; - if (!(file instanceof File)) { + if (!data || typeof data !== "string") { return new Response( - JSON.stringify({ error: "No file provided" }), + JSON.stringify({ error: "No image data provided" }), { status: 400 } ); } - if (!ALLOWED_TYPES.has(file.type)) { + if (!mimeType || !ALLOWED_TYPES.has(mimeType)) { return new Response( JSON.stringify({ error: "Only PNG, JPEG, and WebP images are accepted" }), { status: 400 } ); } - if (file.size > MAX_IMAGE_SIZE) { + // Decode base64 + let bytes: ArrayBuffer; + try { + const binary = atob(data); + const arr = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) arr[i] = binary.charCodeAt(i); + bytes = arr.buffer; + } catch { return new Response( - JSON.stringify({ error: "Image must be under 5 MB" }), + JSON.stringify({ error: "Invalid base64 data" }), { status: 400 } ); } - const bytes = await file.arrayBuffer(); + if (bytes.byteLength > MAX_IMAGE_SIZE) { + return new Response( + JSON.stringify({ error: "Image must be under 5 MB" }), + { status: 400 } + ); + } // Validate file magic bytes to prevent type spoofing if (bytes.byteLength < 12) { @@ -55,22 +76,15 @@ export const POST: APIRoute = async ({ request, cookies }) => { } const header = new Uint8Array(bytes, 0, 12); - const MAGIC: Record = { - "image/png": [0x89, 0x50, 0x4e, 0x47], - "image/jpeg": [0xff, 0xd8, 0xff], - "image/webp": [0x52, 0x49, 0x46, 0x46], - }; - - const expected = MAGIC[file.type]; + const expected = MAGIC[mimeType]; if (!expected || !expected.every((b, i) => header[i] === b)) { return new Response( JSON.stringify({ error: "File content does not match declared type" }), { status: 400 } ); } - if (file.type === "image/webp") { - const webp = [0x57, 0x45, 0x42, 0x50]; // "WEBP" at offset 8 - if (!webp.every((b, i) => header[8 + i] === b)) { + if (mimeType === "image/webp") { + if (!WEBP_SIG.every((b, i) => header[8 + i] === b)) { return new Response( JSON.stringify({ error: "File content does not match declared type" }), { status: 400 } @@ -86,7 +100,7 @@ export const POST: APIRoute = async ({ request, cookies }) => { { method: "POST", headers: { - "Content-Type": file.type, + "Content-Type": mimeType, Authorization: `Bearer ${accessJwt}`, }, body: bytes, diff --git a/src/pages/write.astro b/src/pages/write.astro index c45885f..f067bf8 100644 --- a/src/pages/write.astro +++ b/src/pages/write.astro @@ -267,13 +267,14 @@ const editBlobs = editEntry?.blobs ?? []; uploadStatus.textContent = "uploading..."; uploadStatus.className = "text-xs text-muted"; - const formData = new FormData(); - formData.append("file", file); - try { + const buf = await file.arrayBuffer(); + const base64 = btoa(String.fromCharCode(...new Uint8Array(buf))); + const res = await fetch("/api/upload-image", { method: "POST", - body: formData, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ data: base64, mimeType: file.type, name: file.name }), }); const data = await res.json();