v1.2.1: small bugfixes and welcome page.

This commit is contained in:
2025-07-08 14:37:27 +02:00
parent 590abbf649
commit d6fdc5c63d
3 changed files with 81 additions and 49 deletions

13
html/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<title>Test page</title>
</head>
<body>
<h1>Hey there!</h1>
<h2>You're seeing this page because you haven't set up PyWebServer yet!</h2>
<h2>This page confirms that PyWebServer can read and serve files from your PC.</h2>
<h2>To make this go away, please edit the file `pywebsrv.conf` and edit the `directory` key to your directory of choice!</h2>
<p>Here you can simulate a 404 error: <a href="/uuh">Click me for a 404 error!</a></p>
</body>
</html>

View File

@@ -9,9 +9,6 @@ directory:<Enter directory here>
# You can use FQDNs, IP-addresses and localhost, # You can use FQDNs, IP-addresses and localhost,
# Support for multiple hosts is coming. # Support for multiple hosts is coming.
host:localhost host:localhost
# Ignores the host parameter (except for localhost) and allows everything.
# DANGER! For obvious reasons this isn't recommended.
allow-all:0
# Enables HTTP support. (Only enables/disables the HTTP port.) # Enables HTTP support. (Only enables/disables the HTTP port.)
http:1 http:1
# Enables HTTPS support. (Only enables/disables the HTTPS port.) # Enables HTTPS support. (Only enables/disables the HTTPS port.)
@@ -26,6 +23,11 @@ disable-autocertgen:0
# block-ip:0.0.0.0,1.1.1.1,2.2.2.2 # block-ip:0.0.0.0,1.1.1.1,2.2.2.2
# If you wish to block User-Agents, this function is coming though. # If you wish to block User-Agents, this function is coming though.
# block-ua:(NULL) # block-ua:(NULL)
# This function is deprecated, allows a connection with no Host header.
# You should NEVER have to enable this! It can pose a risk to security! # TEST: experimental non-defined keys go here:
# allow-nohost:0 # keyfile key
key-file:/home/nova/PyWebServer/key.pem
# certfile keys
cert-file:/home/nova/PyWebServer/cert.pem
# allowed-methods, csv's
allowed-methods:GET

View File

