"""Tiny HTTP server for the neo4j-browser static files and Neo4j discovery.""" import asyncio import json import logging import mimetypes import os from pathlib import Path from urllib.parse import unquote, urlsplit from .backend import NEO4J_VERSION log = logging.getLogger("peo4c.http") class HttpServer: def __init__(self, browser_dir: str, bolt_port: int): self.root = Path(browser_dir).resolve() self.bolt_port = bolt_port async def handle(self, reader, writer): try: request = await asyncio.wait_for(reader.readuntil(b"\r\n\r\n"), timeout=30) lines = request.decode("latin-1").split("\r\n") method, target, _ = (lines[0].split(" ") + ["", "", ""])[:3] headers = {} for line in lines[1:]: k, _, v = line.partition(":") headers[k.strip().lower()] = v.strip() status, extra, body = self.respond(method, target, headers) head = [f"HTTP/1.1 {status}", f"Content-Length: {len(body)}", "Connection: close", "X-Content-Type-Options: nosniff"] head += [f"{k}: {v}" for k, v in extra.items()] writer.write(("\r\n".join(head) + "\r\n\r\n").encode("latin-1")) if method != "HEAD": writer.write(body) await writer.drain() except (asyncio.IncompleteReadError, asyncio.LimitOverrunError, asyncio.TimeoutError, ConnectionResetError, BrokenPipeError): pass except Exception: # noqa: BLE001 log.exception("HTTP request failed") finally: writer.close() def respond(self, method, target, headers): if method not in ("GET", "HEAD"): return "405 Method Not Allowed", {"Allow": "GET, HEAD"}, b"" path = unquote(urlsplit(target).path) if path in ("/", ""): if "text/html" in headers.get("accept", ""): return "302 Found", {"Location": "/browser/"}, b"" return "200 OK", {"Content-Type": "application/json"}, discovery_document(headers, self.bolt_port) if path == "/browser": return "301 Moved Permanently", {"Location": "/browser/"}, b"" if not path.startswith("/browser/"): return "404 Not Found", {"Content-Type": "text/plain"}, b"Not found\n" file = (self.root / path[len("/browser/"):]).resolve() if file.is_dir(): file = file / "index.html" if not file.is_relative_to(self.root) or not file.is_file(): return "404 Not Found", {"Content-Type": "text/plain"}, b"Not found\n" ctype = mimetypes.guess_type(file.name)[0] or "application/octet-stream" cache = "no-cache" if file.name.endswith(".html") else "public, max-age=86400" return "200 OK", {"Content-Type": ctype, "Cache-Control": cache}, file.read_bytes() def discovery_document(headers, bolt_port) -> bytes: """The JSON Neo4j serves at ``/`` so clients can find the Bolt endpoint.""" host = headers.get("host") or "localhost" if host.startswith("["): # IPv6 literal, e.g. [::1]:7474 host = host[:host.index("]") + 1] else: host = host.split(":", 1)[0] port = os.environ.get("PEO4C_ADVERTISED_BOLT_PORT", str(bolt_port)) return json.dumps({ "bolt_routing": f"neo4j://{host}:{port}", "bolt_direct": f"bolt://{host}:{port}", "neo4j_version": NEO4J_VERSION, "neo4j_edition": "community", }).encode()