From 4d4a44fd061b961e96a95fbe283ff528d057d26e Mon Sep 17 00:00:00 2001 From: Nova Date: Wed, 20 Aug 2025 13:58:58 +0200 Subject: [PATCH] v1.4, first parts of 2.0 are merged in --- README.md | 39 ++++++++++++++++++++++- pywebsrv.py | 92 +++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 109 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 18fe440..c6f205b 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,43 @@ The upstream of this project is on my own [Gitea instance](https://git.novacow.c 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 +### The little tiny easier route +Installing and running PyWebServer is very simple. +First off, download the latest release from the 'Releases' tab, choose the Zip variant if unsure. +When it's done downloading, unpack the files in a directory of choice, for the purpose of this README, +I've chosen `./pywebserver/` (for Windows: `.\pywebserver\`). +From there, open up your favorite text editor and open the file `pywebsrv.conf` in the directory you unpacked PyWebServer. +In there, you should see this somewhere: +``` +# Here you choose what directory PyWebServer looks in for files. +directory: +``` +After the colon, enter your directory where you have your website stored. +After that, make sure you have installed Python. Here's how you can install Python: +Linux: +```bash +sudo apt install python3 # Debian / Ubuntu +sudo dnf install python3 # Fedora / Nobara +sudo pacman -S python3 # Arch and derivatives. +``` +macOS: +```bash +brew install python3 +``` +Windows: +```powershell +# You can change the `3.12` with whatever version you want/need. +winget install -e --id Python.Python.3.12 --scope machine +``` +Then, in the terminal window you have open, go to the directory you unpacked PyWebServer and type this: +``` +python3 ./pywebsrv.py +# For Windows users, if the above command doesn't work, try this: +py ./pywebsrv.py +``` +And there you go! You've now set up PyWebServer! + +### The little tiny harder route Installing and running PyWebServer is very simple. Assuming you're running Linux: ```bash @@ -48,4 +85,4 @@ For every 1.x version there will be support until 2 newer versions come out. So that means that 1.0 will still be supported when 1.1 comes out, but no longer be supported when 1.2 comes out. ### 2.x I am planning on releasing a 2.x version with will have a lot more advanced features, like nginx's server block emulation amongst other things. -When 2.0 will come out, the last version of 1.x will be supported for a while longer, but no new features will be added. +When 2.0 will come out, 1.x will be developed further, but 2.0 will be the main focus. diff --git a/pywebsrv.py b/pywebsrv.py index 74496bd..65da83c 100644 --- a/pywebsrv.py +++ b/pywebsrv.py @@ -12,6 +12,8 @@ License: Contact: E-mail: nova@novacow.ch +NOTE: Once 2.0 is released, PyWebServer will become the Amethyst Web Server + This is PyWebServer, an ultra minimalist webserver, meant to still have a lot standard webserver features. A comprehensive list is below: Features: @@ -40,39 +42,35 @@ import mimetypes import threading import ssl import socket +import re import signal import sys try: from certgen import AutoCertGen except ImportError: - print( - "WARN: You need the AutoCertGen plugin! Please install it from\n" - "https://git.novacow.ch/Nova/AutoCertGen/" - ) + # just do nothing, it's not working anyway. + # print( + # "WARN: You need the AutoCertGen plugin! Please install it from\n" + # "https://git.novacow.ch/Nova/AutoCertGen/" + # ) + pass class FileHandler: CONFIG_FILE = "pywebsrv.conf" - DEFAULT_CONFIG = ( - "port:8080\nport-https:8443\nhttp:1" - "\nhttps:0\ndirectory:{cwd}\nhost:localhost" - "\nallow-localhost:1" - ) 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): - self.on_first_run() - return True - return False - - def on_first_run(self): - with open(self.config_path, "w") as f: - f.write(self.DEFAULT_CONFIG.format(cwd=os.getcwd())) + self.cached_conf = None + if not os.path.exists(self.config_path): + print( + "The pywebsrv.conf file needs to be in the same directory " + "as pywebsrv.py! Get the default config file from:\n" + "https://git.novacow.ch/Nova/PyWebServer/raw/branch/main/pywebsrv.conf" + ) + exit(1) def read_file(self, file_path): if "../" in file_path: @@ -163,9 +161,55 @@ class FileHandler: return value return None + def read_new_config(self, option, host=None): + """ + Reads the configuration file and returns a dict + """ + if self.cached_conf is None: + with open(self.config_path, "r", encoding="utf-8") as fh: + text = fh.read() + + blocks = re.findall( + r'^(host\s+(\S+)|globals)\s*\{([^}]*)\}', text, re.MULTILINE + ) + parsed = {} + host_list = [] + for tag, hostname, body in blocks: + section = hostname if hostname else "globals" + if hostname: + host_list.append(hostname) + kv = {} + for line in body.splitlines(): + line = line.strip() + if not line or ":" not in line or line.starswith("#"): + continue + + key, rest = line.split(":", 1) + key = key.strip() + rest = rest.strip() + + # Split comma-separated values (e.g. GET,PUT) + if "," in rest: + kv[key] = [item.strip() for item in rest.split(",")] + else: + kv[key] = rest + parsed[section] = kv + parsed["globals"]["hosts"] = host_list + self.cached_conf = parsed + else: + parsed = self.cached_conf + if option == "host": + try: + return host_list + except Exception: + return parsed["globals"]["hosts"] + section = parsed.get(host or "globals", {}) + return section.get(option) + def autocert(self): """ Generate some self-signed certificates using AutoCertGen + TODO: doesn't work, need to fix. probably add `./` to $PATH """ autocert = AutoCertGen() autocert.gen_cert() @@ -229,6 +273,12 @@ class RequestParser: else: return True +# +# class ProxyServer: +# def __init__( +# self, +# ): + class WebServer: def __init__( @@ -455,7 +505,7 @@ class WebServer: status_message = messages.get(status_code) headers = ( f"HTTP/1.1 {status_code} {status_message}\r\n" - f"Server: PyWebServer/1.2.1\r\n" + f"Server: PyWebServer/1.4\r\n" f"Content-Type: {content_type}\r\n" f"Content-Length: {len(binary_data)}\r\n" f"Connection: close\r\n\r\n" @@ -492,7 +542,7 @@ class WebServer: # Don't encode yet, if 302 status code we have to include location. headers = ( f"HTTP/1.1 {status_code} {status_message}\r\n" - f"Server: PyWebServer/1.2.1\r\n" + f"Server: PyWebServer/1.4\r\n" f"Content-Length: {len(body)}\r\n" f"Connection: close\r\n\r\n" ).encode()