feat(webdav3): implement proxy download functionality with CORS support
This commit is contained in:
parent
e6f4ecdc29
commit
80bffaf9ad
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@ files/*
|
|||
node_modules/*
|
||||
package-lock.json
|
||||
__pycache__/*
|
||||
api/*
|
||||
71
webdav3.py
71
webdav3.py
|
|
@ -43,7 +43,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
|
|||
from socketserver import ThreadingMixIn
|
||||
import urllib.request, urllib.parse, urllib.error, urllib.parse
|
||||
from time import timezone, strftime, localtime, gmtime
|
||||
import os, sys, re, shutil, struct, threading, uuid, hashlib, mimetypes, base64, socket
|
||||
import os, sys, re, shutil, struct, threading, uuid, hashlib, mimetypes, base64, socket, json
|
||||
|
||||
_ws_lock = threading.Lock()
|
||||
_ws_clients: set = set() # connected WebSocket sockets
|
||||
|
|
@ -541,8 +541,17 @@ class DAVRequestHandler(BaseHTTPRequestHandler):
|
|||
return False
|
||||
|
||||
def do_OPTIONS(self):
|
||||
# CORS preflight for /proxy-download (called by browser before POST)
|
||||
if self.path == '/proxy-download':
|
||||
self.send_response(204)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
self.send_header('Content-Length', '0')
|
||||
self.end_headers()
|
||||
return
|
||||
if self.WebAuth():
|
||||
return
|
||||
return
|
||||
self.send_response(200, DAVRequestHandler.server_version)
|
||||
self.send_header('Allow', 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, LOCK, UNLOCK, MOVE, COPY')
|
||||
self.send_header('Content-length', '0')
|
||||
|
|
@ -936,6 +945,64 @@ class DAVRequestHandler(BaseHTTPRequestHandler):
|
|||
self.send_header('Content-length', '0')
|
||||
self.end_headers()
|
||||
|
||||
# ── Proxy download ─────────────────────────────────────────────────────────
|
||||
# The browser cannot fetch third-party binary URLs that return duplicate
|
||||
# Access-Control-Allow-Origin headers (e.g. Assembly64 /search/bin/).
|
||||
# POST /proxy-download { "url": "...", "dest": "/sd/downloads/file.d64",
|
||||
# "headers": { "Client-Id": "Ultimate", ... } }
|
||||
# The server fetches the URL with Python (no CORS restrictions) and saves
|
||||
# the binary to the filesystem under the WebDAV root.
|
||||
|
||||
def _json_response(self, status: int, data: dict):
|
||||
body = json.dumps(data).encode('utf-8')
|
||||
self.send_response(status)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Content-Length', str(len(body)))
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_POST(self):
|
||||
if self.path != '/proxy-download':
|
||||
self.send_error(404, 'Not found')
|
||||
return
|
||||
try:
|
||||
size = int(self.headers.get('Content-Length', 0))
|
||||
body = self.rfile.read(size)
|
||||
req_data = json.loads(body)
|
||||
url = req_data['url']
|
||||
dest = req_data['dest']
|
||||
extra_headers = req_data.get('headers', {})
|
||||
except Exception as e:
|
||||
self._json_response(400, {'error': f'Bad request: {e}'})
|
||||
return
|
||||
# Map virtual dest path (/sd/downloads/foo.d64) to filesystem path
|
||||
vpath = urllib.parse.unquote(dest).lstrip('/')
|
||||
if '..' in vpath.split('/'):
|
||||
self._json_response(400, {'error': 'Invalid path'})
|
||||
return
|
||||
fspath = os.path.join(self.server.root.fsname, *vpath.split('/'))
|
||||
# Fetch the URL server-side (no browser CORS restrictions apply here)
|
||||
try:
|
||||
req = urllib.request.Request(url, headers=extra_headers)
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
data = resp.read()
|
||||
except Exception as e:
|
||||
_log('PROXY', f'Fetch failed: {url} → {e}')
|
||||
self._json_response(502, {'error': f'Fetch failed: {e}'})
|
||||
return
|
||||
# Save to filesystem, creating directories as needed
|
||||
try:
|
||||
os.makedirs(os.path.dirname(fspath), exist_ok=True)
|
||||
with open(fspath, 'wb') as f:
|
||||
f.write(data)
|
||||
except Exception as e:
|
||||
_log('PROXY', f'Save failed: {fspath} → {e}')
|
||||
self._json_response(500, {'error': f'Save failed: {e}'})
|
||||
return
|
||||
_log('PROXY', f'{url} → {fspath} ({len(data)} bytes)')
|
||||
self._json_response(200, {'ok': True, 'bytes': len(data)})
|
||||
|
||||
def split_path(self, path):
|
||||
"""Splits path string in form '/dir1/dir2/file' into parts"""
|
||||
p = path.split('/')[1:]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user