Version 0.3.0

Enhanced multithreading engine, bugs fixed and more!
This commit is contained in:
Bill Gates
2026-08-07 15:06:13 +02:00
parent bd78ab9225
commit 3276224943
8 changed files with 259 additions and 120 deletions
+132 -90
View File
@@ -61,7 +61,7 @@ except ImportError:
)
# pass
AMETHYST_BUILD_NUMBER = "0083"
AMETHYST_BUILD_NUMBER = "0.3.0-0114-mt-tryout1"
AMETHYST_REPO = "https://git.novacow.ch/Nova/PyWebServer/"
@@ -143,7 +143,7 @@ class FileHandler:
def read_file(self, file_path, directory=None):
if "../" in file_path or "%" in file_path:
return 403, None
if file_path == "api.py":
if file_path == "pes.py":
return 404, None
if directory is not None:
@@ -192,7 +192,7 @@ class RequestParser:
def extract_header(self, header: str, request: bytes | str):
if isinstance(request, bytes):
request = request.decode("utf-8", "ignore")
request = request.decode("iso-8859-1", "ignore")
lines = request.splitlines()
for line in lines:
if line.startswith(header):
@@ -200,13 +200,13 @@ class RequestParser:
return value
return None
def parse_request_line(self, line, host):
def parse_request_line(self, line, host, no_mod=False):
"""Parses the HTTP request line."""
try:
method, path, version = line.split(" ")
except ValueError:
return None, None, None
if path.endswith("/") or ("." not in path):
if (path.endswith("/") or ("." not in path)) and (not no_mod):
if not path.endswith("/"):
path += "/"
index = self.file_handler.read_config("index", host) or "index.html"
@@ -276,6 +276,7 @@ class RequestParser:
class ProxyServer:
def __init__(self, fh):
self.file_handler: FileHandler = fh
self.rq: RequestParser = RequestParser()
@staticmethod
def recv_all(sock):
@@ -293,16 +294,24 @@ class ProxyServer:
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")
# nhost will include http or https.
if nhost.startswith("https"):
nhost = nhost[6:-1]
do_tls = True
elif nhost.startswith("http"):
nhost = nhost[5:-1]
do_tls = False
else:
raise SyntaxError(
"Syntax error in config! Key: `proxy` Reason: `Expected http([...]) or https([...]), not "
f"{nhost[:6]}[...]{nhost[-1:]}!`"
)
if force_tls is True:
do_tls = True
print(f"\n\n\nnhost: {nhost}\n\n\n")
if ":" in nhost:
nport = int(nhost.split(":")[1])
nhost = nhost.split(":")[0]
@@ -311,47 +320,52 @@ class ProxyServer:
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
print("Waiting on TCP start.")
return self.tcp_send(nhost, nport, data, do_tls)
except Exception as e:
raise Exception(f"Server replied unexpected. Reply from Python subsystem: {e}")
@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:"):
header_end = data.find(b"\r\n\r\n")
if header_end == -1:
return data
header_bytes = data[:header_end]
body = data[header_end + 4:]
headers = header_bytes.decode("iso-8859-1")
lines = headers.split("\r\n")
new_lines = []
for line in lines:
lower = line.lower()
if lower.startswith("host:"):
if port not in [80, 443]:
new_line = f"Host: {host}:{port}"
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
line = f"Host: {host}"
elif lower.startswith("connection:"):
line = "Connection: close"
new_lines.append(line)
rebuilt_headers = "\r\n".join(new_lines).encode("iso-8859-1")
return rebuilt_headers + b"\r\n\r\n" + body
@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):
def tcp_send(self, host, port, data: bytes, do_tls: booll):
try:
with socket.create_connection((host, port), timeout=10) as raw_sock:
raw_sock.settimeout(10)
@@ -363,11 +377,35 @@ class ProxyServer:
) as ssock:
ssock.sendall(data)
print("data reached")
return self.recv_all(ssock)
resp = self.recv_all(ssock)
if self.rq.extract_header("Transfer-Encoding", resp) == "chunked":
ssock.sendall(b"TRANSER-ENCODING IS NOT SUPPORTED")
ssock.close()
resp = (
"HTTP/1.1 502 Gateway Error\r\nConnection: close\r\nContent-Length: 270\r\n"
f"Server: Amethyst/build-{AMETHYST_BUILD_NUMBER}\r\n\r\n"
"Gateway Error.\nThe upstream server tried to use a a transfer mode not "
"yet supported\nExact error:\nE_DATA_STREAMING_NOT_SUPPORTED\n"
"The unsupported mode:\nTransfer-Encoding: chunked.\n"
"There is no fix. The problem lies with the proxy, and is not a fault of the upstream server."
)
return resp
else:
print(f"\n\n\nraw data: {data}\n\n\n")
raw_sock.sendall(data)
print("Waiting for response...")
resp = self.recv_all(raw_sock)
print(f"resp = {resp}")
if self.rq.extract_header("Transfer-Encoding", resp) is not None:
raw_sock.sendall(b"TRANSER-ENCODING IS NOT SUPPORTED")
raw_sock.close()
resp = (
"HTTP/1.1 502 Gateway Error\r\nConnection: close\r\nContent-Length: 270\r\n"
f"Server: Amethyst/build-{AMETHYST_BUILD_NUMBER}\r\n\r\n"
"Gateway Error.\nThe upstream server tried to use a a transfer mode not "
"yet supported\nExact error:\nE_DATA_STREAMING_NOT_SUPPORTED\n"
"The unsupported mode:\nTransfer-Encoding: chunked.\n"
"There is no fix. The problem lies with the proxy, and is not a fault of the upstream server."
)
return resp
except Exception:
raise
@@ -412,9 +450,11 @@ class WebServer:
self.http_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
self.http_socket.bind(("::", self.http_port))
self.http_socket.settimeout(25)
self.https_socket_raw = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
self.https_socket_raw.bind(("::", self.https_port))
self.https_socket_raw.settimeout(25)
self.proxy_handler = ProxyServer(self.file_handler)
@@ -447,40 +487,39 @@ class WebServer:
self.running = True
def start(self, http, https):
signal.signal(signal.SIGINT, 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 self.skip_ssl is True:
print("WARN: You have enabled HTTPS without SSL!!")
yn = input("Is this intended behaviour? [y/N] ")
if yn.lower() == "n":
exit(1)
https_thread.start()
self.start_https()
else:
self.https_socket.close()
if http is True:
http_thread.start()
self.start_http()
else:
self.http_socket.close()
http_thread.join()
https_thread.join()
def start_http(self):
self.http_socket.listen(5)
print(f"HTTP server listening on port {self.http_port}...")
while self.running:
try:
conn, addr = self.http_socket.accept()
self.handle_connection(conn, addr)
except Exception as e:
print(f"HTTP error: {e}")
if self.file_handler.read_config("threading") is True:
threading.Thread(
target=self.handle_connection,
args=(conn, addr),
daemon=True
).start()
else:
self.handle_connection(conn, addr)
except OSError:
break
except Exception as e:
if not "timeout" in f"{e}":
print(f"HTTP error: {e}")
def start_https(self):
self.https_socket.listen(5)
@@ -488,13 +527,19 @@ class WebServer:
while self.running:
try:
conn, addr = self.https_socket.accept()
self.handle_connection(conn, addr)
except Exception as e:
print(
f"HTTPS error: {e}"
) # be ready for ssl errors if you use a self-sign!!
if self.file_handler.read_config("threading") is True:
threading.Thread(
target=self.handle_connection,
args=(conn, addr),
daemon=True
).start()
else:
self.handle_connection(conn, addr)
except OSError:
break
except Exception as e:
if not "timeout" in f"{e}":
print(f"HTTPS error: {e}")
def handle_connection(self, conn, addr):
try:
@@ -528,22 +573,7 @@ class WebServer:
body += chunk
data += body
#
# print(f"\n\nbody length {len(body)}\n\n")
# print("headers len", len(headers))
# print("rest len", len(rest))
# print("body len", len(body))
# print("content_length", content_length)
#
# print("last 200 bytes of body:")
# print(repr(body[-200:]))
# print("body starts with:")
# print(repr(body[:100]))
# print(f"body: {body}")
print(b"data: " + headers + b"\r\n\r\n" + body)
# # data = conn.recv(32768)
# print(f"len(data) = {len(data)}")
request = data.decode(errors="ignore")
request = data.decode("iso-8859-1", errors="ignore")
if not data:
response = self.build_response(
400, "Bad Request"
@@ -617,14 +647,14 @@ class WebServer:
value = self.file_handler.read_config("proxy", host)
if ":" in value:
host = value.split(":")[0]
port = int(value.split(":")[1])
port = int(value.split(":")[1][:-1])
else:
host = value
port = 443
return self.proxy_handler.try_connection(
host,
port,
data.encode(),
data.encode("iso-8859-1"),
orig_host,
)
@@ -643,13 +673,25 @@ class WebServer:
or self.file_handler.base_dir
)
if self.file_handler.read_config("apimode", host) is True:
if bool(self.file_handler.read_config("pesmode", 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
import pes
try:
pesclass = pes.PES()
threadcompat = pesclass.THREAD_SAFETY
# if not threadcompat:
return pesclass.on_request(data)
except Exception as e:
return 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 in their Python Extension Script",
)
apiclass = api.API()
return apiclass.on_request(data)
if bool(self.file_handler.read_config("621mode", host)) is True:
return self.build_response(621, "")
file_content, mimetype = self.file_handler.read_file(path, directory)
@@ -749,14 +791,14 @@ class WebServer:
if status_code == 621:
headers = (
f"HTTP/1.1 {status_code} {status_message}\r\n"
"HTTP/1.1 302 UwU Nya!\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}")
"Content-Length: 0\r\n"
"Connection: close\r\n"
"Note: congrats, you found a funny. i guess.\r\n"
"Host: https://e621.net/posts/\r\n\r\n"
).encode("iso-8859-1")
body = "".encode("iso-8859-1")
return headers + body
def shutdown(self, signum, frame):