4 Commits
Author SHA1 Message Date
Nova bab41a0084 Merge branch '2.0' of https://git.novacow.ch/Nova/PyWebServer into 2.0 2026-08-10 16:35:14 +02:00
Nova cec8a80714 First feature-freeze code cleanup version 2026-08-10 16:34:45 +02:00
Nova ba9c2dff67 Update CHANGELOG.md 2026-08-10 16:03:33 +02:00
Nova 39a92ede21 Update README.md 2026-08-10 16:03:10 +02:00
3 changed files with 16 additions and 46 deletions
+1 -1
View File
@@ -12,7 +12,7 @@
* Fixed bugs pertaining to proxy * Fixed bugs pertaining to proxy
* Attempted fix at hanging socket by introducing a default 25 second timeout. * Attempted fix at hanging socket by introducing a default 2 second timeout.
## Configuration changes ## Configuration changes
+4 -2
View File
@@ -9,15 +9,17 @@ Once a milestone is hit (e.g. a new feature fully implemented), I'll publish a r
## Approaching 1.0.0! ## Approaching 1.0.0!
Amethyst is finally approaching 1.0.0! Very very soon I will feature-freeze the project and begin just fixing bugs and cleaning up code! This may take a bit because the codebase is very cluttered, and because all features are there in a basic state, it would be better to fix and clean up what I have, so I have a workable codebase for implementing new features, and because new features aren't going to be added anyway, I might as well fully release the project! I have feature-freezed the project! Any further release is just cleaning up the codebase and getting some snazzy new default webpage or something idk yet xdd.
## Currently working features: ## Currently working features:
* New configuration is ~95% done, most features work. * New configuration!
* 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! * Proxy almost working!
* Multithreading!
* Python Execution Script!
## Project status: ## Project status:
+11 -43
View File
@@ -47,9 +47,8 @@ import threading
import ssl import ssl
import socket import socket
import signal import signal
# Experimental imports go here
import select import select
import subprocess # Experimental imports go here
try: try:
if not os.getcwd() in sys.path: if not os.getcwd() in sys.path:
@@ -63,7 +62,7 @@ except ImportError:
) )
# pass # pass
AMETHYST_BUILD_NUMBER = "0.3.1-0130-mt-tryout2" AMETHYST_BUILD_NUMBER = "0.99.0-0134-ff"
AMETHYST_REPO = "https://git.novacow.ch/Nova/PyWebServer/" AMETHYST_REPO = "https://git.novacow.ch/Nova/PyWebServer/"
@@ -115,7 +114,6 @@ class ConfigParser:
if host: if host:
value = self.data["hosts"].get(host, {}).get(key) value = self.data["hosts"].get(host, {}).get(key)
elif key == "hosts": elif key == "hosts":
print(f"\n\n\nHosts!\nHosts: {self.data['hosts']}\n\n\n")
value = list(self.data["hosts"].keys()) value = list(self.data["hosts"].keys())
else: else:
value = self.data["globals"].get(key) value = self.data["globals"].get(key)
@@ -172,9 +170,6 @@ class FileHandler:
return 0 return 0
def read_config(self, key, host_name=None): def read_config(self, key, host_name=None):
print(
f"\n\n\nQuery!\nkey: {key}\nhost_name: {host_name}\nret: {self.cfg.query_config(key, host_name)}"
)
return self.cfg.query_config(key, host_name) return self.cfg.query_config(key, host_name)
def autocert(self): def autocert(self):
@@ -190,7 +185,6 @@ class RequestParser:
def __init__(self): def __init__(self):
self.file_handler = FileHandler() self.file_handler = FileHandler()
self.hosts = self.file_handler.read_config("hosts") self.hosts = self.file_handler.read_config("hosts")
print(f"Hosts: {self.hosts}")
def extract_header(self, header: str, request: bytes | str): def extract_header(self, header: str, request: bytes | str):
if isinstance(request, bytes): if isinstance(request, bytes):
@@ -260,7 +254,6 @@ 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.rsplit(":", 1)[0] host = host.rsplit(":", 1)[0]
host = host.lstrip() host = host.lstrip()
@@ -296,9 +289,7 @@ class ProxyServer:
def try_connection( def try_connection(
self, host: str, port: int, data: bytes, chost: str, force_tls: bool = None self, host: str, port: int, data: bytes, chost: str, force_tls: bool = None
): ):
print(f"\n\n\nchost: {chost}\n\n\n")
nhost = self.file_handler.read_config("proxy", chost) nhost = self.file_handler.read_config("proxy", chost)
print(f"\n\n\nnhost: {nhost}\n\n\n")
# nhost will include http or https. # nhost will include http or https.
if nhost.startswith("https"): if nhost.startswith("https"):
nhost = nhost[6:-1] nhost = nhost[6:-1]
@@ -313,16 +304,13 @@ class ProxyServer:
) )
if force_tls is True: if force_tls is True:
do_tls = True do_tls = True
print(f"\n\n\nnhost: {nhost}\n\n\n")
if ":" in nhost: if ":" in nhost:
nport = int(nhost.split(":")[1]) nport = int(nhost.split(":")[1])
nhost = nhost.split(":")[0] nhost = nhost.split(":")[0]
else: else:
nport = port nport = port
print(f"{nhost}, {nport}, {data}")
data = self.reset_host(nhost, nport, data) data = self.reset_host(nhost, nport, data)
try: try:
print("Waiting on TCP start.")
return self.tcp_send(nhost, nport, data, do_tls) return self.tcp_send(nhost, nport, data, do_tls)
except Exception as e: except Exception as e:
raise Exception(f"Server replied unexpected. Reply from Python subsystem: {e}") raise Exception(f"Server replied unexpected. Reply from Python subsystem: {e}")
@@ -378,7 +366,6 @@ class ProxyServer:
raw_sock, server_hostname=server_hostname raw_sock, server_hostname=server_hostname
) as ssock: ) as ssock:
ssock.sendall(data) ssock.sendall(data)
print("data reached")
resp = self.recv_all(ssock) resp = self.recv_all(ssock)
if self.rq.extract_header("Transfer-Encoding", resp) == "chunked": if self.rq.extract_header("Transfer-Encoding", resp) == "chunked":
ssock.sendall(b"TRANSER-ENCODING IS NOT SUPPORTED") ssock.sendall(b"TRANSER-ENCODING IS NOT SUPPORTED")
@@ -393,9 +380,7 @@ class ProxyServer:
) )
return resp return resp
else: else:
print(f"\n\n\nraw data: {data}\n\n\n")
raw_sock.sendall(data) raw_sock.sendall(data)
print("Waiting for response...")
resp = self.recv_all(raw_sock) resp = self.recv_all(raw_sock)
if self.rq.extract_header("Transfer-Encoding", resp) is not None: if self.rq.extract_header("Transfer-Encoding", resp) is not None:
raw_sock.sendall(b"TRANSER-ENCODING IS NOT SUPPORTED") raw_sock.sendall(b"TRANSER-ENCODING IS NOT SUPPORTED")
@@ -454,11 +439,9 @@ class WebServer:
self.http_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) self.http_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
self.http_socket.bind(("::", self.http_port)) self.http_socket.bind(("::", self.http_port))
# self.http_socket.settimeout(1)
self.https_socket_raw = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) self.https_socket_raw = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
self.https_socket_raw.bind(("::", self.https_port)) self.https_socket_raw.bind(("::", self.https_port))
# self.https_socket_raw.settimeout(1)
self.proxy_handler = ProxyServer(self.file_handler) self.proxy_handler = ProxyServer(self.file_handler)
@@ -493,20 +476,27 @@ class WebServer:
def start(self, http, https): def start(self, http, https):
signal.signal(signal.SIGINT, self.shutdown) signal.signal(signal.SIGINT, self.shutdown)
signal.signal(signal.SIGTERM, self.shutdown) signal.signal(signal.SIGTERM, self.shutdown)
http_thread = threading.Thread(target=self.start_http, daemon=True)
https_thread = threading.Thread(target=self.start_https, daemon=True)
if https is True: if https is True:
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": if yn.lower() == "n":
exit(1) exit(1)
self.start_https() https_thread.start()
else: else:
self.https_socket.close() self.https_socket.close()
if http is True: if http is True:
self.start_http() http_thread.start()
else: else:
self.http_socket.close() self.http_socket.close()
http_thread.join()
https_thread.join()
def start_http(self): def start_http(self):
self.http_socket.listen(5) self.http_socket.listen(5)
print(f"HTTP server listening on port {self.http_port}...") print(f"HTTP server listening on port {self.http_port}...")
@@ -535,7 +525,6 @@ class WebServer:
except OSError as e: except OSError as e:
if not self.running: if not self.running:
break break
print(f"OSError! {e}")
continue continue
except Exception as e: except Exception as e:
print(f"HTTP error: {e}") print(f"HTTP error: {e}")
@@ -568,7 +557,6 @@ class WebServer:
except OSError as e: except OSError as e:
if not self.running: if not self.running:
break break
print(f"OSError! {e}")
continue continue
except Exception as e: except Exception as e:
print(f"HTTPS error: {e}") print(f"HTTPS error: {e}")
@@ -591,14 +579,10 @@ class WebServer:
if line.lower().startswith(b"content-length:"): if line.lower().startswith(b"content-length:"):
content_length = int(line.split(b":")[1].strip()) content_length = int(line.split(b":")[1].strip())
# print(f"Content-Length to server: {content_length}")
# Read body # Read body
body = rest body = rest
print(f"Rest length: {len(rest)}")
while len(body) < content_length: while len(body) < content_length:
chunk = conn.recv(4096) chunk = conn.recv(4096)
# print(f"\n\nrecv returned {len(chunk)}\n\n")
if not chunk: if not chunk:
print("\n\nsocket closed\n\n") print("\n\nsocket closed\n\n")
break break
@@ -618,7 +602,6 @@ 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}")
@@ -633,10 +616,8 @@ class WebServer:
conn.close() conn.close()
def handle_request(self, data, addr): def handle_request(self, data, addr):
# print(f"data: {data}")
request_line = data.splitlines()[0] request_line = data.splitlines()[0]
# Extract host from headers, never works though
for line in data.splitlines(): for line in data.splitlines():
if "Host" in line: if "Host" in line:
host = line.split(":", 1)[1].strip() host = line.split(":", 1)[1].strip()
@@ -735,7 +716,6 @@ class WebServer:
file_content, mimetype = self.file_handler.read_file(path, directory) 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!!
return self.build_response(403, self.http_403_html) return self.build_response(403, self.http_403_html)
if file_content == 404: if file_content == 404:
return self.build_response(404, self.http_404_html) return self.build_response(404, self.http_404_html)
@@ -789,7 +769,6 @@ class WebServer:
200: "OK", 200: "OK",
204: "No Content", 204: "No Content",
302: "Found", 302: "Found",
304: "Not Modified", # TODO KEKL
400: "Bad Request", 400: "Bad Request",
403: "Forbidden", 403: "Forbidden",
404: "Not Found", 404: "Not Found",
@@ -803,8 +782,6 @@ class WebServer:
if isinstance(body, str): if isinstance(body, str):
body = body.encode() body = body.encode()
# TODO: dont encode yet, and i encode. awesome comments here.
# 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: Amethyst/build-{AMETHYST_BUILD_NUMBER}\r\n" f"Server: Amethyst/build-{AMETHYST_BUILD_NUMBER}\r\n"
@@ -813,13 +790,6 @@ class WebServer:
).encode() ).encode()
if status_code == 302: if status_code == 302:
# 302 currently only happens when the reload is triggered.
# Why not 307, Moved Permanently? Because browsers will cache the
# response and not send the reload command.
# 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"
@@ -864,9 +834,7 @@ def main():
http_port = file_handler.read_config("port") http_port = file_handler.read_config("port")
https_port = file_handler.read_config("https-port") https_port = file_handler.read_config("https-port")
http_enabled = bool(file_handler.read_config("http")) http_enabled = bool(file_handler.read_config("http"))
print(http_enabled)
https_enabled = bool(file_handler.read_config("https")) https_enabled = bool(file_handler.read_config("https"))
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)