You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			168 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
		
		
			
		
	
	
			168 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			TypeScript
		
	
| 
											9 months ago
										 | 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; | ||
|  |   } | ||
|  | }; | ||
|  | 
 | ||
|  | 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; | ||
|  | 
 | ||
|  | export const runtime = "edge"; |