Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 108 additions & 13 deletions httpuploader.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import argparse
import base64
from email.parser import BytesHeaderParser
from email.policy import default as email_policy
import gzip
import hashlib
import io
Expand All @@ -16,7 +18,6 @@
from wsgiref.util import FileWrapper
from urllib.parse import parse_qs
from tempfile import TemporaryFile
from cgi import FieldStorage
from pprint import pformat
from datetime import datetime, timedelta, timezone
from configparser import ConfigParser, ExtendedInterpolation
Expand All @@ -31,6 +32,90 @@
allow = list, download, mkdir, upload
"""


def _get_multipart_boundary(content_type):
headers = BytesHeaderParser(policy=email_policy).parsebytes(
"Content-Type: {}\r\n\r\n".format(content_type).encode("latin-1")
)
boundary = headers.get_param("boundary", header="content-type")
if boundary is None:
return None
return boundary.encode("latin-1")


class _LimitedReader:
def __init__(self, fp, content_length):
self.fp = fp
self.remaining = content_length

def readline(self):
if self.remaining <= 0:
return b""
line = self.fp.readline(min(CHUNKSIZE, self.remaining))
self.remaining -= len(line)
return line


def _multipart_boundary_type(line, boundary):
marker = b"--" + boundary
stripped = line.rstrip(b"\r\n")
if stripped == marker:
return "part"
if stripped == marker + b"--":
return "final"
return None


def _strip_multipart_line_break(line):
if line.endswith(b"\r\n"):
return line[:-2]
if line.endswith(b"\n"):
return line[:-1]
return line


def _skip_to_first_multipart_boundary(reader, boundary):
while True:
line = reader.readline()
if not line:
return True
boundary_type = _multipart_boundary_type(line, boundary)
if boundary_type == "final":
return True
if boundary_type == "part":
return False


def _read_multipart_headers(reader):
lines = []
while True:
line = reader.readline()
if not line or line in (b"\r\n", b"\n"):
break
lines.append(line)
return BytesHeaderParser(policy=email_policy).parsebytes(b"".join(lines))


def _consume_multipart_part(reader, boundary, output=None):
pending = None
while True:
line = reader.readline()
if not line:
if pending and output is not None:
output.write(pending)
return True

boundary_type = _multipart_boundary_type(line, boundary)
if boundary_type:
if pending and output is not None:
output.write(_strip_multipart_line_break(pending))
return boundary_type == "final"

if pending and output is not None:
output.write(pending)
pending = line


LOCAL_TZ = datetime.now(timezone(timedelta(0))).astimezone().tzinfo


Expand Down Expand Up @@ -447,18 +532,28 @@ def checksum(self, path, args):

def upload(self, path, args):
content_type = self.env.get("CONTENT_TYPE", "")
if content_type.startswith("multipart/form-data"):
fs = FieldStorage(fp=self.env["wsgi.input"], environ=self.env)
for key in fs:
if fs[key].file:
pn = path / fs[key].filename
with pn.open("wb") as saved:
while 1:
chunk = fs[key].file.read(CHUNKSIZE)
if len(chunk) > 0:
saved.write(chunk)
else:
break
if content_type.lower().startswith("multipart/form-data"):
boundary = _get_multipart_boundary(content_type)
if boundary is None:
raise HTUPLError(400, "Bad request", "No multipart boundary")

content_length = int(self.env.get("CONTENT_LENGTH", "0"))
if not content_length:
raise HTUPLError(400, "Bad request", "No Content-length header")

reader = _LimitedReader(self.env["wsgi.input"], content_length)
final_boundary = _skip_to_first_multipart_boundary(reader, boundary)
while not final_boundary:
headers = _read_multipart_headers(reader)
filename = headers.get_filename()
if filename:
pn = path / filename
with pn.open("wb") as saved:
final_boundary = _consume_multipart_part(
reader, boundary, saved
)
else:
final_boundary = _consume_multipart_part(reader, boundary)
else:
fp = self.env["wsgi.input"]
content_length = int(self.env.get("CONTENT_LENGTH", "0"))
Expand Down