diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts index bb7743bda40..c5446edcda6 100644 --- a/app/api/webdav/[...path]/route.ts +++ b/app/api/webdav/[...path]/route.ts @@ -1,167 +1,187 @@ import { NextRequest, NextResponse } from "next/server"; -import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant"; -import { getServerSideConfig } from "@/app/config/server"; -const config = getServerSideConfig(); - -const mergedAllowedWebDavEndpoints = [ - ...internalAllowedWebDavEndpoints, - ...config.allowedWebDavEndpoints, -].filter((domain) => Boolean(domain.trim())); - -const normalizeUrl = (url: string) => { - try { - return new URL(url); - } catch (err) { - return null; - } +// 配置常量 +const ALLOWED_METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PROPFIND", "MKCOL"]; +const ALLOWED_HEADERS = [ + "authorization", + "content-type", + "accept", + "depth", + "destination", + "overwrite", + "content-length" +]; +const CORS_HEADERS = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": ALLOWED_METHODS.join(","), + "Access-Control-Allow-Headers": ALLOWED_HEADERS.join(", "), + "Access-Control-Max-Age": "86400", }; +const TIMEOUT = 30000; // 30 seconds -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"); - let 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, - }, - ); - } +// WebDAV 服务器端点配置 +const ENDPOINT = process.env.WEBDAV_ENDPOINT || "http://localhost:8080"; - 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, - }, - ); - } +export const runtime = "edge"; - // 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, - }, - ); - } +// 路径拼接函数 +function joinPaths(...parts: string[]): string { + return parts + .map(part => part.replace(/^\/+|\/+$/g, '')) + .filter(Boolean) + .join('/'); +} - // 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, - }, - ); +// 重试机制 +async function makeRequest(url: string, options: RequestInit, retries = 3) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), TIMEOUT); + + for (let i = 0; i < retries; i++) { + try { + const response = await fetch(url, { + ...options, + signal: controller.signal + }); + clearTimeout(timeoutId); + return response; + } catch (error) { + console.error(`Attempt ${i + 1} failed:`, error); + if (error.name === 'AbortError') { + throw new Error('Request Timeout'); + } + if (i === retries - 1) throw error; + await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); + } } + throw new Error('Max retries reached'); +} - 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; - +export async function handler( + req: NextRequest, + { params }: { params: { path: string[] } } +) { try { - fetchResult = await fetch(targetUrl, fetchOptions); - } finally { - console.log( - "[Any Proxy]", - targetUrl, - { - method: method, - }, - { - status: fetchResult?.status, - statusText: fetchResult?.statusText, - }, + const method = req.method; + + console.log(`[Proxy Request] ${method} ${req.url}`); + console.log("Request Headers:", Object.fromEntries(req.headers)); + + if (method === "OPTIONS") { + return new NextResponse(null, { + status: 204, + headers: CORS_HEADERS, + }); + } + + if (!ALLOWED_METHODS.includes(method)) { + return NextResponse.json( + { error: "Method Not Allowed" }, + { status: 405, headers: CORS_HEADERS } + ); + } + + // 构建目标 URL(使用自定义的 joinPaths 函数替代 path.join) + const targetUrlObj = new URL(ENDPOINT); + const pathSegments = params.path || []; + targetUrlObj.pathname = joinPaths(targetUrlObj.pathname, ...pathSegments); + const targetUrl = targetUrlObj.toString(); + + // 其余代码保持不变... + const headers = new Headers(); + req.headers.forEach((value, key) => { + if (ALLOWED_HEADERS.includes(key.toLowerCase())) { + headers.set(key, value); + } + }); + + const depth = req.headers.get('depth'); + if (depth) { + headers.set('depth', depth); + } + + const authHeader = req.headers.get('authorization'); + if (authHeader) { + headers.set('authorization', authHeader); + } + + let requestBody: BodyInit | null = null; + if (["POST", "PUT"].includes(method)) { + try { + const contentLength = req.headers.get('content-length'); + if (contentLength && parseInt(contentLength) > 10 * 1024 * 1024) { + requestBody = req.body; + headers.set('transfer-encoding', 'chunked'); + } else { + requestBody = await req.blob(); + } + } catch (error) { + console.error("[Request Body Error]", error); + return NextResponse.json( + { error: "Invalid Request Body" }, + { status: 400, headers: CORS_HEADERS } + ); + } + } + + const fetchOptions: RequestInit = { + method, + headers, + body: requestBody, + redirect: "manual", + cache: 'no-store', + next: { + revalidate: 0 + } + }; + + let response: Response; + try { + console.log(`[Proxy Forward] ${method} ${targetUrl}`); + response = await makeRequest(targetUrl, fetchOptions); + } catch (error) { + console.error("[Proxy Error]", error); + const status = error.message === 'Request Timeout' ? 504 : 500; + return NextResponse.json( + { error: error.message || "Internal Server Error" }, + { status, headers: CORS_HEADERS } + ); + } + + const responseHeaders = new Headers(response.headers); + ["set-cookie", "server"].forEach((header) => { + responseHeaders.delete(header); + }); + + Object.entries({ + ...CORS_HEADERS, + "Content-Security-Policy": "upgrade-insecure-requests", + "Strict-Transport-Security": "max-age=31536000; includeSubDomains" + }).forEach(([key, value]) => { + responseHeaders.set(key, value); + }); + + console.log(`[Proxy Response] ${response.status} ${response.statusText}`); + console.log("Response Headers:", Object.fromEntries(responseHeaders)); + + return new NextResponse(response.body, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders, + }); + } catch (error) { + console.error("[Global Error]", error); + return NextResponse.json( + { error: "Internal Server Error", details: error.message }, + { status: 500, headers: CORS_HEADERS } ); } - - return fetchResult; } -export const PUT = handle; -export const GET = handle; -export const OPTIONS = handle; - -export const runtime = "edge"; +export const GET = handler; +export const POST = handler; +export const PUT = handler; +export const DELETE = handler; +export const OPTIONS = handler; +export const PROPFIND = handler; +export const MKCOL = handler;