Files
SRCmail/app/api/fetch-ical/route.ts
T

93 lines
2.8 KiB
TypeScript

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 });
}
}