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/*
|
node_modules/*
|
||||||
package-lock.json
|
package-lock.json
|
||||||
__pycache__/*
|
__pycache__/*
|
||||||
|
api/*
|
||||||
71
webdav3.py
71
webdav3.py
|
|
@ -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:]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user