y'know I probably should have uploaded this to gh sooner
@@ -0,0 +1,4 @@
|
|||||||
|
database.txt
|
||||||
|
database.txt.bak
|
||||||
|
__pycache__/
|
||||||
|
*/videos/*
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
In Visual Studio Code, go to File > Open Workspace From File..., then select the file "htmlproject.code-workspace"
|
||||||
|
Then, in the corner when prompted to install extensions, select "Show Recommendations" and install all 3 Python
|
||||||
|
extensions, then install one of the browser debug extensions, and edit the file ".vscode/launch.json" to start the
|
||||||
|
correct browser debug extension.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
Googlebot
|
||||||
|
Bingbot
|
||||||
|
GPTBot
|
||||||
|
Claude-Web
|
||||||
|
ClaudeBot
|
||||||
|
CCBot
|
||||||
|
FacebookBot
|
||||||
|
Applebot-Extended
|
||||||
|
Applebot
|
||||||
|
Diffbot
|
||||||
|
Googlebot-Image
|
||||||
|
Googlebot-Video
|
||||||
|
Slurp
|
||||||
|
DuckDuckBot
|
||||||
|
Baiduspider
|
||||||
|
YandexBot
|
||||||
|
HTTrack
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
class Database:
|
||||||
|
def __init__(self, file):
|
||||||
|
self.database = {}
|
||||||
|
self.file = file
|
||||||
|
self.backup = file + ".bak"
|
||||||
|
if not os.path.exists(file):
|
||||||
|
open(self.file, "wt").write("{}")
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
try:
|
||||||
|
with open(self.file, "rt") as f:
|
||||||
|
self.database = json.loads(f.read())
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
print(f"[db] Failed to read {self.file}, attempting fallback to {self.backup}")
|
||||||
|
try:
|
||||||
|
with open(self.backup, "rt") as f:
|
||||||
|
self.database = json.loads(f.read())
|
||||||
|
print(f"[db] Fallback successful, restoring {self.file} from backup")
|
||||||
|
shutil.copy2(self.backup, self.file)
|
||||||
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
|
print(f"[db] Backup read also failed: {e} — starting with empty database")
|
||||||
|
self.database = {}
|
||||||
|
|
||||||
|
def write(self):
|
||||||
|
if os.path.exists(self.file):
|
||||||
|
shutil.copy2(self.file, self.backup)
|
||||||
|
with open(self.file, "wt") as f:
|
||||||
|
f.write(json.dumps(self.database))
|
||||||
|
|
||||||
|
def database(file):
|
||||||
|
return Database(file)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
python-magic
|
||||||
|
urllib3
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
from socket import *
|
||||||
|
import os, sys, random
|
||||||
|
import math, datetime
|
||||||
|
import re, magic
|
||||||
|
import html
|
||||||
|
import urllib
|
||||||
|
import urllib.parse
|
||||||
|
import userManagement
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import calendar
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
running = True
|
||||||
|
|
||||||
|
ALLOWED_METHODS = {"GET", "POST", "HEAD"}
|
||||||
|
|
||||||
|
script_directory = os.path.dirname(os.path.abspath(sys.argv[0]))
|
||||||
|
webroot_real = os.path.realpath(script_directory + "/webroot")
|
||||||
|
|
||||||
|
trying = True
|
||||||
|
while trying:
|
||||||
|
try:
|
||||||
|
serverSocket = socket(AF_INET, SOCK_STREAM)
|
||||||
|
serverSocket.bind(('', 8123))
|
||||||
|
serverSocket.listen(128)
|
||||||
|
trying = False
|
||||||
|
except Exception as e:
|
||||||
|
print("Waiting for socket to be free...")
|
||||||
|
print(e)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
recentConnections = []
|
||||||
|
recentConnectionsExtended = []
|
||||||
|
recentConnections_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_modified(path: str) -> str:
|
||||||
|
mtime = os.path.getmtime(path)
|
||||||
|
return time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(mtime))
|
||||||
|
|
||||||
|
def is_newer_than(path: str, date_str: str) -> bool:
|
||||||
|
mtime = os.path.getmtime(path)
|
||||||
|
parsed = time.strptime(date_str, "%a, %d %b %Y %H:%M:%S GMT")
|
||||||
|
return mtime > calendar.timegm(parsed)
|
||||||
|
|
||||||
|
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."""
|
||||||
|
sock.settimeout(timeout)
|
||||||
|
data = b""
|
||||||
|
try:
|
||||||
|
while b"\r\n\r\n" not in data and b"\n\n" not in data:
|
||||||
|
chunk = sock.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
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)
|
||||||
|
elif b"\n\n" in data:
|
||||||
|
header_part, body_so_far = data.split(b"\n\n", 1)
|
||||||
|
else:
|
||||||
|
return data.decode(errors="replace")
|
||||||
|
|
||||||
|
# Check for Content-Length and read remaining body bytes
|
||||||
|
content_length = 0
|
||||||
|
for line in header_part.decode(errors="replace").splitlines():
|
||||||
|
if line.lower().startswith("content-length:"):
|
||||||
|
try:
|
||||||
|
content_length = int(line.split(":", 1)[1].strip())
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
break
|
||||||
|
|
||||||
|
while len(body_so_far) < content_length:
|
||||||
|
chunk = sock.recv(4096)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
body_so_far += chunk
|
||||||
|
|
||||||
|
return (header_part + b"\r\n\r\n" + body_so_far).decode(errors="replace")
|
||||||
|
except TimeoutError:
|
||||||
|
return data.decode(errors="replace")
|
||||||
|
|
||||||
|
blocklist_lock = threading.Lock()
|
||||||
|
|
||||||
|
def addtoblocklist(IP):
|
||||||
|
with blocklist_lock:
|
||||||
|
with open("/etc/nginx/blocklist.txt", "a") as blocklistfile:
|
||||||
|
blocklistfile.write(IP + " 1;\n")
|
||||||
|
subprocess.run(["sudo","systemctl","reload","nginx"])
|
||||||
|
|
||||||
|
blocked_agents_lock = threading.Lock()
|
||||||
|
|
||||||
|
def load_blocked_agents(path: str) -> set:
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
return {line.strip().lower() for line in f if line.strip() and not line.startswith("#")}
|
||||||
|
except FileNotFoundError:
|
||||||
|
return set()
|
||||||
|
|
||||||
|
blocked_agents_file = script_directory + "/blocked_agents.txt"
|
||||||
|
blocked_agents = load_blocked_agents(blocked_agents_file)
|
||||||
|
|
||||||
|
def handle_connection(connectionSocket, addr):
|
||||||
|
global recentConnections
|
||||||
|
global recentConnectionsExtended
|
||||||
|
|
||||||
|
sourceIP, sourcePort = addr
|
||||||
|
|
||||||
|
responseType = "200 OK"
|
||||||
|
responseHeaders = b""
|
||||||
|
responsecontent = b""
|
||||||
|
fullRequestPath = ""
|
||||||
|
contentType = ""
|
||||||
|
|
||||||
|
message = recv_full(connectionSocket)
|
||||||
|
connectionSocket.settimeout(None)
|
||||||
|
try:
|
||||||
|
messages = message.replace('\r\n', '\n').split('\n')
|
||||||
|
|
||||||
|
requestheaders = []
|
||||||
|
for i in range(len(messages)):
|
||||||
|
if i > 0:
|
||||||
|
if len(messages[i]) == 0:
|
||||||
|
break
|
||||||
|
requestheaders.append(messages[i])
|
||||||
|
ClientTargetHost = ''
|
||||||
|
ClientUserAgent = ''
|
||||||
|
ClientContentLength = ''
|
||||||
|
ClientAcceptTypes = ''
|
||||||
|
ClientAcceptLanguage = ''
|
||||||
|
ClientAcceptEncoding = ''
|
||||||
|
ClientConnectionType = ''
|
||||||
|
ClientUseHTTPS = ''
|
||||||
|
ClientSecFetchDest = ''
|
||||||
|
ClientSecFetchMode = ''
|
||||||
|
ClientSecFetchSite = ''
|
||||||
|
ClientSecFetchUser = ''
|
||||||
|
XForwardedFor = ''
|
||||||
|
IfModifiedSince = ''
|
||||||
|
Cookies = {}
|
||||||
|
|
||||||
|
for i in requestheaders:
|
||||||
|
if i.startswith("Host: "):
|
||||||
|
ClientTargetHost = i[6:]
|
||||||
|
elif i.startswith("User-Agent: "):
|
||||||
|
ClientUserAgent = i[12:]
|
||||||
|
elif i.startswith("Content-Length: "):
|
||||||
|
ClientContentLength = i[16:]
|
||||||
|
elif i.startswith("Accept: "):
|
||||||
|
ClientAcceptTypes = i[8:]
|
||||||
|
elif i.startswith("Accept-Language: "):
|
||||||
|
ClientAcceptLanguage = i[17:]
|
||||||
|
elif i.startswith("Accept-Encoding: "):
|
||||||
|
ClientAcceptEncoding = i[17:]
|
||||||
|
elif i.startswith("Connection: "):
|
||||||
|
ClientConnectionType = i[12:]
|
||||||
|
elif i.startswith("Upgrade-Insecure-Requests: "):
|
||||||
|
ClientUseHTTPS = i[27:]
|
||||||
|
elif i.startswith("Sec-Fetch-Dest: "):
|
||||||
|
ClientSecFetchDest = i[16:]
|
||||||
|
elif i.startswith("Sec-Fetch-Mode: "):
|
||||||
|
ClientSecFetchMode = i[16:]
|
||||||
|
elif i.startswith("Sec-Fetch-Site: "):
|
||||||
|
ClientSecFetchSite = i[16:]
|
||||||
|
elif i.startswith("Sec-Fetch-User: "):
|
||||||
|
ClientSecFetchUser = i[16:]
|
||||||
|
elif i.startswith("X-Forwarded-For: "):
|
||||||
|
XForwardedFor = i[17:]
|
||||||
|
elif i.startswith("If-Modified-Since: "):
|
||||||
|
IfModifiedSince = i[19:]
|
||||||
|
elif i.startswith("Cookie: "):
|
||||||
|
# Parse cookie header correctly into a flat dict
|
||||||
|
cookie_str = i[8:]
|
||||||
|
for pair in cookie_str.split("; "):
|
||||||
|
k = pair.split("=", 1)
|
||||||
|
if len(k) == 2:
|
||||||
|
Cookies[k[0].strip()] = k[1].strip()
|
||||||
|
|
||||||
|
if XForwardedFor == '':
|
||||||
|
sourceIPvisual = sourceIP
|
||||||
|
else:
|
||||||
|
sourceIPvisual = XForwardedFor.strip()
|
||||||
|
|
||||||
|
print(sourceIPvisual + ":" + str(sourcePort) + " [" + ClientUserAgent + "] requested \"" + messages[0] + "\"")
|
||||||
|
if any(sub in messages[0] for sub in [".env","php",".git","cgi","SDK/webLanguage"]):
|
||||||
|
addtoblocklist(sourceIPvisual)
|
||||||
|
return
|
||||||
|
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
with recentConnections_lock:
|
||||||
|
recentConnections.append([sourceIPvisual, now])
|
||||||
|
recentConnectionsExtended.append([sourceIPvisual, now])
|
||||||
|
recentConnections = [
|
||||||
|
entry for entry in recentConnections
|
||||||
|
if now <= entry[1] + datetime.timedelta(seconds=60)
|
||||||
|
]
|
||||||
|
recentConnectionsExtended = [
|
||||||
|
entry for entry in recentConnectionsExtended
|
||||||
|
if now <= entry[1] + datetime.timedelta(seconds=200)
|
||||||
|
]
|
||||||
|
rateLimitCount = {}
|
||||||
|
rateLimitCountExtended = {}
|
||||||
|
for entry in recentConnections:
|
||||||
|
rateLimitCount[entry[0]] = rateLimitCount.get(entry[0], 0) + 1
|
||||||
|
for entry in recentConnectionsExtended:
|
||||||
|
rateLimitCountExtended[entry[0]] = rateLimitCountExtended.get(entry[0], 0) + 1
|
||||||
|
|
||||||
|
print(rateLimitCount)
|
||||||
|
|
||||||
|
if rateLimitCountExtended[sourceIPvisual] >= 200:
|
||||||
|
addtoblocklist(sourceIPvisual)
|
||||||
|
return
|
||||||
|
|
||||||
|
elif rateLimitCount[sourceIPvisual] >= 100:
|
||||||
|
responseType = "429 Too Many Requests (please stop)"
|
||||||
|
responsecontent = b"429 Too Many Requests.\nPlease stop.\n"
|
||||||
|
responseHeaders += b"Retry-After: 9999999999999999999 \n"
|
||||||
|
|
||||||
|
elif rateLimitCount[sourceIPvisual] >= 50:
|
||||||
|
responseType = "429 Too Many Requests"
|
||||||
|
responsecontent = open(script_directory + "/webroot/429.html", "rb").read()
|
||||||
|
fullRequestPath = script_directory + "/webroot/429.html"
|
||||||
|
contentType = "text/html"
|
||||||
|
responseHeaders += b"Retry-After: 60\n"
|
||||||
|
|
||||||
|
elif len(messages[0]) < 10:
|
||||||
|
responseType = "400 Bad Request"
|
||||||
|
responsecontent = open(script_directory + "/webroot/400.html", "rb").read()
|
||||||
|
fullRequestPath = script_directory + "/webroot/400.html"
|
||||||
|
contentType = "text/html"
|
||||||
|
|
||||||
|
else:
|
||||||
|
requestType = messages[0].split()[0]
|
||||||
|
|
||||||
|
# Validate HTTP method
|
||||||
|
if requestType not in ALLOWED_METHODS:
|
||||||
|
responseType = "405 Method Not Allowed"
|
||||||
|
responsecontent = b"<h1>405 Method Not Allowed</h1>"
|
||||||
|
fullRequestPath = ""
|
||||||
|
contentType = "text/html"
|
||||||
|
responseHeaders += b"Allow: GET, POST, HEAD\n"
|
||||||
|
else:
|
||||||
|
# URL-decode the path before sanitizing to catch encoded traversal sequences
|
||||||
|
raw_path = (messages[0].split()[1] + "?").split("?")[0]
|
||||||
|
decoded_path = urllib.parse.unquote(raw_path)
|
||||||
|
|
||||||
|
# Normalize and build the candidate path, then realpath-check it
|
||||||
|
candidate_path = os.path.realpath(script_directory + "/webroot" + decoded_path)
|
||||||
|
|
||||||
|
with blocked_agents_lock:
|
||||||
|
agent_blocked = any(agent in ClientUserAgent.lower() for agent in blocked_agents)
|
||||||
|
if agent_blocked and decoded_path != "/robots.txt":
|
||||||
|
responseType = "403 Forbidden"
|
||||||
|
responsecontent = b"<h1>403 Forbidden</h1>"
|
||||||
|
contentType = "text/html"
|
||||||
|
|
||||||
|
if not candidate_path.startswith(webroot_real + os.sep) and candidate_path != webroot_real:
|
||||||
|
responseType = "403 Forbidden"
|
||||||
|
responsecontent = b"<h1>403 Forbidden</h1>"
|
||||||
|
fullRequestPath = ""
|
||||||
|
contentType = "text/html"
|
||||||
|
else:
|
||||||
|
fullRequestPath = candidate_path
|
||||||
|
requestPath = decoded_path # exposed to exec'd scripts via script_env
|
||||||
|
|
||||||
|
argumentsRaw = (messages[0].split()[1] + "?").split("?")[1]
|
||||||
|
arguments = argumentsRaw.split("&")
|
||||||
|
argumentsDict = {}
|
||||||
|
for i in arguments:
|
||||||
|
j = i.split("=")
|
||||||
|
if len(j) == 2:
|
||||||
|
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"):
|
||||||
|
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)
|
||||||
|
responseType = "500 Internal Server Error"
|
||||||
|
fullRequestPath = script_directory + "/webroot/500.html"
|
||||||
|
contentType = "text/html"
|
||||||
|
if os.path.exists(fullRequestPath):
|
||||||
|
with open(fullRequestPath, "rb") as file:
|
||||||
|
responsecontent = file.read()
|
||||||
|
else:
|
||||||
|
responsecontent = ("<h1 style=\"color: #a00;\">Error 500: Internal Server Error.</h1><br><p>Additionally, While handling that error, we encountered error 404. (page not found!)<br>Error: " + str(e) + "</p>").encode()
|
||||||
|
|
||||||
|
print("Serving " + fullRequestPath + " to " + sourceIPvisual + ":" + str(sourcePort))
|
||||||
|
|
||||||
|
if contentType != "":
|
||||||
|
if contentType[:5] == "image":
|
||||||
|
responseHeaders += ("Last-Modified: " + get_last_modified(fullRequestPath) + "\n").encode()
|
||||||
|
print(IfModifiedSince)
|
||||||
|
if IfModifiedSince != '':
|
||||||
|
try:
|
||||||
|
if is_newer_than(fullRequestPath, IfModifiedSince):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
responsecontent = b''
|
||||||
|
responseType = "304 Not Modified"
|
||||||
|
except Exception as e:
|
||||||
|
print("excepted: ")
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
responseHeaders += ("Content-Length: " + str(len(responsecontent)) + '\n').encode()
|
||||||
|
responseHeaders += ("Content-Type: " + contentType + "\n").encode()
|
||||||
|
responseHeaders += b"Server: Xbox 360\n"
|
||||||
|
|
||||||
|
response = ("HTTP/1.1 " + responseType + "\n").encode()
|
||||||
|
response += responseHeaders
|
||||||
|
response += b"\n"
|
||||||
|
response += responsecontent
|
||||||
|
|
||||||
|
numchunks = math.ceil(len(response) / 1024) if len(response) > 0 else 1
|
||||||
|
responsechunks = []
|
||||||
|
for i in range(numchunks):
|
||||||
|
responsechunks.append(response[(1024 * i):(1024 * i + 1024)])
|
||||||
|
try:
|
||||||
|
transferStarted = datetime.datetime.now()
|
||||||
|
lastSentTime = datetime.datetime.now()
|
||||||
|
for i in range(len(responsechunks)):
|
||||||
|
connectionSocket.send(responsechunks[i])
|
||||||
|
if datetime.datetime.now() >= transferStarted + datetime.timedelta(seconds=10):
|
||||||
|
if datetime.datetime.now() >= lastSentTime + datetime.timedelta(seconds=0.5):
|
||||||
|
lastSentTime = datetime.datetime.now()
|
||||||
|
print("Sending chunk " + str(i + 1) + " of " + str(numchunks) + " to " + sourceIP + ":" + str(sourcePort) + " for file \"" + fullRequestPath + "\"")
|
||||||
|
except BrokenPipeError:
|
||||||
|
print("Broken Pipe!")
|
||||||
|
except ConnectionResetError:
|
||||||
|
print("Connection Reset!")
|
||||||
|
except TimeoutError:
|
||||||
|
print("Send timed out!")
|
||||||
|
print("Served " + fullRequestPath + " to " + sourceIPvisual + ":" + str(sourcePort))
|
||||||
|
|
||||||
|
connectionSocket.close()
|
||||||
|
|
||||||
|
|
||||||
|
while running:
|
||||||
|
print('Waiting for a connection...')
|
||||||
|
connectionSocket, addr = serverSocket.accept()
|
||||||
|
t = threading.Thread(target=handle_connection, args=(connectionSocket, addr))
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
print("Server Stopped.")
|
||||||
|
serverSocket.close()
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
import bcrypt
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import secrets
|
||||||
|
import datetime
|
||||||
|
import mydatabase
|
||||||
|
from cryptography.fernet import Fernet
|
||||||
|
|
||||||
|
db = mydatabase.database("database.txt")
|
||||||
|
|
||||||
|
# In-memory session store: { token: { "username": str, "expires": datetime } }
|
||||||
|
_sessions = {}
|
||||||
|
SESSION_LIFETIME_MINUTES = 60
|
||||||
|
|
||||||
|
# Load bad passwords list once at import time
|
||||||
|
_badpasswords = set()
|
||||||
|
try:
|
||||||
|
with open("badpasswords.txt") as passlist:
|
||||||
|
for line in passlist:
|
||||||
|
word = line.strip()
|
||||||
|
if word:
|
||||||
|
_badpasswords.add(word.lower())
|
||||||
|
except FileNotFoundError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Internal helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _derive_key(password: str, salt: bytes) -> bytes:
|
||||||
|
"""Derive a 32-byte key from a password and salt using PBKDF2-HMAC-SHA256.
|
||||||
|
This is used to encrypt/decrypt per-user data — the server never stores it."""
|
||||||
|
return hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 390000)
|
||||||
|
|
||||||
|
|
||||||
|
def _fernet(key: bytes) -> Fernet:
|
||||||
|
return Fernet(base64.urlsafe_b64encode(key))
|
||||||
|
|
||||||
|
|
||||||
|
def _encrypt(data: str, key: bytes) -> str:
|
||||||
|
"""Encrypt a JSON string, return a base64 string safe for JSON storage."""
|
||||||
|
return _fernet(key).encrypt(data.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _decrypt(data: str, key: bytes) -> str:
|
||||||
|
"""Decrypt a base64 string back to a JSON string. Raises on bad key/data."""
|
||||||
|
return _fernet(key).decrypt(data.encode("utf-8")).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _purge_expired_sessions():
|
||||||
|
now = datetime.datetime.utcnow()
|
||||||
|
expired = [t for t, s in _sessions.items() if s["expires"] <= now]
|
||||||
|
for t in expired:
|
||||||
|
del _sessions[t]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public API — all functions return ("success"|"fail", payload)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def register(username: str, password: str) -> tuple:
|
||||||
|
"""Create a new account.
|
||||||
|
Returns ("success", None) or ("fail", reason_string)."""
|
||||||
|
if not username or not password:
|
||||||
|
return ("fail", "username and password are required")
|
||||||
|
if len(username) > 64:
|
||||||
|
return ("fail", "username too long")
|
||||||
|
if len(password) < 8:
|
||||||
|
return ("fail", "password too short")
|
||||||
|
if password.lower() in _badpasswords:
|
||||||
|
return ("fail", "password is too common")
|
||||||
|
|
||||||
|
db.read()
|
||||||
|
users = db.database.setdefault("users", {})
|
||||||
|
|
||||||
|
if username in users:
|
||||||
|
return ("fail", "user already exists")
|
||||||
|
|
||||||
|
# bcrypt handles its own salt internally
|
||||||
|
pw_hash = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
|
||||||
|
|
||||||
|
# Separate salt for PBKDF2 key derivation (not reusing the bcrypt salt)
|
||||||
|
kdf_salt = os.urandom(32)
|
||||||
|
key = _derive_key(password, kdf_salt)
|
||||||
|
initial_data = _encrypt(json.dumps({}), key)
|
||||||
|
|
||||||
|
users[username] = {
|
||||||
|
"pw_hash": pw_hash,
|
||||||
|
"kdf_salt": base64.b64encode(kdf_salt).decode("utf-8"),
|
||||||
|
"user_data": initial_data,
|
||||||
|
}
|
||||||
|
db.write()
|
||||||
|
return ("success", None)
|
||||||
|
|
||||||
|
|
||||||
|
def login(username: str, password: str) -> tuple:
|
||||||
|
"""Authenticate and return a session token.
|
||||||
|
Returns ("success", token_string) or ("fail", reason_string)."""
|
||||||
|
db.read()
|
||||||
|
user = db.database.get("users", {}).get(username)
|
||||||
|
if user is None:
|
||||||
|
# Run a dummy bcrypt check to avoid timing-based username enumeration
|
||||||
|
bcrypt.checkpw(password.encode("utf-8"), bcrypt.hashpw(b"dummy", bcrypt.gensalt()))
|
||||||
|
return ("fail", "invalid username or password")
|
||||||
|
|
||||||
|
if not bcrypt.checkpw(password.encode("utf-8"), user["pw_hash"].encode("utf-8")):
|
||||||
|
return ("fail", "invalid username or password")
|
||||||
|
|
||||||
|
_purge_expired_sessions()
|
||||||
|
token = secrets.token_urlsafe(32)
|
||||||
|
_sessions[token] = {
|
||||||
|
"username": username,
|
||||||
|
"expires": datetime.datetime.utcnow() + datetime.timedelta(minutes=SESSION_LIFETIME_MINUTES),
|
||||||
|
}
|
||||||
|
return ("success", token)
|
||||||
|
|
||||||
|
|
||||||
|
def logout(token: str) -> tuple:
|
||||||
|
"""Invalidate a session token.
|
||||||
|
Returns ("success", None) or ("fail", reason_string)."""
|
||||||
|
if token in _sessions:
|
||||||
|
del _sessions[token]
|
||||||
|
return ("success", None)
|
||||||
|
return ("fail", "invalid or expired session")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_session(token: str) -> tuple:
|
||||||
|
"""Check whether a token is valid and not expired.
|
||||||
|
Returns ("success", username) or ("fail", reason_string)."""
|
||||||
|
_purge_expired_sessions()
|
||||||
|
session = _sessions.get(token)
|
||||||
|
if session is None:
|
||||||
|
return ("fail", "invalid or expired session")
|
||||||
|
return ("success", session["username"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_data(token: str, password: str) -> tuple:
|
||||||
|
"""Retrieve the decrypted user data dict for the session owner.
|
||||||
|
Password is required to derive the decryption key.
|
||||||
|
Returns ("success", data_dict) or ("fail", reason_string)."""
|
||||||
|
status, username = validate_session(token)
|
||||||
|
if status != "success":
|
||||||
|
return ("fail", username)
|
||||||
|
|
||||||
|
db.read()
|
||||||
|
user = db.database.get("users", {}).get(username)
|
||||||
|
if user is None:
|
||||||
|
return ("fail", "user not found")
|
||||||
|
|
||||||
|
kdf_salt = base64.b64decode(user["kdf_salt"])
|
||||||
|
key = _derive_key(password, kdf_salt)
|
||||||
|
try:
|
||||||
|
data = json.loads(_decrypt(user["user_data"], key))
|
||||||
|
except Exception:
|
||||||
|
return ("fail", "decryption failed — wrong password or corrupted data")
|
||||||
|
return ("success", data)
|
||||||
|
|
||||||
|
|
||||||
|
def set_user_data(token: str, password: str, data: dict) -> tuple:
|
||||||
|
"""Encrypt and store a user data dict for the session owner.
|
||||||
|
Password is required to derive the encryption key.
|
||||||
|
Returns ("success", None) or ("fail", reason_string)."""
|
||||||
|
status, username = validate_session(token)
|
||||||
|
if status != "success":
|
||||||
|
return ("fail", username)
|
||||||
|
|
||||||
|
db.read()
|
||||||
|
users = db.database.get("users", {})
|
||||||
|
user = users.get(username)
|
||||||
|
if user is None:
|
||||||
|
return ("fail", "user not found")
|
||||||
|
|
||||||
|
# Verify password before allowing a write
|
||||||
|
if not bcrypt.checkpw(password.encode("utf-8"), user["pw_hash"].encode("utf-8")):
|
||||||
|
return ("fail", "invalid password")
|
||||||
|
|
||||||
|
kdf_salt = base64.b64decode(user["kdf_salt"])
|
||||||
|
key = _derive_key(password, kdf_salt)
|
||||||
|
user["user_data"] = _encrypt(json.dumps(data), key)
|
||||||
|
db.write()
|
||||||
|
return ("success", None)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_account(token: str, password: str) -> tuple:
|
||||||
|
"""Permanently delete an account and invalidate the session.
|
||||||
|
Returns ("success", None) or ("fail", reason_string)."""
|
||||||
|
status, username = validate_session(token)
|
||||||
|
if status != "success":
|
||||||
|
return ("fail", username)
|
||||||
|
|
||||||
|
db.read()
|
||||||
|
users = db.database.get("users", {})
|
||||||
|
user = users.get(username)
|
||||||
|
if user is None:
|
||||||
|
return ("fail", "user not found")
|
||||||
|
|
||||||
|
if not bcrypt.checkpw(password.encode("utf-8"), user["pw_hash"].encode("utf-8")):
|
||||||
|
return ("fail", "invalid password")
|
||||||
|
|
||||||
|
del users[username]
|
||||||
|
db.write()
|
||||||
|
|
||||||
|
# Invalidate all sessions for this user
|
||||||
|
to_remove = [t for t, s in _sessions.items() if s["username"] == username]
|
||||||
|
for t in to_remove:
|
||||||
|
del _sessions[t]
|
||||||
|
|
||||||
|
return ("success", None)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Bad Request</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-image: url("/Images/DingusGrass.jpg");
|
||||||
|
background-color: #0f8340;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100svh
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
background-color: white;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 5mm;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.dense {
|
||||||
|
margin: 1mm;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="center">
|
||||||
|
<h1 style="color: #a00;" class="dense">Error 400: Bad Request.</h1>
|
||||||
|
<p class="dense">
|
||||||
|
There is an issue with your web request.<br>If possible, please verify all required name/value pairs are present in the query string of the URL.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Page Not Found!</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-image: url("/Images/DingusGrass.jpg");
|
||||||
|
background-color: #0f8340;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100svh
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
background-color: white;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 5mm;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.dense {
|
||||||
|
margin: 1mm;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="center">
|
||||||
|
<h1 style="color: #a00;" class="dense">Error 404: Page not found.</h1>
|
||||||
|
<p class="dense">
|
||||||
|
The page you have requested does not exist.<br>Please verify you typed the URL correctly and try again.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Too Many Requests</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #0f8340;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100svh
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
background-color: white;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 5mm;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.dense {
|
||||||
|
margin: 1mm;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="center">
|
||||||
|
<h1 style="color: #a00;" class="dense">Error 429: Too many Requests</h1>
|
||||||
|
<p class="dense">
|
||||||
|
You are sending too many requests. If this is caused by an error in a page, please contact the site admin.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Internal Server Error.</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-image: url("/Images/DingusGrass.jpg");
|
||||||
|
background-color: #0f8340;
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100svh
|
||||||
|
}
|
||||||
|
.center {
|
||||||
|
background-color: white;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 5mm;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
.dense {
|
||||||
|
margin: 1mm;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="center">
|
||||||
|
<h1 style="color: #a00;" class="dense">Error 500: Internal Server Error.</h1>
|
||||||
|
<p class="dense">
|
||||||
|
An error has occured on the server.<br>Please try again later. If this continues to occur, please contact the server administrator.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 156 B |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 631 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 3.3 MiB |
|
After Width: | Height: | Size: 40 MiB |
|
After Width: | Height: | Size: 221 B |
|
After Width: | Height: | Size: 2.1 MiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
webroot_real = os.path.realpath(script_directory + "/webroot")
|
||||||
|
folderPath = os.path.realpath(script_directory + "/webroot" + requestPath)
|
||||||
|
|
||||||
|
if not folderPath.startswith(webroot_real + os.sep) and folderPath != webroot_real:
|
||||||
|
responseType = "403 Forbidden"
|
||||||
|
contentType = "text/html"
|
||||||
|
responsecontent = b"<h1>403 Forbidden</h1>"
|
||||||
|
else:
|
||||||
|
filesList = list(os.scandir(folderPath))
|
||||||
|
responseType = "200 OK"
|
||||||
|
contentType = "text/html"
|
||||||
|
|
||||||
|
template = open(script_directory + "/webroot/Templates/fileList.html").read()
|
||||||
|
template = template.replace("<!--dir-->", html.escape(requestPath))
|
||||||
|
listText = "<li>Folder: <a href=\"..\">..</a></li>"
|
||||||
|
for i in filesList:
|
||||||
|
temp = "File: "
|
||||||
|
funnynameorsomethingidk = i.name
|
||||||
|
if os.path.isdir(folderPath + "/" + funnynameorsomethingidk):
|
||||||
|
temp = "Folder: "
|
||||||
|
funnynameorsomethingidk += "/"
|
||||||
|
link = requestPath + "/" + funnynameorsomethingidk
|
||||||
|
link = link.replace("//", "/")
|
||||||
|
if link.endswith(".py"):
|
||||||
|
link = "/raw/?p=" + html.escape(link)
|
||||||
|
else:
|
||||||
|
link = html.escape(link)
|
||||||
|
listText += ("<li>" + temp + "<a href='" + link + "'>" + html.escape(i.name) + "</a></li>")
|
||||||
|
template = template.replace("<!--fileList-->", listText)
|
||||||
|
responsecontent = template.encode()
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Directory Listing of <!--dir--></h1>
|
||||||
|
<ul>
|
||||||
|
<!--fileList-->
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<div class="navbar no-print">
|
||||||
|
<span class="nav-button">
|
||||||
|
<a href="/?<!--args-->p=/">
|
||||||
|
<div class="nav-content">
|
||||||
|
<img src="/Images/mudkip.png" alt="Page Icon" style="height:100%; margin: 0px;">
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span class="nav-button">
|
||||||
|
<a href="/?<!--args-->p=/Projects">
|
||||||
|
<div class="nav-content">
|
||||||
|
Projects
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a href="/?<!--args-->p=/Projects/wiitop/" class="nav-button">
|
||||||
|
<div class="nav-content">
|
||||||
|
Wiitop
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<span class="nav-button">
|
||||||
|
<a href="/?<!--args-->p=/Contact" >
|
||||||
|
<div class="nav-content">
|
||||||
|
Contact
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span class="nav-button">
|
||||||
|
<a href="/?<!--args-->p=/webring" >
|
||||||
|
<div class="nav-content">
|
||||||
|
Webring?
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span class="nav-button" style="float: right; margin-right:0;">
|
||||||
|
<div class="nav-content">
|
||||||
|
<label for="theme">Theme: </label>
|
||||||
|
<select name="theme" id="theme" onchange="changeTheme(this.value)">
|
||||||
|
<option value="default">Page Default</option>
|
||||||
|
<option value="Light"<!--themeLight-->>Light</option>
|
||||||
|
<option value="Dark"<!--themeDark-->>Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<!--userbar-->
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
function replaceUrlParam(url, paramName, paramValue) {
|
||||||
|
if (paramValue == null) {
|
||||||
|
paramValue = '';
|
||||||
|
}
|
||||||
|
var pattern = new RegExp('\\b('+paramName+'=).*?(&|#|$)');
|
||||||
|
if (url.search(pattern)>=0) {
|
||||||
|
return url.replace(pattern,'$1' + paramValue + '$2');
|
||||||
|
}
|
||||||
|
url = url.replace(/[?#]$/,'');
|
||||||
|
return url + (url.indexOf('?')>0 ? '&' : '?') + paramName + '=' + paramValue;
|
||||||
|
}
|
||||||
|
function changeTheme(theme) {
|
||||||
|
window.location.href = replaceUrlParam('/?<!--args-->p=<!--path-->', "theme", theme)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!--head-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="bgimage"></div>
|
||||||
|
<div id="grid-container">
|
||||||
|
<header>
|
||||||
|
<!--header-->
|
||||||
|
</header>
|
||||||
|
<article>
|
||||||
|
<a id="oldmodelink" style="color: rgba(127,0,255,0);" href="/?v=old&p=<!--path-->">View in Compatibility Mode</a>
|
||||||
|
<br>
|
||||||
|
<!--content-->
|
||||||
|
<br>
|
||||||
|
</article>
|
||||||
|
<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>
|
||||||
|
<span style="float: left;">This page last updated: <!--updateddate--></span>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
Navigation<br>
|
||||||
|
<div>
|
||||||
|
<a href="/?<!--args-->&p=/">
|
||||||
|
Home Page
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<a href="/?<!--args-->&p=/Projects">
|
||||||
|
Projects
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<div>
|
||||||
|
<a href="/?<!--args-->&p=/Projects/wiitop/">
|
||||||
|
Wiitop
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
<a href="/?<!--args-->&p=/Contact" >
|
||||||
|
Contact Page
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<a href="/?<!--args-->&p=/webring" >
|
||||||
|
Webring?
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<a href="/?<!--args-->returnto=<!--path-->&p=/login" >
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<!--head-->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
Using Compatibility Mode.<br>Click <a href="/?v=new&p=<!--path-->">here</a> for modern browsers.
|
||||||
|
<div id="grid-container">
|
||||||
|
<header>
|
||||||
|
<!--header-->
|
||||||
|
</header>
|
||||||
|
<article>
|
||||||
|
<br>
|
||||||
|
<!--content-->
|
||||||
|
<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>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
responseType = "200 OK"
|
||||||
|
contentType = "text/plain"
|
||||||
|
responsecontent = datetime.datetime.now().strftime("%I:%M:%S%p on %B %d, %Y").encode()
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Title: Contact Me<br>
|
||||||
|
Icon: /favicon.ico<br>
|
||||||
|
Style: /styles/newLight.css<br>
|
||||||
|
Description: Contact Info<br>
|
||||||
|
Author: SuperbeeLavaman<br>
|
||||||
|
Content:
|
||||||
|
<div class="article-content-block">
|
||||||
|
<h1>Contact Page</h1>
|
||||||
|
<p>
|
||||||
|
This page contains contact information, for if you want to tell me anything or ask me a question.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Phone:</strong> <a href="tel:9714442780">(971) 444-2780</a><br>
|
||||||
|
<strong>Email:</strong> <a href="mailto:loganlee.jk@outlook.com">loganlee.jk@outlook.com</a><!--, <a href="mailto:superbee@superbeelavaman.onthewifi.com">superbee@superbeelavaman.onthewifi.com</a>--><br>
|
||||||
|
<strong>Discord:</strong> <a href="https://discordapp.com/users/763802866921242646">superbee_hashtag_3637</a><br>
|
||||||
|
<strong>Matrix:</strong> <a href="https://matrix.to/#/%40superbeelavaman%3Asuperbeelavaman.duckdns.org">@superbeelavaman:superbeelavaman.duckdns.org</a><br>
|
||||||
|
<strong>Nintendo Switch friend code:</strong> SW-7690-5791-7118<br>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Title: Projects<br>
|
||||||
|
Icon: /favicon.ico<br>
|
||||||
|
Style: /styles/newLight.css<br>
|
||||||
|
Description: My recent projects.
|
||||||
|
Author: SuperbeeLavaman<br>
|
||||||
|
Content:
|
||||||
|
<div class="article-content-block">
|
||||||
|
<h1>Projects</h1>
|
||||||
|
<p>This page will list recent projects and projects I'm currently working on, if I think to write about them. It will probably not have a lot of information on it.</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="/?<!--args-->p=/Projects/wiitop/"><b>Preparation for making the Wiitop</b></a>
|
||||||
|
<br>As you may have noticed, I have an 88x31 button leading to Wii-Linux.org. One of the projects they have is a laptop that uses a Nintendo Wii
|
||||||
|
as the main computer portion of the laptop. As you would expect, several modifications are expected to be performed to the Wii that is being
|
||||||
|
used in the Wiitop.
|
||||||
|
<br><a href="/?<!--args-->p=/Projects/wiitop/"><b>[Read More]</b></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
Title: The Wiitop<br>
|
||||||
|
Icon: /favicon.ico<br>
|
||||||
|
Style: /styles/newLight.css<br>
|
||||||
|
Description: I'm working on making the first prototype Wiitop*<br>
|
||||||
|
Author: SuperbeeLavaman<br>
|
||||||
|
Content:
|
||||||
|
<div class="article-content-block">
|
||||||
|
<h1>Preparation for making the Wiitop</h1>
|
||||||
|
<p> As you may have noticed, I have an 88x31 button leading to Wii-Linux.org. One of the projects they have is a laptop that uses a Nintendo Wii
|
||||||
|
as the main computer portion of the laptop. As you would expect, several modifications are expected to be performed to the Wii that is being
|
||||||
|
used in the Wiitop.
|
||||||
|
<br>
|
||||||
|
<br>The Wiitop is specified to be a 14" laptop, with a trimmed Wii inside. The publically available spec is quite out of date from what has been
|
||||||
|
talked about in the Wii-Linux server, and unfortunately while it specifies an 854x480 screen, those are effectively impossible to get. As
|
||||||
|
<i>Lily lily lily lily</i> recommended, 720x576@50 letterboxes cleanly into 800x600, which is a slightly more common LCD resolution.
|
||||||
|
<br>Since I'm likely to be the first person to actually construct a Wiitop, my decisions will likely have a strong effect on how the design is
|
||||||
|
finalized. A 4:3 aspect ratio makes sense for a device that often displays 4:3 content, and a 4:3 laptop makes certain components easier to
|
||||||
|
arrange. Since I've never done a Wii trim before, I'll be using the <a href="https://bitbuilt.net/forums/threads/the-definitive-wii-trimming-guide.198/">OMGWTF</a>
|
||||||
|
trim. For the power supply, I will be designing a custom board that includes an STM32-based CrOS_EC, a BQ25703A charge controller, and various
|
||||||
|
regulators, which will be controllable from Linux.
|
||||||
|
<br>The following is written during or after the actions listed above.
|
||||||
|
<br>
|
||||||
|
<h2>The Wii to use</h2>
|
||||||
|
I have an RVL-CPU-40 wii that has gone through a lot.
|
||||||
|
<br>At one point, it was running with no shell and no heatsink, in a freezer. During that time, it was nicknamed <i>Friizer</i>, and continues to
|
||||||
|
retain that name.
|
||||||
|
<br><abbr title="Friizer in the freezer."><a href="/Images/projects/wiitop/friizerfreezer.jpg"><img src="/Images/projects/wiitop/friizerfreezer.jpg" alt="Friizer in the freezer" class="fitinpageplease"></a></abbr>
|
||||||
|
<br>
|
||||||
|
<br>Recently, I delidded the CPU and SoC of Friizer. I then decided to prepare for trimming and do research for what I would need to do to make the Wiitop.
|
||||||
|
<br><abbr title="Friizer has been delidded."><a href="/Images/projects/wiitop/friizerdelid.jpg"><img src="/Images/projects/wiitop/friizerdelid.jpg" alt="Friizer has been delidded." class="fitinpageplease""></a></abbr>
|
||||||
|
<br><abbr title="Marked for the OMGWTF trim"><a href="/Images/projects/wiitop/friizermarked.jpg"><img src="/Images/projects/wiitop/friizermarked.jpg" alt="Marked for the OMGWTF trim" class="fitinpageplease"></a></abbr>
|
||||||
|
<br>
|
||||||
|
<br>[THIS PAGE IS UNDER CONSTRUCTION]
|
||||||
|
<br><abbr title="construction."><img src=/Images/underconstruction.gif alt="construction." class="fitinpageplease"></abbr>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Title: My Website<br>
|
||||||
|
Icon: /favicon.ico<br>
|
||||||
|
Style: /styles/newLight.css<br>
|
||||||
|
Description: I've made a website. There's not much content yet.<br>
|
||||||
|
Author: SuperbeeLavaman<br>
|
||||||
|
Content:
|
||||||
|
<div class="article-content-block">
|
||||||
|
<h1>SuperbeeLavaman's Website</h1>
|
||||||
|
<p>
|
||||||
|
Idk what to put on my website but it exists.
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
<p style="font-family: comicsansms;">Comic Sans MS</p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
Title: Login<br>
|
||||||
|
Icon: /favicon.ico<br>
|
||||||
|
Style: /styles/newLight.css<br>
|
||||||
|
Description: Login.<br>
|
||||||
|
Author: SuperbeeLavaman<br>
|
||||||
|
Content:
|
||||||
|
<div class="article-content-block">
|
||||||
|
<h1>Login</h1>
|
||||||
|
<p><!--errortext--></p>
|
||||||
|
<p>
|
||||||
|
<form action="/login/">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><label for="username">Username: </label></td>
|
||||||
|
<td><input type="text" name="username" id="username"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label for="password">Password: </label></td>
|
||||||
|
<td><input type="password" name="password" id="password"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><!--formargs--></td>
|
||||||
|
<td><input type="submit" name="submit" id="submit" value="Submit"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<p>No Account? <a href="/?<!--args-->p=/signup/">Sign Up.</a></p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
Title: Resume<br>
|
||||||
|
Icon: /favicon.ico<br>
|
||||||
|
Style: /styles/newDark.css<br>
|
||||||
|
Description: Logan's Resume<br>
|
||||||
|
Author: Logan Joy-Koer (SuperbeeLavaman)<br>
|
||||||
|
Content:
|
||||||
|
<div class="article-content-block">
|
||||||
|
<section class="container">
|
||||||
|
<h1>Logan Joy-Koer</h1>
|
||||||
|
<h6 style="margin-top: 0px" class="subtitle">
|
||||||
|
Gervais, OR |
|
||||||
|
<a href="mailto:loganlee.jk@outlook.com">loganlee.jk@outlook.com</a> |
|
||||||
|
<a href="tel:9714442780">(971) 444-2780</a> |<br>
|
||||||
|
<a href="https://www.github.com/superbeelavaman">github.com/superbeelavaman</a> |
|
||||||
|
<a href="https://superbeelavaman.duckdns.org/?p=/">https://superbeelavaman.duckdns.org</a> |<br>
|
||||||
|
<a href="https://matrix.to/#/%40superbeelavaman%3Asuperbeelavaman.duckdns.org">@superbeelavaman:superbeelavaman.duckdns.org</a> |
|
||||||
|
CompTIA Tech+ Certified |
|
||||||
|
</h6>
|
||||||
|
</section>
|
||||||
|
<section class="container">
|
||||||
|
<h2>Summary</h2>
|
||||||
|
<p style="margin-top: 0px">Linux enthusiast with hands-on experience in embedded hardware, low-level software, and systems programming. Comfortable across x86, PowerPC/POWER ISA, eZ80, and ARM CPU architectures. Passionate about unusual and older hardware and making things work in environments they weren't intended to.</p>
|
||||||
|
</section>
|
||||||
|
<section class="container">
|
||||||
|
<h2>Projects & Hands-On Experience</h2>
|
||||||
|
<h3>Xbox 360 PBX Server <span class="subtitle">Personal Lab</span></h3>
|
||||||
|
<p style="margin-top: 0px"><ul>
|
||||||
|
<li>Runs Arch Linux (ArchPOWER) on Xbox 360 hardware</li>
|
||||||
|
<li>Compiled and configured Asterisk 23.2.2 from source for a full home PBX system</li>
|
||||||
|
<li>Integrated a Cisco VG224 as a SIP/FXS gateway; successfully registered all 24 extensions via chan_pjsip</li>
|
||||||
|
<li>Set up dnsmasq for PTR resolution and managed routing across two subnets</li>
|
||||||
|
</ul></p>
|
||||||
|
<h3>Multiple Workstations <span class="subtitle">Personal Lab</span></h3>
|
||||||
|
<p style="margin-top: 0px"><ul>
|
||||||
|
<li>Deployed and Configured several workstations to conveniently perform tasks from several locations</li>
|
||||||
|
<li>Custom-compiled Linux Kernel builds for nonstandard hardware</li>
|
||||||
|
<li>Became familiar with several Linux distros including but not limited to Ubuntu, Arch, Adélie, Void, and more</li>
|
||||||
|
</ul></p>
|
||||||
|
</section>
|
||||||
|
<section class="container">
|
||||||
|
<h2>Prior Work Experience</h2>
|
||||||
|
<h3>Crew Member/Cook - Domino's Pizza <span class="subtitle">Woodburn, OR | July 2024 - August 2025</span></h3>
|
||||||
|
<ul style="margin-top: 0px;">
|
||||||
|
<li>Prepared orders accurately with time pressure during high volume hours</li>
|
||||||
|
<li>Handled customer service tasks including taking orders and resolving complaints both in person and over the phone</li>
|
||||||
|
<li>Maintained food safety and cleanliness standards consistently across shifts</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="container">
|
||||||
|
<h2>Skills</h2>
|
||||||
|
<p style="margin-top: 0px; margin-bottom: 0px;">Operating Systems:</p>
|
||||||
|
<ul style="margin-top: 0px;">
|
||||||
|
<li>Arch Linux</li>
|
||||||
|
<li>Linux Mint</li>
|
||||||
|
<li>Debian</li>
|
||||||
|
<li>Ubuntu</li>
|
||||||
|
<li>Adélie</li>
|
||||||
|
<li>Void</li>
|
||||||
|
<li>PostMarketOS</li>
|
||||||
|
<li>Windows 98/ME</li>
|
||||||
|
<li>Windows XP</li>
|
||||||
|
<li>Windows Vista/7</li>
|
||||||
|
<li>Windows 10</li>
|
||||||
|
</ul>
|
||||||
|
<p>Familiar CPU Architectures: x86/AMD64, PowerPC/POWER ISA, ARM, eZ80</p>
|
||||||
|
<p>Networking: VoIP/SIP (Asterisk, PJSIP), Cisco IOS, VLANs, NAT, dnsmasq, DNS</p>
|
||||||
|
<p>Hardware: Embedded systems, PCB design (KiCad), Soldering</p>
|
||||||
|
<p>Tools: Git, Vim, SSH, Nginx, Sledgehammer</p>
|
||||||
|
<p>Languages: C, Python, Shell scripting [Bash], JavaScript, HTML, CSS</p>
|
||||||
|
</section>
|
||||||
|
<section class="container">
|
||||||
|
<h2>Certifications</h2>
|
||||||
|
<p style="margin-top: 0px;">CompTIA Tech+ (FC0-U71) - Issued May 22, 2025</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
Title: Sign Up<br>
|
||||||
|
Icon: /favicon.ico<br>
|
||||||
|
Style: /styles/newLight.css<br>
|
||||||
|
Description: Sign up.<br>
|
||||||
|
Author: SuperbeeLavaman<br>
|
||||||
|
Content:
|
||||||
|
<div class="article-content-block">
|
||||||
|
<h1>Sign Up</h1>
|
||||||
|
<p><!--errortext--></p>
|
||||||
|
<p>
|
||||||
|
<form action="/signup/">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td><label for="username">Username: </label></td>
|
||||||
|
<td><input type="text" name="username" id="username"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label for="password">Password: </label></td>
|
||||||
|
<td><input type="password" name="password" id="password"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><label for="password"> Repeat Password: </label></td>
|
||||||
|
<td><input type="password" name="password2" id="password2"></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><!--formargs--></td>
|
||||||
|
<td><input type="submit" name="submit" id="submit" value="Submit"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
<p>Already have an account? <a href="/?<!--args-->p=/login/">Sign In.</a></p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
Title: Webring<br>
|
||||||
|
Icon: /favicon.ico<br>
|
||||||
|
Style: /styles/newLight.css<br>
|
||||||
|
Description: This site is part of a Webring. Read more here.<br>
|
||||||
|
Author: SuperbeeLavaman, PiplupRox Productions<br>
|
||||||
|
Content:
|
||||||
|
<div class="article-content-block">
|
||||||
|
<h1>This site is part of a <a href="https://en.wikipedia.org/wiki/Webring">Webring</a></h1>
|
||||||
|
<p>
|
||||||
|
This webring includes the Porypedia Wiki, PiplupRox Productions, and more! Using the top bar here, you can navigate to other pages on this site, and from here you can navigate to other pages in the webring.
|
||||||
|
<br><br>
|
||||||
|
I am part of this webring as a result of being one of 3 active community members in the Porygondoliers community, and PiplupRox was adamant about creating a webring
|
||||||
|
<br>
|
||||||
|
Blame him if you think this is weird.
|
||||||
|
<br><br>
|
||||||
|
<div style="width: min-content;">
|
||||||
|
<a href="https://pipluprox-productions.neocities.org/webring"><img class="center" src="/Images/webring/porygondolier_community_webring.gif"></a>
|
||||||
|
<div style="display: flex; justify-content: center;">
|
||||||
|
<a href="https://pipluprox-productions.neocities.org/webring?action=previous"><img src="/Images/webring/porygondolier_webring_prev.gif"></a>
|
||||||
|
<a href="https://pipluprox-productions.neocities.org/webring?action=random"><img src="/Images/webring/porygondolier_webring_rand.gif"></a>
|
||||||
|
<a href="https://pipluprox-productions.neocities.org/webring?action=next"><img src="/Images/webring/porygondolier_webring_next.gif"></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,143 @@
|
|||||||
|
page = ""
|
||||||
|
args = ""
|
||||||
|
responseType = "200 OK"
|
||||||
|
contentType = "text/html"
|
||||||
|
for i in argumentsDict:
|
||||||
|
argumentsDict[i] = urllib.parse.unquote(argumentsDict[i])
|
||||||
|
if "p" in argumentsDict:
|
||||||
|
path = argumentsDict["p"]
|
||||||
|
|
||||||
|
content_real = os.path.realpath(script_directory + "/webroot/content")
|
||||||
|
filePath = os.path.realpath(script_directory + "/webroot/content/" + path)
|
||||||
|
if not filePath.startswith(content_real + os.sep) and filePath != content_real:
|
||||||
|
responseType = "403 Forbidden"
|
||||||
|
responsecontent = b"<h1>403 Forbidden</h1>"
|
||||||
|
contentType = "text/html"
|
||||||
|
else:
|
||||||
|
if "v" in argumentsDict and argumentsDict["v"] in ("new", "old"):
|
||||||
|
version = argumentsDict["v"]
|
||||||
|
else:
|
||||||
|
version = "new"
|
||||||
|
|
||||||
|
if os.path.exists(filePath):
|
||||||
|
if os.path.isdir(filePath):
|
||||||
|
if os.path.exists(filePath + "/" + version + "index.html"):
|
||||||
|
filePath += ("/" + version + "index.html")
|
||||||
|
else:
|
||||||
|
filePath += "/index.html"
|
||||||
|
if version == "new":
|
||||||
|
page = open(script_directory + "/webroot/Templates/index.html").read()
|
||||||
|
header = open(script_directory + "/webroot/Templates/header.html").read()
|
||||||
|
elif version == "old":
|
||||||
|
page = open(script_directory + "/webroot/Templates/oldindex.html").read()
|
||||||
|
header = open(script_directory + "/webroot/Templates/oldheader.html").read()
|
||||||
|
page = page.replace("<!--header-->", header)
|
||||||
|
|
||||||
|
# Resolve session cookie and build userbar before other replacements,
|
||||||
|
# as the logged-out userbar contains <!--args--> and <!--path--> itself
|
||||||
|
session_token = Cookies.get("session", "")
|
||||||
|
if session_token:
|
||||||
|
session_status, session_user = userManagement.validate_session(session_token)
|
||||||
|
else:
|
||||||
|
session_status = "fail"
|
||||||
|
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")
|
||||||
|
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"
|
||||||
|
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"
|
||||||
|
else:
|
||||||
|
pfp_src = "/Images/default_pfp.gif"
|
||||||
|
userbar = (
|
||||||
|
'<span class="nav-button" style="float:right; margin-right:0;">'
|
||||||
|
'<a href="/logout/">'
|
||||||
|
'<div class="nav-content">Logout</div>'
|
||||||
|
'</a></span>'
|
||||||
|
'<span class="nav-button" style="float:right; margin-right:0;">'
|
||||||
|
'<div class="nav-content">'
|
||||||
|
'<img src="' + pfp_src + '" alt="Profile Picture" style="height:100%; vertical-align:middle;">'
|
||||||
|
' Logged in as ' + html.escape(session_user) +
|
||||||
|
'</div></span>'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
userbar = (
|
||||||
|
'<span class="nav-button" style="float:right; margin-right:0;">'
|
||||||
|
'<a href="/?<!--args-->returnto=<!--path-->&p=/login/">'
|
||||||
|
'<div class="nav-content">Login</div>'
|
||||||
|
'</a></span>'
|
||||||
|
)
|
||||||
|
|
||||||
|
page = page.replace("<!--userbar-->", userbar)
|
||||||
|
|
||||||
|
content = open(filePath).read()
|
||||||
|
title = content.split("Title: ")[1].split("<br>\n")[0]
|
||||||
|
icon = content.split("Icon: ")[1].split("<br>\n")[0]
|
||||||
|
style = content.split("Style: ")[1].split("<br>\n")[0]
|
||||||
|
description = content.split("Description: ")[1].split("<br>\n")[0]
|
||||||
|
author = content.split("Author: ")[1].split("<br>\n")[0]
|
||||||
|
content = content.split("Content: \n")[1]
|
||||||
|
updateddate = get_last_modified(filePath)
|
||||||
|
|
||||||
|
if version == "old":
|
||||||
|
style = "/styles/oldmain.css"
|
||||||
|
if "theme" in argumentsDict:
|
||||||
|
theme = argumentsDict["theme"]
|
||||||
|
if os.path.exists(script_directory + "/webroot/styles/" + version + theme + ".css"):
|
||||||
|
style = "/styles/" + version + theme + ".css"
|
||||||
|
args += "theme=" + theme + "&"
|
||||||
|
page = page.replace("<!--theme" + theme + "-->", " selected")
|
||||||
|
page = re.sub('<!--theme.*?-->', '', page, flags=re.DOTALL)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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=\"""" + html.escape(style) + """\">
|
||||||
|
<meta charset=\"UTF-8\">
|
||||||
|
<meta name=\"description\" content=\"""" + description + """\">
|
||||||
|
<meta name=\"author\" content=\"""" + author + """\">
|
||||||
|
<meta property=\"og:title\" content=\"""" + title + """\">
|
||||||
|
<meta property=\"og:description\" content=\"""" + description + """\">
|
||||||
|
<meta property=\"og:type\" content=\"website\">
|
||||||
|
<meta property=\"twitter:title\" content=\"""" + title + """\">
|
||||||
|
<meta property=\"twitter:description\" content=\"""" + description + """\">
|
||||||
|
"""
|
||||||
|
|
||||||
|
args += "v=" + version + "&"
|
||||||
|
|
||||||
|
formargs = ""
|
||||||
|
for i in argumentsDict:
|
||||||
|
formargs += "<input type=\"hidden\" name=\"" + html.escape(i) + "\" value=\"" + html.escape(argumentsDict[i]) + "\">"
|
||||||
|
|
||||||
|
errortext = ""
|
||||||
|
if "errortext" in argumentsDict:
|
||||||
|
errortext = "<p class=\"error\">" + html.escape(argumentsDict["errortext"]) + "</p>"
|
||||||
|
|
||||||
|
page = page.replace("<!--content-->", content)
|
||||||
|
page = page.replace("<!--head-->", head)
|
||||||
|
page = page.replace("<!--path-->", urllib.parse.quote(path, safe=''))
|
||||||
|
page = page.replace("<!--args-->", args)
|
||||||
|
page = page.replace("<!--formargs-->", formargs)
|
||||||
|
page = page.replace("<!--errortext-->", errortext)
|
||||||
|
page = page.replace("<!--updateddate-->", updateddate)
|
||||||
|
page = page.replace("\n", "")
|
||||||
|
page = page.replace(" ", "")
|
||||||
|
|
||||||
|
else:
|
||||||
|
responseType = "404 Not Found"
|
||||||
|
fullRequestPath = script_directory + "/webroot/404.html"
|
||||||
|
page = open(fullRequestPath).read()
|
||||||
|
|
||||||
|
responsecontent = page.encode()
|
||||||
|
|
||||||
|
else:
|
||||||
|
responseType = "301 Moved Permanently"
|
||||||
|
responseHeaders += "Location: /?p=/\n".encode()
|
||||||
|
page = "<html><head><title>Redirecting...</title><meta http-equiv=\"refresh\" content=\"0; url='/?p=/'\" /></head><body><p><a href=\"/?p=/\">Redirect</a></p></body></html>"
|
||||||
|
responsecontent = page.encode()
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
page = ""
|
||||||
|
args = ""
|
||||||
|
responseType = "200 OK"
|
||||||
|
contentType = "text/html"
|
||||||
|
|
||||||
|
if "username" in argumentsDict and "password" in argumentsDict:
|
||||||
|
username = urllib.parse.unquote(argumentsDict["username"])
|
||||||
|
password = urllib.parse.unquote(argumentsDict["password"])
|
||||||
|
|
||||||
|
# Validate and sanitize returnpath
|
||||||
|
returnpath = "/"
|
||||||
|
returnto = ""
|
||||||
|
if "returnto" in argumentsDict:
|
||||||
|
candidate = urllib.parse.unquote(argumentsDict["returnto"])
|
||||||
|
resolved = os.path.realpath(script_directory + "/webroot" + candidate)
|
||||||
|
webroot_real = os.path.realpath(script_directory + "/webroot")
|
||||||
|
if resolved.startswith(webroot_real + os.sep) or resolved == webroot_real:
|
||||||
|
returnpath = candidate
|
||||||
|
returnto = "&returnto=" + urllib.parse.quote(returnpath)
|
||||||
|
|
||||||
|
status, result = userManagement.login(username, password)
|
||||||
|
|
||||||
|
if status == "success":
|
||||||
|
token = result
|
||||||
|
responseHeaders += ("Set-Cookie: session=" + token + "; Path=/; HttpOnly; SameSite=Strict\n").encode()
|
||||||
|
url = "/?p=" + urllib.parse.quote(returnpath)
|
||||||
|
else:
|
||||||
|
url = "/?p=/login/" + returnto + "&errortext=" + urllib.parse.quote("Invalid username or password.")
|
||||||
|
|
||||||
|
page = ("<html><head><title>Redirecting...</title>"
|
||||||
|
"<meta http-equiv=\"refresh\" content=\"0; url='" + html.escape(url) + "'\" />"
|
||||||
|
"</head><body><p><a href=\"" + html.escape(url) + "\">Redirect</a></p></body></html>")
|
||||||
|
else:
|
||||||
|
responseType = "400 Bad Request"
|
||||||
|
fullRequestPath = script_directory + "/webroot/400.html"
|
||||||
|
page = open(fullRequestPath).read()
|
||||||
|
|
||||||
|
# Clear sensitive locals
|
||||||
|
username = ""
|
||||||
|
password = ""
|
||||||
|
|
||||||
|
responsecontent = page.encode()
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
responseType = "200 OK"
|
||||||
|
contentType = "text/html"
|
||||||
|
|
||||||
|
session_token = Cookies.get("session", "")
|
||||||
|
if session_token:
|
||||||
|
userManagement.logout(session_token)
|
||||||
|
|
||||||
|
responseHeaders += b"Set-Cookie: session=; Path=/; Max-Age=0; HttpOnly; SameSite=Strict\n"
|
||||||
|
|
||||||
|
returnpath = "/"
|
||||||
|
if "returnto" in argumentsDict:
|
||||||
|
candidate = urllib.parse.unquote(argumentsDict["returnto"])
|
||||||
|
resolved = os.path.realpath(script_directory + "/webroot" + candidate)
|
||||||
|
webroot_real = os.path.realpath(script_directory + "/webroot")
|
||||||
|
if resolved.startswith(webroot_real + os.sep) or resolved == webroot_real:
|
||||||
|
returnpath = candidate
|
||||||
|
|
||||||
|
url = "/?p=" + urllib.parse.quote(returnpath)
|
||||||
|
responseType = "302 Found"
|
||||||
|
responseHeaders += ("Location: " + url + "\n").encode()
|
||||||
|
responsecontent = (
|
||||||
|
"<html><head><title>Redirecting...</title>"
|
||||||
|
"<meta http-equiv=\"refresh\" content=\"0; url='" + html.escape(url) + "'\" />"
|
||||||
|
"</head><body><p><a href=\"" + html.escape(url) + "\">Redirect</a></p></body></html>"
|
||||||
|
).encode()
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
page = ""
|
||||||
|
responseType = "200 OK"
|
||||||
|
contentType = "text/html"
|
||||||
|
|
||||||
|
if "p" in argumentsDict:
|
||||||
|
path = urllib.parse.unquote(argumentsDict["p"])
|
||||||
|
|
||||||
|
webroot_real = os.path.realpath(script_directory + "/webroot")
|
||||||
|
filePath = os.path.realpath(script_directory + "/webroot/" + path)
|
||||||
|
if not filePath.startswith(webroot_real + os.sep) and filePath != webroot_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()
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<html>
|
||||||
|
<head></head>
|
||||||
|
<body style='background-image: url("/Images/test.png")'></body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Googlebot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Bingbot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: GPTBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Claude-Web
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: ClaudeBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: CCBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: FacebookBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Applebot-Extended
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Applebot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Diffbot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Googlebot-Image
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Googlebot-Video
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Slurp
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: DuckDuckBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Baiduspider
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: YandexBot
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: HTTrack
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: Jon Arbuckle
|
||||||
|
Disallow: /robots.txt
|
||||||
|
# trollage
|
||||||
|
|
||||||
|
User-agent: Garfield
|
||||||
|
Disallow: /Lasagne/
|
||||||
|
|
||||||
|
User-agent: archive.org_bot
|
||||||
|
Disallow:
|
||||||
|
|
||||||
|
User-agent: Engineer TF2
|
||||||
|
Disallow: /teleporting/bread
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
page = ""
|
||||||
|
args = ""
|
||||||
|
responseType = "200 OK"
|
||||||
|
contentType = "text/html"
|
||||||
|
|
||||||
|
if "username" in argumentsDict and "password" in argumentsDict and "password2" in argumentsDict:
|
||||||
|
username = urllib.parse.unquote(argumentsDict["username"])
|
||||||
|
password = urllib.parse.unquote(argumentsDict["password"])
|
||||||
|
password2 = urllib.parse.unquote(argumentsDict["password2"])
|
||||||
|
|
||||||
|
# Validate and sanitize returnpath
|
||||||
|
returnpath = "/"
|
||||||
|
returnto = ""
|
||||||
|
if "returnto" in argumentsDict:
|
||||||
|
candidate = urllib.parse.unquote(argumentsDict["returnto"])
|
||||||
|
resolved = os.path.realpath(script_directory + "/webroot" + candidate)
|
||||||
|
webroot_real = os.path.realpath(script_directory + "/webroot")
|
||||||
|
if resolved.startswith(webroot_real + os.sep) or resolved == webroot_real:
|
||||||
|
returnpath = candidate
|
||||||
|
returnto = "&returnto=" + urllib.parse.quote(returnpath)
|
||||||
|
|
||||||
|
error = ""
|
||||||
|
if password != password2:
|
||||||
|
error = "Passwords do not match."
|
||||||
|
elif username == password:
|
||||||
|
error = "Username cannot match password."
|
||||||
|
|
||||||
|
if error == "":
|
||||||
|
status, reason = userManagement.register(username, password)
|
||||||
|
if status == "fail":
|
||||||
|
error = reason
|
||||||
|
|
||||||
|
if error == "":
|
||||||
|
# Auto-login after successful registration
|
||||||
|
status, token = userManagement.login(username, password)
|
||||||
|
if status == "success":
|
||||||
|
responseHeaders += ("Set-Cookie: session=" + token + "; Path=/; HttpOnly; SameSite=Strict\n").encode()
|
||||||
|
url = "/?p=" + urllib.parse.quote(returnpath)
|
||||||
|
else:
|
||||||
|
url = "/?p=/signup/" + returnto + "&errortext=" + urllib.parse.quote(error)
|
||||||
|
|
||||||
|
page = ("<html><head><title>Redirecting...</title>"
|
||||||
|
"<meta http-equiv=\"refresh\" content=\"0; url='" + html.escape(url) + "'\" />"
|
||||||
|
"</head><body><p><a href=\"" + html.escape(url) + "\">Redirect</a></p></body></html>")
|
||||||
|
else:
|
||||||
|
responseType = "400 Bad Request"
|
||||||
|
fullRequestPath = script_directory + "/webroot/400.html"
|
||||||
|
page = open(fullRequestPath).read()
|
||||||
|
|
||||||
|
# Clear sensitive locals
|
||||||
|
username = ""
|
||||||
|
password = ""
|
||||||
|
password2 = ""
|
||||||
|
|
||||||
|
responsecontent = page.encode()
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: comicsansms;
|
||||||
|
src: url(/fonts/comic.ttf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (display: grid) {
|
||||||
|
#oldmodelink {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fitinpageplease {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #f5b;
|
||||||
|
}
|
||||||
|
.bgimage {
|
||||||
|
background-color: #88a;
|
||||||
|
background-image: url("/Images/DingusSun.jpg");
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
position: fixed;
|
||||||
|
top:-10%;
|
||||||
|
left:-10%;
|
||||||
|
filter: blur(8px);
|
||||||
|
-webkit-filter: blur(8px);
|
||||||
|
width: 120%;
|
||||||
|
height: 130vh;
|
||||||
|
height: 130svh;
|
||||||
|
margin: none;
|
||||||
|
}
|
||||||
|
.gray {
|
||||||
|
background-color: #333;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
padding: 1%;
|
||||||
|
margin: none;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top:0;
|
||||||
|
}
|
||||||
|
.bottomright {
|
||||||
|
position:fixed;
|
||||||
|
right:20;
|
||||||
|
bottom:20;
|
||||||
|
}
|
||||||
|
#grid-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
display:grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100svh;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
width: 65%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.article-content-block {
|
||||||
|
background-color: #222;
|
||||||
|
padding: 3%;
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
height: 48px;
|
||||||
|
padding:none;
|
||||||
|
|
||||||
|
}
|
||||||
|
.nav-button {
|
||||||
|
position:relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #333;
|
||||||
|
height: 32px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.nav-content {
|
||||||
|
height: 32px;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.nav-content img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.definition {
|
||||||
|
cursor: help;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
}
|
||||||
|
.dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
width: max-content;
|
||||||
|
background-color: #444;
|
||||||
|
z-index: 1;
|
||||||
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
.nav-button a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-button:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #b5f;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #88f;
|
||||||
|
}
|
||||||
|
li a {
|
||||||
|
color: #f5b;
|
||||||
|
}
|
||||||
|
h6 a {
|
||||||
|
color: #b5f;
|
||||||
|
}
|
||||||
|
.subtitle, .subtitle * {
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.nav-button:hover .dropdown-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.dropdown-content .nav-button {
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f44;
|
||||||
|
color: #000;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0.3%
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.no-print, .no-print * {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.article-content-block {
|
||||||
|
break-inside: auto;
|
||||||
|
}
|
||||||
|
h2, h3 {
|
||||||
|
break-after: avoid;
|
||||||
|
}
|
||||||
|
p, ul, li {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
#grid-container {
|
||||||
|
display: block;
|
||||||
|
position: static;
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
.bgimage {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: comicsansms;
|
||||||
|
src: url(/fonts/comic.ttf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (display: grid) {
|
||||||
|
#oldmodelink {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fitinpageplease {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #503;
|
||||||
|
}
|
||||||
|
.bgimage {
|
||||||
|
background-color: #0f8340;
|
||||||
|
background-image: url("/Images/DingusGrass.jpg");
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
position: fixed;
|
||||||
|
top:-10%;
|
||||||
|
left:-10%;
|
||||||
|
filter: blur(8px);
|
||||||
|
-webkit-filter: blur(8px);
|
||||||
|
width: 120%;
|
||||||
|
height: 130vh;
|
||||||
|
height: 130svh;
|
||||||
|
margin: none;
|
||||||
|
}
|
||||||
|
.gray {
|
||||||
|
background-color: #333;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
padding: 1%;
|
||||||
|
margin: none;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top:0;
|
||||||
|
}
|
||||||
|
.bottomright {
|
||||||
|
position:fixed;
|
||||||
|
right:20;
|
||||||
|
bottom:20;
|
||||||
|
}
|
||||||
|
#grid-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
display:grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100svh;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
width: 65%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.article-content-block {
|
||||||
|
background-color: #ddd;
|
||||||
|
padding: 3%;
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
height: 48px;
|
||||||
|
padding:none;
|
||||||
|
|
||||||
|
}
|
||||||
|
.nav-button {
|
||||||
|
position:relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #333;
|
||||||
|
height: 32px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.nav-content {
|
||||||
|
height: 32px;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.nav-content img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.definition {
|
||||||
|
cursor: help;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
}
|
||||||
|
.dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
width: max-content;
|
||||||
|
background-color: #444;
|
||||||
|
z-index: 1;
|
||||||
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
.nav-button a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-button:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #305;
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
h6 a {
|
||||||
|
color: #305;
|
||||||
|
}
|
||||||
|
.subtitle, .subtitle * {
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
li a {
|
||||||
|
color: #503;
|
||||||
|
}
|
||||||
|
.nav-button:hover .dropdown-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.dropdown-content .nav-button {
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f44;
|
||||||
|
color: #000;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0.3%
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
.no-print, .no-print * {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.article-content-block {
|
||||||
|
break-inside: auto;
|
||||||
|
}
|
||||||
|
h2, h3 {
|
||||||
|
break-after: avoid;
|
||||||
|
}
|
||||||
|
p, ul, li {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
#grid-container {
|
||||||
|
display: block;
|
||||||
|
position: static;
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
.bgimage {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: comicsansms;
|
||||||
|
src: url(/fonts/comic.ttf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (display: grid) {
|
||||||
|
#oldmodelink {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fitinpageplease {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bgimage {
|
||||||
|
background-position: center;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
position: fixed;
|
||||||
|
top:-10%;
|
||||||
|
left:-10%;
|
||||||
|
filter: blur(8px);
|
||||||
|
-webkit-filter: blur(8px);
|
||||||
|
width: 120%;
|
||||||
|
height: 130vh;
|
||||||
|
height: 130svh;
|
||||||
|
margin: none;
|
||||||
|
}
|
||||||
|
.gray {
|
||||||
|
background-color: #333;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
padding: 1%;
|
||||||
|
margin: none;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
position: sticky;
|
||||||
|
top:0;
|
||||||
|
}
|
||||||
|
.bottomright {
|
||||||
|
position:fixed;
|
||||||
|
right:20;
|
||||||
|
bottom:20;
|
||||||
|
}
|
||||||
|
#grid-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
display:grid;
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100svh;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
width: 65%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
min-width: 600px;
|
||||||
|
}
|
||||||
|
.article-content-block {
|
||||||
|
padding: 3%;
|
||||||
|
}
|
||||||
|
.navbar {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
height: 48px;
|
||||||
|
padding:none;
|
||||||
|
}
|
||||||
|
.nav-button {
|
||||||
|
position:relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
border: 2px solid #000;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #333;
|
||||||
|
height: 32px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.nav-content {
|
||||||
|
height: 32px;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.nav-content img {
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
object-fit: cover;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
.definition {
|
||||||
|
cursor: help;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
}
|
||||||
|
.dropdown-content {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
margin-left: -8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
width: max-content;
|
||||||
|
background-color: #444;
|
||||||
|
z-index: 1;
|
||||||
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
.nav-button a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-button:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
.subtitle, .subtitle * {
|
||||||
|
font-size: 10pt;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
.nav-button:hover .dropdown-content {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.dropdown-content .nav-button {
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f44;
|
||||||
|
color: #000;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0.3%;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
* * {
|
||||||
|
color: #000
|
||||||
|
}
|
||||||
|
.no-print, .no-print * {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
article {
|
||||||
|
width: 100%;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.article-content-block {
|
||||||
|
break-inside: auto;
|
||||||
|
}
|
||||||
|
h2, h3 {
|
||||||
|
break-after: avoid;
|
||||||
|
}
|
||||||
|
p, ul, li {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
#grid-container {
|
||||||
|
display: block;
|
||||||
|
position: static;
|
||||||
|
min-height: unset;
|
||||||
|
}
|
||||||
|
.bgimage {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.fitinpageplease {
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
body {
|
||||||
|
color: #f5b;
|
||||||
|
}
|
||||||
|
.bgimage {
|
||||||
|
background-color: #88a;
|
||||||
|
background-image: url("/Images/DingusSun.jpg");
|
||||||
|
}
|
||||||
|
.article-content-block {
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #b5f;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #88f;
|
||||||
|
}
|
||||||
|
li a {
|
||||||
|
color: #f5b;
|
||||||
|
}
|
||||||
|
h6 a {
|
||||||
|
color: #b5f;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
body {
|
||||||
|
color: #503;
|
||||||
|
}
|
||||||
|
.bgimage {
|
||||||
|
background-color: #0f8340;
|
||||||
|
background-image: url("/Images/DingusGrass.jpg");
|
||||||
|
}
|
||||||
|
.article-content-block {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #305;
|
||||||
|
}
|
||||||
|
h6 a {
|
||||||
|
color: #305;
|
||||||
|
}
|
||||||
|
li a {
|
||||||
|
color: #503;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: comicsansms;
|
||||||
|
src: url(/fonts/comic.ttf);
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
margin-left: 32px;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
color: #503;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
.definition {
|
||||||
|
cursor: help;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
color: #305;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #503;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f44;
|
||||||
|
color: #000;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 0.3%
|
||||||
|
}
|
||||||