fixed up and added some things
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user