about summary refs log tree commit diff
diff options
context:
space:
mode:
authorUltraQbik <no1skill@yandex.ru>2024-08-19 23:31:36 +0300
committerUltraQbik <no1skill@yandex.ru>2024-08-19 23:31:36 +0300
commitb860eb988c0d9fa977b4075ff42e6093b1e52809 (patch)
tree31392f61ae503afb3013521d29f38a19dda027ba
parentd770c270eb035d3757deff83898f08ffc332b66b (diff)
downloadhttpy-b860eb988c0d9fa977b4075ff42e6093b1e52809.tar.gz
httpy-b860eb988c0d9fa977b4075ff42e6093b1e52809.zip
initial
-rw-r--r--.gitignore2
-rw-r--r--css/styles.css46
-rw-r--r--index.html33
-rw-r--r--main.py258
-rw-r--r--page/favicon.icobin0 -> 318 bytes
-rw-r--r--page/robots.txt2
-rw-r--r--requirements.txt2
7 files changed, 342 insertions, 1 deletions
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 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <title> The YES's Page </title>
+        <link rel="stylesheet" href="css/styles.css">
+    </head>
+    <body>
+    <header> <b> Welcome To The Mighty HTML Page </b> </header>
+        <div id="cool-div">
+            <section style="padding-top: 10px">
+                <h1> What is this? </h1>
+                <p> > This is an HTML page! </p>
+                <p> > It exists! </p>
+            </section>
+            <section>
+                <h1> Why does this exist? </h1>
+                <p> > Funny haha </p>
+                <p> > I'm learning how to webpage from nothing </p>
+                <p> > The webserver was written using python </p>
+                <p> > Python with only* standard libraries, without flask (or anything similar) </p>
+                <p> > only 2 additional libraries are 'aiofiles' and 'htmlmin' </p>
+            </section>
+            <section>
+                <h1> Useless button section? </h1>
+                <button id="cool-butt"> <b> Useless button section </b> </button>
+            </section>
+        </div>
+    <footer>
+        <p> Hehe <i>foot</i>er </p>
+        <p> P.S. CSS is a mess :< </p>
+    </footer>
+    </body>
+</html>
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'<h1>' + b'UwU ' * 2000 + b'</h1>'
+
+        # 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
--- /dev/null
+++ b/page/favicon.ico
Binary files differdiff --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