feat(webdav3): implement proxy download functionality with CORS support

This commit is contained in:
Jaime Idolpx 2026-06-15 01:54:22 -04:00
parent e6f4ecdc29
commit 80bffaf9ad
2 changed files with 70 additions and 2 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ files/*
node_modules/*
package-lock.json
__pycache__/*
api/*

View File

@ -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:]