fix(encoding): handle non-ASCII filenames on upload and download
Cyrillic names arrived as question marks and were stored that way. Upload: BytesParser defaults to the compat32 policy, which wraps a non-ASCII Content-Disposition in a Header with the unknown-8bit charset, so get_filename() returned one replacement character per UTF-8 byte and the mangled name was written to disk. Parse with policy=HTTP, which decodes headers as UTF-8. Download: send_header() encodes latin-1 strict, so a Cyrillic name raised UnicodeEncodeError. Emit both Content-Disposition forms per RFC 5987 — an ASCII fallback plus percent-encoded filename*.
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user