@@ -31,6 +31,8 @@ Simple to understand and mod codebase.
All GNU GPL-3-or-above license. (Do with it what you want.) All GNU GPL-3-or-above license. (Do with it what you want.)
Library aswell as a standalone script: Library aswell as a standalone script:
You can easily get access to other parts of the script if you need it. You can easily get access to other parts of the script if you need it.
TODO: actually put normal comments in
""" """
import os import os
@@ -55,7 +57,7 @@ class FileHandler:
DEFAULT_CONFIG = ( DEFAULT_CONFIG = (
"port:8080\nport-https:8443\nhttp:1" "port:8080\nport-https:8443\nhttp:1"
"\nhttps:0\ndirectory:{cwd}\nhost:localhost" "\nhttps:0\ndirectory:{cwd}\nhost:localhost"
"allow-all:1\nallow-localhost:1" "\nallow-localhost:1"
) )
def __init__(self, base_dir=None): def __init__(self, base_dir=None):
@@ -109,10 +111,10 @@ class FileHandler:
"http", "http",
"https", "https",
"port-https", "port-https",
"allow-all",
"allow-nohost",
"allow-localhost", "allow-localhost",
"disable-autocertgen", "disable-autocertgen",
"key-file",
"cert-file"
] ]
if option not in valid_options: if option not in valid_options:
return None return None
@@ -127,25 +129,20 @@ class FileHandler:
key = key.lower() key = key.lower()
if key == option: if key == option:
if option == "host": if option == "host":
seperated_values = value.split(",", 0) seperated_values = value.split(",", -1)
return [value.lower() for value in seperated_values] return [value.lower() for value in seperated_values]
if option == "port" or option == "port-https": if option == "port" or option == "port-https":
return int(value) return int(value)
if ( if (
option == "http" option == "http"
or option == "https" or option == "https"
or option == "allow-all"
or option == "allow-localhost" or option == "allow-localhost"
or option == "disable-autocertgen" or option == "disable-autocertgen"
or option == "allow-nohost"
): ):
return bool(int(value)) return bool(int(value))
if option == "directory": if option == "directory":
if value == "<Enter directory here>": if value == "<Enter directory here>":
print( return os.path.join(os.getcwd(), "html")
"FATAL: You haven't set up PyWebServer! Please edit pywebsrv.conf!"
)
exit(1)
if value.endswith("/"): if value.endswith("/"):
value = value.rstrip("/") value = value.rstrip("/")
return value return value
@@ -162,10 +159,8 @@ class FileHandler:
class RequestParser: class RequestParser:
def __init__(self): def __init__(self):
self.allowed_methods_file = "allowedmethods.conf"
self.file_handler = FileHandler() self.file_handler = FileHandler()
self.hosts = self.file_handler.read_config("host") self.hosts = self.file_handler.read_config("host")
self.all_allowed = self.file_handler.read_config("allow-all")
def parse_request_line(self, line): def parse_request_line(self, line):
"""Parses the HTTP request line.""" """Parses the HTTP request line."""
@@ -187,9 +182,8 @@ class RequestParser:
allowed_methods = ["GET"] allowed_methods = ["GET"]
# While the logic for PUT, DELETE, etc. is not added, we shouldn't # While the logic for PUT, DELETE, etc. is not added, we shouldn't
# allow for it to attempt it. # allow for it to attempt it.
# if os.path.isfile(self.allowed_methods_file): # Prepatched for new update.
# with open(self.allowed_methods_file, "r") as f: # allowed_methods = self.file_handler.read_config("allowed-methods")
# allowed_methods = [line.strip() for line in f]
return method in allowed_methods return method in allowed_methods
def host_parser(self, host): def host_parser(self, host):
@@ -197,7 +191,7 @@ class RequestParser:
Parses the host and makes sure it's allowed in Parses the host and makes sure it's allowed in
Mfw im in an ugly code writing contest and my opponent is nova while writing a side project Mfw im in an ugly code writing contest and my opponent is nova while writing a side project
""" """
host = str(host) host = f"{host}"
if ":" in host: if ":" in host:
host = host.split(":", 1)[0] host = host.split(":", 1)[0]
host = host.lstrip() host = host.lstrip()
@@ -206,9 +200,9 @@ class RequestParser:
host == "localhost" or host == "127.0.0.1" host == "localhost" or host == "127.0.0.1"
) and self.file_handler.read_config("allow-localhost"): ) and self.file_handler.read_config("allow-localhost"):
return True return True
if host not in self.hosts and self.all_allowed is False: if host not in self.hosts:
return False return False
elif host not in self.hosts and self.all_allowed is True: else:
return True return True
@@ -228,6 +222,7 @@ class WebServer:
if not os.path.exists(self.cert_file) or not os.path.exists(self.key_file): if not os.path.exists(self.cert_file) or not os.path.exists(self.key_file):
if not os.path.exists(self.cert_file) and not os.path.exists(self.key_file): if not os.path.exists(self.cert_file) and not os.path.exists(self.key_file):
pass pass
# maybe warn users we purge their key/cert files? xdd
elif not os.path.exists(self.cert_file): elif not os.path.exists(self.cert_file):
os.remove(self.key_file) os.remove(self.key_file)
elif not os.path.exists(self.key_file): elif not os.path.exists(self.key_file):
@@ -243,11 +238,8 @@ class WebServer:
else: else:
self.skip_ssl = True self.skip_ssl = True
# TODO: change this to something like oh no you fucked up, go fix idiot
self.no_host_req_response = ( self.no_host_req_response = (
"Connecting via this host is disallowed\r\n" "This host cannot be reached without sending a `Host` header."
"You may also be using a very old browser!\r\n"
"Ask the owner of this website to set allow-all to 1!"
) )
# ipv6 when????/??//?????//? # ipv6 when????/??//?????//?
@@ -271,17 +263,17 @@ class WebServer:
self.http_404_html = ( self.http_404_html = (
"<html><head><title>HTTP 404 - PyWebServer</title></head>" "<html><head><title>HTTP 404 - PyWebServer</title></head>"
"<body><center><h1>HTTP 404 - Not Found!</h1><p>Running PyWebServer/1.2</p>" "<body><center><h1>HTTP 404 - Not Found!</h1><p>Running PyWebServer/1.2.1</p>"
"</center></body></html>" "</center></body></html>"
) )
self.http_403_html = ( self.http_403_html = (
"<html><head><title>HTTP 403 - PyWebServer</title></head>" "<html><head><title>HTTP 403 - PyWebServer</title></head>"
"<body><center><h1>HTTP 403 - Forbidden</h1><p>Running PyWebServer/1.2</p>" "<body><center><h1>HTTP 403 - Forbidden</h1><p>Running PyWebServer/1.2.1</p>"
"</center></body></html>" "</center></body></html>"
) )
self.http_405_html = ( self.http_405_html = (
"<html><head><title>HTTP 405 - PyWebServer</title></head>" "<html><head><title>HTTP 405 - PyWebServer</title></head>"
"<body><center><h1>HTTP 405 - Method not allowed</h1><p>Running PyWebServer/1.2</p>" "<body><center><h1>HTTP 405 - Method not allowed</h1><p>Running PyWebServer/1.2.1</p>"
"</center></body></html>" "</center></body></html>"
) )
@@ -295,14 +287,13 @@ class WebServer:
https_thread = threading.Thread(target=self.start_https, daemon=True) https_thread = threading.Thread(target=self.start_https, daemon=True)
if https is True: if https is True:
if self.skip_ssl is True:
print("WARN: You have enabled HTTPS without SSL!!")
yn = input("Is this intended behaviour? [y/N] ")
https_thread.start() https_thread.start()
if http is True: if http is True:
http_thread.start() http_thread.start()
# print(
# f"Server running:\n - HTTP on port {self.http_port}\n - HTTPS on port {self.https_port}"
# )
http_thread.join() http_thread.join()
https_thread.join() https_thread.join()
@@ -350,6 +341,7 @@ class WebServer:
conn.close() conn.close()
def handle_request(self, data, addr): def handle_request(self, data, addr):
print(f"len data: {len(data)}")
if not data: if not data:
return self.build_response(400, "Bad Request") # user did fucky-wucky return self.build_response(400, "Bad Request") # user did fucky-wucky
if len(data) > 8192: if len(data) > 8192:
@@ -368,13 +360,9 @@ class WebServer:
) )
break break
else: else:
if (
self.file_handler.read_config("allow-nohost") is True
): # no host is stupid
pass
return self.build_response( return self.build_response(
403, self.no_host_req_response.encode() 400, self.no_host_req_response.encode()
) # the default (i hope to god) )
method, path, version = self.parser.parse_request_line(request_line) method, path, version = self.parser.parse_request_line(request_line)
@@ -382,9 +370,12 @@ class WebServer:
if path == "/?pywebsrv_reload_conf=1": if path == "/?pywebsrv_reload_conf=1":
print("Got reload command! Reloading configuration...") print("Got reload command! Reloading configuration...")
self.file_handler.base_dir = self.file_handler.read_config("directory") self.file_handler.base_dir = self.file_handler.read_config("directory")
return self.build_response(204, "") return self.build_response(302, "")
if not all([method, path, version]) or not self.parser.is_method_allowed( if not all([method, path, version]):
return self.build_response(400, "Bad Request")
if not self.parser.is_method_allowed(
method method
): ):
return self.build_response(405, self.http_405_html) return self.build_response(405, self.http_405_html)
@@ -407,12 +398,12 @@ class WebServer:
# make this useful. # make this useful.
mimetype = mimetype[0] mimetype = mimetype[0]
if "text/" not in mimetype: if "text/" not in mimetype:
return self.build_binary_response(200, file_content, path, mimetype) return self.build_binary_response(200, file_content, mimetype)
return self.build_response(200, file_content) return self.build_response(200, file_content)
@staticmethod @staticmethod
def build_binary_response(status_code, binary_data, filename, content_type): def build_binary_response(status_code, binary_data, content_type):
"""Handles binary files like MP3s.""" """Handles binary files like MP3s."""
messages = { messages = {
200: "OK", 200: "OK",
@@ -424,7 +415,7 @@ class WebServer:
status_message = messages.get(status_code) status_message = messages.get(status_code)
headers = ( headers = (
f"HTTP/1.1 {status_code} {status_message}\r\n" f"HTTP/1.1 {status_code} {status_message}\r\n"
f"Server: PyWebServer/1.2\r\n" f"Server: PyWebServer/1.2.1\r\n"
f"Content-Type: {content_type}\r\n" f"Content-Type: {content_type}\r\n"
f"Content-Length: {len(binary_data)}\r\n" f"Content-Length: {len(binary_data)}\r\n"
f"Connection: close\r\n\r\n" f"Connection: close\r\n\r\n"
@@ -433,8 +424,7 @@ class WebServer:
) )
return headers.encode() + binary_data return headers.encode() + binary_data
@staticmethod def build_response(self, status_code, body):
def build_response(status_code, body):
""" """
For textfiles we'll not have to guess MIME-types, though the other function For textfiles we'll not have to guess MIME-types, though the other function
build_binary_response will be merged in here anyway. build_binary_response will be merged in here anyway.
@@ -442,6 +432,7 @@ class WebServer:
messages = { messages = {
200: "OK", 200: "OK",
204: "No Content", 204: "No Content",
302: "Found",
304: "Not Modified", # TODO KEKL 304: "Not Modified", # TODO KEKL
400: "Bad Request", 400: "Bad Request",
403: "Forbidden", 403: "Forbidden",
@@ -456,13 +447,39 @@ class WebServer:
if isinstance(body, str): if isinstance(body, str):
body = body.encode() body = body.encode()
# TODO: dont encode yet, and i encode. awesome comments here.
# Don't encode yet, if 302 status code we have to include location.
headers = ( headers = (
f"HTTP/1.1 {status_code} {status_message}\r\n" f"HTTP/1.1 {status_code} {status_message}\r\n"
f"Server: PyWebServer/1.2\r\n" f"Server: PyWebServer/1.2.1\r\n"
f"Content-Length: {len(body)}\r\n" f"Content-Length: {len(body)}\r\n"
f"Connection: close\r\n\r\n" f"Connection: close\r\n\r\n"
).encode() ).encode()
if status_code == 302:
# 302 currently only happens when the reload is triggered.
# Why not 307, Moved Permanently? Because browsers will cache the
# response and not send the reload command.
host = self.file_handler.read_config("host")[0]
port = self.file_handler.read_config("port-https") or self.file_handler.read_config("port")
if port != 80 and port != 443:
if port == 8443:
host = f"https://{host}:{port}/"
else:
host = f"http://{host}:{port}/"
else:
if port == 443:
host = f"https://{host}/"
else:
host = f"http://{host}/"
headers = (
f"HTTP/1.1 {status_code} {status_message}\r\n"
f"Location: {host}\r\n"
f"Server: PyWebServer/1.2.1\r\n"
f"Content-Length: {len(body)}\r\n"
f"Connection: close\r\n\r\n"
).encode()
return headers + body return headers + body
def shutdown(self, signum, frame): def shutdown(self, signum, frame):