almost ready now pwease? uwu

This commit is contained in:
2026-03-28 23:50:34 +01:00
parent b48b682da7
commit 96eba42c04
6 changed files with 182 additions and 184 deletions
+4 -3
View File
@@ -7,17 +7,18 @@ Every save I do increments the build number by 1, I won't publish all of them, b
Once a milestone is hit (e.g. a new feature fully implemented), I'll publish a release! Once a milestone is hit (e.g. a new feature fully implemented), I'll publish a release!
## Currently working features: ## Currently working features:
* New configuration is ~75% done, most features work. * New configuration is ~95% done, most features work.
* Fixed **A LOT** of unreported bugs from the old code. * Fixed **A LOT** of unreported bugs from the old code.
* More resilliency against errors. * More resilliency against errors.
* Improved security. * Improved security.
* Proxy almost working!
## Project status: ## Project status:
Amethyst will stay in beta for a while, I want all features to work, put I will make pre-release versions that are mostly stable. 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.
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. 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.
## Install instructions: ## Install instructions:
Install Python, and change the provided config. Install Python, execute `amethyst.py` and change the provided config.
## Minimum requirements: ## Minimum requirements:
Python 3.8+ Python 3.8+
+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
}
+161 -115
View File
@@ -52,7 +52,7 @@ import sys
try: try:
if not os.getcwd() in sys.path: if not os.getcwd() in sys.path:
sys.path.append(f"{os.getcwd()}") sys.path.append(f"{os.getcwd()}")
from certgen import AutoCertGen 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(
@@ -61,7 +61,7 @@ except ImportError:
) )
# pass # pass
AMETHYST_BUILD_NUMBER = "0053" AMETHYST_BUILD_NUMBER = "b0.2.0-0072"
AMETHYST_REPO = "https://git.novacow.ch/Nova/PyWebServer/" AMETHYST_REPO = "https://git.novacow.ch/Nova/PyWebServer/"
@@ -119,21 +119,19 @@ class ConfigParser:
class FileHandler: class FileHandler:
CONFIG_FILE = "pywebsrv.conf"
new_conf = "new_conf.conf"
def __init__(self, base_dir=None): def __init__(self, base_dir=None):
# this is a fucking clusterfuck. # this is a fucking clusterfuck.
self.config_path = os.path.join(os.getcwd(), self.CONFIG_FILE) self.config_file = "amethyst.conf"
self.new_conf = os.path.join(os.getcwd(), self.new_conf) self.config_path = os.path.join(os.getcwd(), self.config_file)
self.base_dir = self.read_config("directory") with open(self.config_path, "r") as f:
with open(self.new_conf, "r") as f:
self.cfg = ConfigParser(f.read()) self.cfg = ConfigParser(f.read())
self.base_dir = self.read_config("directory")
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!! # TODO: fix this please!!
@@ -167,72 +165,7 @@ class FileHandler:
f.write(data) f.write(data)
return 0 return 0
def read_config(self, option): def read_config(self, key, host_name=None):
"""
clean code, whats that????
TODO: docs
"""
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
return None
def read_new_config(self, key, host_name=None):
print( print(
f"\n\n\nQuery!\nkey: {key}\nhost_name: {host_name}\nret: {self.cfg.query_config(key, host_name)}" f"\n\n\nQuery!\nkey: {key}\nhost_name: {host_name}\nret: {self.cfg.query_config(key, host_name)}"
) )
@@ -250,7 +183,7 @@ 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_new_config("hosts") self.hosts = self.file_handler.read_config("hosts")
print(f"Hosts: {self.hosts}") print(f"Hosts: {self.hosts}")
def parse_request_line(self, line, host): def parse_request_line(self, line, host):
@@ -258,28 +191,40 @@ class RequestParser:
try: try:
method, path, version = line.split(" ") method, path, version = line.split(" ")
except ValueError: except ValueError:
return "DELETE", "/this/is/a/bogus/request", "HTTP/1.0" return None, None, None
if path.endswith("/") or ("." not in path): if path.endswith("/") or ("." not in path):
if not path.endswith("/"): if not path.endswith("/"):
path += "/" path += "/"
index = self.file_handler.read_new_config("index", host) or "index.html" index = self.file_handler.read_config("index", host) or "index.html"
path += f"{index}" path += f"{index}"
return method, path, version return method, path, version
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): def ua_is_allowed(self, ua, host=None):
"""Parses and matches UA to block""" """Parses and matches UA to block"""
return True
# del host
# _list = self.file_handler.read_config("block-ua")
# if _list is None:
# return True
# match, literal = self.file_handler.parse_match_blocks(_list)
# if ua in literal:
# return False
# for _ua in match:
# if _ua.lower() in ua.lower():
# return False
# return True # 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:
return False
for _ua in match:
if _ua.lower() in ua.lower():
return False
return True
def is_method_allowed(self, method, host=None): def is_method_allowed(self, method, host=None):
""" """
@@ -288,11 +233,7 @@ class RequestParser:
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
# allow for it to attempt it.
# Prepatched for new update.
allowed_methods = self.file_handler.read_new_config("allowed-methods", host)
if allowed_methods is None: if allowed_methods is None:
allowed_methods = ["GET"] allowed_methods = ["GET"]
return method in allowed_methods return method in allowed_methods
@@ -308,18 +249,101 @@ class RequestParser:
host = host.rsplit(":", 1)[0] host = host.rsplit(":", 1)[0]
host = host.lstrip() host = host.lstrip()
host = host.rstrip() host = host.rstrip()
if (
host == "localhost" or host == "127.0.0.1" or host == "[::1]"
) and self.file_handler.read_new_config("allow-localhost"):
return True
if self.hosts is None: if self.hosts is None:
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:
def __init__(self, fh):
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"
@@ -328,8 +352,8 @@ class WebServer:
self.https_port = int(https_port) self.https_port = int(https_port)
self.file_handler = FileHandler() self.file_handler = FileHandler()
self.parser = RequestParser() self.parser = RequestParser()
self.cert_file = self.file_handler.read_new_config("cert") or cert_file self.cert_file = self.file_handler.read_config("cert") or cert_file
self.key_file = self.file_handler.read_new_config("key") or key_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
@@ -362,6 +386,8 @@ class WebServer:
self.https_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) self.https_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
self.https_socket.bind(("::", self.https_port)) self.https_socket.bind(("::", 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
self.ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) self.ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
@@ -438,7 +464,7 @@ 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( response = self.build_response(
@@ -452,6 +478,7 @@ 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}")
@@ -474,6 +501,9 @@ 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."
@@ -485,7 +515,7 @@ class WebServer:
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_is_allowed(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."
@@ -495,31 +525,47 @@ class WebServer:
return self.build_response(400, "You cannot connect without a User-Agent.") return self.build_response(400, "You cannot connect without a User-Agent.")
if ":" in host: if ":" in host:
host2 = host.rsplit(":", 1)[0] host = host.rsplit(":", 1)[0]
else: else:
host2 = host host = host
method, path, version = self.parser.parse_request_line(request_line, host2) 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, "", host=host2) return self.build_response(302, "", host=host)
if not self.parser.is_method_allowed(method): if not self.parser.is_method_allowed(method):
return self.build_response(405, self.http_405_html) return self.build_response(405, self.http_405_html)
directory = ( directory = (
self.file_handler.read_new_config("directory", host2) self.file_handler.read_config("directory", host)
or self.file_handler.base_dir or self.file_handler.base_dir
) )
if self.file_handler.read_new_config("apimode", host2) is True: if self.file_handler.read_config("apimode", host) is True:
if not os.path.join(os.getcwd(), directory) in sys.path: if not os.path.join(os.getcwd(), directory) in sys.path:
sys.path.append(f"{os.path.join(os.getcwd(), directory)}") sys.path.append(f"{os.path.join(os.getcwd(), directory)}")
import api import api
@@ -657,11 +703,11 @@ def main():
input("Press <Enter> to continue. ") input("Press <Enter> to continue. ")
file_handler = FileHandler() file_handler = FileHandler()
file_handler.base_dir = file_handler.read_config("directory") file_handler.base_dir = file_handler.read_config("directory")
http_port = file_handler.read_new_config("port") or 8080 http_port = file_handler.read_config("port") or 8080
https_port = file_handler.read_new_config("https-port") or 8443 https_port = file_handler.read_config("https-port") or 8443
http_enabled = bool(file_handler.read_new_config("http")) or True http_enabled = bool(file_handler.read_config("http")) or True
print(http_enabled) print(http_enabled)
https_enabled = bool(file_handler.read_new_config("https")) or False https_enabled = bool(file_handler.read_config("https")) or False
print(https_enabled) 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)
+1 -1
View File
@@ -8,7 +8,7 @@
<h1>Hello from Amethyst!</h1> <h1>Hello from Amethyst!</h1>
<h2>This page confirms Amethyst can read files from your PC or server and serve them to your browser!</h2> <h2>This page confirms Amethyst can read files from your PC or server and serve them to your browser!</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>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>This server runs Amethyst Pre-Rel Build 0053</p> <p>This server runs Amethyst Pre-Rel 0.2.0-0072</p>
</center> </center>
</body> </body>
</html> </html>
-32
View File
@@ -1,32 +0,0 @@
# WARNING: This is an alpha spec of NSCL 2.0!!
host 192.168.2.196 {
directory:/home/nova/Downloads/test/html
allowed-methods:GET
block-ua:match("Discordbot"),match("Google")
}
host localhost {
directory:/home/nova/PyWebServer/html2
allowed-methods:GET,PUT
block-ip:10.1.100.2
apimode:0
block-ua:match("Discordbot")
}
host 192.168.1.213 {
directory:/home/nova/PyWebServer/html
allowed-methods:GET,PUT
block-ip:10.1.100.2
block-ua:match("Discordbot")
}
globals {
http:1
https:1
port:8080
https-port:8443
allow-localhost:1
key:/home/nova/PyWebServer/ssl/key.pem
cert:/home/nova/PyWebServer/ssl/cert.pem
}
-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:/home/nova/PyWebServer/html
# Host defenition, what hosts you can connect via.
# You can use FQDNs, IP-addresses and localhost,
# Support for multiple hosts is coming.
host:localhost,10.185.213.118
# 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.
block-ua:match(Discordbot),match(google)
# 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