← back to FashionCrawler

fashioncrawler/utils/html_renderer.py

73 lines

import os
from http.server import BaseHTTPRequestHandler, HTTPServer

from jinja2 import Environment, FileSystemLoader


# TODO: Render html in io_utils and then serve it here.
class RenderAndServe(HTTPServer):
    def __init__(
        self,
        context,
        template_path="fashioncrawler/resources/templates",
        template_name="base_template.html.j2",
        server_address=("localhost", 8000),
    ) -> None:
        self.template_path = template_path
        self.template_name = template_name
        self.server_address = server_address
        self.env = Environment(loader=FileSystemLoader(self.template_path))
        self.template = self.env.get_template(self.template_name)
        self.context = context

        super().__init__(self.server_address, MyHandler)


class MyHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/":
            # TODO: figure out how to fix the type
            rendered_html = self.server.template.render(**self.server.context)
            self.send_response(200)
            self.send_header("Content-type", "text/html")
            self.end_headers()
            self.wfile.write(rendered_html.encode("utf-8"))
        elif self.path.endswith(".css"):
            # Realpath containment — resolve both the requested file path AND
            # the template root via os.path.realpath, then verify the
            # requested file lives under the template root before opening.
            # Closes a path-traversal read where /../../../etc/anything.css
            # would escape template_path. Localhost-only dev server so the
            # original risk was low, but the containment check is cheap.
            # (Security punch-list Tier 3 #18 — 2026-05-20.)
            base = os.path.realpath(self.server.template_path)
            requested = os.path.realpath(os.path.join(self.server.template_path, self.path.lstrip("/")))
            if not (requested == base or requested.startswith(base + os.sep)):
                self.send_error(404, "File not found")
                return
            if os.path.exists(requested):
                with open(requested, "rb") as file:
                    self.send_response(200)
                    self.send_header("Content-type", "text/css")
                    self.end_headers()
                    self.wfile.write(file.read())
            else:
                self.send_error(404, "File not found")
        else:
            self.send_error(404, "File not found")


def render_and_serve(context, **kwargs):
    """Renders a Jinja2 template and serves it over an HTTP server.

    Args:
        context (dict): The context data to pass to the template.

    Keyword Args:
        template_path (str, optional): The path to the template directory. Defaults to "fashioncrawler/resources/templates".
        template_name (str, optional): The name of the template to render. Defaults to "base_template.html.j2".
        server_address (tuple, optional): The address for the HTTP server. Defaults to ("localhost", 8000).
    """
    server = RenderAndServe(context, **kwargs)
    return server