# Hi. # I was tempted to implement my own custom HTTP server to show how an HTTP server works. # But I'm pretty sure that's out of the scope of this demo. # After all, HTTPS is just HTTP with TLS on top of it, and I'm not about to program Diffe-Hellman for this. # Anyways, holy yap sesh. # Import the HTTPStatus "enumeration" to have more readable code. # An enumeration is just a thing with multiple diffent possibilities, and there are only so many HTTP # status codes outlined by HTTP specifications. # In reality, HTTP statuses are intentgers, with 1XX-5XX, with the first number dictating the category # of error. # For example, the HTTPStatus.NOT_FOUND would be a 404 error status code, # And a HTTPStatus.OK would be the 200 success status code from http import HTTPStatus # This (flask) is the HTTP server library we are using. # Response is the Python object representing what we return to the client # Request is the Python object representing the request from the client # send_file is a function that turns the contents of the specified file into a Response # Flask is the server itself. from flask import Flask, Response, request, send_file # Cyrptographically secure, random bits. # On Linux this is reading the bits from the special file /dev/urandom # Less specifically, this uses a lot of the system's entropy (non-deterministic things based off practically # random events, like the users recent mouse movements and secure positions) to generate random bits. from os import urandom # The threading library allows you to spawn multiple processes from within this one program. # This is called concurrency, with each Thread being a concurrent process spun from a main thread. # The Thread object represents one of those other Threads within the process. # The Event object just allows for cross-thread communications. # In this program, we will spin two threads up, one for the HTTP server and one for the HTTP server. # We will use the Event object as a way to tell the servers to shut down. from threading import Thread, Event # The make_server function is just a sort of wrapper for the HTTP server to allow things like shutting down # the server through a function call. Normally the call to run a Flask server is blocking I believe, # meaning it stops program execution until it returns, which doesn't happen under normal circumstances. from werkzeug.serving import make_server # This is for type hinting. This is mostly for me, so that I know the type that each variable should be. # Optional means that it can either be the None object, or whatever is within the brackets following the # "Optional" type hint. # For example, Optional[str] means that the value will either be None or a string. from typing import Optional # More type hinting... from types import FrameType # This is what allows us to handle signals like Ctrl+C (SIGINT), which we will use to trigger the Event # for the Threads to know to shut the server down import signal # We only use this as a way to tell the system the program excuted sucessfully, and as intented. import sys # Used as a way to encode the password, or way of "hashing" it. from base64 import b64encode as base64_encode # The IP address the server will listen on. SERVER_IP: str = "127.0.0.1" # The exepected HTTP host header. # This can use used to only response if the URL in the web browser contains "sniphbank.com" # rather than accessing the IP (127.0.0.1) directly. Right now, this is not enabled. HTTP_HOST: str = "sniphbank.com" # The port for the HTTP socket. # 80 is the default port, and what the browser automatically connects to when given "http://" HTTP_PORT: int = 80 # The port for the HTTPS socket. # 443 is the default port, and what the browser automatically connects to when given "https://" HTTPS_PORT: int = 443 # This will be used to tell the Flask server whether or not it should be running in Debug mode. DEBUG: bool = False # Required to be in global scope # The Flask server itself. app = Flask(__name__) # A secret key required for secure operations. app.secret_key = urandom(24) # The logic for what happens when someone visits the root of the HTTP server, so # "http(s)://sniphbank.com", as the browser automatically requests "/" @app.route("/") def index(): # We simply return the contents of the index.html file stored in the "website" directory in this # projects structure. return send_file("./website/index.html") # The logic for when a user clicks on the "submit" button. # The HTML login form's submit button sends an HTTP "POST" to "/login", and the response is then # awaited for as if you clicked on a link to a new page. @app.route("/login", methods=["POST"]) def login(): # There isn't really a reason to validate the user for the demo, # just decide on a username that might pertain to whomever's # personal information # The "user" input field in the HTML form. user = request.form["username"] # Likewise, the "password" input field in the HTML form. # We turn this into bytes so we can encode it with base64 password = request.form["password"].encode() # In a real web application, the values are pulled from a database and not hardcoded. # If the username is not "ronniej" if user != "ronniej": # Then we return the respponse "User not found" with the HTTP 401 status code (UNAUTHORIZED) return Response("User not found", HTTPStatus.UNAUTHORIZED) # Also in a real application, the password will be hashed rather than merely base64 encoded. # The difference with it being hashed is that it's easly reversable, someone with access to this # code (or in a real case, a database) would be able to easily reverse this to get the genuine # password. # The point of this here is to showcase that password is in plaintext until it gets to the server. if base64_encode(password).decode() != "Z29GQUxDT05TMTIzCg==": # Likewise if the password doesn't match, we return a similar response. return Response("Incorrect password", HTTPStatus.UNAUTHORIZED) # In order to get this far, the username and password are correct, so we can send the webpage. return send_file("./website/account.html") # CSS, or Cascading Style Sheets, is the very thing that makes the web useable. # Otherwise, every element in HTML (Hypertext Markup Language, NOT a programming language) # would be completely black text on white background, and blocks placed top to bottom # If you wanna try it yourself, feel free to run this code and rename/move/change the file path # below. The server will return a "404: Not Found" error and the browser won't be able to render # the webpage as expected. @app.route("/css/style.css", methods=["GET"]) def stylesheet(): return send_file("./website/css/style.css") # Very similar, the favicon.ico is automatically requested from the server by the Web Browser # This is the lil icon that is displayed on the tab next to the HTML page's title. @app.route("/favicon.ico", methods=["GET"]) def favicon(): return send_file("./website/favicon.ico") # This is a lil fancy, but I thought it was a fair obstraction to handle the threading in a...more... # "readable" way...? # Basically, we are creating a class that is based off the "Thread" class, so it shares the same properties # and methods. # Here, we are going to add some things to it, for example, the Flask server, the IP and port we want # to listen on, and the SSL Context if we are using HTTPS class ServerThread(Thread): def __init__( self, app: Flask, host: str, port: int, ssl_context: Optional[tuple[str, str]] = None, name: str = "ServerThread", ) -> None: super().__init__(name=name, daemon=True) # threaded=True is telling the werkzeug server that it can use threads to handle multiple # requests at the same time. Not really important for the demo I don't think, and I'll # likely get rid of it. self.server = make_server( host, port, app, threaded=True, # This is either None or the public SSL cert and private key (or is it the other way around...?) ssl_context=ssl_context, # tuple (cert, key) or SSLContext ) def run(self): # serve_forever blocks until shutdown() is called self.server.serve_forever() def shutdown(self): self.server.shutdown() def _install_signal_handlers(stop_event: Event): def handler(signum: signal.Signals, frame: Optional[FrameType]): stop_event.set() # Handle Ctrl+C and SIGTERM signal.signal(signal.SIGINT, handler) try: signal.signal(signal.SIGTERM, handler) except AttributeError: # SIGTERM may not be available on some platforms (e.g., Windows Python in some contexts) # I'm always on Linux, so if you wanna run this, then you should either use WSL or Podman. # (Podman is a generally more performant and secure alternative to Docker, if you are familiar) pass # This block literally just says "If this is the program being run, do the following" if __name__ == "__main__": # Create the stop event stop_event = Event() # Install the signal handler for SIGINT (Ctrl+C signal) _install_signal_handlers(stop_event) # Our HTTP server thread. http_server = ServerThread( app, SERVER_IP, HTTP_PORT, ssl_context=None, name="HTTP-Thread" ) # Similarly, the HTTPS server thread. https_server = ServerThread( app, SERVER_IP, HTTPS_PORT, ssl_context=("./certs/cert.pem", "./certs/key.pem"), name="HTTPS-Thread", ) try: # Start the HTTP and HTTPS server threads http_server.start() https_server.start() # Make sure there's some output to show that the servers running. print(f"HTTP server running at http://{SERVER_IP}:{HTTP_PORT}") print(f"HTTPS server running at https://{SERVER_IP}:{HTTPS_PORT}") print("Press Ctrl+C to stop.") # Wait until a signal requests shutdown stop_event.wait() except KeyboardInterrupt: # Typically how you handle Ctrl+C, but since we have a singal hanlder it's pointless. # Doesn't hurt anyone though. pass finally: # Stop both servers. for srv in (http_server, https_server): try: srv.shutdown() except Exception: pass # Join both Threads (meaning take their execution and join it with the main thread's). for srv in (http_server, https_server): srv.join(timeout=5) # Output that the servers shut down gracefully (enough) print("Servers stopped!") # This just tells the shell that "hey, we exited as expected." sys.exit(0)