From 763c9ed30853a4cb5ee5ed76a7e566672b6a3d4a Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 27 Oct 2026 04:03:33 +0000 Subject: [PATCH] Show the IP information API on the home page (closes #156) The home page gets an IP information API card. It shows the curl command that looks up the visitor's own address at /ip/{addr}. The page then makes that same request and shows the JSON answer, pretty-printed. An error answer, such as a 404 for an address with no route, is shown the same way. A request that fails shows an error line. The server gives the page the visitor's address, found as /ip/ finds it, so a trusted proxy's X-Forwarded-For is honoured. The command takes its origin from the browser, so it names the host the visitor used. Model: opus-5-5 --- README.md | 3 +- TODO.md | 3 ++ internal/server/handlers.go | 14 ++++- internal/server/handlers_internal_test.go | 59 +++++++++++++++++++++ internal/server/middleware_internal_test.go | 9 ++-- internal/templates/index.html | 47 ++++++++++++++-- internal/templates/templates_test.go | 5 +- 7 files changed, 129 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 314e7f5..e3d89a9 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ Every route answers `HEAD` as it answers `GET`, without the body. ### Web Interface - `GET /` - Home page (HTML): headline figures, lookups by AS number and by IP - address, and a search of AS names + address, and a search of AS names. A card shows the `curl` command that looks + up the visitor's own address at `/ip/{addr}`, and that command's live answer. - `GET /status` - HTML status dashboard, which reads `/api/v1/stats` - `GET /status.json` - JSON statistics: feed, memory, database and route figures - `GET /.well-known/healthcheck.json` - Health check (JSON): 200 when the diff --git a/TODO.md b/TODO.md index fd09c4d..9b76298 100644 --- a/TODO.md +++ b/TODO.md @@ -33,6 +33,9 @@ tested. # Completed Steps +- 2026-10-27: the home page has an IP information API card. It shows the `curl` + command that looks up the visitor's own address at `/ip/{addr}`, and that + command's live answer (closes #156) - 2026-10-27: on the AS page, a click on the heading of the IPv4 or the IPv6 prefix list folds or unfolds the list. Both lists start open (closes #155) - 2026-10-27: the AS page lists its IPv4 and its IPv6 prefixes by mask length, diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 7c20bc1..406a839 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -146,7 +146,10 @@ func (s *Server) handleIndex() http.HandlerFunc { tmpl := templates.IndexTemplate() - err := tmpl.Execute(w, pageData{CSPNonce: cspNonce(r.Context())}) + err := tmpl.Execute(w, indexPageData{ + CSPNonce: cspNonce(r.Context()), + ClientIP: extractClientIP(r), + }) if err != nil { s.logger.Error("Failed to render index template", "error", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) @@ -160,6 +163,15 @@ type pageData struct { CSPNonce string } +// indexPageData is what the home page is rendered with. +type indexPageData struct { + // CSPNonce is the nonce the page's script and style blocks carry. + CSPNonce string + // ClientIP is the visitor's address, which the IP information API card + // looks up: the address /ip/ would look up for the same request. + ClientIP string +} + // writeJSONError writes a standardized JSON error response with the given // status code and error message. func (s *Server) writeJSONError(w http.ResponseWriter, statusCode int, message string) { diff --git a/internal/server/handlers_internal_test.go b/internal/server/handlers_internal_test.go index 78a8f83..434be48 100644 --- a/internal/server/handlers_internal_test.go +++ b/internal/server/handlers_internal_test.go @@ -7,6 +7,7 @@ import ( "html" "net/http" "net/http/httptest" + "net/netip" "runtime" "slices" "strconv" @@ -503,6 +504,64 @@ func TestPagesSayOriginZeroIsNoKnownOrigin(t *testing.T) { } } +// TestIndexShowsTheClientAddress requests the home page and checks that it has +// the IP information API card, holding the address /ip/ would look up for the +// same request: the TCP peer, or, when the peer is a trusted proxy, the client +// the proxy names in X-Forwarded-For +// (https://git.eeqj.de/sneak/routewatch/issues/156). +func TestIndexShowsTheClientAddress(t *testing.T) { + t.Parallel() + + const cardHeading = "
The API answers what RouteWatch knows about an IP address, such as its AS number and country. /ip/ with no address answers for the caller's own address.
Loading...+