Version 0.3.0
Enhanced multithreading engine, bugs fixed and more!
This commit is contained in:
+132
-90
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user