8 Commits

Author SHA1 Message Date
590abbf649 Uuuh? 2025-06-15 13:33:52 +02:00
87a2505395 Merge branch 'main' of https://git.novacow.ch/Nova/PyWebServer 2025-06-15 13:33:14 +02:00
118a342e9d Update to v1.2, crude fix for #6 and for #5 2025-06-15 13:32:19 +02:00
9cd0528ab3 Update README.md 2025-05-04 14:54:51 +02:00
98aab9539a Made an oopsie with conf file 2025-05-03 23:49:38 +02:00
6244650180 Fix for #4, more permanent solution 2025-05-03 23:47:30 +02:00
c4a1140d83 Merge branch 'main' of https://git.novacow.ch/Nova/PyWebServer 2025-05-03 23:42:01 +02:00
43dd3d1b44 Small #3 acknowlegdement in code 2025-04-18 22:03:30 +02:00
5 changed files with 48 additions and 107 deletions

View File

@@ -1,8 +1,5 @@
# PyWebServer # PyWebServer
# PyWebServer is undergoing major security updates!
# Please use commit [7ac160f625](https://git.novacow.ch/Nova/PyWebServer/src/commit/7ac160f6259e637c2337a672e56f105f4cdd2d2a) as the source for now!!
## GitHub ## GitHub
The upstream of this project is on my own [Gitea instance](https://git.novacow.ch/Nova/PyWebServer/). The upstream of this project is on my own [Gitea instance](https://git.novacow.ch/Nova/PyWebServer/).
Because of that I'll mostly reply to issues and PRs there, you can submit issues and PRs on GitHub, but it might take longer before I read it. Because of that I'll mostly reply to issues and PRs there, you can submit issues and PRs on GitHub, but it might take longer before I read it.

View File

@@ -1,21 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDfTCCAmWgAwIBAgIUbZA2WZ1Q7ZGmYttO+f6w5tFXZLMwDQYJKoZIhvcNAQEL
BQAwazELMAkGA1UEBhMCTk8xETAPBgNVBAgMCE5vcmRsYW5kMQ4wDAYDVQQHDAVC
b2TDuDEXMBUGA1UECgwOTm92YSdzIHRlc3QgQ0ExIDAeBgNVBAMMF05vdmEncyB0
ZXN0aW5nIENBIENlcnQuMB4XDTI1MDMwNDIyMjMwNFoXDTI2MDMwNDIyMjMwNFow
bDELMAkGA1UEBhMCWloxDzANBgNVBAgMBkdsb2JhbDEPMA0GA1UEBwwGR2xvYmFs
MSAwHgYDVQQKDBdOb3ZhJ3MgdGVzdCBjZXJ0aWZpY2F0ZTEZMBcGA1UEAwwQTm92
YSdzIHRlc3QgY2VydDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANmh
gdz5oi+Z1ci0oA1q4NeSeU7b58TkRPvz7g2th4x1OjOhyEA2qG2sOKpjwZ9FB7Ce
TPenZ3M3ISq5MQxGJdHB5tzP86d4fbnRldqS3hs+XW+OYvVWcIonHr8OQXsx1qFP
2yJGIVRMDcxarFg4ZnIk/M5LsgogrYnhOVhg9mi58tLKp+Q+D10RwDPppi0/e5Ud
XM4qrkysY0rA1DwiAgj5MSWwnDCTeUbZDA+znBV5b521VS2XkoVhy49A3lCO2YHc
zAdoyLwAUl84lDN5oQPlqkMN2kEDJw2UDxpCFmzdVvMX30uuQY+vpYI0suwrDBye
0VxkAwX5qI454SLydE8CAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0G
CSqGSIb3DQEBCwUAA4IBAQCIRzTVzeRxWFmBg2wo1W9QXdVorAALw+xcceypHdrA
GYTW7WYLmxXHTSy414p0KFdQ9/CgUpXE0LxwD1gLmWlKEheqlh2T9FPBUK/axZvG
00o/YtAaSDHtiC+OcEzPfTFxEpdOoMMBoCpyLBt+0CgfV1BJFRK9Hw7ZOaVQ2eLC
nxBypEKf3hv0gtGaKnm+vFYDm4Az3+CojtzJiR07WUsPn5HvbOgH6k7jmKuFiR2w
FpPrErVbbLMCZB7+uxfaJyQaEc9DmUf+LDFLbVkM7gk1o249WLjRR5d8MatkwEPN
auYdVlrb/CpxTbNzzipFCX+hnFojuFjXp266woplKleW
-----END CERTIFICATE-----

View File

@@ -9,7 +9,7 @@ class AutoCertGen:
def __init__(self): def __init__(self):
pass pass
def gen_cert(): def gen_cert(self):
# Generate private key # Generate private key
private_key = rsa.generate_private_key( private_key = rsa.generate_private_key(
public_exponent=65537, public_exponent=65537,
@@ -55,7 +55,7 @@ class AutoCertGen:
) )
# Save certificate # Save certificate
with open("certificate.pem", "wb") as f: with open("cert.pem", "wb") as f:
f.write(certificate.public_bytes(serialization.Encoding.PEM)) f.write(certificate.public_bytes(serialization.Encoding.PEM))
print("Self-signed certificate and private key generated for HTTPS server!") print("Self-signed certificate and private key generated for HTTPS server!")

27
key.pem
View File

@@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA2aGB3PmiL5nVyLSgDWrg15J5TtvnxORE+/PuDa2HjHU6M6HI
QDaobaw4qmPBn0UHsJ5M96dnczchKrkxDEYl0cHm3M/zp3h9udGV2pLeGz5db45i
9VZwiicevw5BezHWoU/bIkYhVEwNzFqsWDhmciT8zkuyCiCtieE5WGD2aLny0sqn
5D4PXRHAM+mmLT97lR1cziquTKxjSsDUPCICCPkxJbCcMJN5RtkMD7OcFXlvnbVV
LZeShWHLj0DeUI7ZgdzMB2jIvABSXziUM3mhA+WqQw3aQQMnDZQPGkIWbN1W8xff
S65Bj6+lgjSy7CsMHJ7RXGQDBfmojjnhIvJ0TwIDAQABAoIBAACkfu8pl4Z/dEei
7OQNQDuytYP7lzwYFnIN/tJwhDlwcSsM27wAzU+Blis+nyg6unKVjRGgH2iSLZlk
MZZhMKRlZ6qYPJZufySIz2H1VA2NihYVvAoQZsWppugWgS/9bi5Mv49i2J9YmCPV
0rNx+y90F4D+bTilbw28qgAuTRvzCzTYqcOLnBvjfHfhh1gzOADB4zHGjEb4qwWd
GCPGs85tzfT2Bez6GTCvzNEf8kmGO8EwynZk20SPkswcMIQhES1S6wC3zOi0C9+Y
B4dVnfgtukvsgG+AAtBo8rx6iVIKlGMU3xex9+aZPiJ8O8A/zOJU34IpdDMf7Oha
bK44pgUCgYEA9mK6tmSgN6Sqji9r7hWSse+tX6faeIeFSDIo33pre2jNyM1qTBHY
VZ54CoGa02PLRclqci0TsRaN5Gh+wLVzLW2NDLMEZFXIELF4vGexGnOWLYrxp4hm
uk8rQskoa7/pE7gyjgbYjXqn+wM2ifyc/XXFwTbjbFrj7zPkEdhNmV0CgYEA4h+I
MLn+4PvABojLekU8EHVLAjnWbKYie/a0ELYDz+DZiGgtU21q4HgaOI8SSRA/UvFW
l1i75NmKALT/d89Bok0THmfWAIIPzbsboRJe8f8uce9ICwdmbYKHCJwmgDyhq4ic
UoDzWAuUQa144tcC91Mop5VYa5Ee8TYswIuybZsCgYEA0oQw/D6mFmT/xVUHZvnP
yXD8Ncr5hBpm6vTQr4Gt7Ffz3CqHNE/bA+zOrEtouk1+FTavWLbjKGAZBJu0EXv3
2UzNQ5iBnCkfNAQvIOuICw3Pt0IMkBSfkXirgfjWLJpgz5SGvYtj5B51AKgSJXxN
ttK2EQyQ7LgMIQm5SPYD95ECgYEAlJykpWGYYcUTLzg4guN91lNAOPZKNp35i/9X
2KPHXZgpX70YDPycgWpt0T42hk5nT9vNTSrEUmOmj1BllhhgyopdRl54B11zhYKz
Zejs/Z74p2jbsGPsrYxbswztQNqYZmQiWRbm17bEeWXJTUyCZooA7iL5Objm3SD9
yI4HdoECgYEAzNAa7QJy/bgjuaP8fNx+vfsgMWQ9WT12IXnoiJN4I6mKBrSoJGJ2
EeM41K1lRglI70WDHFPVn7AQvLiFgfWRoI0ucT65VUzYHT+m2g4p7wwb4wPLkhoj
nMLthqEoO+CMIrdGSUVmOwlQ4SnKn9G9a2R4yAEJzkItsYDWD+/hzTc=
-----END RSA PRIVATE KEY-----

View File

@@ -34,6 +34,7 @@ Library aswell as a standalone script:
""" """
import os import os
import mimetypes
import threading import threading
import ssl import ssl
import socket import socket
@@ -59,6 +60,7 @@ class FileHandler:
def __init__(self, base_dir=None): def __init__(self, base_dir=None):
self.config_path = os.path.join(os.getcwd(), self.CONFIG_FILE) self.config_path = os.path.join(os.getcwd(), self.CONFIG_FILE)
self.base_dir = self.read_config("directory")
def check_first_run(self): def check_first_run(self):
if not os.path.isfile(self.config_path): if not os.path.isfile(self.config_path):
@@ -70,23 +72,21 @@ class FileHandler:
with open(self.config_path, "w") as f: with open(self.config_path, "w") as f:
f.write(self.DEFAULT_CONFIG.format(cwd=os.getcwd())) f.write(self.DEFAULT_CONFIG.format(cwd=os.getcwd()))
def didnt_confirm(self):
os.remove(self.config_path)
def read_file(self, file_path): def read_file(self, file_path):
if "../" in file_path: if "../" in file_path:
return 403 return 403, None
full_path = os.path.join(self.base_dir, file_path.lstrip("/")) full_path = os.path.join(self.base_dir, file_path.lstrip("/"))
if not os.path.isfile(full_path): if not os.path.isfile(full_path):
return 404 return 404, None
try: try:
mimetype = mimetypes.guess_type(full_path)
with open(full_path, "rb") as f: with open(full_path, "rb") as f:
return f.read() return f.read(), mimetype
except Exception as e: except Exception as e:
print(f"Error reading file {full_path}: {e}") print(f"Error reading file {full_path}: {e}")
return 500 return 500, None
def write_file(self, file_path, data): def write_file(self, file_path, data):
if "../" in file_path: if "../" in file_path:
@@ -146,6 +146,8 @@ class FileHandler:
"FATAL: You haven't set up PyWebServer! Please edit pywebsrv.conf!" "FATAL: You haven't set up PyWebServer! Please edit pywebsrv.conf!"
) )
exit(1) exit(1)
if value.endswith("/"):
value = value.rstrip("/")
return value return value
return value return value
return None return None
@@ -183,9 +185,11 @@ class RequestParser:
Should (for now) only be GET as I haven't implemented the logic for PUT Should (for now) only be GET as I haven't implemented the logic for PUT
""" """
allowed_methods = ["GET"] allowed_methods = ["GET"]
if os.path.isfile(self.allowed_methods_file): # While the logic for PUT, DELETE, etc. is not added, we shouldn't
with open(self.allowed_methods_file, "r") as f: # allow for it to attempt it.
allowed_methods = [line.strip() for line in f] # if os.path.isfile(self.allowed_methods_file):
# with open(self.allowed_methods_file, "r") as f:
# 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,6 +201,7 @@ class RequestParser:
if ":" in host: if ":" in host:
host = host.split(":", 1)[0] host = host.split(":", 1)[0]
host = host.lstrip() host = host.lstrip()
host = host.rstrip()
if ( if (
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"):
@@ -221,6 +226,12 @@ class WebServer:
# me when no certificate and key file # me when no certificate and 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) or not os.path.exists(self.key_file):
if not os.path.exists(self.cert_file) and not os.path.exists(self.key_file):
pass
elif not os.path.exists(self.cert_file):
os.remove(self.key_file)
elif not os.path.exists(self.key_file):
os.remove(self.cert_file)
print("WARN: No HTTPS certificate was found!") print("WARN: No HTTPS certificate was found!")
if self.file_handler.read_config("disable-autocertgen") is True: if self.file_handler.read_config("disable-autocertgen") is True:
print("WARN: AutoCertGen is disabled, ignoring...") print("WARN: AutoCertGen is disabled, ignoring...")
@@ -260,17 +271,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.1</p>" "<body><center><h1>HTTP 404 - Not Found!</h1><p>Running PyWebServer/1.2</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.1</p>" "<body><center><h1>HTTP 403 - Forbidden</h1><p>Running PyWebServer/1.2</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.1</p>" "<body><center><h1>HTTP 405 - Method not allowed</h1><p>Running PyWebServer/1.2</p>"
"</center></body></html>" "</center></body></html>"
) )
@@ -288,9 +299,9 @@ class WebServer:
if http is True: if http is True:
http_thread.start() http_thread.start()
print( # print(
f"Server running:\n - HTTP on port {self.http_port}\n - HTTPS on port {self.https_port}" # 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()
@@ -341,6 +352,8 @@ class WebServer:
def handle_request(self, data, addr): def handle_request(self, data, addr):
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:
return self.build_response(413, "Request too long")
request_line = data.splitlines()[0] request_line = data.splitlines()[0]
@@ -365,12 +378,18 @@ class WebServer:
method, path, version = self.parser.parse_request_line(request_line) method, path, version = self.parser.parse_request_line(request_line)
# Figure out a better way to reload config
if path == "/?pywebsrv_reload_conf=1":
print("Got reload command! Reloading configuration...")
self.file_handler.base_dir = self.file_handler.read_config("directory")
return self.build_response(204, "")
if not all([method, path, version]) or not self.parser.is_method_allowed( if not all([method, path, version]) or 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)
file_content = self.file_handler.read_file(path) file_content, mimetype = self.file_handler.read_file(path)
if file_content == 403: if file_content == 403:
print("WARN: Directory traversal attack prevented.") # look ma, security!! print("WARN: Directory traversal attack prevented.") # look ma, security!!
@@ -386,13 +405,14 @@ class WebServer:
# A really crude implementation of binary files. Later in 2.0 I'll actually # A really crude implementation of binary files. Later in 2.0 I'll actually
# make this useful. # make this useful.
if path.endswith((".mp3", ".png", ".jpg", ".jpeg", ".gif")): mimetype = mimetype[0]
return self.build_binary_response(200, file_content, path) if "text/" not in mimetype:
return self.build_binary_response(200, file_content, path, 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): def build_binary_response(status_code, binary_data, filename, content_type):
"""Handles binary files like MP3s.""" """Handles binary files like MP3s."""
messages = { messages = {
200: "OK", 200: "OK",
@@ -402,21 +422,9 @@ class WebServer:
500: "Internal Server Error", 500: "Internal Server Error",
} }
status_message = messages.get(status_code) status_message = messages.get(status_code)
# In the spirit of keeping stuff small, we'll just guess and see.
content_type = "application/octet-stream"
if filename.endswith(".mp3"):
content_type = "audio/mpeg"
elif filename.endswith(".png"):
content_type = "image/png"
elif filename.endswith(".jpg") or filename.endswith(".jpeg"):
content_type = "image/jpeg"
elif filename.endswith(".gif"):
content_type = "image/gif"
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.1\r\n" f"Server: PyWebServer/1.2\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,12 +441,15 @@ class WebServer:
""" """
messages = { messages = {
200: "OK", 200: "OK",
204: "No Content",
304: "Not Modified", # TODO KEKL 304: "Not Modified", # TODO KEKL
400: "Bad Request", 400: "Bad Request",
403: "Forbidden", 403: "Forbidden",
404: "Not Found", 404: "Not Found",
405: "Method Not Allowed", 405: "Method Not Allowed",
413: "Payload Too Large",
500: "Internal Server Error", 500: "Internal Server Error",
635: "Go Away",
} }
status_message = messages.get(status_code) status_message = messages.get(status_code)
@@ -447,7 +458,7 @@ class WebServer:
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.1\r\n" f"Server: PyWebServer/1.2\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()
@@ -464,26 +475,7 @@ class WebServer:
def main(): def main():
file_handler = FileHandler() file_handler = FileHandler()
first_run = file_handler.check_first_run() file_handler.check_first_run()
if first_run is True:
print(
"*******************************************************************\n"
"* WARNING!! *\n"
"*******************************************************************\n"
"You have installed PyWebServer for the first time!\n"
"PyWebServer comes with test keys and certificates!\n"
"THESE SHOULD UNDER NO CIRCUMSTANCE BE USED IN ANYTHING BUT LOCAL TESTING!!!\n"
"IF YOU DON'T FOLLOW THESE INSTRUCTIONS YOU ARE PUTTING ALL YOUR TRAFFIC IN DANGER!!!\n"
"PLEASE REMOVE THEM ASAP IF YOU'RE USING THIS IN ANY FORM OF PRODUCTION!!!\n"
"*******************************************************************\n"
"* WARNING!! *\n"
"*******************************************************************\n"
)
confirm = input("Do you understand? [y/N] ")
if confirm != "y":
print("User did not confirm, exiting!")
file_handler.didnt_confirm()
exit(1)
file_handler.base_dir = file_handler.read_config("directory") file_handler.base_dir = file_handler.read_config("directory")
http_port = file_handler.read_config("port") or 8080 http_port = file_handler.read_config("port") or 8080
https_port = file_handler.read_config("port-https") or 8443 https_port = file_handler.read_config("port-https") or 8443