diff --git a/.gitignore b/.gitignore index 71a0957..72eaddc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ files/* node_modules/* package-lock.json __pycache__/* +api/* \ No newline at end of file diff --git a/webdav3.py b/webdav3.py index 86a708c..b600bad 100644 --- a/webdav3.py +++ b/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:]