fixed up and added some things

This commit is contained in:
2026-06-15 23:59:34 +02:00
parent 96eba42c04
commit 095f516a55
4 changed files with 140 additions and 24 deletions
+40 -4
View File
@@ -3,7 +3,7 @@
## A word of warning! ## A word of warning!
Currently Amethyst is in very early alpha stage, a lot of things will be broken, names won't be correct, 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! 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. Every save I do increments the build number by 1, I won't publish all of them, but some of them will be published.
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:
@@ -21,13 +21,49 @@ They can be found as the `amethyst-prerel-0.a.b` releases. I won't guarantee 100
Install Python, execute `amethyst.py` and change the provided config. Install Python, execute `amethyst.py` and change the provided config.
## Minimum requirements: ## Minimum requirements:
Python 3.8+ Python 3.10+
And whatever PC that happens to run that. And whatever PC that happens to run that.
I recommend Python 3.10 or above though, with a PC running: I recommend Python 3.12 or above though, with a PC running:
* Windows 8.1+ * Windows 8.1+
* macOS 10.15+ * macOS 10.15+
* Linux 4.19+ * Linux 4.19+
* FreeBSD 13.2R+ * FreeBSD 13.2R+
* Some other somewhat recent OS. * Some other somewhat recent OS.
## Currently W.I.P. Check back later! ## The webserver itself:
The Amethyst webserver is meant to be easy to use and configure. Its configuration takes inspiration from nginx and Caddyfile.
The language the configuration is made in is AmethystConf.
The default config is as follows:
```amethystconf
host * {
directory:./html
apimode:0
block-ua:match("Discordbot")
index:index2.html
}
globals {
http:1
https:1
port:8080
https-port:8443
key:./key.pem
cert:./cert.pem
max-length:8192
}
}
```
It uses a key-value syntax, and uses a `:` as its seperator. A few key directives:
`host`, followed by a hostname signifies a host that will be available. Similar to nginx's `server_name` directive.
`globals` signifies all values that are of global importance, like the key and certificate file.
`directory` signifies the directory to look in for files on that specific host.
`index`, while not in the default config, signifies what path should be returned if the client only asks for `/` (or any subpaths without files).
`apimode` signifies if the API mode must be enabled, allowing the server to run custom Python code to manipulate the request or file further.
`block-ua` signifies if a specific (or loosely matched) User-Agent must be blocked from accessing the site.
`proxy` signifies if a the server needs to get the response from a different (remote) server but still needs to be available at this host.
`max-length` signifies the maximum length a request may have.
AmethystConf has only 4 datatypes: `String`, `Boolean`, `Function` and `None`. A quick rundown:
`String` is the everything datatype. Everything is assumed to be a `String` unless it falls under the other categories.
`Boolean` is the datatype used to enable/disable features. A `Boolean` can have one of two possible values: `1` or `0`.
`Function` is the datatype used in `match()`, it signifies that the parser has to do some work on this string before it can use it.
`None` is the datatype assigned to any key without a value.
+3 -1
View File
@@ -4,6 +4,7 @@ host * {
directory:./html directory:./html
apimode:0 apimode:0
block-ua:match("Discordbot") block-ua:match("Discordbot")
index:index2.html
} }
globals { globals {
@@ -12,5 +13,6 @@ globals {
port:8080 port:8080
https-port:8443 https-port:8443
key:./key.pem key:./key.pem
cert./cert.pem cert:./cert.pem
max-length:8192
} }
+96 -18
View File
@@ -61,7 +61,7 @@ except ImportError:
) )
# pass # pass
AMETHYST_BUILD_NUMBER = "b0.2.0-0072" AMETHYST_BUILD_NUMBER = "0083"
AMETHYST_REPO = "https://git.novacow.ch/Nova/PyWebServer/" AMETHYST_REPO = "https://git.novacow.ch/Nova/PyWebServer/"
@@ -111,11 +111,15 @@ class ConfigParser:
def query_config(self, key, host=None): def query_config(self, key, host=None):
if host: if host:
return self.data["hosts"].get(host, {}).get(key) value = self.data["hosts"].get(host, {}).get(key)
if key == "hosts": elif key == "hosts":
print(f"\n\n\nHosts!\nHosts: {self.data['hosts']}\n\n\n") print(f"\n\n\nHosts!\nHosts: {self.data['hosts']}\n\n\n")
return list(self.data["hosts"].keys()) value = list(self.data["hosts"].keys())
return self.data["globals"].get(key) else:
value = self.data["globals"].get(key)
if value == "0" or value == "1":
value = int(value)
return value
class FileHandler: class FileHandler:
@@ -161,7 +165,7 @@ class FileHandler:
if "../" in file_path or "%" 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, "wb") as f:
f.write(data) f.write(data)
return 0 return 0
@@ -186,6 +190,16 @@ class RequestParser:
self.hosts = self.file_handler.read_config("hosts") self.hosts = self.file_handler.read_config("hosts")
print(f"Hosts: {self.hosts}") print(f"Hosts: {self.hosts}")
def extract_header(self, header: str, request: bytes | str):
if isinstance(request, bytes):
request = request.decode("utf-8", "ignore")
lines = request.splitlines()
for line in lines:
if line.startswith(header):
value = line.split(":")[1][1:]
return value
return None
def parse_request_line(self, line, host): def parse_request_line(self, line, host):
"""Parses the HTTP request line.""" """Parses the HTTP request line."""
try: try:
@@ -263,6 +277,19 @@ class ProxyServer:
def __init__(self, fh): def __init__(self, fh):
self.file_handler: FileHandler = fh self.file_handler: FileHandler = fh
@staticmethod
def recv_all(sock):
chunks = []
while True:
try:
data = sock.recv(4096)
if not data:
break
chunks.append(data)
except socket.timeout:
break
return b"".join(chunks)
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
): ):
@@ -336,10 +363,12 @@ class ProxyServer:
) as ssock: ) as ssock:
ssock.sendall(data) ssock.sendall(data)
print("data reached") print("data reached")
return ssock.recv(512000) return self.recv_all(ssock)
else: else:
raw_sock.sendall(data) raw_sock.sendall(data)
return raw_sock.recv(512000) resp = self.recv_all(raw_sock)
print(f"resp = {resp}")
return resp
except Exception: except Exception:
raise raise
@@ -354,6 +383,7 @@ class WebServer:
self.parser = RequestParser() self.parser = RequestParser()
self.cert_file = self.file_handler.read_config("cert") or cert_file 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.key_file = self.file_handler.read_config("key") or key_file
self.max_length = int(self.file_handler.read_config("max-length")) or 8192
self.skip_ssl = False self.skip_ssl = False
# me when no certificate and key file # me when no certificate and key file
@@ -383,8 +413,8 @@ 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.https_socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) self.https_socket_raw = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
self.https_socket.bind(("::", self.https_port)) self.https_socket_raw.bind(("::", self.https_port))
self.proxy_handler = ProxyServer(self.file_handler) self.proxy_handler = ProxyServer(self.file_handler)
@@ -395,7 +425,7 @@ class WebServer:
certfile=self.cert_file, keyfile=self.key_file certfile=self.cert_file, keyfile=self.key_file
) )
self.https_socket = self.ssl_context.wrap_socket( self.https_socket = self.ssl_context.wrap_socket(
self.https_socket, server_side=True self.https_socket_raw, server_side=True
) )
self.http_404_html = ( self.http_404_html = (
@@ -430,8 +460,12 @@ class WebServer:
if yn.lower() == "n": if yn.lower() == "n":
exit(1) exit(1)
https_thread.start() https_thread.start()
else:
self.https_socket.close()
if http is True: if http is True:
http_thread.start() http_thread.start()
else:
self.http_socket.close()
http_thread.join() http_thread.join()
https_thread.join() https_thread.join()
@@ -464,13 +498,57 @@ class WebServer:
def handle_connection(self, conn, addr): def handle_connection(self, conn, addr):
try: try:
data = conn.recv(32768) data = b""
# Read headers
while b"\r\n\r\n" not in data:
chunk = conn.recv(4096)
if not chunk:
break
data += chunk
headers, _, rest = data.partition(b"\r\n\r\n")
# Parse Content-Length
content_length = 0
for line in headers.split(b"\r\n"):
if line.lower().startswith(b"content-length:"):
content_length = int(line.split(b":")[1].strip())
# print(f"Content-Length to server: {content_length}")
# Read body
body = rest
print(f"Rest length: {len(rest)}")
while len(body) < content_length:
chunk = conn.recv(4096)
# print(f"\n\nrecv returned {len(chunk)}\n\n")
if not chunk:
print("\n\nsocket closed\n\n")
break
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(errors="ignore")
if not data: if not data:
response = self.build_response( response = self.build_response(
400, "Bad Request" 400, "Bad Request"
) # user did fucky-wucky ) # user did fucky-wucky
elif len(data) > 8192: elif len(data) > self.max_length:
response = self.build_response(413, "Request too long") response = self.build_response(413, "Request too long")
else: else:
response = self.handle_request(request, addr) response = self.handle_request(request, addr)
@@ -493,7 +571,7 @@ class WebServer:
conn.close() conn.close()
def handle_request(self, data, addr): def handle_request(self, data, addr):
print(f"data: {data}") # print(f"data: {data}")
request_line = data.splitlines()[0] request_line = data.splitlines()[0]
# Extract host from headers, never works though # Extract host from headers, never works though
@@ -703,11 +781,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_config("port") or 8080 http_port = file_handler.read_config("port")
https_port = file_handler.read_config("https-port") or 8443 https_port = file_handler.read_config("https-port")
http_enabled = bool(file_handler.read_config("http")) or True http_enabled = bool(file_handler.read_config("http"))
print(http_enabled) print(http_enabled)
https_enabled = bool(file_handler.read_config("https")) or False https_enabled = bool(file_handler.read_config("https"))
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 0.2.0-0072</p> <p>This server runs Amethyst build 0080</p>
</center> </center>
</body> </body>
</html> </html>