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:
+3
-1
@@ -1,4 +1,6 @@
|
||||
database.txt
|
||||
database.txt.bak
|
||||
*.bak
|
||||
__pycache__/
|
||||
*/videos/*
|
||||
userdata/*
|
||||
*server.log
|
||||
|
||||
@@ -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,6 +294,21 @@ def handle_connection(connectionSocket, addr):
|
||||
argumentsDict[j[0]] = j[1]
|
||||
|
||||
if responseType == "200 OK":
|
||||
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)
|
||||
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:
|
||||
if os.path.exists(fullRequestPath):
|
||||
if os.path.isdir(fullRequestPath):
|
||||
indexFiles = ["/index.html", "/index.py"]
|
||||
@@ -290,8 +323,8 @@ def handle_connection(connectionSocket, addr):
|
||||
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"]
|
||||
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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -18,7 +18,8 @@
|
||||
<footer class="gray">
|
||||
<p style="margin: 0px; display:flex; align-items: center; justify-content: space-between;">
|
||||
<span style="display: flex; align-items: center; margin-right:10px;">(c) 2026 Logan Joy-Koer (SuperbeeLavaman)
|
||||
<a style="margin-left: 10px" href="http://wii-linux.org/"><img style="display: block;" src="Images/wl88x31.gif" alt="Wii-Linux NOW!" class=no-print></a></span>
|
||||
<a style="margin-left: 10px" href="http://wii-linux.org/"><img style="display: block;" src="Images/wl88x31.gif" alt="Wii-Linux NOW!" class=no-print></a>
|
||||
<img style="margin-left: 0px; display: block;" src="Images/wl88x31mw.gif" alt="Made with Wii-Linux"></span>
|
||||
<span style="float: left;">This page last updated: <!--updateddate--></span>
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<br>
|
||||
</article>
|
||||
<footer>
|
||||
<p style="margin: 0px; display:flex; align-items:center;"><span style="margin-right: 10px;">(c) 2026 Logan Joy-Koer (SuperbeeLavaman)</span><a style="display: inline-block;" href="http://wii-linux.org/"><img style="margin: 0px; display: block;" src="Images/wl88x31.gif" alt="Wii-Linux NOW!"></a></p>
|
||||
<p style="margin: 0px; display:flex; align-items:center;"><span style="margin-right: 10px;">(c) 2026 Logan Joy-Koer (SuperbeeLavaman)</span><a style="display: inline-block;" href="http://wii-linux.org/"><img style="margin: 0px; display: block;" src="Images/wl88x31.gif" alt="Wii-Linux NOW!"></a><img style="margin: 0px; display: block;" src="Images/wl88x31mw.gif" alt="Made with Wii-Linux"></p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Title: Projects<br>
|
||||
Icon: /favicon.ico<br>
|
||||
Style: /styles/newLight.css<br>
|
||||
Description: My recent projects.
|
||||
Description: My recent projects.<br>
|
||||
Author: SuperbeeLavaman<br>
|
||||
Content:
|
||||
<div class="article-content-block">
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
contentType = "text/html"
|
||||
responseType = "999 \033[31mH\033[91me\033[33ml\033[32ml\033[34mo\033[35m,\033[95m \033[31mW\033[91mo\033[33mr\033[32ml\033[34md\033[35m!\033[0m"
|
||||
page = "<html><head><title>hehehe</title></head><body><p>hellorld</p></body></html>"
|
||||
responsecontent = page.encode()
|
||||
+6
-6
@@ -43,13 +43,13 @@ if "p" in argumentsDict:
|
||||
session_user = ""
|
||||
|
||||
if session_status == "success":
|
||||
pfp_base = os.path.realpath(script_directory + "/webroot/Images/profileimages")
|
||||
pfp_real_gif = os.path.realpath(script_directory + "/webroot/Images/profileimages/" + session_user + ".gif")
|
||||
pfp_real_jpg = os.path.realpath(script_directory + "/webroot/Images/profileimages/" + session_user + ".jpg")
|
||||
pfp_base = os.path.realpath(script_directory + "/userdata/profileimages")
|
||||
pfp_real_gif = os.path.realpath(script_directory + "/userdata/profileimages/" + session_user + ".gif")
|
||||
pfp_real_jpg = os.path.realpath(script_directory + "/userdata/profileimages/" + session_user + ".jpg")
|
||||
if pfp_real_gif.startswith(pfp_base + os.sep) and os.path.exists(pfp_real_gif):
|
||||
pfp_src = "/Images/profileimages/" + html.escape(session_user) + ".gif"
|
||||
pfp_src = "/usercontent/?p=profileimages/" + html.escape(session_user) + ".gif"
|
||||
elif pfp_real_jpg.startswith(pfp_base + os.sep) and os.path.exists(pfp_real_jpg):
|
||||
pfp_src = "/Images/profileimages/" + html.escape(session_user) + ".jpg"
|
||||
pfp_src = "/usercontent/?p=profileimages/" + html.escape(session_user) + ".jpg"
|
||||
else:
|
||||
pfp_src = "/Images/default_pfp.gif"
|
||||
userbar = (
|
||||
@@ -97,7 +97,7 @@ if "p" in argumentsDict:
|
||||
head = "<title>" + title + """</title>
|
||||
<link rel=\"shortcut icon\" type=\"image/x-icon\" href=\"""" + icon + """\">
|
||||
<link rel=\"icon\" type=\"image/x-icon\" href=\"""" + icon + """\">
|
||||
<link rel=\"stylesheet\" type=\"text/css\" href=\"/styles/newCommon.css\">
|
||||
""" + ("<link rel=\"stylesheet\" type=\"text/css\" href=\"/styles/newCommon.css\">" if version=="new" else "") +"""
|
||||
<link rel=\"stylesheet\" type=\"text/css\" href=\"""" + html.escape(style) + """\">
|
||||
<meta charset=\"UTF-8\">
|
||||
<meta name=\"description\" content=\"""" + description + """\">
|
||||
|
||||
@@ -19,7 +19,7 @@ if "p" in argumentsDict:
|
||||
listText = "<li>Folder: <a href=\"/raw/?p=" + html.escape(os.path.dirname(path)) + "\">..</a></li>"
|
||||
for i in filesList:
|
||||
temp = "File: "
|
||||
if os.path.isdir(filePath + i.name):
|
||||
if os.path.isdir(filePath + '/' + i.name):
|
||||
temp = "Folder: "
|
||||
link = path.rstrip("/") + "/" + i.name
|
||||
listText += ("<li>" + temp + "<a href=\"/raw/?p=" + html.escape(link) + "\">" + html.escape(i.name) + "</a></li>")
|
||||
@@ -27,7 +27,8 @@ if "p" in argumentsDict:
|
||||
responsecontent = page.encode()
|
||||
else:
|
||||
contentType = magic.Magic(mime=True).from_file(filePath)
|
||||
if contentType in (contentTypes + ["application/x-python-code", "text/x-python"]):
|
||||
print(contentType)
|
||||
if contentType in (contentTypes + ["application/x-python-code", "text/x-python", "text/x-script.python"]):
|
||||
if contentType.startswith("text"):
|
||||
contentType = "text/plain"
|
||||
responsecontent = open(filePath, "rb").read()
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<!--
|
||||
Profile picture upload widget.
|
||||
Drop into a Content: block.
|
||||
|
||||
Client-side, this only checks that the file is *some* readable image -
|
||||
no type or dimension restriction, since the server now accepts any
|
||||
recognizable image format, center-crops it to square, downscales to
|
||||
32x32, and always transcodes to GIF. The username is derived from the
|
||||
session cookie server-side, never from the client.
|
||||
-->
|
||||
<div id="pfp-upload">
|
||||
<img id="pfp-preview" src="/Images/default_pfp.gif" alt="Profile picture preview"
|
||||
style="width:64px; height:64px; image-rendering:pixelated; display:block; margin-bottom:8px; border:1px solid #888;">
|
||||
|
||||
<input type="file" id="pfp-file" accept="image/*">
|
||||
|
||||
<p id="pfp-status" style="margin-top:8px;"></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
const fileInput = document.getElementById("pfp-file");
|
||||
const preview = document.getElementById("pfp-preview");
|
||||
const status = document.getElementById("pfp-status");
|
||||
|
||||
function setStatus(msg, isError) {
|
||||
status.textContent = msg;
|
||||
status.style.color = isError ? "#c0392b" : "#2c7a2c";
|
||||
}
|
||||
|
||||
fileInput.addEventListener("change", async () => {
|
||||
const file = fileInput.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setStatus("Checking image...", false);
|
||||
|
||||
let objectUrl;
|
||||
try {
|
||||
// Just confirm the browser can decode it as an image at all - the
|
||||
// actual format whitelist, cropping, and resizing happens server-side.
|
||||
await new Promise((resolve, reject) => {
|
||||
objectUrl = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(new Error("That doesn't look like a readable image."));
|
||||
img.src = objectUrl;
|
||||
});
|
||||
|
||||
// Looks good locally - show the preview immediately.
|
||||
preview.src = objectUrl;
|
||||
|
||||
setStatus("Uploading...", false);
|
||||
|
||||
const response = await fetch("/upload?name=profileimages", {
|
||||
method: "POST",
|
||||
credentials: "same-origin", // send the session cookie
|
||||
body: file,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
setStatus("Upload failed: " + text, true);
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus("Profile picture updated!", false);
|
||||
} catch (err) {
|
||||
setStatus(err.message || "Something went wrong.", true);
|
||||
} finally {
|
||||
fileInput.value = "";
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,204 @@
|
||||
# webroot/upload/index.py
|
||||
#
|
||||
# Three supported flows:
|
||||
#
|
||||
# 1. POST /upload?name=profileimages
|
||||
# Profile picture upload. The username is NEVER taken from the
|
||||
# client - it's derived from the validated session cookie, so one
|
||||
# logged-in user can't overwrite another user's picture. The body
|
||||
# can be any recognizable image format; it's center-cropped to
|
||||
# square, downscaled to 32x32, and always saved as a GIF (chosen
|
||||
# over JPEG for compatibility with old browsers, and because JPEG
|
||||
# compression artifacts are rough at 32x32 anyway) at
|
||||
# userdata/profileimages/<username>.gif, overwriting any previous
|
||||
# picture. Non-image or unreadable input -> 415.
|
||||
#
|
||||
# 2. POST /upload?name=somefilename.txt
|
||||
# Saves the request body to userdata/somefilename.txt exactly as named.
|
||||
# Response body: "Uploaded <n> bytes to <filename>"
|
||||
#
|
||||
# 3. PUT with no explicit name, e.g.:
|
||||
# curl https://superbeelavaman.onthewifi.com -s -T /tmp/logs.txt.gz
|
||||
# curl has no path in the URL, so it auto-fills the request path with
|
||||
# the local filename ("PUT /logs.txt.gz"). server.py routes ALL PUT
|
||||
# requests here regardless of path (see the PUT branch in server.py),
|
||||
# since that path is client-controlled and won't exist on disk.
|
||||
# This flow hashes the body, keeps the extension (if any) from the
|
||||
# implicit filename, saves as userdata/<hash>.<ext>, and returns a
|
||||
# plain-text URL in the bashupload-compatible style:
|
||||
# https://superbeelavaman.onthewifi.com/usercontent/?path=<hash>.<ext>
|
||||
#
|
||||
# The request body (requestBody, set by server.py's recv_full()) is raw
|
||||
# bytes, not a lossily-decoded string, so binary files (images, .gz, etc.)
|
||||
# round-trip correctly.
|
||||
#
|
||||
# Flow 1 requires Pillow: pip install Pillow --break-system-packages
|
||||
import hashlib
|
||||
import io
|
||||
from PIL import Image as PILImage
|
||||
|
||||
PROFILE_IMAGE_SIZE = 32
|
||||
|
||||
USERCONTENT_URL_BASE = "https://superbeelavaman.onthewifi.com/usercontent/"
|
||||
MAX_UPLOAD_BYTES = 4 * 1024 * 1024 * 1024 # 4 GB
|
||||
HASH_LENGTH = 16 # hex chars taken from sha256; raise for lower collision odds
|
||||
|
||||
userdata_real = os.path.realpath(script_directory + "/userdata")
|
||||
|
||||
if requestType not in ("POST", "PUT"):
|
||||
responseType = "405 Method Not Allowed"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"405 Method Not Allowed. Use POST /upload?name=<filename> or PUT with -T.\n"
|
||||
responseHeaders += b"Allow: POST, PUT\n"
|
||||
|
||||
else:
|
||||
explicit_name = urllib.parse.unquote(argumentsDict.get("name", "")).strip()
|
||||
|
||||
try:
|
||||
content_length = int(ClientContentLength) if ClientContentLength else 0
|
||||
except ValueError:
|
||||
content_length = 0
|
||||
|
||||
if content_length <= 0:
|
||||
responseType = "400 Bad Request"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"400 Bad Request: missing or empty request body.\n"
|
||||
|
||||
elif content_length > MAX_UPLOAD_BYTES:
|
||||
responseType = "413 Payload Too Large"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"413 Payload Too Large.\n"
|
||||
|
||||
else:
|
||||
# requestBody is the raw, unmodified body as bytes (see recv_full()
|
||||
# / handle_connection() in server.py) - safe for binary uploads.
|
||||
body_bytes = requestBody
|
||||
|
||||
if len(body_bytes) > MAX_UPLOAD_BYTES:
|
||||
responseType = "413 Payload Too Large"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"413 Payload Too Large.\n"
|
||||
|
||||
elif explicit_name == "profileimages":
|
||||
# --- Flow 1: profile picture upload ---
|
||||
# Username comes from the session cookie, never from the
|
||||
# client, so this can't be used to overwrite someone else's
|
||||
# picture no matter what the request claims.
|
||||
session_token = Cookies.get("session", "")
|
||||
if session_token:
|
||||
session_status, session_user = userManagement.validate_session(session_token)
|
||||
else:
|
||||
session_status, session_user = "fail", ""
|
||||
|
||||
if session_status != "success":
|
||||
responseType = "401 Unauthorized"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"401 Unauthorized: log in before uploading a profile picture.\n"
|
||||
else:
|
||||
try:
|
||||
source_image = PILImage.open(io.BytesIO(body_bytes))
|
||||
source_image.load() # force full decode now, so bad/corrupt data 415s here
|
||||
except Exception:
|
||||
source_image = None
|
||||
|
||||
if source_image is None:
|
||||
responseType = "415 Unsupported Media Type"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"415 Unsupported Media Type: not a recognized image format.\n"
|
||||
else:
|
||||
# Center-crop to square, then downscale to 32x32.
|
||||
w, h = source_image.size
|
||||
side = min(w, h)
|
||||
left = (w - side) // 2
|
||||
top = (h - side) // 2
|
||||
cropped = source_image.crop((left, top, left + side, top + side))
|
||||
resized = cropped.resize(
|
||||
(PROFILE_IMAGE_SIZE, PROFILE_IMAGE_SIZE), PILImage.LANCZOS
|
||||
)
|
||||
|
||||
# GIF has no real alpha channel (just binary transparency),
|
||||
# so flatten any transparency onto white before quantizing
|
||||
# down to a GIF-safe palette.
|
||||
if resized.mode in ("RGBA", "LA") or (
|
||||
resized.mode == "P" and "transparency" in resized.info
|
||||
):
|
||||
rgba = resized.convert("RGBA")
|
||||
flattened = PILImage.new("RGB", rgba.size, (255, 255, 255))
|
||||
flattened.paste(rgba, mask=rgba.split()[3])
|
||||
else:
|
||||
flattened = resized.convert("RGB")
|
||||
|
||||
gif_image = flattened.convert("P", palette=PILImage.ADAPTIVE)
|
||||
|
||||
pfp_dir = os.path.realpath(userdata_real + "/profileimages")
|
||||
os.makedirs(pfp_dir, exist_ok=True)
|
||||
|
||||
# Defense in depth: session_user should already be a
|
||||
# clean username, but don't let a stray path separator
|
||||
# in it escape pfp_dir.
|
||||
safe_username = re.sub(r"[^A-Za-z0-9_-]", "", session_user)
|
||||
target_path = os.path.realpath(pfp_dir + "/" + safe_username + ".gif")
|
||||
|
||||
if not safe_username or not target_path.startswith(pfp_dir + os.sep):
|
||||
responseType = "403 Forbidden"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"403 Forbidden.\n"
|
||||
else:
|
||||
gif_image.save(target_path, format="GIF")
|
||||
responseType = "200 OK"
|
||||
contentType = "text/plain"
|
||||
responsecontent = (
|
||||
"Profile picture updated: profileimages/" + safe_username + ".gif\n"
|
||||
).encode()
|
||||
|
||||
elif explicit_name:
|
||||
# --- Flow 2: explicit ?name=... (unchanged prior behavior) ---
|
||||
filename = os.path.basename(explicit_name)
|
||||
if (
|
||||
filename in (".", "..")
|
||||
or filename.startswith(".")
|
||||
or not re.fullmatch(r"[A-Za-z0-9._-]{1,128}", filename)
|
||||
):
|
||||
responseType = "400 Bad Request"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"400 Bad Request: invalid 'name' parameter.\n"
|
||||
else:
|
||||
os.makedirs(userdata_real, exist_ok=True)
|
||||
target_path = os.path.realpath(userdata_real + "/" + filename)
|
||||
if not target_path.startswith(userdata_real + os.sep):
|
||||
responseType = "403 Forbidden"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"403 Forbidden.\n"
|
||||
else:
|
||||
with open(target_path, "wb") as f:
|
||||
f.write(body_bytes)
|
||||
responseType = "200 OK"
|
||||
contentType = "text/plain"
|
||||
responsecontent = (
|
||||
"Uploaded " + str(len(body_bytes)) + " bytes to " + filename + "\n"
|
||||
).encode()
|
||||
|
||||
else:
|
||||
# --- Flow 3: no name given -> hash-name it, return a URL ---
|
||||
# Pull an extension (if any) from curl's implicit filename, i.e.
|
||||
# the request path it auto-fills for `-T`, e.g. "/logs.txt.gz".
|
||||
implicit_name = os.path.basename(requestPath.rstrip("/"))
|
||||
_, ext = os.path.splitext(implicit_name)
|
||||
if not re.fullmatch(r"\.[A-Za-z0-9]{1,16}", ext or ""):
|
||||
ext = ""
|
||||
|
||||
file_hash = hashlib.sha256(body_bytes).hexdigest()[:HASH_LENGTH]
|
||||
filename = file_hash + ext
|
||||
|
||||
os.makedirs(userdata_real, exist_ok=True)
|
||||
target_path = os.path.realpath(userdata_real + "/" + filename)
|
||||
if not target_path.startswith(userdata_real + os.sep):
|
||||
responseType = "403 Forbidden"
|
||||
contentType = "text/plain"
|
||||
responsecontent = b"403 Forbidden.\n"
|
||||
else:
|
||||
with open(target_path, "wb") as f:
|
||||
f.write(body_bytes)
|
||||
responseType = "200 OK"
|
||||
contentType = "text/plain"
|
||||
responsecontent = (USERCONTENT_URL_BASE + "?p=" + filename + "\n").encode()
|
||||
@@ -0,0 +1,45 @@
|
||||
page = ""
|
||||
responseType = "200 OK"
|
||||
contentType = "text/html"
|
||||
|
||||
if "p" in argumentsDict:
|
||||
path = urllib.parse.unquote(argumentsDict["p"])
|
||||
|
||||
userdata_real = os.path.realpath(script_directory + "/userdata")
|
||||
filePath = os.path.realpath(script_directory + "/userdata/" + path)
|
||||
if not filePath.startswith(userdata_real + os.sep) and filePath != userdata_real:
|
||||
responseType = "403 Forbidden"
|
||||
contentType = "text/html"
|
||||
responsecontent = b"<h1>403 Forbidden</h1>"
|
||||
elif os.path.exists(filePath):
|
||||
if os.path.isdir(filePath):
|
||||
filesList = list(os.scandir(filePath))
|
||||
page = open(script_directory + "/webroot/Templates/fileList.html").read()
|
||||
page = page.replace("<!--dir-->", html.escape(path))
|
||||
listText = "<li>Folder: <a href=\"/raw/?p=" + html.escape(os.path.dirname(path)) + "\">..</a></li>"
|
||||
for i in filesList:
|
||||
temp = "File: "
|
||||
if os.path.isdir(filePath + i.name):
|
||||
temp = "Folder: "
|
||||
link = path.rstrip("/") + "/" + i.name
|
||||
listText += ("<li>" + temp + "<a href=\"/raw/?p=" + html.escape(link) + "\">" + html.escape(i.name) + "</a></li>")
|
||||
page = page.replace("<!--fileList-->", listText)
|
||||
responsecontent = page.encode()
|
||||
else:
|
||||
contentType = magic.Magic(mime=True).from_file(filePath)
|
||||
if contentType in (contentTypes + ["application/x-python-code", "text/x-python"]):
|
||||
if contentType.startswith("text"):
|
||||
contentType = "text/plain"
|
||||
responsecontent = open(filePath, "rb").read()
|
||||
else:
|
||||
responseType = "415 Unsupported Media Type"
|
||||
contentType = ""
|
||||
responsecontent = b""
|
||||
else:
|
||||
responseType = "404 Not Found"
|
||||
fullRequestPath = script_directory + "/webroot/404.html"
|
||||
responsecontent = open(fullRequestPath, "rb").read()
|
||||
else:
|
||||
responseType = "400 Bad Request"
|
||||
fullRequestPath = script_directory + "/webroot/400.html"
|
||||
responsecontent = open(fullRequestPath, "rb").read()
|
||||
Reference in New Issue
Block a user