165 lines
5.3 KiB
Python
165 lines
5.3 KiB
Python
"""The one dynamic page on mews.page.
|
|
|
|
The site is static apart from this: a form posts a page address to /result, the
|
|
page is fetched and checked, and the author gets the findings back. Nothing here
|
|
runs JavaScript or sets a cookie, because the response is itself a Mews page.
|
|
"""
|
|
|
|
from collections.abc import Iterable
|
|
import os
|
|
import threading
|
|
from urllib.parse import parse_qs
|
|
|
|
from mews import db, pages
|
|
from mews.check import submit
|
|
from mews.fetch import Fetcher
|
|
|
|
# Outbound checks are slow and this runs on a small machine, so only a couple
|
|
# happen at once; the rest of the queue is turned away rather than piled up.
|
|
_CHECKS = threading.BoundedSemaphore(2)
|
|
_REQUESTS = threading.BoundedSemaphore(8)
|
|
_SLOT_WAIT = 5.0
|
|
|
|
MAX_BODY = 4096
|
|
|
|
BUSY = "The checker is busy right now. Try again in a few minutes."
|
|
|
|
|
|
def directory_path() -> str:
|
|
"""Where the directory page is written."""
|
|
return os.environ.get("MEWS_DIRECTORY", "directory.html")
|
|
|
|
|
|
def _start(start_response, status: str, body: bytes) -> list[bytes]:
|
|
start_response(
|
|
status,
|
|
[
|
|
("Content-Type", "text/html; charset=utf-8"),
|
|
("Content-Length", str(len(body))),
|
|
("Cache-Control", "no-store"),
|
|
],
|
|
)
|
|
return [body]
|
|
|
|
|
|
def _client(environ: dict) -> str:
|
|
"""Return the reader's address, as the proxy reports it.
|
|
|
|
Caddy sets X-Real-IP from the connection it accepted. The server binds to
|
|
the loopback address only, so nothing else can set that header.
|
|
"""
|
|
return environ.get("HTTP_X_REAL_IP") or environ.get("REMOTE_ADDR", "")
|
|
|
|
|
|
def _form(environ: dict) -> dict[str, list[str]]:
|
|
try:
|
|
length = min(int(environ.get("CONTENT_LENGTH") or 0), MAX_BODY)
|
|
except ValueError:
|
|
return {}
|
|
if length <= 0:
|
|
return {}
|
|
raw = environ["wsgi.input"].read(length)
|
|
return parse_qs(raw.decode("utf-8", "replace"), keep_blank_values=True)
|
|
|
|
|
|
def application(environ: dict, start_response) -> Iterable[bytes]:
|
|
"""Handle one request."""
|
|
path = environ.get("PATH_INFO", "/")
|
|
method = environ.get("REQUEST_METHOD", "GET")
|
|
|
|
if path != "/result":
|
|
return _start(
|
|
start_response,
|
|
"404 Not Found",
|
|
pages.message(
|
|
"Page not found",
|
|
["That address isn't part of this site."],
|
|
[("/", "Mews"), ("/directory", "The directory")],
|
|
).encode(),
|
|
)
|
|
|
|
if method != "POST":
|
|
return _start(
|
|
start_response,
|
|
"405 Method Not Allowed",
|
|
pages.message(
|
|
"Nothing to show",
|
|
["Results appear here after you check a page."],
|
|
[("/check", "Check a page")],
|
|
).encode(),
|
|
)
|
|
|
|
form = _form(environ)
|
|
raw_url = (form.get("url") or [""])[0].strip()
|
|
listing = "list" in form
|
|
|
|
if not _REQUESTS.acquire(blocking=False):
|
|
return _start(start_response, "503 Service Unavailable", _busy())
|
|
try:
|
|
if not _CHECKS.acquire(timeout=_SLOT_WAIT):
|
|
return _start(start_response, "503 Service Unavailable", _busy())
|
|
try:
|
|
body = _check(raw_url, _client(environ), listing)
|
|
finally:
|
|
_CHECKS.release()
|
|
finally:
|
|
_REQUESTS.release()
|
|
return _start(start_response, "200 OK", body)
|
|
|
|
|
|
def _busy() -> bytes:
|
|
return pages.message("Busy", [BUSY], [("/check", "Try again")]).encode()
|
|
|
|
|
|
def _check(raw_url: str, client: str, listing: bool) -> bytes:
|
|
"""Run one check and render the page the author gets back."""
|
|
connection = db.connect()
|
|
try:
|
|
with Fetcher() as fetcher:
|
|
outcome = submit(
|
|
connection,
|
|
raw_url,
|
|
client=db.ip_hash(connection, client),
|
|
listing=listing,
|
|
fetcher=fetcher,
|
|
directory=directory_path() if listing else None,
|
|
)
|
|
except ValueError as error:
|
|
# The directory page refused to publish itself. The site is listed; the
|
|
# page will be rebuilt by the next recheck pass.
|
|
return pages.message(
|
|
"Listed, but the directory didn't rebuild",
|
|
[str(error), "Your site is listed and will appear shortly."],
|
|
[("/directory", "The directory")],
|
|
).encode()
|
|
finally:
|
|
connection.close()
|
|
|
|
if outcome.report is None:
|
|
return pages.message(
|
|
"We couldn't check that page",
|
|
[outcome.message or "Something went wrong. Try again."],
|
|
[("/check", "Try again"), ("/spec/0.1", "The spec")],
|
|
).encode()
|
|
|
|
command = f"uv run mewslint.py {outcome.report.url or raw_url}"
|
|
return pages.report(outcome.report, listed=outcome.listed, command=command).encode()
|
|
|
|
|
|
def serve() -> None:
|
|
"""Run the app for real, on the loopback address only."""
|
|
# Imported here so the validator and the command line tools do not pull in
|
|
# a web server.
|
|
from waitress import serve as waitress_serve # noqa: PLC0415
|
|
|
|
listen = os.environ.get("MEWS_LISTEN", "127.0.0.1:8394")
|
|
host = listen.rsplit(":", 1)[0]
|
|
if host not in ("127.0.0.1", "::1", "localhost"):
|
|
raise SystemExit(
|
|
"mewsd serves on the loopback address only, behind a reverse proxy. "
|
|
f"Set MEWS_LISTEN to 127.0.0.1 with a port, not {listen}."
|
|
)
|
|
waitress_serve(application, listen=listen, threads=4, ident="mews.page")
|
|
|
|
|
|
app = application
|