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:
Logan Joy-Koer
2026-08-23 21:00:22 -07:00
parent 7e9816e52d
commit 853004d433
17 changed files with 419 additions and 54 deletions
+75 -42
View File
@@ -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)