From d9cc26c6a545e110615957890f36163dff4db7c0 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 02:24:11 +0300 Subject: Change path map a bit --- main.py | 49 +++++++++---------------------------------------- 1 file changed, 9 insertions(+), 40 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 0b9f991..f68f6dd 100644 --- a/main.py +++ b/main.py @@ -13,36 +13,23 @@ from src import APIv1 from src.socks import * from src.request import Request from src.minimizer import minimize_html +from src.status_code import * # path mapping PATH_MAP = { - "/": - {"path": "www/index.html", - "compress": True}, - "/index.html": - {"path": "www/index.html", - "compress": True}, - "/robots.txt": - {"path": "www/robots.txt", - "compress": False}, - "/favicon.ico": - {"path": "www/favicon.ico", - "compress": False}, - "/css/styles.css": - {"path": "css/styles.css", - "compress": True}, - "/about": - {"path": "www/about.html", - "compress": True}, - "/test": - {"path": "www/test.html", - "compress": True}, + "/": {"path": "www/index.html"}, + "/index.html": {"path": "www/index.html"}, + "/robots.txt": {"path": "www/robots.txt"}, + "/favicon.ico": {"path": "www/favicon.ico"}, + "/css/styles.css": {"path": "css/styles.css"}, + "/about": {"path": "www/about.html"}, + "/test": {"path": "www/test.html"}, } # API API_VERSIONS = { - "APIv1" + "APIv1": {"supported": True} } # internal path map @@ -51,24 +38,6 @@ I_PATH_MAP = { } -def get_response_code(code: int) -> bytes: - match code: - case 200: - return b'200 OK' - case 400: - return b'400 Bad Request' - case 401: - return b'401 Unauthorized' - case 403: - return b'403 Forbidden' - case 404: - return b'404 Not Found' - case 6969: - return b'6969 UwU' - case _: # in any other case return bad request response - return get_response_code(400) - - class HTTPServer: """ The mighty HTTP server -- cgit 1.4.1 From 1743ab0a4fdb380cca245875a3f9f5b17d049019 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 02:39:59 +0300 Subject: Add basic stuff --- main.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) (limited to 'main.py') diff --git a/main.py b/main.py index f68f6dd..c202dea 100644 --- a/main.py +++ b/main.py @@ -38,6 +38,48 @@ I_PATH_MAP = { } +class HTTPServer: + """ + The mightier HTTP server! + Now uses threading + """ + + def __init__(self, *, port: int, packet_size: int = 2048): + # SSL context + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.check_hostname = False + context.load_cert_chain( + certfile=r"C:\Certbot\live\qubane.ddns.net\fullchain.pem", # use your own path here + keyfile=r"C:\Certbot\live\qubane.ddns.net\privkey.pem") # here too + + # Sockets + self.sock: ssl.SSLSocket = context.wrap_socket( + socket.socket(socket.AF_INET, socket.SOCK_STREAM), + server_side=True) + self.packet_size: int = packet_size + self.port: int = port + + # client thread list + self.client_threads: list[threading.Thread] = [] + + # add signaling + self.stop_event = threading.Event() + signal.signal(signal.SIGINT, self._signal_interrupt) + + def _signal_interrupt(self, *args): + """ + Checks for CTRL+C keyboard interrupt, to properly stop the HTTP server + """ + + # stop all threads + self.stop_event.set() + for thread in self.client_threads: + thread.join() + + # close server socket + self.sock.close() + + class HTTPServer: """ The mighty HTTP server @@ -76,7 +118,6 @@ class HTTPServer: """ # setup signaling - signal.signal(signal.SIGINT, self.interrupt) # bind and start listening to port self.socket.bind(('', self.bind_port)) -- cgit 1.4.1 From c95930a8d1e9724a0b720c6fe4e648d0ee267800 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 02:49:53 +0300 Subject: Add start method --- main.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) (limited to 'main.py') diff --git a/main.py b/main.py index c202dea..0ab634f 100644 --- a/main.py +++ b/main.py @@ -59,8 +59,9 @@ class HTTPServer: self.packet_size: int = packet_size self.port: int = port - # client thread list + # client thread list and server thread self.client_threads: list[threading.Thread] = [] + self.server_thread: threading.Thread | None = None # add signaling self.stop_event = threading.Event() @@ -75,10 +76,31 @@ class HTTPServer: self.stop_event.set() for thread in self.client_threads: thread.join() + self.server_thread.join() # close server socket self.sock.close() + def start(self): + """ + Method to start the web server + """ + + # bind and start listening to port + self.sock.bind(('', self.port)) + self.sock.setblocking(False) + self.sock.listen() + + # listen and respond handler + while not self.stop_event.is_set(): + # accept new client + client = ssl_sock_accept(self.sock)[0] + + # create thread for new client and append it to the list + th = threading.Thread(target=lambda x: x, args=[client]) # TODO: this line + self.client_threads.append(th) + th.start() + class HTTPServer: """ -- cgit 1.4.1 From 46a76eff285459d25f142d2ce2628425d4e69e94 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 15:44:06 +0300 Subject: More graceful shutdown There is a weird issue when a user tries to load a page, doesn't get a response (because that wasn't implemented yet), and when the webserver is attempted to be shutdown, it still tries to fetch user's request --- main.py | 480 +++++++++++++++++++++++++++++++--------------------------------- 1 file changed, 231 insertions(+), 249 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 0ab634f..4edd2ba 100644 --- a/main.py +++ b/main.py @@ -4,16 +4,16 @@ The mighty silly webserver written in python for no good reason import ssl +import time import gzip import socket import brotli import signal import threading from src import APIv1 -from src.socks import * +from src.status_code import * from src.request import Request from src.minimizer import minimize_html -from src.status_code import * # path mapping @@ -56,12 +56,11 @@ class HTTPServer: self.sock: ssl.SSLSocket = context.wrap_socket( socket.socket(socket.AF_INET, socket.SOCK_STREAM), server_side=True) - self.packet_size: int = packet_size + self.buf_len: int = packet_size self.port: int = port # client thread list and server thread self.client_threads: list[threading.Thread] = [] - self.server_thread: threading.Thread | None = None # add signaling self.stop_event = threading.Event() @@ -76,10 +75,6 @@ class HTTPServer: self.stop_event.set() for thread in self.client_threads: thread.join() - self.server_thread.join() - - # close server socket - self.sock.close() def start(self): """ @@ -94,271 +89,258 @@ class HTTPServer: # listen and respond handler while not self.stop_event.is_set(): # accept new client - client = ssl_sock_accept(self.sock)[0] + client = self._accept() + if client is None: + break # create thread for new client and append it to the list - th = threading.Thread(target=lambda x: x, args=[client]) # TODO: this line + th = threading.Thread(target=self._client_thread, args=[client]) self.client_threads.append(th) th.start() + # close server socket + self.sock.close() -class HTTPServer: - """ - The mighty HTTP server - """ - - def __init__(self, *, port: int, packet_size: int = 2048): - # ssl context - self.context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - self.context.load_cert_chain( - certfile=r"C:\Certbot\live\qubane.ddns.net\fullchain.pem", # use your own path here - keyfile=r"C:\Certbot\live\qubane.ddns.net\privkey.pem" # here too - ) - self.context.check_hostname = False - - # sockets - self.socket: ssl.SSLSocket = self.context.wrap_socket( - socket.socket(socket.AF_INET, socket.SOCK_STREAM), server_side=True) - self.packet_size: int = packet_size - self.bind_port: int = port - - # list of connected clients - self.clients: list[socket.socket] = [] - - def interrupt(self, *args, **kwargs): - """ - Interrupts the web server - """ - - self.socket.close() - for client in self.clients: - client.close() - - def start(self): - """ - Method to start the web server - """ - - # setup signaling - - # bind and start listening to port - self.socket.bind(('', self.bind_port)) - self.socket.listen() - self.socket.setblocking(False) - - # start listening - self._listen_thread() - - def _listen_thread(self): + def _client_thread(self, client: ssl.SSLSocket): """ - Listening for new connections + Handles client's requests + :param client: client ssl socket """ - # start listening - while True: - # try to accept new connection + while not self.stop_event.is_set(): try: - client = ssl_sock_accept(self.socket)[0] + # get client's request + request = self._recv_request(client) + if request is None: + break - # if socket was closed -> break + print(request, end="\n\n") except OSError as e: print(e) - print("Closed.") - break - - # else append to client list and create new task - threading.Thread(target=self.client_handle, args=[client]).start() - - def client_handle(self, client: ssl.SSLSocket): - """ - Handles client's connection - """ - - while True: - # receive request from client - raw_request = self._recvall(client) - - if raw_request == b'': break - # decode request - request: Request = Request.create(raw_request) - - # # log request - # async with aiofiles.open("logs.log", "a") as f: - # await f.write(f"IP: {client.getpeername()[0]}\n{request}\n\n") - - threading.Thread(target=self.handle_request, args=[client, request]).start() - - def handle_request(self, client: ssl.SSLSocket, request: Request): - # handle requests - try: - match request.type: - case "GET": - self.handle_get_request(client, request) - case _: - pass - - # break on exception - except Exception as e: - print(e) - - # # close connection (stop page loading) - # self._close_client(client) - - @staticmethod - def handle_get_request(client: ssl.SSLSocket, request: Request): - """ - Handles user's GET request - """ - - # get available compression methods - compressions = [x.strip() for x in getattr(request, "Accept-Encoding", "").split(",")] - - # check if request path is in the PATH_MAP - if request.path in PATH_MAP: - # if it is -> return file from that path - with open(PATH_MAP[request.path]["path"], "rb") as f: - data = f.read() - - # pre-compress data for HTML files - if PATH_MAP[request.path]["path"][-4:] == "html": - data = minimize_html(data) - - # add brotli compression header (if supported) - headers = {} - if "br" in compressions: - headers["Content-Encoding"] = "br" - - # else add gzip compression (if supported) - elif "gzip" in compressions: - headers["Content-Encoding"] = "gzip" - - # send 200 response with the file to the client - HTTPServer._send(client, 200, data, headers) - - # if it's an API request - elif (api_version := request.path.split("/")[1]) in API_VERSIONS: - data = b'' - headers = {} - match api_version: - case "APIv1": - status, data, headers = APIv1.respond(client, request) - case _: - status = 400 - - # if status is not 200 -> send bad response - if status != 200: - HTTPServer._bad_response(client, status) - return - - # send data if no error - HTTPServer._send(client, status, data, headers) - - # in case of error, return error page - else: - HTTPServer._bad_response(client, 404) - - @staticmethod - def _bad_response(client: ssl.SSLSocket, status_code: int): - """ - Sends a bad response page to the client. - :param client: client - :param status_code: status code - """ - - with open(I_PATH_MAP["/err/response.html"]["path"], "r") as f: - data = f.read() - - # format error response - data = data.format(status_code=get_response_code(status_code).decode("ascii")) - - # send response to the client - HTTPServer._send(client, status_code, data.encode("ascii")) - - @staticmethod - def _send(client: ssl.SSLSocket, response: int, data: bytes = None, headers: dict[str, str] = None): - """ - Sends client response code + headers + data - :param client: client - :param response: response code - :param data: data - :param headers: headers to include - """ + # close the connection once stop even was set or an error occurred + client.close() - # if data was not given - if data is None: - data = bytes() - - # if headers were not given - if headers is None: - headers = dict() - - # check for 'content-encoding' header - if headers.get("Content-Encoding") == "br": - data = brotli.compress(data) - - elif headers.get("Content-Encoding") == "gzip": - data = gzip.compress(data) - - # add 'Content-Length' header if not present - if headers.get("Content-Length") is None: - headers["Content-Length"] = len(data) - - # format headers - byte_header = bytearray() - for key, value in headers.items(): - byte_header += f"{key}: {value}\r\n".encode("ascii") - - # send response to the client - client.sendall( - b'HTTP/1.1 ' + - get_response_code(response) + - b'\r\n' + - byte_header + # if empty, we'll just get b'\r\n\r\n' - b'\r\n' + - data - ) - - def _recvall(self, client: ssl.SSLSocket) -> bytes: + def _recv_request(self, client: ssl.SSLSocket) -> Request | None: """ - Receive All (just receives the whole message, instead of 1 packet at a time) + Receive request from client + :return: request + :raises: anything that recv raises """ - # create message buffer - buffer: bytearray = bytearray() - - # start fetching the message - while True: - try: - # fetch packet - message = ssl_sock_recv(client, self.packet_size) - except OSError: - break - - # that happens when user stops loading the page - if message == b'': - break - - # append fetched message to the buffer - buffer += message - - # check for EoF + buffer = bytearray() + while not self.stop_event.is_set(): + buffer += client.recv(self.buf_len) if buffer[-4:] == b'\r\n\r\n': - # return the received message - return buffer + return Request.create(buffer) + return None - # return empty buffer on error - return b'' - - def _close_client(self, client: socket.socket): + def _accept(self) -> ssl.SSLSocket | None: """ - Closes a client + socket.accept, but for more graceful closing """ - client.close() - if client in self.clients: - self.clients.remove(client) + while not self.stop_event.is_set(): + try: + return self.sock.accept()[0] + except BlockingIOError: + time.sleep(0.001) + return None + + +# class HTTPServer: +# +# def client_handle(self, client: ssl.SSLSocket): +# """ +# Handles client's connection +# """ +# +# while True: +# # receive request from client +# raw_request = self._recvall(client) +# +# if raw_request == b'': +# break +# +# # decode request +# request: Request = Request.create(raw_request) +# +# # # log request +# # async with aiofiles.open("logs.log", "a") as f: +# # await f.write(f"IP: {client.getpeername()[0]}\n{request}\n\n") +# +# threading.Thread(target=self.handle_request, args=[client, request]).start() +# +# def handle_request(self, client: ssl.SSLSocket, request: Request): +# # handle requests +# try: +# match request.type: +# case "GET": +# self.handle_get_request(client, request) +# case _: +# pass +# +# # break on exception +# except Exception as e: +# print(e) +# +# # # close connection (stop page loading) +# # self._close_client(client) +# +# @staticmethod +# def handle_get_request(client: ssl.SSLSocket, request: Request): +# """ +# Handles user's GET request +# """ +# +# # get available compression methods +# compressions = [x.strip() for x in getattr(request, "Accept-Encoding", "").split(",")] +# +# # check if request path is in the PATH_MAP +# if request.path in PATH_MAP: +# # if it is -> return file from that path +# with open(PATH_MAP[request.path]["path"], "rb") as f: +# data = f.read() +# +# # pre-compress data for HTML files +# if PATH_MAP[request.path]["path"][-4:] == "html": +# data = minimize_html(data) +# +# # add brotli compression header (if supported) +# headers = {} +# if "br" in compressions: +# headers["Content-Encoding"] = "br" +# +# # else add gzip compression (if supported) +# elif "gzip" in compressions: +# headers["Content-Encoding"] = "gzip" +# +# # send 200 response with the file to the client +# HTTPServer._send(client, 200, data, headers) +# +# # if it's an API request +# elif (api_version := request.path.split("/")[1]) in API_VERSIONS: +# data = b'' +# headers = {} +# match api_version: +# case "APIv1": +# status, data, headers = APIv1.respond(client, request) +# case _: +# status = 400 +# +# # if status is not 200 -> send bad response +# if status != 200: +# HTTPServer._bad_response(client, status) +# return +# +# # send data if no error +# HTTPServer._send(client, status, data, headers) +# +# # in case of error, return error page +# else: +# HTTPServer._bad_response(client, 404) +# +# @staticmethod +# def _bad_response(client: ssl.SSLSocket, status_code: int): +# """ +# Sends a bad response page to the client. +# :param client: client +# :param status_code: status code +# """ +# +# with open(I_PATH_MAP["/err/response.html"]["path"], "r") as f: +# data = f.read() +# +# # format error response +# data = data.format(status_code=get_response_code(status_code).decode("ascii")) +# +# # send response to the client +# HTTPServer._send(client, status_code, data.encode("ascii")) +# +# @staticmethod +# def _send(client: ssl.SSLSocket, response: int, data: bytes = None, headers: dict[str, str] = None): +# """ +# Sends client response code + headers + data +# :param client: client +# :param response: response code +# :param data: data +# :param headers: headers to include +# """ +# +# # if data was not given +# if data is None: +# data = bytes() +# +# # if headers were not given +# if headers is None: +# headers = dict() +# +# # check for 'content-encoding' header +# if headers.get("Content-Encoding") == "br": +# data = brotli.compress(data) +# +# elif headers.get("Content-Encoding") == "gzip": +# data = gzip.compress(data) +# +# # add 'Content-Length' header if not present +# if headers.get("Content-Length") is None: +# headers["Content-Length"] = len(data) +# +# # format headers +# byte_header = bytearray() +# for key, value in headers.items(): +# byte_header += f"{key}: {value}\r\n".encode("ascii") +# +# # send response to the client +# client.sendall( +# b'HTTP/1.1 ' + +# get_response_code(response) + +# b'\r\n' + +# byte_header + # if empty, we'll just get b'\r\n\r\n' +# b'\r\n' + +# data +# ) +# +# def _recvall(self, client: ssl.SSLSocket) -> bytes: +# """ +# Receive All (just receives the whole message, instead of 1 packet at a time) +# """ +# +# # create message buffer +# buffer: bytearray = bytearray() +# +# # start fetching the message +# while True: +# try: +# # fetch packet +# message = ssl_sock_recv(client, self.packet_size) +# except OSError: +# break +# +# # that happens when user stops loading the page +# if message == b'': +# break +# +# # append fetched message to the buffer +# buffer += message +# +# # check for EoF +# if buffer[-4:] == b'\r\n\r\n': +# # return the received message +# return buffer +# +# # return empty buffer on error +# return b'' +# +# def _close_client(self, client: socket.socket): +# """ +# Closes a client +# """ +# +# client.close() +# if client in self.clients: +# self.clients.remove(client) def main(): -- cgit 1.4.1 From abb678c3582aec27329bd3cc77daf88472e30f90 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 15:54:44 +0300 Subject: Add timeout to prevent client thread from running forever --- main.py | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'main.py') diff --git a/main.py b/main.py index 4edd2ba..46740e9 100644 --- a/main.py +++ b/main.py @@ -107,6 +107,7 @@ class HTTPServer: :param client: client ssl socket """ + client.settimeout(5) while not self.stop_event.is_set(): try: # get client's request @@ -115,6 +116,9 @@ class HTTPServer: break print(request, end="\n\n") + except TimeoutError: + print("Client timeout") + break except OSError as e: print(e) break -- cgit 1.4.1 From d8d3b42a6665c218b8214c18cce161a16bab0a7c Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 15:56:44 +0300 Subject: Check received message length to prevent infinite loop in _recv_request method --- main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'main.py') diff --git a/main.py b/main.py index 46740e9..18772f3 100644 --- a/main.py +++ b/main.py @@ -135,7 +135,10 @@ class HTTPServer: buffer = bytearray() while not self.stop_event.is_set(): - buffer += client.recv(self.buf_len) + msg = client.recv(self.buf_len) + if len(msg) == 0: + break + buffer += msg if buffer[-4:] == b'\r\n\r\n': return Request.create(buffer) return None -- cgit 1.4.1 From 82178d2facb95ee953ab1e97f5ee15f94b1d3472 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 17:39:20 +0300 Subject: Basic responses working --- main.py | 91 ++-------------------------------------------------------- src/request.py | 40 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 88 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 18772f3..735e427 100644 --- a/main.py +++ b/main.py @@ -5,14 +5,12 @@ The mighty silly webserver written in python for no good reason import ssl import time -import gzip import socket -import brotli import signal import threading from src import APIv1 +from src.request import * from src.status_code import * -from src.request import Request from src.minimizer import minimize_html @@ -115,7 +113,8 @@ class HTTPServer: if request is None: break - print(request, end="\n\n") + with open("www/index.html", "rb") as file: + send_response(client, file.read(), STATUS_CODE_OK) except TimeoutError: print("Client timeout") break @@ -264,90 +263,6 @@ class HTTPServer: # # # send response to the client # HTTPServer._send(client, status_code, data.encode("ascii")) -# -# @staticmethod -# def _send(client: ssl.SSLSocket, response: int, data: bytes = None, headers: dict[str, str] = None): -# """ -# Sends client response code + headers + data -# :param client: client -# :param response: response code -# :param data: data -# :param headers: headers to include -# """ -# -# # if data was not given -# if data is None: -# data = bytes() -# -# # if headers were not given -# if headers is None: -# headers = dict() -# -# # check for 'content-encoding' header -# if headers.get("Content-Encoding") == "br": -# data = brotli.compress(data) -# -# elif headers.get("Content-Encoding") == "gzip": -# data = gzip.compress(data) -# -# # add 'Content-Length' header if not present -# if headers.get("Content-Length") is None: -# headers["Content-Length"] = len(data) -# -# # format headers -# byte_header = bytearray() -# for key, value in headers.items(): -# byte_header += f"{key}: {value}\r\n".encode("ascii") -# -# # send response to the client -# client.sendall( -# b'HTTP/1.1 ' + -# get_response_code(response) + -# b'\r\n' + -# byte_header + # if empty, we'll just get b'\r\n\r\n' -# b'\r\n' + -# data -# ) -# -# def _recvall(self, client: ssl.SSLSocket) -> bytes: -# """ -# Receive All (just receives the whole message, instead of 1 packet at a time) -# """ -# -# # create message buffer -# buffer: bytearray = bytearray() -# -# # start fetching the message -# while True: -# try: -# # fetch packet -# message = ssl_sock_recv(client, self.packet_size) -# except OSError: -# break -# -# # that happens when user stops loading the page -# if message == b'': -# break -# -# # append fetched message to the buffer -# buffer += message -# -# # check for EoF -# if buffer[-4:] == b'\r\n\r\n': -# # return the received message -# return buffer -# -# # return empty buffer on error -# return b'' -# -# def _close_client(self, client: socket.socket): -# """ -# Closes a client -# """ -# -# client.close() -# if client in self.clients: -# self.clients.remove(client) def main(): diff --git a/src/request.py b/src/request.py index 0437dfd..d9e8a86 100644 --- a/src/request.py +++ b/src/request.py @@ -1,3 +1,10 @@ +import gzip +import brotli +from typing import Any +from ssl import SSLSocket +from src.status_code import StatusCode + + class Request: """ Just a request @@ -54,3 +61,36 @@ class Request: def __str__(self): return '\n'.join([f"{key}: {val}" for key, val in self.__dict__.items()]) + + +def send_response(sock: SSLSocket, data: bytes, status: StatusCode, headers: dict[str, Any] = None): + """ + Sends response to client. + Probably ran inside a daemonic thread + :param sock: client's socket + :param data: raw data to send + :param status: status code + :param headers: headers to include + """ + + # process header data + if headers is None: + headers = dict() + if headers.get("Content-Encoding") is not None: + if headers["Content-Encoding"] == "br": + data = brotli.compress(data) + elif headers["Content-Encoding"] == "gzip": + data = gzip.compress(data) + if headers.get("Content-Length") is None: + headers["Content-Length"] = len(data) + if headers.get("Connection") is None: + headers["Connection"] = "close" + + # generate basic message + message = b'HTTP/1.1 ' + status.__bytes__() + b'\r\n' + for key, value in headers.items(): + message += f"{key}: {value}\r\n".encode("ascii") + message += b'\r\n' + data + + # send message + sock.sendall(message) -- cgit 1.4.1 From 19117444cf6362ee4aa6bdb8ef547daa91885265 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 17:53:35 +0300 Subject: Add request handler method --- main.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 735e427..cbc2808 100644 --- a/main.py +++ b/main.py @@ -32,7 +32,7 @@ API_VERSIONS = { # internal path map I_PATH_MAP = { - "/err/response.html": {"path": "www/err/response.html"} + "/err/response": {"path": "www/err/response.html"} } @@ -101,7 +101,7 @@ class HTTPServer: def _client_thread(self, client: ssl.SSLSocket): """ - Handles client's requests + Handles getting client's requests :param client: client ssl socket """ @@ -113,8 +113,7 @@ class HTTPServer: if request is None: break - with open("www/index.html", "rb") as file: - send_response(client, file.read(), STATUS_CODE_OK) + threading.Thread(target=self._client_request_handler, args=[client, request], daemon=True).start() except TimeoutError: print("Client timeout") break @@ -125,6 +124,13 @@ class HTTPServer: # close the connection once stop even was set or an error occurred client.close() + def _client_request_handler(self, client, request): + """ + Handles responses to client's requests + :param client: client + :param request: client's request + """ + def _recv_request(self, client: ssl.SSLSocket) -> Request | None: """ Receive request from client -- cgit 1.4.1 From 1edec8c5837a227d8871d593926cb10748d4c856 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 21:00:16 +0300 Subject: Working website (I think) --- main.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) (limited to 'main.py') diff --git a/main.py b/main.py index cbc2808..de605b6 100644 --- a/main.py +++ b/main.py @@ -4,8 +4,10 @@ The mighty silly webserver written in python for no good reason import ssl +import gzip import time import socket +import brotli import signal import threading from src import APIv1 @@ -117,7 +119,7 @@ class HTTPServer: except TimeoutError: print("Client timeout") break - except OSError as e: + except Exception as e: print(e) break @@ -131,6 +133,78 @@ class HTTPServer: :param request: client's request """ + match request.type: + case "GET" | "HEAD": + response = self._handle_get(client, request) + # case "POST": # Not Implemented + # response = self._handle_post(client, request) + case _: + with open(I_PATH_MAP["/err/response"]["path"], "r", encoding="ascii") as file: + data = file.read().format(status_code=str(STATUS_CODE_NOT_FOUND)).encode("ascii") + response = Response(data, STATUS_CODE_NOT_FOUND) + + # process header data + if response.headers.get("Content-Encoding") is None: + supported_compressions = [x.strip() for x in getattr(request, "Accept-Encoding", "").split(",")] + if "br" in supported_compressions: + response.headers["Content-Encoding"] = "br" + response.data = brotli.compress(response.data) + elif "gzip" in supported_compressions: + response.headers["Content-Encoding"] = "gzip" + response.data = gzip.compress(response.data) + if response.headers.get("Content-Length") is None: + response.headers["Content-Length"] = len(response.data) + if response.headers.get("Connection") is None: + response.headers["Connection"] = "close" + + # generate basic message + message = b'HTTP/1.1 ' + response.status.__bytes__() + b'\r\n' + for key, value in response.headers.items(): + message += f"{key}: {value}\r\n".encode("ascii") + message += b'\r\n' + response.data + + # send message + client.sendall(message) + + def _handle_get(self, client, request) -> Response: + """ + Handles GET / HEAD requests from a client + """ + + split_path = request.path.split("/", maxsplit=16)[1:] + if request.path in PATH_MAP: # assume browser + filepath = PATH_MAP[request.path]["path"] + with open(filepath, "rb") as file: + data = file.read() + + if request.type == "GET": + return Response(data, STATUS_CODE_OK) + elif request.type == "HEAD": + return Response(b'', STATUS_CODE_OK, {"Content-Length": len(data)}) + else: + raise TypeError("Called GET handler for non-GET request") + + elif len(split_path) >= 2 and split_path[0] in API_VERSIONS: # assume script + # unsupported API version + if not API_VERSIONS[split_path[0]]: + if request.type == "GET" or request.type == "HEAD": + return Response(b'', STATUS_CODE_BAD_REQUEST) + else: + raise TypeError("Called GET handler for non-GET request") + + return APIv1.api_call(client, request) + + else: # assume browser + with open(I_PATH_MAP["/err/response"]["path"], "r", encoding="ascii") as file: + data = file.read() + data = data.format(status_code=str(STATUS_CODE_NOT_FOUND)).encode("ascii") + return Response(data, STATUS_CODE_NOT_FOUND) + + def _handle_post(self, client, request) -> Response: + """ + Handles POSt request from a client + """ + def _recv_request(self, client: ssl.SSLSocket) -> Request | None: """ Receive request from client -- cgit 1.4.1 From 9afb912c6471376cbc4907b58ec2904fceb0a797 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 21:14:48 +0300 Subject: Update compression thing --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'main.py') diff --git a/main.py b/main.py index de605b6..755da18 100644 --- a/main.py +++ b/main.py @@ -144,7 +144,7 @@ class HTTPServer: response = Response(data, STATUS_CODE_NOT_FOUND) # process header data - if response.headers.get("Content-Encoding") is None: + if response.headers.get("Content-Encoding") is None and response.compress: supported_compressions = [x.strip() for x in getattr(request, "Accept-Encoding", "").split(",")] if "br" in supported_compressions: response.headers["Content-Encoding"] = "br" -- cgit 1.4.1 From 45d6af4831ce0cc99ad80b181c4aa4230c9b6bf5 Mon Sep 17 00:00:00 2001 From: UltraQbik Date: Fri, 23 Aug 2024 21:25:37 +0300 Subject: remove timeout Removed timeout because it interrupts client's downloads --- main.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'main.py') diff --git a/main.py b/main.py index 755da18..3bbd9d4 100644 --- a/main.py +++ b/main.py @@ -107,7 +107,7 @@ class HTTPServer: :param client: client ssl socket """ - client.settimeout(5) + # client.settimeout(5) while not self.stop_event.is_set(): try: # get client's request @@ -126,7 +126,7 @@ class HTTPServer: # close the connection once stop even was set or an error occurred client.close() - def _client_request_handler(self, client, request): + def _client_request_handler(self, client: ssl.SSLSocket, request: Request): """ Handles responses to client's requests :param client: client @@ -166,7 +166,7 @@ class HTTPServer: # send message client.sendall(message) - def _handle_get(self, client, request) -> Response: + def _handle_get(self, client: ssl.SSLSocket, request: Request) -> Response: """ Handles GET / HEAD requests from a client """ @@ -200,7 +200,7 @@ class HTTPServer: data = data.format(status_code=str(STATUS_CODE_NOT_FOUND)).encode("ascii") return Response(data, STATUS_CODE_NOT_FOUND) - def _handle_post(self, client, request) -> Response: + def _handle_post(self, client: ssl.SSLSocket, request: Request) -> Response: """ Handles POSt request from a client """ -- cgit 1.4.1