diff --git a/server.py b/server.py index cd9fd07..6ca4de9 100644 --- a/server.py +++ b/server.py @@ -1,6 +1,7 @@ """HTTP server and request handler.""" import email.parser +import email.policy import hashlib import hmac import http.server @@ -28,6 +29,16 @@ def file_type(path: Path) -> str: return 'other' +def _disposition_filename(name: str) -> str: + """Content-Disposition filename params: ASCII fallback + RFC 5987 UTF-8 form. + + HTTP headers are latin-1 only, so a non-ASCII name must be percent-encoded. + """ + ascii_name = name.encode("ascii", "replace").decode("ascii") + ascii_name = ascii_name.replace("\\", "_").replace('"', "_").replace("?", "_") + return f'filename="{ascii_name}"; filename*=UTF-8\'\'{urllib.parse.quote(name, safe="")}' + + class ShareServer(http.server.ThreadingHTTPServer): def __init__(self, addr, handler, *, upload_dir: Path, html: bytes, ssl_ctx: ssl.SSLContext | None = None, token: str = "", token_is_hash: bool = False): super().__init__(addr, handler) @@ -208,10 +219,9 @@ fetch('auth',{method:'POST',headers:{'Content-Type':'application/json'},body:JSO self.send_error(404) return ct = mimetypes.guess_type(safe.name)[0] or "application/octet-stream" - fname = safe.name.replace("\\", "\\\\").replace('"', '\\"') self.send_response(200) self.send_header("Content-Type", ct) - self.send_header("Content-Disposition", f'attachment; filename="{fname}"') + self.send_header("Content-Disposition", f"attachment; {_disposition_filename(safe.name)}") self.send_header("Content-Length", str(st.st_size)) self.send_header("Connection", "close") self.end_headers() @@ -274,7 +284,9 @@ fetch('auth',{method:'POST',headers:{'Content-Type':'application/json'},body:JSO def _save_parts(self, content_type: str, body: bytes) -> int: header = f"Content-Type: {content_type}\r\n\r\n".encode() - msg = email.parser.BytesParser().parsebytes(header + body) + # policy=HTTP decodes headers as UTF-8; the default compat32 policy mangles + # non-ASCII filenames into replacement characters. + msg = email.parser.BytesParser(policy=email.policy.HTTP).parsebytes(header + body) count = 0 for part in msg.walk(): fname = part.get_filename()