# SPDX-License-Identifier: Apache-2.0 """A tiny local site for the examples: a login (with a 2FA step), a CSV download and a checkout form. python examples/fixture_site.py # http://0.0.0.0:8765 python examples/fixture_site.py 9000 # another port The browser runs inside the gateway, so it must reach this site: for a gateway in Docker on the same machine use http://host.docker.internal:8765 (on Linux start the container with --add-host=host.docker.internal:host-gateway). That is the default of WEBPILOT_FIXTURE in the examples. Pages: / links to everything below /login user "demo", password "demo-password" (store them in the vault as site "fixture") /account "Signed in as demo" once logged in (a cookie), else "Please log in" /verify asks for a 6-digit code: something only a person can do (see handoff.py) /files a link that downloads report.csv (Content-Disposition: attachment) /checkout email, name, country and a "Place order" button (see record_and_replay.py) Standard library only; nothing is stored. Not for real use: the password is in this file. """ from __future__ import annotations import html import sys from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from urllib.parse import parse_qs USER, PASSWORD = "demo", "demo-password" CSV = "city,country,population\nLjubljana,Slovenia,295504\nDublin,Ireland,592713\nMaribor,Slovenia,96211\n" PAGE = """{title}

{title}

{body}""" def page(title: str, body: str) -> bytes: return PAGE.format(title=html.escape(title), body=body).encode() class Handler(BaseHTTPRequestHandler): def _send(self, status: int, body: bytes, ctype: str = "text/html; charset=utf-8", **headers: str) -> None: self.send_response(status) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) for k, v in headers.items(): self.send_header(k.replace("_", "-"), v) self.end_headers() self.wfile.write(body) def _form(self) -> dict[str, str]: n = int(self.headers.get("Content-Length") or 0) return {k: v[0] for k, v in parse_qs(self.rfile.read(n).decode()).items()} def do_GET(self) -> None: path = self.path.split("?")[0] signed_in = "sid=demo" in (self.headers.get("Cookie") or "") if path == "/": links = "".join( f'
  • {p}
  • ' for p in ["/login", "/account", "/verify", "/files", "/checkout"] ) self._send(200, page("Fixture site", f"")) elif path == "/login": form = ( '
    ' '' '' '
    ' ) self._send(200, page("Sign in", form)) elif path == "/account": self._send(200, page("Account", f"

    {'Signed in as demo' if signed_in else 'Please log in'}

    ")) elif path == "/verify": form = ( '

    We sent a 6-digit code to your phone.

    ' '
    ' ) self._send(200, page("Verify it's you", form)) elif path == "/files": self._send(200, page("Files", '

    Download report (CSV)

    ')) elif path == "/files/report.csv": self._send(200, CSV.encode(), "text/csv", Content_Disposition='attachment; filename="report.csv"') elif path == "/checkout": form = ( '
    ' '' '
    ' ) self._send(200, page("Checkout", form)) else: self._send(404, page("Not found", "

    No such page.

    ")) def do_POST(self) -> None: path, f = self.path.split("?")[0], self._form() if path == "/login": if f.get("username") == USER and f.get("password") == PASSWORD: self._send(303, b"", Location="/account", Set_Cookie="sid=demo; Path=/; HttpOnly; SameSite=Lax") else: self._send( 401, page("Sign in", '

    Wrong username or password.

    Try again'), ) elif path == "/verify": ok = f.get("code", "").isdigit() and len(f.get("code", "")) == 6 self._send( 200, page( "Verified" if ok else "Verify it's you", "

    " + ("Verified." if ok else "That code is not valid.") + "

    ", ), ) elif path == "/checkout": who = html.escape(f"{f.get('name', '')} <{f.get('email', '')}>") self._send( 200, page("Order placed", f"

    Ordered for {who} to {html.escape(f.get('country', ''))}

    ") ) else: self._send(404, page("Not found", "")) def log_message(self, fmt: str, *args: object) -> None: sys.stderr.write("fixture: " + (fmt % args) + "\n") if __name__ == "__main__": port = int(sys.argv[1]) if len(sys.argv) > 1 else 8765 print(f"fixture site on http://0.0.0.0:{port} (from a gateway in Docker: http://host.docker.internal:{port})") ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever()