From 45810e3fc81f96793deeb9a912313a48750d7fd4 Mon Sep 17 00:00:00 2001 From: sneak Date: Thu, 1 Jan 2026 05:37:12 +0700 Subject: [PATCH] Fix prefix URL routing for encoded CIDR notation Change route from wildcard /prefix/* to explicit /prefix/{prefix}/{len} to properly handle URL-encoded IPv6 addresses with CIDR notation. - Separate prefix and length into individual path parameters - Add prefixURL template function for generating correct links - Remove url.QueryUnescape from handlers (chi handles decoding) --- internal/server/handlers.go | 39 +++++++++++---------------- internal/server/routes.go | 4 +-- internal/templates/as_detail.html | 4 +-- internal/templates/prefix_length.html | 2 +- internal/templates/templates.go | 21 +++++++++++++-- 5 files changed, 40 insertions(+), 30 deletions(-) diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 59a9720..b69f192 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -7,7 +7,6 @@ import ( "errors" "net" "net/http" - "net/url" "runtime" "sort" "strconv" @@ -736,21 +735,18 @@ func (s *Server) handleASDetailJSON() http.HandlerFunc { // handlePrefixDetailJSON returns prefix details as JSON func (s *Server) handlePrefixDetailJSON() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // Get wildcard parameter (everything after /prefix/) - prefixParam := chi.URLParam(r, "*") - if prefixParam == "" { - writeJSONError(w, http.StatusBadRequest, "Prefix parameter is required") + // Get prefix and length from URL params + prefixParam := chi.URLParam(r, "prefix") + lenParam := chi.URLParam(r, "len") + + if prefixParam == "" || lenParam == "" { + writeJSONError(w, http.StatusBadRequest, "Prefix and length parameters are required") return } - // URL decode the prefix parameter - prefix, err := url.QueryUnescape(prefixParam) - if err != nil { - writeJSONError(w, http.StatusBadRequest, "Invalid prefix parameter") - - return - } + // Combine prefix and length into CIDR notation + prefix := prefixParam + "/" + lenParam routes, err := s.db.GetPrefixDetailsContext(r.Context(), prefix) if err != nil { @@ -903,21 +899,18 @@ func (s *Server) handleASDetail() http.HandlerFunc { // handlePrefixDetail returns a handler that serves the prefix detail HTML page func (s *Server) handlePrefixDetail() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // Get wildcard parameter (everything after /prefix/) - prefixParam := chi.URLParam(r, "*") - if prefixParam == "" { - http.Error(w, "Prefix parameter is required", http.StatusBadRequest) + // Get prefix and length from URL params + prefixParam := chi.URLParam(r, "prefix") + lenParam := chi.URLParam(r, "len") + + if prefixParam == "" || lenParam == "" { + http.Error(w, "Prefix and length parameters are required", http.StatusBadRequest) return } - // URL decode the prefix parameter - prefix, err := url.QueryUnescape(prefixParam) - if err != nil { - http.Error(w, "Invalid prefix parameter", http.StatusBadRequest) - - return - } + // Combine prefix and length into CIDR notation + prefix := prefixParam + "/" + lenParam routes, err := s.db.GetPrefixDetailsContext(r.Context(), prefix) if err != nil { diff --git a/internal/server/routes.go b/internal/server/routes.go index 4d110f4..a5e37a7 100644 --- a/internal/server/routes.go +++ b/internal/server/routes.go @@ -28,7 +28,7 @@ func (s *Server) setupRoutes() { // AS and prefix detail pages r.Get("/as/{asn}", s.handleASDetail()) - r.Get("/prefix/*", s.handlePrefixDetail()) + r.Get("/prefix/{prefix}/{len}", s.handlePrefixDetail()) r.Get("/prefixlength/{length}", s.handlePrefixLength()) r.Get("/prefixlength6/{length}", s.handlePrefixLength6()) @@ -45,7 +45,7 @@ func (s *Server) setupRoutes() { r.Get("/stats", s.handleStats()) r.Get("/ip/{ip}", s.handleIPLookup()) r.Get("/as/{asn}", s.handleASDetailJSON()) - r.Get("/prefix/*", s.handlePrefixDetailJSON()) + r.Get("/prefix/{prefix}/{len}", s.handlePrefixDetailJSON()) }) s.router = r diff --git a/internal/templates/as_detail.html b/internal/templates/as_detail.html index 6470587..15ade24 100644 --- a/internal/templates/as_detail.html +++ b/internal/templates/as_detail.html @@ -182,7 +182,7 @@ {{range .IPv4Prefixes}} - {{.Prefix}} + {{.Prefix}} /{{.MaskLength}} {{.LastUpdated.Format "2006-01-02 15:04:05"}} {{.LastUpdated | timeSince}} @@ -211,7 +211,7 @@ {{range .IPv6Prefixes}} - {{.Prefix}} + {{.Prefix}} /{{.MaskLength}} {{.LastUpdated.Format "2006-01-02 15:04:05"}} {{.LastUpdated | timeSince}} diff --git a/internal/templates/prefix_length.html b/internal/templates/prefix_length.html index 56555a0..ca5f8f2 100644 --- a/internal/templates/prefix_length.html +++ b/internal/templates/prefix_length.html @@ -93,7 +93,7 @@ {{ range .Prefixes }} - {{ .Prefix }} + {{ .Prefix }} {{ .Age }} diff --git a/internal/templates/templates.go b/internal/templates/templates.go index 0163234..68186ea 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -5,6 +5,7 @@ import ( _ "embed" "html/template" "net/url" + "strings" "sync" "time" @@ -43,8 +44,9 @@ var ( ) const ( - hoursPerDay = 24 - daysPerMonth = 30 + hoursPerDay = 24 + daysPerMonth = 30 + cidrPartCount = 2 // A CIDR has two parts: prefix and length ) // timeSince returns a human-readable duration since the given time @@ -80,6 +82,20 @@ func timeSince(t time.Time) string { return t.Format("2006-01-02") } +// prefixURL generates a URL path for a prefix in CIDR notation. +// Takes a prefix like "192.168.1.0/24" and returns "/prefix/192.168.1.0/24" +// with the prefix part URL-encoded to handle IPv6 colons. +func prefixURL(cidr string) string { + // Split CIDR into prefix and length + parts := strings.SplitN(cidr, "/", cidrPartCount) + if len(parts) != cidrPartCount { + // Fallback if no slash found + return "/prefix/" + url.PathEscape(cidr) + "/0" + } + + return "/prefix/" + url.PathEscape(parts[0]) + "/" + parts[1] +} + // initTemplates parses all embedded templates func initTemplates() { var err error @@ -90,6 +106,7 @@ func initTemplates() { funcs := template.FuncMap{ "timeSince": timeSince, "urlEncode": url.QueryEscape, + "prefixURL": prefixURL, "appName": func() string { return version.Name }, "appAuthor": func() string { return version.Author }, "appAuthorURL": func() string { return version.AuthorURL },