diff --git a/internal/server/handlers.go b/internal/server/handlers.go index d34131d..ab5da75 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "net" "net/http" "runtime" "strconv" @@ -458,16 +459,160 @@ func (s *Server) handlePrefixDetailJSON() http.HandlerFunc { // handleASDetail returns a handler that serves the AS detail HTML page func (s *Server) handleASDetail() http.HandlerFunc { - return func(w http.ResponseWriter, _ *http.Request) { - // TODO: Implement AS detail HTML page - http.Error(w, "Not implemented", http.StatusNotImplemented) + return func(w http.ResponseWriter, r *http.Request) { + asnStr := chi.URLParam(r, "asn") + asn, err := strconv.Atoi(asnStr) + if err != nil { + http.Error(w, "Invalid ASN", http.StatusBadRequest) + + return + } + + asInfo, prefixes, err := s.db.GetASDetails(asn) + if err != nil { + if errors.Is(err, database.ErrNoRoute) { + http.Error(w, "AS not found", http.StatusNotFound) + } else { + s.logger.Error("Failed to get AS details", "error", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + + return + } + + // Group prefixes by IP version + const ipVersionV4 = 4 + var ipv4Prefixes, ipv6Prefixes []database.LiveRoute + for _, p := range prefixes { + if p.IPVersion == ipVersionV4 { + ipv4Prefixes = append(ipv4Prefixes, p) + } else { + ipv6Prefixes = append(ipv6Prefixes, p) + } + } + + // Prepare template data + data := struct { + ASN *database.ASN + IPv4Prefixes []database.LiveRoute + IPv6Prefixes []database.LiveRoute + TotalCount int + IPv4Count int + IPv6Count int + }{ + ASN: asInfo, + IPv4Prefixes: ipv4Prefixes, + IPv6Prefixes: ipv6Prefixes, + TotalCount: len(prefixes), + IPv4Count: len(ipv4Prefixes), + IPv6Count: len(ipv6Prefixes), + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + tmpl := templates.ASDetailTemplate() + if err := tmpl.Execute(w, data); err != nil { + s.logger.Error("Failed to render AS detail template", "error", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } } } // handlePrefixDetail returns a handler that serves the prefix detail HTML page func (s *Server) handlePrefixDetail() http.HandlerFunc { - return func(w http.ResponseWriter, _ *http.Request) { - // TODO: Implement prefix detail HTML page - http.Error(w, "Not implemented", http.StatusNotImplemented) + return func(w http.ResponseWriter, r *http.Request) { + prefix := chi.URLParam(r, "prefix") + if prefix == "" { + http.Error(w, "Prefix parameter is required", http.StatusBadRequest) + + return + } + + routes, err := s.db.GetPrefixDetails(prefix) + if err != nil { + if errors.Is(err, database.ErrNoRoute) { + http.Error(w, "Prefix not found", http.StatusNotFound) + } else { + s.logger.Error("Failed to get prefix details", "error", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + + return + } + + // Group by origin AS and collect unique AS info + type ASNInfo struct { + Number int + Handle string + Description string + PeerCount int + } + originMap := make(map[int]*ASNInfo) + for _, route := range routes { + if _, exists := originMap[route.OriginASN]; !exists { + // Get AS info from database + asInfo, _, _ := s.db.GetASDetails(route.OriginASN) + handle := "" + description := "" + if asInfo != nil { + handle = asInfo.Handle + description = asInfo.Description + } + originMap[route.OriginASN] = &ASNInfo{ + Number: route.OriginASN, + Handle: handle, + Description: description, + PeerCount: 0, + } + } + originMap[route.OriginASN].PeerCount++ + } + + // Get the first route to extract some common info + var maskLength, ipVersion int + if len(routes) > 0 { + // Parse CIDR to get mask length and IP version + _, ipNet, err := net.ParseCIDR(prefix) + if err == nil { + ones, _ := ipNet.Mask.Size() + maskLength = ones + if ipNet.IP.To4() != nil { + ipVersion = 4 + } else { + ipVersion = 6 + } + } + } + + // Convert origin map to sorted slice + var origins []*ASNInfo + for _, origin := range originMap { + origins = append(origins, origin) + } + + // Prepare template data + data := struct { + Prefix string + MaskLength int + IPVersion int + Routes []database.LiveRoute + Origins []*ASNInfo + PeerCount int + OriginCount int + }{ + Prefix: prefix, + MaskLength: maskLength, + IPVersion: ipVersion, + Routes: routes, + Origins: origins, + PeerCount: len(routes), + OriginCount: len(originMap), + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + tmpl := templates.PrefixDetailTemplate() + if err := tmpl.Execute(w, data); err != nil { + s.logger.Error("Failed to render prefix detail template", "error", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } } } diff --git a/internal/templates/as_detail.html b/internal/templates/as_detail.html new file mode 100644 index 0000000..60ce777 --- /dev/null +++ b/internal/templates/as_detail.html @@ -0,0 +1,228 @@ + + +
+ + +{{.ASN.Description}}
+ {{end}} + +Prefix | +Mask Length | +Last Updated | +Age | +
---|---|---|---|
{{.Prefix}} | +/{{.MaskLength}} | +{{.LastUpdated.Format "2006-01-02 15:04:05"}} | +{{.LastUpdated | timeSince}} | +
Prefix | +Mask Length | +Last Updated | +Age | +
---|---|---|---|
{{.Prefix}} | +/{{.MaskLength}} | +{{.LastUpdated.Format "2006-01-02 15:04:05"}} | +{{.LastUpdated | timeSince}} | +
No prefixes announced by this AS
+IPv{{.IPVersion}} Prefix{{if .MaskLength}} • /{{.MaskLength}}{{end}}
+ +Origin AS | +Peer IP | +AS Path | +Next Hop | +Last Updated | +Age | +
---|---|---|---|---|---|
+ AS{{.OriginASN}} + | +{{.PeerIP}} | +{{range $i, $as := .ASPath}}{{if $i}} → {{end}}{{$as}}{{end}} | +{{.NextHop}} | +{{.LastUpdated.Format "2006-01-02 15:04:05"}} | +{{.LastUpdated | timeSince}} | +
No routes found for this prefix
+