# 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/.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 bytes to " # # 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/., and returns a # plain-text URL in the bashupload-compatible style: # https://superbeelavaman.onthewifi.com/usercontent/?path=. # # 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= 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()