Introduction
So a few weeks ago I was scrolling on social media and found a meme that said “Python developers trying to create anything without importing 50 libraries”. Now as a python developer I got triggered. We are not some low IQ people who chose python because they can’t learn anything else and build anything without borrowing other’s code. We chose python because it’s simple and because it has a big ecosystem. So I set out to prove that meme wrong.
The Challenge
The challenge is simple, make a working website using python. The only catch: I can’t use any installed libraries. I have to use what python provides by default. To avoid accidently importing libraries the first thing I did was create a virtual environment.
virtualenv env
The Idea
Now the simplest way to make a website with python would be to serve a server using the http.server module, make an index.html page and call it a day. But that would be no fun would it? That’s just serving a server not making a website. So just like the type of person I am I overcomplicated everything and decided to create a MVT framework. MVT stands for Model View Template. It’s an architecture used by many web frameworks like Django and Flask. It essentially separates the workflow into three components. The template is what’s seen by the user. The view is the server side logic run when an http request is made. And the Model is where the data lives. The final goal is to create a simple notes app.
The server
The first step would be to serve a server. As I already mentioned in the previous section, it can be done pretty easily using the http.server module.
from http.server import HTTPServer, SimpleHTTPRequestHandler
host="localhost"
port=8000
server=HTTPServer((host, port), SimpleHTTPRequestHandler)
print(f"Serving on http://{host}:{port}")
self.server.serve_forever()
We need a bit more customizability however so instead of using SimpleHTTPRequestHandler I wrote my own handler
from http.server import HTTPServer, BaseHTTPRequestHandler
class ViewHandler(BaseHTTPRequestHandler):
pass
host="localhost"
port=8000
server=HTTPServer((host, port), ViewHandler)
print(f"Serving on http://{host}:{port}")
self.server.serve_forever()
I also created an App class to hold everything
class App:
def __init__(self,host,port):
self.host=host
self.port=port
self.server=HTTPServer((host, port), ViewHandler)
def run(self):
print(f"Serving on http://{self.host}:{self.port}")
self.server.serve_forever()
Views and routing
Now came the first real challenge. Creating a handler that can run different views based on what route we are in. Now the BaseHTTPRequestHandler from which we are inheriting comes with methods like do_get and do_post which run when a GET request or a post request is sent. But it doesn’t handle routing. This was something I had to implement myself. Now the first idea I had was to have views as functions and a dict called ROUTES where each route is paired with it’s respective view
ROUTES={
"/": home,
"/about/":about,
}
But I felt a bit more fancy and decided to make it so that the ROUTES dict will be created automatically using decorators. Something similar is done by Flask.
@route("/")
def home():
...
So I wrote the route decorator
ROUTES={}
def route(path,methods=("GET",)):
def decorator(func):
for method in methods:
ROUTES[(path.rstrip("/") or "/",method)]=func
return func
return decorator
Now before the server runs all the routes will be in the ROUTES dict ready to be used by our handler.
PS: The dict keys of ROUTES are both the route and the method. It will be important later
Time to write the handler.
from urllib.parse import parse_qs #built in module
class ViewHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.dispatch("GET")
def do_POST(self):
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length).decode('utf-8')
data = parse_qs(body)
data={key: value[0] for key, value in data.items()}
self.dispatch("POST",data)
def dispatch(self,method,data=None):
parts=self.path.split("/")
path = "/" + parts[1] if len(parts) > 1 and parts[1] else "/"
args = parts[2:]
view=ROUTES.get((path,method))
if not view:
self.send_error(404, "Not Found")
return
try:
if method=="POST":
response = view(data,*args)
else:
response = view(*args)
except TypeError:
self.send_error(400, "Bad Request")
return
self.send_response(response.get("status",200))
for key, value in response.get("headers", {}).items():
self.send_header(key, value)
self.end_headers()
self.wfile.write(response.get("body","<h1>Unexpected error</h1>"))
Now I know this is a lot and I didn’t do a great job writing comments, so I will try to explain as simply as I can. So the first thing it does is load the data if it’s a post request and call the dispatch method. If it’s a get request it directly calls the dispatch method. In the dispatch method it first get the route that it is in. Then it finds the corresponding route from the ROUTES dict and calls the function. Everything after the second “/” is passed as arguments to the function. The final part is some simple error handling and edge cases.
With that I was officially done with the V part of MVT. Time to move on to the M.
Models
Now models is probably the simplest part of MVT. In fact I don’t need models at all, I can just use raw SQL. But no. We are sophisticated people. So I wrote an ORM, using the built in sqlite3 module that comes with python.
import sqlite3
from abc import ABC,abstractmethod
class Model(ABC):
@classmethod
def _get_connection(cls):
conn = sqlite3.connect("database.db")
conn.row_factory = sqlite3.Row
return conn, conn.cursor()
@property
@abstractmethod
def table_name(cls):
return
@property
@abstractmethod
def fields(cls):
return
@classmethod
def create_table(cls):
'''Create the table'''
columns_sql = ", ".join(
f"{name} {datatype}"
for name, datatype in cls.fields.items()
)
sql = f'''
CREATE TABLE IF NOT EXISTS {cls.table_name} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
{columns_sql}
)
'''
conn, cursor = cls._get_connection()
with conn:
cursor.execute(sql)
conn.close()
@classmethod
def insert(cls, data: dict):
'''Insert a row into the table'''
fields = ", ".join(data.keys())
placeholders = ", ".join(["?"] * len(data))
values = tuple(data.values())
sql = f"""
INSERT INTO {cls.table_name} ({fields})
VALUES ({placeholders})
"""
conn, cursor = cls._get_connection()
with conn:
cursor.execute(sql, values)
conn.close()
@classmethod
def all(cls):
'''Get all the rows from the table'''
sql = f"SELECT * FROM {cls.table_name}"
conn, cursor = cls._get_connection()
with conn:
cursor.execute(sql)
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
@classmethod
def get(cls, id: int):
'''Specifically reserved for fetching using ID'''
sql = f"SELECT * FROM {cls.table_name} WHERE id=?"
conn, cursor = cls._get_connection()
with conn:
cursor.execute(sql, (id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
@classmethod
def filter(cls, **kwargs):
'''Filter data'''
sql_filter = " AND ".join(f"{k}=?" for k in kwargs.keys())
sql = f"SELECT * FROM {cls.table_name} WHERE {sql_filter}"
conn, cursor = cls._get_connection()
with conn:
cursor.execute(sql, tuple(kwargs.values()))
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
@classmethod
def custom_filter(cls, sql_filter: str):
'''Create a custom filter using raw sql'''
sql = f"SELECT * FROM {cls.table_name} {sql_filter}"
conn, cursor = cls._get_connection()
with conn:
cursor.execute(sql)
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
@classmethod
def update(cls, id: int, **data):
'''Updates a row'''
fields = ", ".join(f"{k}=?" for k in data)
values = tuple(data.values()) + (id,)
sql = f"UPDATE {cls.table_name} SET {fields} WHERE id=?"
conn, cursor = cls._get_connection()
with conn:
cursor.execute(sql, values)
conn.close()
@classmethod
def delete(cls, id: int):
'''Deletes a row'''
sql = f"DELETE FROM {cls.table_name} WHERE id=?"
conn, cursor = cls._get_connection()
with conn:
cursor.execute(sql, (id,))
conn.close()
Again that’s a lot. But this time it’s much more easier to understand. Basically Model is an abstract class. Which simply means something has to inherit Model. An object can’t be created directly with it. The _get_connection() method opens a connection to the database. Then it has class methods which executes different SQL queries to perform CRUD operations.
And we are done with M as well. Only thing left is T.
Template
Now this was by far the hardest part of the project. A templating engine like Jinja is huge! It has to have a lexer, a parser, it has to handle a million different edge cases and it has to be secure from vulnerabilities like template injection. This is more Compiler Design Theory than Web Dev. I started to question myself. Do I need a templating engine? Really? Of course I do, I am sophisticated. So I wrote the world’s most insecure templating engine ever. I gave it the permission to execute arbitrary python code directly inside html.
import re
def parse(html,context):
pattern_variable=re.compile(r"(?<!\[)\[(?!\[)(.*?)(?<!\])\](?!\])")
pattern_code = re.compile(r"\[\[(.*?)\]\]",re.DOTALL)
def resolve_variables(match):
key = match.group(1)
value = context
for part in key.split("."):
value = value.get(part, "")
return str(value)
def resolve_code(match):
code = match.group(1)
buffer = []
def out(content):
buffer.append(str(content)+"\n")
exec(code, {}, {"out": out, "context": context})
return "".join(buffer)
output=pattern_variable.sub(resolve_variables,html)
output=pattern_code.sub(resolve_code,output)
return output
The parse function is the entire templating engine. The two regex patterns are to detect [] and [[]]. [] will be used for variables and [[]] will be used for code. And then the nested functions execute the necessary code to create the output. The parse functions takes html code with templating and a context dict returns the parsed html code. I know penetration testers are drooling right now. I also created a render function to load html files.
def render(file_path,context={}):
'''Renders an html file'''
try:
with open(file_path, "r") as f:
body = f.read()
body=parse(body,context)
return {
"status": 200,
"headers": {"Content-Type": "text/html"},
"body": body.encode()
}
except FileNotFoundError:
return {
"status": 404,
"headers": {"Content-Type": "text/plain"},
"body": b"Template not found"
}
Final web app
Now that I was done creating abstraction after abstraction, it was finally time to create the actual website. Which was now very very trivial as the hard work was already done.
from module.views.base import App,route,render
from module.models.orm import Model
app=App("localhost",8000)
class Note(Model):
table_name="notes"
fields={
"title":"TEXT",
"contents":"TEXT",
}
@route("/")
def home():
return render("templates/index.html")
@route("/",("POST",))
def home_post(data):
Note.insert(data)
return render("templates/index.html")
@route("/notes")
def notes(id=None):
if not id:
notes=Note.all()
return render("templates/notes.html",{"notes":notes})
else:
note=Note.get(id)
return render("templates/notes_ind.html",{"note":note})
if __name__=='__main__':
tables: list[Model] = [Note]
for table in tables:
table.create_table()
app.run()
This is how a template looks like
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
<title>Document</title>
</head>
<body>
<ul class="nav justify-content-end">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="/">Create Note</a>
</li>
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Notes</a>
</li>
</ul>
<div class="container my-4">
<div class="row row-cols-4">
[[for note in context.get("notes"):
out(f'''
<div class="col">
<div class="card" style="width: 18-rm;">
<div class="card-body">
<h5 class="card-title">{note.get('title')}</h5>
<a href="/notes/{note.get('id')}" class="btn btn-primary">Open</a>
</div>
</div>
</div>
''')]]
</div>
<ul>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
crossorigin="anonymous"></script>
</body>
</html>
How the code of the final app works is left as an exercise to the reader. And I kinda don’t wanna write anymore.
So that’s it