py-utils/src/byteb4rb1e/utils/http/server/__init__.py
2025-06-20 20:33:37 +02:00

129 lines
4.1 KiB
Python

from dataclasses import dataclass
from http.server import SimpleHTTPRequestHandler
from byteb4rb1e.utils.io import ChunksIO
@dataclass
class HandlerOptions:
"""configuration options of the HTTP POST method handler
"""
max_chunk_size: int = ChunksIO.max_chunk_size
# default (in memory) buffer size in bytes (from KiB) of the sliding buffer
# reading from the pure (unchunked) client read stream
buffer_size: int = 512 * 1024
@dataclass
class ServerOptions:
"""configuration options of the HTTP server
"""
handler: HandlerOptions
hostname: str = ''
port: int = 8000
class MultipartUploadHandler(SimpleHTTPRequestHandler):
"""Simple, yet compliant HTTP/1.0 MIME Multipart Upload Handler
Implementation of a RFC1341 & RFC7578 compliant server for handling
multipart uploads.
This is meant as a utility for debugging MIME Multipart upload clients
Support for:
- client 'Expect' header
- chunked transfer-encoding
"""
media_subtypes = [
'mixed',
'alternative',
'parallel',
'digest',
'form-data'
]
def do_POST(self):
h_content_type = self.headers.get('Content-Type')
h_expect = self.headers.get('Expect')
h_transfer_encoding = self.headers.get('Transfer-Encoding')
if h_content_type == None:
self.send_error(400, 'Missing \'Content-Type\' header')
content_type_segments = [s.strip() for s in h_content_type.split(';')]
try:
media_type, media_subtype = content_type_segments[0].split('/', 1)
except IndexError:
self.send_error(
400,
'no value was supplied for \'Content-Type\' header'
)
except ValueError:
self.send_error(
400,
'unable to parse media type and subtype from ' +
'first (semicolon-delimited) segment of \'Content-Type\' ' +
f'header value: {content_type_segments[0]}'
)
if media_type != 'multipart':
self.send_error(
400,
'unsupported media type in \'Content-Type\' header value: ' +
f'{media_type}'
)
elif media_subtype not in self.media_subtypes:
self.send_error(
400,
'unsupported media sub-type in \'Content-Type\' header value: ' +
f'{media_type}. Must be one of {", ".join(self.media_subtypes)}'
)
if h_transfer_encoding:
if h_transfer_encoding != 'chunked':
self.send_error(
501,
f'unable to handle transfer-encoding: {h_transfer_encoding}'
)
content_type_params = {v[0].strip():v[1].strip() for v in [
s.split('=', 1) for s in content_type_segments[1:]
]}
boundary = content_type_params.get('boundary', '')
boundary_len = len(boundary)
if boundary == '':
self.send_error(
400,
'missing \'boundary\' parameter in \'Content-Type\' header field'
)
elif boundary_len > 70:
self.send_error(
400,
'\'boundary\' parameter value in \'Content-Type\' too long. ' +
f'Is {boundary_len} characters long, must be less than 70.'
)
del content_type_params['boundary']
content_type_params_keys = content_type_params.keys()
if len(content_type_params_keys) > 0:
self.send_error(
400,
'None other than \'boundary\' parameter in \'Content-Type\'' +
'header expected. Also received ' +
'{\', \'.join(content_type_param_keys)}'
)
self.handle_expect_100()
# read the first 4-bytes of the body to check if it has a preamble
# indication
# well great... curl is not RFC 1341 compliant. And RFC 1341 is asking
# for tolerance towards non-compliant clients...
self.send_response(200, 'OK')
self.end_headers()