Rebrand the Rust client crate (coven/ → hh/, package+binary "hack-house"), README, CLI strings, and branch (coven → hack-house). Gitea repo renamed cmd-chat → hack-house to match. Crypto/server logic unchanged; selftest + golden-vector test still green, binary is now `hack-house`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
from __future__ import annotations
|
|
|
|
import email.utils
|
|
import unicodedata
|
|
|
|
from typing import NamedTuple
|
|
from urllib.parse import unquote
|
|
|
|
from sanic.headers import parse_content_header
|
|
from sanic.log import logger
|
|
|
|
from .parameters import RequestParameters
|
|
|
|
|
|
class File(NamedTuple):
|
|
"""Model for defining a file.
|
|
|
|
It is a `namedtuple`, therefore you can iterate over the object, or
|
|
access the parameters by name.
|
|
|
|
Args:
|
|
type (str, optional): The mimetype, defaults to "text/plain".
|
|
body (bytes): Bytes of the file.
|
|
name (str): The filename.
|
|
"""
|
|
|
|
type: str
|
|
body: bytes
|
|
name: str
|
|
|
|
|
|
def parse_multipart_form(body, boundary):
|
|
"""Parse a request body and returns fields and files
|
|
|
|
Args:
|
|
body (bytes): Bytes request body.
|
|
boundary (bytes): Bytes multipart boundary.
|
|
|
|
Returns:
|
|
tuple[RequestParameters, RequestParameters]: A tuple containing fields and files as `RequestParameters`.
|
|
""" # noqa: E501
|
|
files = {}
|
|
fields = {}
|
|
|
|
form_parts = body.split(boundary)
|
|
for form_part in form_parts[1:-1]:
|
|
file_name = None
|
|
content_type = "text/plain"
|
|
content_charset = "utf-8"
|
|
field_name = None
|
|
line_index = 2
|
|
line_end_index = 0
|
|
while not line_end_index == -1:
|
|
line_end_index = form_part.find(b"\r\n", line_index)
|
|
form_line = form_part[line_index:line_end_index].decode("utf-8")
|
|
line_index = line_end_index + 2
|
|
|
|
if not form_line:
|
|
break
|
|
|
|
colon_index = form_line.index(":")
|
|
idx = colon_index + 2
|
|
form_header_field = form_line[0:colon_index].lower()
|
|
form_header_value, form_parameters = parse_content_header(
|
|
form_line[idx:]
|
|
)
|
|
|
|
if form_header_field == "content-disposition":
|
|
field_name = form_parameters.get("name")
|
|
file_name = form_parameters.get("filename")
|
|
|
|
# non-ASCII filenames in RFC2231, "filename*" format
|
|
if file_name is None and form_parameters.get("filename*"):
|
|
encoding, _, value = email.utils.decode_rfc2231(
|
|
form_parameters["filename*"]
|
|
)
|
|
file_name = unquote(value, encoding=encoding)
|
|
|
|
# Normalize to NFC (Apple MacOS/iOS send NFD)
|
|
# Notes:
|
|
# - No effect for Windows, Linux or Android clients which
|
|
# already send NFC
|
|
# - Python open() is tricky (creates files in NFC no matter
|
|
# which form you use)
|
|
if file_name is not None:
|
|
file_name = unicodedata.normalize("NFC", file_name)
|
|
|
|
elif form_header_field == "content-type":
|
|
content_type = form_header_value
|
|
content_charset = form_parameters.get("charset", "utf-8")
|
|
|
|
if field_name:
|
|
post_data = form_part[line_index:-4]
|
|
if file_name is None:
|
|
value = post_data.decode(content_charset)
|
|
if field_name in fields:
|
|
fields[field_name].append(value)
|
|
else:
|
|
fields[field_name] = [value]
|
|
else:
|
|
form_file = File(
|
|
type=content_type, name=file_name, body=post_data
|
|
)
|
|
if field_name in files:
|
|
files[field_name].append(form_file)
|
|
else:
|
|
files[field_name] = [form_file]
|
|
else:
|
|
logger.debug(
|
|
"Form-data field does not have a 'name' parameter "
|
|
"in the Content-Disposition header"
|
|
)
|
|
|
|
return RequestParameters(fields), RequestParameters(files)
|