ChatGPT-Next-Web/app/api/webdav/[...path]/route.ts

169 lines
3.8 KiB
TypeScript

import type { NextRequest } from 'next/server';
import { getServerSideConfig } from '@/app/config/server';
import { NextResponse } from 'next/server';
import { internalAllowedWebDavEndpoints, STORAGE_KEY } from '../../../constant';
const config = getServerSideConfig();
const mergedAllowedWebDavEndpoints = [
...internalAllowedWebDavEndpoints,
...config.allowedWebDavEndpoints,
].filter(domain => Boolean(domain.trim()));
function normalizeUrl(url: string) {
try {
return new URL(url);
} catch (err) {
return null;
}
}
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
if (req.method === 'OPTIONS') {
return NextResponse.json({ body: 'OK' }, { status: 200 });
}
const folder = STORAGE_KEY;
const fileName = `${folder}/backup.json`;
const requestUrl = new URL(req.url);
let endpoint = requestUrl.searchParams.get('endpoint');
const proxy_method = requestUrl.searchParams.get('proxy_method') || req.method;
// Validate the endpoint to prevent potential SSRF attacks
if (
!endpoint
|| !mergedAllowedWebDavEndpoints.some((allowedEndpoint) => {
const normalizedAllowedEndpoint = normalizeUrl(allowedEndpoint);
const normalizedEndpoint = normalizeUrl(endpoint as string);
return (
normalizedEndpoint
&& normalizedEndpoint.hostname === normalizedAllowedEndpoint?.hostname
&& normalizedEndpoint.pathname.startsWith(
normalizedAllowedEndpoint.pathname,
)
);
})
) {
return NextResponse.json(
{
error: true,
msg: 'Invalid endpoint',
},
{
status: 400,
},
);
}
if (!endpoint?.endsWith('/')) {
endpoint += '/';
}
const endpointPath = params.path.join('/');
const targetPath = `${endpoint}${endpointPath}`;
// only allow MKCOL, GET, PUT
if (
proxy_method !== 'MKCOL'
&& proxy_method !== 'GET'
&& proxy_method !== 'PUT'
) {
return NextResponse.json(
{
error: true,
msg: `you are not allowed to request ${targetPath}`,
},
{
status: 403,
},
);
}
// for MKCOL request, only allow request ${folder}
if (proxy_method === 'MKCOL' && !targetPath.endsWith(folder)) {
return NextResponse.json(
{
error: true,
msg: `you are not allowed to request ${targetPath}`,
},
{
status: 403,
},
);
}
// for GET request, only allow request ending with fileName
if (proxy_method === 'GET' && !targetPath.endsWith(fileName)) {
return NextResponse.json(
{
error: true,
msg: `you are not allowed to request ${targetPath}`,
},
{
status: 403,
},
);
}
// for PUT request, only allow request ending with fileName
if (proxy_method === 'PUT' && !targetPath.endsWith(fileName)) {
return NextResponse.json(
{
error: true,
msg: `you are not allowed to request ${targetPath}`,
},
{
status: 403,
},
);
}
const targetUrl = targetPath;
const method = proxy_method || req.method;
const shouldNotHaveBody = ['get', 'head'].includes(
method?.toLowerCase() ?? '',
);
const fetchOptions: RequestInit = {
headers: {
authorization: req.headers.get('authorization') ?? '',
},
body: shouldNotHaveBody ? null : req.body,
redirect: 'manual',
method,
// @ts-ignore
duplex: 'half',
};
let fetchResult;
try {
fetchResult = await fetch(targetUrl, fetchOptions);
} finally {
console.log(
'[Any Proxy]',
targetUrl,
{
method,
},
{
status: fetchResult?.status,
statusText: fetchResult?.statusText,
},
);
}
return fetchResult;
}
export const PUT = handle;
export const GET = handle;
export const OPTIONS = handle;
export const runtime = 'edge';