Several changes I should have committed sooner, they would be better
with seperate commits but I never got around to it
This commit is contained in:
@@ -13,7 +13,7 @@ import subprocess
|
||||
|
||||
running = True
|
||||
|
||||
ALLOWED_METHODS = {"GET", "POST", "HEAD"}
|
||||
ALLOWED_METHODS = {"GET", "POST", "HEAD", "PUT"}
|
||||
|
||||
script_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||
webroot_real = os.path.realpath(script_directory + "/webroot")
|
||||
@@ -47,7 +47,12 @@ def is_newer_than(path: str, date_str: str) -> bool:
|
||||
def recv_full(sock, timeout=5):
|
||||
"""Receive a full HTTP request, not just the first 1024 bytes.
|
||||
Reads until the end of headers, then reads Content-Length more bytes
|
||||
for the body if present."""
|
||||
for the body if present.
|
||||
|
||||
Returns (header_text, body_bytes). Headers are decoded as text (safe,
|
||||
always ASCII/UTF-8 in practice); the body is returned as raw bytes so
|
||||
binary uploads (images, gzip, etc.) aren't corrupted by a lossy text
|
||||
decode. Callers that want the body as text can decode it themselves."""
|
||||
sock.settimeout(timeout)
|
||||
data = b""
|
||||
try:
|
||||
@@ -58,15 +63,15 @@ def recv_full(sock, timeout=5):
|
||||
data += chunk
|
||||
# Split off headers to check Content-Length
|
||||
if b"\r\n\r\n" in data:
|
||||
header_part, body_so_far = data.split(b"\r\n\r\n", 1)
|
||||
header_bytes, body_start = data.split(b"\r\n\r\n", 1)
|
||||
elif b"\n\n" in data:
|
||||
header_part, body_so_far = data.split(b"\n\n", 1)
|
||||
header_bytes, body_start = data.split(b"\n\n", 1)
|
||||
else:
|
||||
return data.decode(errors="replace")
|
||||
return data.decode(errors="replace"), b""
|
||||
|
||||
# Check for Content-Length and read remaining body bytes
|
||||
content_length = 0
|
||||
for line in header_part.decode(errors="replace").splitlines():
|
||||
for line in header_bytes.decode(errors="replace").splitlines():
|
||||
if line.lower().startswith("content-length:"):
|
||||
try:
|
||||
content_length = int(line.split(":", 1)[1].strip())
|
||||
@@ -74,15 +79,25 @@ def recv_full(sock, timeout=5):
|
||||
pass
|
||||
break
|
||||
|
||||
# bytearray, not bytes: bytes += in a loop recopies the whole
|
||||
# buffer on every chunk (quadratic time/memory as it grows).
|
||||
# bytearray.extend() appends in place, so this stays linear even
|
||||
# for multi-GB bodies.
|
||||
body_so_far = bytearray(body_start)
|
||||
while len(body_so_far) < content_length:
|
||||
chunk = sock.recv(4096)
|
||||
chunk = sock.recv(65536)
|
||||
if not chunk:
|
||||
break
|
||||
body_so_far += chunk
|
||||
body_so_far.extend(chunk)
|
||||
|
||||
return (header_part + b"\r\n\r\n" + body_so_far).decode(errors="replace")
|
||||
# Trim to Content-Length: guards against over-reads and, when no
|
||||
# Content-Length was sent at all (content_length stays 0), drops
|
||||
# any stray bytes so a bodyless request doesn't pick up leftovers.
|
||||
body_so_far = bytes(body_so_far[:content_length])
|
||||
|
||||
return header_bytes.decode(errors="replace"), body_so_far
|
||||
except TimeoutError:
|
||||
return data.decode(errors="replace")
|
||||
return data.decode(errors="replace"), b""
|
||||
|
||||
blocklist_lock = threading.Lock()
|
||||
|
||||
@@ -116,10 +131,13 @@ def handle_connection(connectionSocket, addr):
|
||||
fullRequestPath = ""
|
||||
contentType = ""
|
||||
|
||||
message = recv_full(connectionSocket)
|
||||
header_text, requestBody = recv_full(connectionSocket)
|
||||
connectionSocket.settimeout(None)
|
||||
try:
|
||||
messages = message.replace('\r\n', '\n').split('\n')
|
||||
# requestBody is raw bytes (unmodified body, e.g. for uploads).
|
||||
# `messages` below is built only from headers, never the body, so
|
||||
# it stays text-safe even when the body isn't valid UTF-8.
|
||||
messages = header_text.replace('\r\n', '\n').split('\n')
|
||||
|
||||
requestheaders = []
|
||||
for i in range(len(messages)):
|
||||
@@ -242,7 +260,7 @@ def handle_connection(connectionSocket, addr):
|
||||
responsecontent = b"<h1>405 Method Not Allowed</h1>"
|
||||
fullRequestPath = ""
|
||||
contentType = "text/html"
|
||||
responseHeaders += b"Allow: GET, POST, HEAD\n"
|
||||
responseHeaders += b"Allow: GET, POST, HEAD, PUT\n"
|
||||
else:
|
||||
# URL-decode the path before sanitizing to catch encoded traversal sequences
|
||||
raw_path = (messages[0].split()[1] + "?").split("?")[0]
|
||||
@@ -276,32 +294,13 @@ def handle_connection(connectionSocket, addr):
|
||||
argumentsDict[j[0]] = j[1]
|
||||
|
||||
if responseType == "200 OK":
|
||||
if os.path.exists(fullRequestPath):
|
||||
if os.path.isdir(fullRequestPath):
|
||||
indexFiles = ["/index.html", "/index.py"]
|
||||
for i in indexFiles:
|
||||
if os.path.exists(fullRequestPath + i):
|
||||
fullRequestPath += i
|
||||
break
|
||||
else:
|
||||
fullRequestPath = script_directory + "/webroot/Scripts/listFiles.py"
|
||||
if not os.path.exists(fullRequestPath):
|
||||
responseType = "404 Not Found"
|
||||
fullRequestPath = script_directory + "/webroot/404.html"
|
||||
|
||||
plainresponse = False
|
||||
extensions = [".html", ".json", ".png", ".gif", ".ico", ".jpg", ".css", ".txt", ".ttf", ".mp4"]
|
||||
contentTypes = ["text/html", "text/json", "image/png", "image/gif", "image/x-icon", "image/jpeg", "text/css", "text/plain", "application/octet-stream", "video/mp4"]
|
||||
for i in range(len(extensions)):
|
||||
if fullRequestPath.endswith(extensions[i]):
|
||||
plainresponse = True
|
||||
contentType = contentTypes[i]
|
||||
break
|
||||
|
||||
if plainresponse:
|
||||
with open(fullRequestPath, "rb") as file:
|
||||
responsecontent = file.read()
|
||||
elif fullRequestPath.endswith(".py"):
|
||||
if requestType == "PUT":
|
||||
# Raw uploads (e.g. `curl <server> -T localfile`) always route
|
||||
# straight to the upload handler. The requested path is
|
||||
# client-controlled (curl fills it in from the local filename)
|
||||
# and generally won't correspond to a real file in webroot, so
|
||||
# the normal exists()/extension routing below doesn't apply.
|
||||
fullRequestPath = script_directory + "/webroot/upload/index.py"
|
||||
with open(fullRequestPath) as script:
|
||||
local_vars = locals()
|
||||
exec(script.read(), globals(), local_vars)
|
||||
@@ -310,9 +309,43 @@ def handle_connection(connectionSocket, addr):
|
||||
responseHeaders = local_vars.get("responseHeaders", responseHeaders)
|
||||
contentType = local_vars.get("contentType", contentType)
|
||||
else:
|
||||
responseType = "415 Unsupported Media Type"
|
||||
contentType = ""
|
||||
responsecontent = b""
|
||||
if os.path.exists(fullRequestPath):
|
||||
if os.path.isdir(fullRequestPath):
|
||||
indexFiles = ["/index.html", "/index.py"]
|
||||
for i in indexFiles:
|
||||
if os.path.exists(fullRequestPath + i):
|
||||
fullRequestPath += i
|
||||
break
|
||||
else:
|
||||
fullRequestPath = script_directory + "/webroot/Scripts/listFiles.py"
|
||||
if not os.path.exists(fullRequestPath):
|
||||
responseType = "404 Not Found"
|
||||
fullRequestPath = script_directory + "/webroot/404.html"
|
||||
|
||||
plainresponse = False
|
||||
extensions = [ ".html", ".json", ".png", ".gif", ".ico", ".jpg", ".css", ".txt", ".ttf", ".mp4", ".gz", ".img"]
|
||||
contentTypes = ["text/html", "text/json", "image/png", "image/gif", "image/x-icon", "image/jpeg", "text/css", "text/plain", "application/octet-stream", "video/mp4", "application/gzip", "application/octet-stream"]
|
||||
for i in range(len(extensions)):
|
||||
if fullRequestPath.endswith(extensions[i]):
|
||||
plainresponse = True
|
||||
contentType = contentTypes[i]
|
||||
break
|
||||
|
||||
if plainresponse:
|
||||
with open(fullRequestPath, "rb") as file:
|
||||
responsecontent = file.read()
|
||||
elif fullRequestPath.endswith(".py"):
|
||||
with open(fullRequestPath) as script:
|
||||
local_vars = locals()
|
||||
exec(script.read(), globals(), local_vars)
|
||||
responsecontent = local_vars.get("responsecontent", responsecontent)
|
||||
responseType = local_vars.get("responseType", responseType)
|
||||
responseHeaders = local_vars.get("responseHeaders", responseHeaders)
|
||||
contentType = local_vars.get("contentType", contentType)
|
||||
else:
|
||||
responseType = "415 Unsupported Media Type"
|
||||
contentType = ""
|
||||
responsecontent = b""
|
||||
|
||||
except Exception as e:
|
||||
print("error: ", e)
|
||||
|
||||
Reference in New Issue
Block a user