diff --git a/README.md b/README.md
index 61994b3..4703bae 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,12 @@
# 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
+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.
+
## Installing
Installing and running PyWebServer is very simple.
Assuming you're running Linux:
diff --git a/certgen.py b/certgen.py
new file mode 100644
index 0000000..26821ea
--- /dev/null
+++ b/certgen.py
@@ -0,0 +1,61 @@
+from cryptography import x509
+from cryptography.x509.oid import NameOID
+from cryptography.hazmat.primitives import hashes, serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+import datetime
+
+
+class AutoCertGen:
+ def __init__(self):
+ pass
+
+ def gen_cert():
+ # Generate private key
+ private_key = rsa.generate_private_key(
+ public_exponent=65537,
+ key_size=2048,
+ )
+
+ # Define subject and issuer (self-signed)
+ subject = issuer = x509.Name(
+ [
+ x509.NameAttribute(NameOID.COUNTRY_NAME, "ZZ"),
+ x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Some province"),
+ x509.NameAttribute(NameOID.LOCALITY_NAME, "Some place"),
+ x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Some org"),
+ x509.NameAttribute(NameOID.COMMON_NAME, "localhost"),
+ ]
+ )
+
+ # Create certificate
+ certificate = (
+ x509.CertificateBuilder()
+ .subject_name(subject)
+ .issuer_name(issuer)
+ .public_key(private_key.public_key())
+ .serial_number(x509.random_serial_number())
+ .not_valid_before(datetime.datetime.utcnow())
+ .not_valid_after(
+ datetime.datetime.utcnow() + datetime.timedelta(days=365)
+ ) # 1 year validity
+ .add_extension(
+ x509.SubjectAlternativeName([x509.DNSName("localhost")]), critical=False
+ )
+ .sign(private_key, hashes.SHA256())
+ )
+
+ # Save private key
+ with open("key.pem", "wb") as f:
+ f.write(
+ private_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+ )
+
+ # Save certificate
+ with open("certificate.pem", "wb") as f:
+ f.write(certificate.public_bytes(serialization.Encoding.PEM))
+
+ print("Self-signed certificate and private key generated for HTTPS server!")
diff --git a/pywebsrv.py b/pywebsrv.py
index 38babae..ee8375c 100644
--- a/pywebsrv.py
+++ b/pywebsrv.py
@@ -1,4 +1,17 @@
"""
+License:
+ PyWebServer
+ Copyright (C) 2025 Nova
+
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with this program. If not, see .
+
+ Contact:
+ E-mail: nova@novacow.ch
+
This is PyWebServer, an ultra minimalist webserver, meant to still have
a lot standard webserver features. A comprehensive list is below:
Features:
@@ -28,7 +41,7 @@ import signal
import sys
try:
- from autocertgen import AutoCertGen
+ from certgen import AutoCertGen
except ImportError:
print(
"WARN: You need the AutoCertGen plugin! Please install it from\n"
@@ -46,7 +59,6 @@ class FileHandler:
def __init__(self, base_dir=None):
self.config_path = os.path.join(os.getcwd(), self.CONFIG_FILE)
- self.base_dir = self.read_config("directory")
def check_first_run(self):
if not os.path.isfile(self.config_path):
@@ -58,6 +70,9 @@ class FileHandler:
with open(self.config_path, "w") as f:
f.write(self.DEFAULT_CONFIG.format(cwd=os.getcwd()))
+ def didnt_confirm(self):
+ os.remove(self.config_path)
+
def read_file(self, file_path):
if "../" in file_path:
return 403
@@ -127,7 +142,9 @@ class FileHandler:
return bool(int(value))
if option == "directory":
if value == "":
- print("FATAL: You haven't set up PyWebServer! Please edit pywebsrv.conf!")
+ print(
+ "FATAL: You haven't set up PyWebServer! Please edit pywebsrv.conf!"
+ )
exit(1)
return value
return value
@@ -138,10 +155,7 @@ class FileHandler:
Generate some self-signed certificates using AutoCertGen
"""
autocert = AutoCertGen()
- pk = autocert.generate_private_key()
- sub, iss = autocert.generate_issuer_and_subject()
- cert = autocert.build_cert(pk, iss, sub)
- autocert.write_cert(pk, cert)
+ autocert.gen_cert()
class RequestParser:
@@ -318,7 +332,7 @@ class WebServer:
response = self.handle_request(request, addr)
if isinstance(response, str):
- response = response.encode() # if we send text this shouldn't explode
+ response = response.encode()
conn.sendall(response)
except Exception as e:
@@ -370,9 +384,10 @@ class WebServer:
500,
"PyWebServer has encountered a fatal error and cannot serve "
"your request. Contact the owner with this error: FATAL_FILE_RO_ACCESS",
- ) # The user did no fucky-wucky, but the server fucking exploded.
+ ) # When there was an issue with reading we throw this.
- # (try to) detect binary files (eg, mp3) and serve them correctly
+ # A really crude implementation of binary files. Later in 2.0 I'll actually
+ # make this useful.
if path.endswith((".mp3", ".png", ".jpg", ".jpeg", ".gif")):
return self.build_binary_response(200, file_content, path)
@@ -406,12 +421,18 @@ class WebServer:
f"Server: PyWebServer/1.1\r\n"
f"Content-Type: {content_type}\r\n"
f"Content-Length: {len(binary_data)}\r\n"
- f"Connection: close\r\n\r\n" # connection close bcuz im lazy
+ f"Connection: close\r\n\r\n"
+ # Connection close is done because it is way easier to implement.
+ # It's not like this program will see production use anyway.
)
return headers.encode() + binary_data
@staticmethod
def build_response(status_code, body):
+ """
+ For textfiles we'll not have to guess MIME-types, though the other function
+ build_binary_response will be merged in here anyway.
+ """
messages = {
200: "OK",
304: "Not Modified", # TODO KEKL
@@ -436,8 +457,7 @@ class WebServer:
return headers + body
def shutdown(self, signum, frame):
- print(f"\nRecieved signal {signum}")
- print("\nShutting down server...")
+ print("\nRecieved signal to exit!\nShutting down server...")
self.running = False
self.http_socket.close()
self.https_socket.close()
@@ -446,7 +466,27 @@ class WebServer:
def main():
file_handler = FileHandler()
- file_handler.check_first_run()
+ 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")
http_port = file_handler.read_config("port") or 8080
https_port = file_handler.read_config("port-https") or 8443
http_enabled = file_handler.read_config("http") or True