Files
sniph-bank/main-doc.py
foreverpyrite 1e666c760d Added DNS yap and fixed password "hash"
Some silly goober left in the Cg=== from the terminal when he ran `echo
"password" | base64`. Cute little terminal escape characters.
2025-12-02 20:46:09 -05:00

244 lines
11 KiB
Python

# 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() != "Z29GQUxDT05TMTIz":
# 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)