169 lines
3.8 KiB
TypeScript
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';
|