9 Commits

Author SHA1 Message Date
Nova 96eba42c04 almost ready now pwease? uwu 2026-03-28 23:50:34 +01:00
Nova b48b682da7 html, branding change and getting ready for pre-rel 2026-03-24 22:50:26 +01:00
Nova 97896c87a7 idk anymore man 2026-03-13 22:13:59 +01:00
Nova 127612d408 smartass 2026-03-03 22:27:56 +01:00
Nova 3ff7a33695 something pog 2026-03-03 22:26:19 +01:00
Nova 04ec2ebec1 holy fuck shit finally works!!!
alpha build 0039
2026-03-02 21:38:28 +01:00
Nova 36c8c95efe amethyst 0018 2026-01-30 08:41:30 +01:00
Nova f5dafb689e Build 0001, first Amethyst version that starts! 2025-08-20 23:51:31 +02:00
Nova 4eada65040 First testing version of what will become 2.0
Partial new config functionality.
2025-08-20 15:39:14 +02:00
6 changed files with 403 additions and 341 deletions
+27 -93
View File
@@ -1,99 +1,33 @@
# PyWebServer # Amethyst Web Server
# PyWebServer is pretty much abandoned, I am working on a continuation called Amethyst which will be PyWebServer 2.0 in vein. It is being worked on in the branch 2.0. ## A word of warning!
Currently Amethyst is in very early alpha stage, a lot of things will be broken, names won't be correct,
promised features missing, but I'm very much working on it live!
Every save I do increments the build number by 1, I won't publish all of them, but most of them will be published.
Once a milestone is hit (e.g. a new feature fully implemented), I'll publish a release!
## Current state ## Currently working features:
Currently I'm in the middle of bringing major improvements to PyWebServer, like a significantly better config * New configuration is ~95% done, most features work.
automatic HTTPS certificate generation using Let's Encrypt, HTTP `PUT`, `POST` and `DELETE` support, * Fixed **A LOT** of unreported bugs from the old code.
HTTP2 support and auto-indexing. * More resilliency against errors.
This all will be part of major release 2, version 2.0. These improvements will partially make their way into 1.x * Improved security.
versions, from 1.4 onwards, mostly as testing ground and compatibility with 2.0 reasons. Most will probably be dead code * Proxy almost working!
until 2.0 properly comes out. Until then, parts of the code may be pretty cluttered. I'll probably enable you to test some feautres
like the new config, but code quality is currently not a main priority.
## GitHub ## Project status:
The host of this project is on my own [Gitea instance](https://git.novacow.ch/Nova/PyWebServer/). Amethyst will stay in beta for a while, I want all features to work, but I will make pre-release versions that are mostly stable.
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. They can be found as the `amethyst-prerel-0.a.b` releases. I won't guarantee 100% stability, but waay more than just some random build.
## Installing ## Install instructions:
### The little tiny easier route Install Python, execute `amethyst.py` and change the provided config.
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:<Enter directory here>
```
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 ## Minimum requirements:
Installing and running PyWebServer is very simple. Python 3.8+
Assuming you're running Linux: And whatever PC that happens to run that.
```bash I recommend Python 3.10 or above though, with a PC running:
git clone https://git.novacow.ch/Nova/PyWebServer.git * Windows 8.1+
cd ./PyWebServer/ * macOS 10.15+
``` * Linux 4.19+
Windows users, make sure you have installed Git, from there: * FreeBSD 13.2R+
```powershell * Some other somewhat recent OS.
git clone https://git.novacow.ch/Nova/PyWebServer.git
Set-Location .\PyWebServer\
```
Then, open `pywebsrv.conf` in your favorite text editor and change the `directory` key to the full path where your files are stored.
After that, put your files in and run this:
Linux:
```bash
python3 /path/to/pywebsrv.py
```
Windows:
```powershell
# If you have installed Python via the Microsoft Store:
python3 \path\to\pywebsrv.py
# Via the python.org website:
py \path\to\pywebsrv.py
```
## SSL Support ## Currently W.I.P. Check back later!
PyWebServer supports SSL/TLS for authentication via HTTPS. In the config file, you should enable the HTTPS port. After that you need to create the certificate.
Currently PyWebServer looks for the `cert.pem` and the `key.pem` files in the root directory of the installation.
## HTTP support
Currently PyWebServer only supports HTTP/1.1, this is very unlikely to change, as most of the modern web today still uses HTTP/1.1.
For methods PyWebServer only supports `GET`, this is being reworked though, check issue [#3](https://git.novacow.ch/Nova/PyWebServer/issues/3) for progress.
## Files support
Unlike other small web servers, PyWebServer has full support for binary files being sent and received (once that logic is put in) over HTTP(S).
## Support
PyWebServer will follow a standard support scheme.
### 1.x
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, 1.x will be developed further, but 2.0 will be the main focus.
+16
View File
@@ -0,0 +1,16 @@
# WARNING: This is an alpha spec of NSCL 2.0!!
host * {
directory:./html
apimode:0
block-ua:match("Discordbot")
}
globals {
http:1
https:1
port:8080
https-port:8443
key:./key.pem
cert./cert.pem
}
+328 -210
View File
@@ -35,6 +35,8 @@ 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 TODO: actually put normal comments in
TODO: INPROG: add typing to all code, new code will feature it by default.
""" """
import os import os
@@ -42,41 +44,108 @@ import mimetypes
import threading import threading
import ssl import ssl
import socket import socket
import re
# import re
import signal import signal
import sys import sys
try: try:
from certgen import AutoCertGen if not os.getcwd() in sys.path:
sys.path.append(f"{os.getcwd()}")
from .certgen import AutoCertGen
except ImportError: except ImportError:
# just do nothing, it's not working anyway. # just do nothing, it's not working anyway.
# print( print(
# "WARN: You need the AutoCertGen plugin! Please install it from\n" "WARN: You need the AutoCertGen plugin! Please install it from\n"
# "https://git.novacow.ch/Nova/AutoCertGen/" "https://git.novacow.ch/Nova/AutoCertGen/"
# ) )
pass # pass
AMETHYST_BUILD_NUMBER = "b0.2.0-0072"
AMETHYST_REPO = "https://git.novacow.ch/Nova/PyWebServer/"
class ConfigParser:
def __init__(self, text):
self.data: dict = {"hosts": {}, "globals": {}}
self._parse(text)
def _parse(self, text):
lines: list = [
line.strip()
for line in text.splitlines()
if line.strip() and not line.strip().startswith("#")
]
current_block: tuple | None = None
current_name: str | None = None
for line in lines:
if line.startswith("host ") and line.endswith("{"):
current_name = line.split()[1]
self.data["hosts"][current_name] = {}
current_block = ("host", current_name)
continue
if line == "globals {":
current_block = ("globals", None)
continue
if line == "}":
current_block = None
current_name = None
continue
if ":" in line and current_block:
key, value = line.split(":", 1)
key: str = key.strip()
value: str = value.strip()
if "," in value:
value = [v.strip() for v in value.split(",")]
if current_block[0] == "host":
self.data["hosts"][current_name][key] = value
else:
self.data["globals"][key] = value
def query_config(self, key, host=None):
if host:
return self.data["hosts"].get(host, {}).get(key)
if key == "hosts":
print(f"\n\n\nHosts!\nHosts: {self.data['hosts']}\n\n\n")
return list(self.data["hosts"].keys())
return self.data["globals"].get(key)
class FileHandler: class FileHandler:
CONFIG_FILE = "pywebsrv.conf"
def __init__(self, base_dir=None): def __init__(self, base_dir=None):
self.config_path = os.path.join(os.getcwd(), self.CONFIG_FILE) # this is a fucking clusterfuck.
self.config_file = "amethyst.conf"
self.config_path = os.path.join(os.getcwd(), self.config_file)
with open(self.config_path, "r") as f:
self.cfg = ConfigParser(f.read())
self.base_dir = self.read_config("directory") self.base_dir = self.read_config("directory")
self.cached_conf = None
if not os.path.exists(self.config_path): if not os.path.exists(self.config_path):
# uuh???
print( print(
"The pywebsrv.conf file needs to be in the same directory " "The amethyst.conf file needs to be in the same directory "
"as pywebsrv.py! Get the default config file from:\n" "as amethyst.py! Get the default config file from:\n"
"https://git.novacow.ch/Nova/PyWebServer/raw/branch/main/pywebsrv.conf" "https://git.novacow.ch/Nova/PyWebServer/raw/branch/2.0/amethyst.conf"
) )
exit(1) exit(1)
# TODO: fix this please!!
def read_file(self, file_path): def read_file(self, file_path, directory=None):
if "../" in file_path: if "../" in file_path or "%" in file_path:
return 403, None return 403, None
if file_path == "api.py":
return 404, None
full_path = os.path.join(self.base_dir, file_path.lstrip("/")) if directory is not None:
full_path = os.path.join(directory, file_path.lstrip("/"))
else:
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, None return 404, None
@@ -88,125 +157,19 @@ class FileHandler:
print(f"Error reading file {full_path}: {e}") print(f"Error reading file {full_path}: {e}")
return 500, None return 500, None
def write_file(self, file_path, data): def write_file(self, file_path, data, directory=None):
if "../" in file_path: if "../" in file_path or "%" in file_path:
return 403 return 403
full_path = os.path.join(self.base_dir, file_path.lstrip("/")) full_path = os.path.join(self.base_dir, file_path.lstrip("/"))
with open(full_path, "a") as f: with open(full_path, "a") as f:
f.write(data) f.write(data)
return 0 return 0
def read_config(self, option): def read_config(self, key, host_name=None):
""" print(
clean code, whats that???? f"\n\n\nQuery!\nkey: {key}\nhost_name: {host_name}\nret: {self.cfg.query_config(key, host_name)}"
TODO: docs )
""" return self.cfg.query_config(key, host_name)
option = option.lower()
valid_options = [
"port",
"directory",
"host",
"http",
"https",
"port-https",
"allow-localhost",
"disable-autocertgen",
"key-file",
"cert-file",
"block-ua"
]
if option not in valid_options:
return None
with open(self.config_path, "r") as f:
for line in f:
if line.startswith("#"):
continue
try:
key, value = line.strip().split(":", 1)
except ValueError:
return None
key = key.lower()
if key == option:
if option == "host":
seperated_values = value.split(",", -1)
return [value.lower() for value in seperated_values]
if option == "block-ua":
seperated_values = value.split(",", -1)
host_to_match = []
literal_blocks = []
for val in seperated_values:
if val.startswith("match(") and val.endswith(")"):
idx = val.index("(")
idx2 = val.index(")")
ua_to_match = val[idx+1:idx2]
host_to_match.append(ua_to_match)
else:
literal_blocks.append(val)
return host_to_match, literal_blocks
if option == "port" or option == "port-https":
return int(value)
if (
option == "http"
or option == "https"
or option == "allow-localhost"
or option == "disable-autocertgen"
):
return bool(int(value))
if option == "directory":
if value == "<Enter directory here>":
return os.path.join(os.getcwd(), "html")
if value.endswith("/"):
value = value.rstrip("/")
return value
return value
if option == "block-ua":
return [], []
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): def autocert(self):
""" """
@@ -220,21 +183,42 @@ class FileHandler:
class RequestParser: class RequestParser:
def __init__(self): def __init__(self):
self.file_handler = FileHandler() self.file_handler = FileHandler()
self.hosts = self.file_handler.read_config("host") self.hosts = self.file_handler.read_config("hosts")
print(f"Hosts: {self.hosts}")
def parse_request_line(self, line): def parse_request_line(self, line, host):
"""Parses the HTTP request line.""" """Parses the HTTP request line."""
try: try:
method, path, version = line.split(" ") method, path, version = line.split(" ")
except ValueError: except ValueError:
return None, None, None return None, None, None
if path.endswith("/"): if path.endswith("/") or ("." not in path):
path += "index.html" if not path.endswith("/"):
path += "/"
index = self.file_handler.read_config("index", host) or "index.html"
path += f"{index}"
return method, path, version return method, path, version
def ua_blocker(self, ua): def parse_match_blocks(self, to_parse: str | list):
if isinstance(to_parse, str):
to_parse = [to_parse]
match = []
literal = []
for block in to_parse:
if block.startswith('match("'):
adx = block[7:-2]
match.append(adx)
else:
literal.append(block)
return match, literal
def ua_is_allowed(self, ua, host=None):
"""Parses and matches UA to block""" """Parses and matches UA to block"""
match, literal = self.file_handler.read_config("block-ua") # return True
_list = self.file_handler.read_config("block-ua", host)
if _list is None:
return True
match, literal = self.parse_match_blocks(_list)
if ua in literal: if ua in literal:
return False return False
for _ua in match: for _ua in match:
@@ -242,18 +226,16 @@ class RequestParser:
return False return False
return True return True
def is_method_allowed(self, method): def is_method_allowed(self, method, host=None):
""" """
Checks if the HTTP method is allowed. Checks if the HTTP method is allowed.
Reads allowed methods from a configuration file. Reads allowed methods from a configuration file.
Falls back to allowing only 'GET' if the file does not exist. Falls back to allowing only 'GET' if the file does not exist.
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 = self.file_handler.read_config("allowed-methods", host)
# While the logic for PUT, DELETE, etc. is not added, we shouldn't if allowed_methods is None:
# allow for it to attempt it. allowed_methods = ["GET"]
# Prepatched for new update.
# allowed_methods = self.file_handler.read_config("allowed-methods")
return method in allowed_methods return method in allowed_methods
def host_parser(self, host): def host_parser(self, host):
@@ -262,36 +244,116 @@ class RequestParser:
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 = f"{host}" host = f"{host}"
print(f"hosts: {self.hosts}, host: {host}, split: {host.rsplit(':', 1)[0]}")
if ":" in host: if ":" in host:
host = host.split(":", 1)[0] host = host.rsplit(":", 1)[0]
host = host.lstrip() host = host.lstrip()
host = host.rstrip() host = host.rstrip()
if ( if self.hosts is None:
host == "localhost" or host == "127.0.0.1"
) and self.file_handler.read_config("allow-localhost"):
return True return True
if host not in self.hosts: if host not in self.hosts:
if "*" in self.hosts:
return "catchall"
return False return False
else: else:
return True return True
#
# class ProxyServer: class ProxyServer:
# def __init__( def __init__(self, fh):
# self, self.file_handler: FileHandler = fh
# ):
def try_connection(
self, host: str, port: int, data: bytes, chost: str, force_tls: bool = None
):
if port in [443, 8443, 9443]:
do_tls = True
else:
if force_tls is True:
do_tls = True
else:
do_tls = False
print(f"\n\n\nchost: {chost}\n\n\n")
nhost = self.file_handler.read_config("proxy", chost)
print(f"\n\n\nnhost: {nhost}\n\n\n")
if ":" in nhost:
nport = int(nhost.split(":")[1])
nhost = nhost.split(":")[0]
else:
nport = port
print(f"{nhost}, {nport}, {data}")
data = self.reset_host(nhost, nport, data)
try:
return self.tcp_send(host, port, data, do_tls)
except Exception:
if do_tls is False:
print("Retrying with TLS...")
return self.try_connection(host, port, data, chost, True)
else:
raise
@staticmethod
def reset_host(host: str, port: int, data: bytes):
data = data.decode()
data = data.splitlines()
for line in data:
print(line)
if line.startswith("Host:"):
if port not in [80, 443]:
new_line = f"Host: {host}:{port}"
else:
new_line = f"Host: {host}"
idx = data.index(line)
data[idx] = new_line
print(f"\n\n\n{idx}\n\n\n")
if line.startswith("Connection:"):
idx = data.index(line)
new_line = "Connection: close"
data[idx] = new_line
data = "\r\n".join(data)
data = f"{data}\r\n\r\n"
print(data)
return data.encode()
# return data
@staticmethod
def create_tls_context():
# Create a context that by default verifies with system CAs
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
def tcp_send(self, host, port, data: bytes, do_tls: bool):
try:
with socket.create_connection((host, port), timeout=10) as raw_sock:
raw_sock.settimeout(10)
if do_tls:
ctx = self.create_tls_context()
server_hostname = host
with ctx.wrap_socket(
raw_sock, server_hostname=server_hostname
) as ssock:
ssock.sendall(data)
print("data reached")
return ssock.recv(512000)
else:
raw_sock.sendall(data)
return raw_sock.recv(512000)
except Exception:
raise
class WebServer: class WebServer:
def __init__( def __init__(
self, http_port=8080, https_port=8443, cert_file="cert.pem", key_file="key.pem" self, http_port=8080, https_port=8443, cert_file="cert.pem", key_file="key.pem"
): ):
self.http_port = http_port self.http_port = int(http_port)
self.https_port = https_port self.https_port = int(https_port)
self.cert_file = cert_file
self.key_file = key_file
self.file_handler = FileHandler() self.file_handler = FileHandler()
self.parser = RequestParser() self.parser = RequestParser()
self.cert_file = self.file_handler.read_config("cert") or cert_file
self.key_file = self.file_handler.read_config("key") or key_file
self.skip_ssl = False self.skip_ssl = False
# me when no certificate and key file # me when no certificate and key file
@@ -318,14 +380,13 @@ class WebServer:
"This host cannot be reached without sending a `Host` header." "This host cannot be reached without sending a `Host` header."
) )
# ipv6 when????/??//?????//? self.http_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
self.http_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.http_socket.bind(("::", self.http_port))
self.http_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.http_socket.bind(("0.0.0.0", self.http_port))
self.https_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.https_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
self.https_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) self.https_socket.bind(("::", self.https_port))
self.https_socket.bind(("0.0.0.0", self.https_port))
self.proxy_handler = ProxyServer(self.file_handler)
if self.skip_ssl is False: if self.skip_ssl is False:
# https gets the ssl treatment!! yaaaay :3 # https gets the ssl treatment!! yaaaay :3
@@ -338,18 +399,18 @@ class WebServer:
) )
self.http_404_html = ( self.http_404_html = (
"<html><head><title>HTTP 404 - PyWebServer</title></head>" "<html><head><title>HTTP 404 - Amethyst</title></head>"
"<body><center><h1>HTTP 404 - Not Found!</h1><p>Running PyWebServer/1.2.1</p>" f"<body><center><h1>HTTP 404 - Not Found!</h1><p>Running Amethyst/build-{AMETHYST_BUILD_NUMBER}</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 - Amethyst</title></head>"
"<body><center><h1>HTTP 403 - Forbidden</h1><p>Running PyWebServer/1.2.1</p>" f"<body><center><h1>HTTP 403 - Forbidden</h1><p>Running Amethyst/build-{AMETHYST_BUILD_NUMBER}</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 - Amethyst</title></head>"
"<body><center><h1>HTTP 405 - Method not allowed</h1><p>Running PyWebServer/1.2.1</p>" f"<body><center><h1>HTTP 405 - Method not allowed</h1><p>Running Amethyst/build-{AMETHYST_BUILD_NUMBER}</p>"
"</center></body></html>" "</center></body></html>"
) )
@@ -366,6 +427,8 @@ class WebServer:
if self.skip_ssl is True: if self.skip_ssl is True:
print("WARN: You have enabled HTTPS without SSL!!") print("WARN: You have enabled HTTPS without SSL!!")
yn = input("Is this intended behaviour? [y/N] ") yn = input("Is this intended behaviour? [y/N] ")
if yn.lower() == "n":
exit(1)
https_thread.start() https_thread.start()
if http is True: if http is True:
http_thread.start() http_thread.start()
@@ -401,10 +464,12 @@ class WebServer:
def handle_connection(self, conn, addr): def handle_connection(self, conn, addr):
try: try:
data = conn.recv(512) data = conn.recv(32768)
request = data.decode(errors="ignore") request = data.decode(errors="ignore")
if not data: if not data:
response = self.build_response(400, "Bad Request") # user did fucky-wucky response = self.build_response(
400, "Bad Request"
) # user did fucky-wucky
elif len(data) > 8192: elif len(data) > 8192:
response = self.build_response(413, "Request too long") response = self.build_response(413, "Request too long")
else: else:
@@ -413,9 +478,17 @@ class WebServer:
if isinstance(response, str): if isinstance(response, str):
response = response.encode() response = response.encode()
print(len(response))
conn.sendall(response) conn.sendall(response)
except Exception as e: except Exception as e:
print(f"Error handling connection: {e}") print(f"Error handling connection: {e}")
response = self.build_response(
500,
"Amethyst is currently unable to serve your request. Below is debug info.\r\n"
f"Error: {e}; Version: amethyst-b{AMETHYST_BUILD_NUMBER}\r\n"
"You cannot do anything at this time, the server owner has made a misconfiguration or there is a bug in the program",
)
conn.sendall(response)
finally: finally:
conn.close() conn.close()
@@ -428,48 +501,79 @@ class WebServer:
if "Host" in line: if "Host" in line:
host = line.split(":", 1)[1].strip() host = line.split(":", 1)[1].strip()
allowed = self.parser.host_parser(host) allowed = self.parser.host_parser(host)
if allowed == "catchall":
host = "*"
allowed = True
if not allowed: if not allowed:
return self.build_response( return self.build_response(
403, "Connecting via this host is disallowed." 403, "Connecting via this host is disallowed."
) )
break break
else: else:
return self.build_response( return self.build_response(400, self.no_host_req_response.encode())
400, self.no_host_req_response.encode()
)
for line in data.splitlines(): for line in data.splitlines():
if "User-Agent" in line: if "User-Agent" in line:
ua = line.split(":", 1)[1].strip() ua = line.split(":", 1)[1].strip()
allowed = self.parser.ua_blocker(ua) allowed = self.parser.ua_is_allowed(ua, host)
if not allowed: if not allowed:
return self.build_response( return self.build_response(
403, "This UA has been blocked by the owner of this site." 403, "This UA has been blocked by the owner of this site."
) )
break break
else: else:
return self.build_response( return self.build_response(400, "You cannot connect without a User-Agent.")
400, "You cannot connect without a User-Agent."
)
method, path, version = self.parser.parse_request_line(request_line) if ":" in host:
host = host.rsplit(":", 1)[0]
else:
host = host
method, path, version = self.parser.parse_request_line(request_line, host)
if not all([method, path, version]): if not all([method, path, version]):
return self.build_response(400, "Bad Request") return self.build_response(400, "Bad Request")
if self.file_handler.read_config("proxy", host) is not None:
orig_host = host
value = self.file_handler.read_config("proxy", host)
if ":" in value:
host = value.split(":")[0]
port = int(value.split(":")[1])
else:
host = value
port = 443
return self.proxy_handler.try_connection(
host,
port,
data.encode(),
orig_host,
)
# Figure out a better way to reload config # Figure out a better way to reload config
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 = FileHandler() self.file_handler = FileHandler()
self.parser = RequestParser() self.parser = RequestParser()
return self.build_response(302, "") return self.build_response(302, "", host=host)
if not self.parser.is_method_allowed( 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)
file_content, mimetype = self.file_handler.read_file(path) directory = (
self.file_handler.read_config("directory", host)
or self.file_handler.base_dir
)
if self.file_handler.read_config("apimode", host) is True:
if not os.path.join(os.getcwd(), directory) in sys.path:
sys.path.append(f"{os.path.join(os.getcwd(), directory)}")
import api
apiclass = api.API()
return apiclass.on_request(data)
file_content, mimetype = self.file_handler.read_file(path, directory)
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!!
@@ -479,16 +583,16 @@ class WebServer:
if file_content == 500: if file_content == 500:
return self.build_response( return self.build_response(
500, 500,
"PyWebServer has encountered a fatal error and cannot serve " "Amethyst has encountered a fatal error and cannot serve "
"your request. Contact the owner with this error: FATAL_FILE_RO_ACCESS", "your request. Contact the owner with this error: FATAL_FILE_RO_ACCESS",
) # When there was an issue with reading we throw this. ) # When there was an issue with reading we throw this.
# A really crude implementation of binary files. Later in 2.0 I'll actually
# make this useful.
mimetype = mimetype[0] mimetype = mimetype[0]
if mimetype is None: if mimetype is None:
# We have to assume it's binary. # We have to assume it's binary.
return self.build_binary_response(200, file_content, "application/octet-stream") return self.build_binary_response(
200, file_content, "application/octet-stream"
)
if "text/" not in mimetype: if "text/" not in mimetype:
return self.build_binary_response(200, file_content, mimetype) return self.build_binary_response(200, file_content, mimetype)
@@ -502,22 +606,22 @@ class WebServer:
403: "Forbidden", 403: "Forbidden",
404: "Not Found", 404: "Not Found",
405: "Method Not Allowed", 405: "Method Not Allowed",
500: "Internal Server Error" 500: "Internal Server Error",
} }
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.4\r\n" f"Server: Amethyst/amethyst-build-{AMETHYST_BUILD_NUMBER}\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"
# Connection close is done because it is way easier to implement. # Connection close is done because it is way easier to implement.
# It's not like this program will see production use anyway. # It's not like this program will see production use anyway.
# Tbh when i'll implement HTTP2
) )
return headers.encode() + binary_data return headers.encode() + binary_data
def build_response(self, status_code, body): @staticmethod
def build_response(status_code, body, host=None):
""" """
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.
@@ -533,7 +637,7 @@ class WebServer:
405: "Method Not Allowed", 405: "Method Not Allowed",
413: "Payload Too Large", 413: "Payload Too Large",
500: "Internal Server Error", 500: "Internal Server Error",
635: "Go Away" 621: "fuck off! :3",
} }
status_message = messages.get(status_code) status_message = messages.get(status_code)
@@ -544,7 +648,7 @@ class WebServer:
# Don't encode yet, if 302 status code we have to include location. # 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.4\r\n" f"Server: Amethyst/build-{AMETHYST_BUILD_NUMBER}\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()
@@ -553,26 +657,28 @@ class WebServer:
# 302 currently only happens when the reload is triggered. # 302 currently only happens when the reload is triggered.
# Why not 307, Moved Permanently? Because browsers will cache the # Why not 307, Moved Permanently? Because browsers will cache the
# response and not send the reload command. # response and not send the reload command.
host = self.file_handler.read_config("host")[0] # if port == 443:
port = self.file_handler.read_config("port-https") or self.file_handler.read_config("port") # host = f"https://{host}/"
if port != 80 and port != 443: # else:
if port == 8443: # host = f"http://{host}/"
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 = ( headers = (
f"HTTP/1.1 {status_code} {status_message}\r\n" f"HTTP/1.1 {status_code} {status_message}\r\n"
f"Location: {host}\r\n" f"Location: {host}\r\n"
f"Server: PyWebServer/1.2.1\r\n" f"Server: Amethyst/build-{AMETHYST_BUILD_NUMBER}\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 == 621:
headers = (
f"HTTP/1.1 {status_code} {status_message}\r\n"
"Server: Amethyst/build-0621\r\n"
"Content-Length: 30\r\n"
f"Connection: close\r\n\r\n"
)
body = "https://e621.net/posts/6155664"
print(f"{headers + body}")
return headers + body return headers + body
def shutdown(self, signum, frame): def shutdown(self, signum, frame):
@@ -584,13 +690,25 @@ class WebServer:
def main(): def main():
print(
"WARNING!!\n"
f"This is Amethyst alpha build {AMETHYST_BUILD_NUMBER}\n"
"Since this is an alpha version of Amethyst, most features aren't working!\n"
"These builds are also very verbose and will spit out a lot on the terminal. "
"As you can imagine, this is for debugging purposes.\n"
"THERE IS ABSOLUTELY NO SUPPORT FOR THESE VERSIONS!\n"
"DO NOT USE THEM IN PRODUCTION SETTINGS!\n"
f"Please report any bugs on {AMETHYST_REPO}\n"
)
input("Press <Enter> to continue. ")
file_handler = FileHandler() file_handler = FileHandler()
# file_handler.check_first_run()
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("https-port") or 8443
http_enabled = file_handler.read_config("http") or True http_enabled = bool(file_handler.read_config("http")) or True
https_enabled = file_handler.read_config("https") or False print(http_enabled)
https_enabled = bool(file_handler.read_config("https")) or False
print(https_enabled)
server = WebServer(http_port=http_port, https_port=https_port) server = WebServer(http_port=http_port, https_port=https_port)
server.start(http_enabled, https_enabled) server.start(http_enabled, https_enabled)
+26
View File
@@ -0,0 +1,26 @@
"""
This is the Amethyst API mode Python interface whatevers.
Docs will follow.
"""
# Below go imports.
import sys
import os
if not os.getcwd() in sys.path:
sys.path.append(os.getcwd())
import pywebsrv
class API:
"""
class
"""
def __init__(self):
# DO NOT USE THIS CLASS FOR PROGRAM, ONLY ON_REQUEST PLEASE!!
# Below go definitions to get things working.
self.build_response = pywebsrv.WebServer.build_binary_response
def on_request(self, req):
return self.build_response(200, "This is a test", "text/html")
+6 -5
View File
@@ -4,10 +4,11 @@
<title>Test page</title> <title>Test page</title>
</head> </head>
<body> <body>
<h1>Hey there!</h1> <center>
<h2>You're seeing this page because you haven't set up PyWebServer yet!</h2> <h1>Hello from Amethyst!</h1>
<h2>This page confirms that PyWebServer can read and serve files from your PC.</h2> <h2>This page confirms Amethyst can read files from your PC or server and serve them to your browser!</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>This is a test page, if you aren't the server owner, they might not have finished setting up their site, be patient. If this doesn't go away after a while, tell them they've made an oopsie</p>
<p>Here you can simulate a 404 error: <a href="/uuh">Click me for a 404 error!</a></p> <p>This server runs Amethyst Pre-Rel 0.2.0-0072</p>
</center>
</body> </body>
</html> </html>
-33
View File
@@ -1,33 +0,0 @@
# Using NSCL 1.3
# Port defenition. What ports to use.
# port is the HTTP port, port-https is the HTTPS port
port:8080
port-https:8443
# Here you choose what directory PyWebServer looks in for files.
directory:<Enter directory here>
# Host defenition, what hosts you can connect via.
# You can use FQDNs, IP-addresses and localhost,
# Support for multiple hosts is coming.
host:localhost
# Enables HTTP support. (Only enables/disables the HTTP port.)
http:1
# Enables HTTPS support. (Only enables/disables the HTTPS port.)
https:1
# Allows the use of localhost to connect.
# The default is on, this is seperate of the host defenition.
allow-localhost:1
# If you're using the webserver in a library form,
# you can disable the AutoCertGen and never trigger it.
disable-autocertgen:0
# If you wish to block IP-addresses, this function is coming though.
# 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.
# block-ua:(NULL)
# TEST: experimental non-defined keys go here:
# 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