import { NextRequest, NextResponse } from 'next/server'; import { isPublicHttpUrl } from '@/lib/security/url-guard'; const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB const FETCH_TIMEOUT_MS = 15000; export async function POST(request: NextRequest) { let body: { url?: string }; try { body = await request.json(); } catch { return NextResponse.json({ error: 'Invalid request body' }, { status: 400 }); } const { url } = body; if (!url || typeof url !== 'string') { return NextResponse.json({ error: 'URL is required' }, { status: 400 }); } if (!(await isPublicHttpUrl(url))) { return NextResponse.json({ error: 'Invalid or disallowed URL' }, { status: 400 }); } try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS); const MAX_REDIRECTS = 5; let currentUrl = url; let response: Response | undefined; for (let i = 0; i <= MAX_REDIRECTS; i++) { if (!(await isPublicHttpUrl(currentUrl))) { clearTimeout(timeout); return NextResponse.json({ error: 'Redirect to disallowed URL' }, { status: 400 }); } response = await fetch(currentUrl, { signal: controller.signal, headers: { 'Accept': 'text/calendar, application/ics, text/plain, */*', 'User-Agent': 'JMAP-Webmail/1.0 Calendar-Fetcher', }, redirect: 'manual', }); if (response.status >= 300 && response.status < 400) { const location = response.headers.get('location'); if (!location) { clearTimeout(timeout); return NextResponse.json({ error: 'Redirect without Location header' }, { status: 502 }); } currentUrl = new URL(location, currentUrl).toString(); continue; } break; } clearTimeout(timeout); if (!response || !response.ok) { return NextResponse.json( { error: `Remote server returned ${response?.status ?? 'unknown'}` }, { status: 502 } ); } const contentLength = response.headers.get('content-length'); if (contentLength && parseInt(contentLength) > MAX_RESPONSE_SIZE) { return NextResponse.json({ error: 'File too large' }, { status: 413 }); } const buffer = await response.arrayBuffer(); if (buffer.byteLength > MAX_RESPONSE_SIZE) { return NextResponse.json({ error: 'File too large' }, { status: 413 }); } return new NextResponse(buffer, { status: 200, headers: { 'Content-Type': 'text/calendar', 'Content-Length': buffer.byteLength.toString(), }, }); } catch (error: unknown) { if (error instanceof Error && error.name === 'AbortError') { return NextResponse.json({ error: 'Request timed out' }, { status: 504 }); } return NextResponse.json({ error: 'Failed to fetch calendar' }, { status: 502 }); } }