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/* node_modules/*
package-lock.json package-lock.json
__pycache__/* __pycache__/*
api/*

View File

@ -43,7 +43,7 @@ from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn from socketserver import ThreadingMixIn
import urllib.request, urllib.parse, urllib.error, urllib.parse import urllib.request, urllib.parse, urllib.error, urllib.parse
from time import timezone, strftime, localtime, gmtime 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_lock = threading.Lock()
_ws_clients: set = set() # connected WebSocket sockets _ws_clients: set = set() # connected WebSocket sockets
@ -541,8 +541,17 @@ class DAVRequestHandler(BaseHTTPRequestHandler):
return False return False
def do_OPTIONS(self): 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(): if self.WebAuth():
return return
self.send_response(200, DAVRequestHandler.server_version) 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('Allow', 'GET, HEAD, POST, PUT, DELETE, OPTIONS, PROPFIND, PROPPATCH, MKCOL, LOCK, UNLOCK, MOVE, COPY')
self.send_header('Content-length', '0') self.send_header('Content-length', '0')
@ -936,6 +945,64 @@ class DAVRequestHandler(BaseHTTPRequestHandler):
self.send_header('Content-length', '0') self.send_header('Content-length', '0')
self.end_headers() 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): def split_path(self, path):
"""Splits path string in form '/dir1/dir2/file' into parts""" """Splits path string in form '/dir1/dir2/file' into parts"""
p = path.split('/')[1:] p = path.split('/')[1:]