From faeba81cb44b0d58ceb2525567dc214dccdb4cc2 Mon Sep 17 00:00:00 2001 From: shiwoslj Date: Sat, 7 Dec 2024 02:59:32 +0800 Subject: [PATCH] route.ts --- app/api/webdav/[...path]/route.ts | 351 ++++++++++++++++-------------- 1 file changed, 190 insertions(+), 161 deletions(-) diff --git a/app/api/webdav/[...path]/route.ts b/app/api/webdav/[...path]/route.ts index bb7743bda..59acae9ae 100644 --- a/app/api/webdav/[...path]/route.ts +++ b/app/api/webdav/[...path]/route.ts @@ -1,167 +1,196 @@ import { NextRequest, NextResponse } from "next/server"; -import { STORAGE_KEY, internalAllowedWebDavEndpoints } from "../../../constant"; -import { getServerSideConfig } from "@/app/config/server"; +import path from "path"; -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, - }, - ); - } - - 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: method, - }, - { - status: fetchResult?.status, - statusText: fetchResult?.statusText, - }, - ); - } - - return fetchResult; -} - -export const PUT = handle; -export const GET = handle; -export const OPTIONS = handle; +// WebDAV 服务器端点配置 +const ENDPOINT = process.env.WEBDAV_ENDPOINT || "http://localhost:8080"; export const runtime = "edge"; + +// 重试机制 +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'); +} + +export async function handler( + req: NextRequest, + { params }: { params: { path: string[] } } +) { + try { + // 获取请求方法 + const method = req.method; + + // 记录请求日志 + console.log(`[Proxy Request] ${method} ${req.url}`); + console.log("Request Headers:", Object.fromEntries(req.headers)); + + // 处理 OPTIONS 请求 + 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 + const targetUrlObj = new URL(ENDPOINT); + targetUrlObj.pathname = path.join(targetUrlObj.pathname, ...(params.path || [])); + const targetUrl = targetUrlObj.toString(); + + // 处理请求头 + const headers = new Headers(); + req.headers.forEach((value, key) => { + if (ALLOWED_HEADERS.includes(key.toLowerCase())) { + headers.set(key, value); + } + }); + + // 特殊处理 WebDAV 相关头 + 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 } + ); + } + } + + // 构建fetch选项 + 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); + }); + + // 添加 CORS 头和安全头 + 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 } + ); + } +} + +// 导出支持的 HTTP 方法 +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;