From b860eb988c0d9fa977b4075ff42e6093b1e52809 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Mon, 19 Aug 2024 23:31:36 +0300 Subject: initial --- .gitignore | 2 +- css/styles.css | 46 ++++++++++ index.html | 33 +++++++ main.py | 258 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ page/favicon.ico | Bin 0 -> 318 bytes page/robots.txt | 2 + requirements.txt | 2 + 7 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 css/styles.css create mode 100644 index.html create mode 100644 main.py create mode 100644 page/favicon.ico create mode 100644 page/robots.txt create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 82f9275..7b6caf3 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,4 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..887a8c7 --- /dev/null +++ b/css/styles.css @@ -0,0 +1,46 @@ +* { + margin: 0; + padding: 0; + background: transparent; +} + +body { + font-family: Arial, Helvetica, sans-serif; + color: white; + background: black; + display: flex; + flex-direction: column; + min-height: 100vh; +} + +header { + background: #202020; + padding: 30px; + text-align: center; + font-size: 30px; +} + +#cool-div { + align-self: center; + width: 50vw; +} + +section { + padding: 20px 10px 10px; + font-size: 20px; +} + +footer { + background: #202020; + padding: 25px; + text-align: center; + font-size: 10px; + margin-top: auto; +} + +#cool-butt { + background: white; + padding: 5px; + width: 100%; + font-size: 30px; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..43533bb --- /dev/null +++ b/index.html @@ -0,0 +1,33 @@ + + + + The YES's Page + + + +
Welcome To The Mighty HTML Page
+
+
+

What is this?

+

> This is an HTML page!

+

> It exists!

+
+
+

Why does this exist?

+

> Funny haha

+

> I'm learning how to webpage from nothing

+

> The webserver was written using python

+

> Python with only* standard libraries, without flask (or anything similar)

+

> only 2 additional libraries are 'aiofiles' and 'htmlmin'

+
+
+

Useless button section?

+ +
+
+ + + diff --git a/main.py b/main.py new file mode 100644 index 0000000..44c8d26 --- /dev/null +++ b/main.py @@ -0,0 +1,258 @@ +""" +The mighty silly webserver written in python for no good reason +""" + + +import time +import json +import gzip +import socket +import asyncio +import htmlmin +import aiofiles +import threading + + +# some constants +PACKET_SIZE = 2048 +PORT = 13700 # using random port cuz why not + + +# response status codes +RESPONSE = { + 200: b'OK', + 400: b'Bad Request', + 401: b'Unauthorized', + 403: b'Forbidden', + 404: b'Not Found', + 6969: b'UwU' +} + + +def get_response(code: int) -> bytes: + return str(code).encode("ascii") + RESPONSE.get(code, b':(') + + +def is_alive(sock: socket.socket) -> bool: + """ + Checks if the socket is still alive + :param sock: socket + :return: boolean (true if socket is alive, false otherwise) + """ + return getattr(sock, '_closed', False) + + +def decode_request(req: str) -> dict[str, str | list | None]: + # request dictionary + request = dict() + + # request type and path + request["type"] = req[:6].split(" ")[0] + request["path"] = req[len(request["type"]) + 1:req.find("\r\n")].split(" ")[0] + + # decode other headers + for line in req.split("\r\n")[1:]: + if len(split := line.split(":")) == 2: + key, value = split + value = value.lstrip(" ") + + # write key value pair + request[key] = value + + return request + + +class HTMLServer: + """ + The very cool webserver + """ + + def __init__(self): + self.sock: socket.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.clients: list[socket.socket] = [] + + # list of allowed paths + self.allowed_paths: dict[str, dict] = { + "/": {"path": "index.html", "encoding": "css/html"}, + "/robots.txt": {"path": "page/robots.txt", "encoding": "text"}, + "/favicon.ico": {"path": "page/favicon.ico", "encoding": "bin"}, + "/css/styles.css": {"path": None, "encoding": "css/html"}, + } + + def run(self): + """ + Function that starts the webserver + """ + + # bind the server to port and start listening + self.sock.bind(('', PORT)) + self.sock.listen() + + # start running thread + t = threading.Thread(target=self._run, daemon=True) + t.start() + + # keep alive + try: + while True: + time.sleep(0.1) + except KeyboardInterrupt: + self.sock.close() + print("Closed.") + + def _run(self): + """ + Run function for threads + :return: + """ + + asyncio.run(self.server_listener()) + + async def server_listener(self): + """ + Listens for new connections, and handles them + """ + + while True: + client, address = self.sock.accept() + self.clients.append(client) + await self.server_handle(client) + + async def server_handle(self, client: socket.socket): + """ + Handles the actual connections (clients) + :param client: connection socket + """ + + # message buffer + buffer = bytearray() + while True: + # try to fetch a message + # die otherwise + try: + message = client.recv(PACKET_SIZE) + except OSError: + break + if message == b'': + break + + # append packet to buffer + buffer += message + + # check EoF (2 blank lines) + if buffer[-4:] == b'\r\n\r\n': + # text buffer + text_buffer = buffer.decode("ascii") + + # decode request + request = decode_request(text_buffer) + + print(f"[{request['type']}] Request from client '{client.getpeername()[0]}'") + + # log that request + async with aiofiles.open("logs.log", "a") as f: + await f.write( + json.dumps( + { + "client": client.getpeername()[0], + "request": request + }, + indent=2 + ) + "\n" + ) + + # handle the request + if request["type"] == "GET": + await self.handle_get_request(client, request) + else: + await self.handle_other_request(client) + + # clear buffer + buffer.clear() + client.close() + self.clients.remove(client) + + async def handle_get_request(self, client: socket.socket, req: dict[str, str | None]): + # if it's yandex + if req.get("from") == "support@search.yandex.ru": + response = get_response(404) + data = b'Nothing...' + + # check UwU path + elif req["path"] == "/UwU" or req["path"] == "/U/w/U": + response = get_response(6969) + data = b'

' + b'UwU ' * 2000 + b'

' + + # otherwise check access + elif req["path"] in self.allowed_paths: + # get path + path = self.allowed_paths[req["path"]]["path"] + + # if path is None, return generic filepath + if path is None: + path = req["path"][1:] + + # check encoding + if self.allowed_paths[req["path"]]["encoding"] == "css/html": + # return text data + async with aiofiles.open(path, "r") as f: + data = htmlmin.minify(await f.read()).encode("ascii") + else: + # return binary / text data + async with aiofiles.open(path, "rb") as f: + data = await f.read() + response = get_response(200) + + # in any other case + else: + response = get_response(403) + data = b'Idk what you are trying to do here :/' + + # make headers + headers = {} + + # check if compression is supported + if req.get("Accept-Encoding"): + encoding_list = [enc.lstrip(" ") for enc in req["Accept-Encoding"].split(",")] + + # check for gzip, and add to headers if present + if "gzip" in encoding_list: + headers["Content-Encoding"] = "gzip" + + # send response + await self.send(client, response, data, headers) + client.close() + + async def handle_other_request(self, client: socket.socket): + # just say 'no' + await self.send(client, get_response(403), b'No. Don\'t do that, that\'s cringe') + client.close() + + async def send(self, client: socket.socket, response: bytes, data: bytes, headers: dict[str, str] | None = None): + # construct headers + formatted_headers = b'' + if headers is not None: + formatted_headers = "".join([f"{key}: {val}\r\n" for key, val in headers.items()]).encode("ascii") + + # check for compression + if headers.get("Content-Encoding") == "gzip": + # compress data + data = gzip.compress(data) + + # construct message + if formatted_headers == b'': + message = b'HTTP/1.1 ' + response + b'\r\n\r\n' + data + else: + message = b'HTTP/1.1 ' + response + b'\r\n' + formatted_headers + b'\r\n' + data + + # send message to client + client.sendall(message) + + +def main(): + server = HTMLServer() + server.run() + + +if __name__ == '__main__': + main() diff --git a/page/favicon.ico b/page/favicon.ico new file mode 100644 index 0000000..5f90164 Binary files /dev/null and b/page/favicon.ico differ diff --git a/page/robots.txt b/page/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/page/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2d404ff --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +aiofiles==24.1.0 +htmlmin==0.1.12 -- cgit 1.4.1