- Change AS path from descriptions to handles (short names) - Make each AS in the path a clickable link to /as/<asn> - Add font-weight to AS links in path for better visibility - Prevent word wrapping on all table columns except AS path - Remove unused maxASDescriptionLength constant
735 lines
23 KiB
Go
735 lines
23 KiB
Go
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"runtime"
|
|
"sort"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
|
"git.eeqj.de/sneak/routewatch/internal/templates"
|
|
"git.eeqj.de/sneak/routewatch/pkg/asinfo"
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// handleRoot returns a handler that redirects to /status
|
|
func (s *Server) handleRoot() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/status", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
// writeJSONError writes a standardized JSON error response
|
|
func writeJSONError(w http.ResponseWriter, statusCode int, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"status": "error",
|
|
"error": map[string]interface{}{
|
|
"msg": message,
|
|
"code": statusCode,
|
|
},
|
|
})
|
|
}
|
|
|
|
// writeJSONSuccess writes a standardized JSON success response
|
|
func writeJSONSuccess(w http.ResponseWriter, data interface{}) error {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
return json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"status": "ok",
|
|
"data": data,
|
|
})
|
|
}
|
|
|
|
// handleStatusJSON returns a handler that serves JSON statistics
|
|
func (s *Server) handleStatusJSON() http.HandlerFunc {
|
|
// Stats represents the statistics response
|
|
type Stats struct {
|
|
Uptime string `json:"uptime"`
|
|
TotalMessages uint64 `json:"total_messages"`
|
|
TotalBytes uint64 `json:"total_bytes"`
|
|
MessagesPerSec float64 `json:"messages_per_sec"`
|
|
MbitsPerSec float64 `json:"mbits_per_sec"`
|
|
Connected bool `json:"connected"`
|
|
GoVersion string `json:"go_version"`
|
|
Goroutines int `json:"goroutines"`
|
|
MemoryUsage string `json:"memory_usage"`
|
|
ASNs int `json:"asns"`
|
|
Prefixes int `json:"prefixes"`
|
|
IPv4Prefixes int `json:"ipv4_prefixes"`
|
|
IPv6Prefixes int `json:"ipv6_prefixes"`
|
|
Peerings int `json:"peerings"`
|
|
Peers int `json:"peers"`
|
|
DatabaseSizeBytes int64 `json:"database_size_bytes"`
|
|
LiveRoutes int `json:"live_routes"`
|
|
IPv4Routes int `json:"ipv4_routes"`
|
|
IPv6Routes int `json:"ipv6_routes"`
|
|
IPv4UpdatesPerSec float64 `json:"ipv4_updates_per_sec"`
|
|
IPv6UpdatesPerSec float64 `json:"ipv6_updates_per_sec"`
|
|
IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
|
|
IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
|
|
}
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Create a 1 second timeout context for this request
|
|
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
|
|
defer cancel()
|
|
|
|
metrics := s.streamer.GetMetrics()
|
|
|
|
// Get database stats with timeout
|
|
statsChan := make(chan database.Stats)
|
|
errChan := make(chan error)
|
|
|
|
go func() {
|
|
dbStats, err := s.db.GetStats()
|
|
if err != nil {
|
|
s.logger.Debug("Database stats query failed", "error", err)
|
|
errChan <- err
|
|
|
|
return
|
|
}
|
|
statsChan <- dbStats
|
|
}()
|
|
|
|
var dbStats database.Stats
|
|
select {
|
|
case <-ctx.Done():
|
|
s.logger.Error("Database stats timeout in status.json")
|
|
writeJSONError(w, http.StatusRequestTimeout, "Database timeout")
|
|
|
|
return
|
|
case err := <-errChan:
|
|
s.logger.Error("Failed to get database stats", "error", err)
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
|
|
return
|
|
case dbStats = <-statsChan:
|
|
// Success
|
|
}
|
|
|
|
uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
|
|
if metrics.ConnectedSince.IsZero() {
|
|
uptime = "0s"
|
|
}
|
|
|
|
const bitsPerMegabit = 1000000.0
|
|
|
|
// Get route counts from database
|
|
ipv4Routes, ipv6Routes, err := s.db.GetLiveRouteCounts()
|
|
if err != nil {
|
|
s.logger.Warn("Failed to get live route counts", "error", err)
|
|
// Continue with zero counts
|
|
}
|
|
|
|
// Get route update metrics
|
|
routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()
|
|
|
|
// Get memory stats
|
|
var memStats runtime.MemStats
|
|
runtime.ReadMemStats(&memStats)
|
|
|
|
stats := Stats{
|
|
Uptime: uptime,
|
|
TotalMessages: metrics.TotalMessages,
|
|
TotalBytes: metrics.TotalBytes,
|
|
MessagesPerSec: metrics.MessagesPerSec,
|
|
MbitsPerSec: metrics.BitsPerSec / bitsPerMegabit,
|
|
Connected: metrics.Connected,
|
|
GoVersion: runtime.Version(),
|
|
Goroutines: runtime.NumGoroutine(),
|
|
MemoryUsage: humanize.Bytes(memStats.Alloc),
|
|
ASNs: dbStats.ASNs,
|
|
Prefixes: dbStats.Prefixes,
|
|
IPv4Prefixes: dbStats.IPv4Prefixes,
|
|
IPv6Prefixes: dbStats.IPv6Prefixes,
|
|
Peerings: dbStats.Peerings,
|
|
Peers: dbStats.Peers,
|
|
DatabaseSizeBytes: dbStats.FileSizeBytes,
|
|
LiveRoutes: dbStats.LiveRoutes,
|
|
IPv4Routes: ipv4Routes,
|
|
IPv6Routes: ipv6Routes,
|
|
IPv4UpdatesPerSec: routeMetrics.IPv4UpdatesPerSec,
|
|
IPv6UpdatesPerSec: routeMetrics.IPv6UpdatesPerSec,
|
|
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
|
|
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
|
|
}
|
|
|
|
if err := writeJSONSuccess(w, stats); err != nil {
|
|
s.logger.Error("Failed to encode stats", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleStats returns a handler that serves API v1 statistics
|
|
func (s *Server) handleStats() http.HandlerFunc {
|
|
// HandlerStatsInfo represents handler statistics in the API response
|
|
type HandlerStatsInfo struct {
|
|
Name string `json:"name"`
|
|
QueueLength int `json:"queue_length"`
|
|
QueueCapacity int `json:"queue_capacity"`
|
|
ProcessedCount uint64 `json:"processed_count"`
|
|
DroppedCount uint64 `json:"dropped_count"`
|
|
AvgProcessTimeMs float64 `json:"avg_process_time_ms"`
|
|
MinProcessTimeMs float64 `json:"min_process_time_ms"`
|
|
MaxProcessTimeMs float64 `json:"max_process_time_ms"`
|
|
}
|
|
|
|
// StatsResponse represents the API statistics response
|
|
type StatsResponse struct {
|
|
Uptime string `json:"uptime"`
|
|
TotalMessages uint64 `json:"total_messages"`
|
|
TotalBytes uint64 `json:"total_bytes"`
|
|
MessagesPerSec float64 `json:"messages_per_sec"`
|
|
MbitsPerSec float64 `json:"mbits_per_sec"`
|
|
Connected bool `json:"connected"`
|
|
GoVersion string `json:"go_version"`
|
|
Goroutines int `json:"goroutines"`
|
|
MemoryUsage string `json:"memory_usage"`
|
|
ASNs int `json:"asns"`
|
|
Prefixes int `json:"prefixes"`
|
|
IPv4Prefixes int `json:"ipv4_prefixes"`
|
|
IPv6Prefixes int `json:"ipv6_prefixes"`
|
|
Peerings int `json:"peerings"`
|
|
Peers int `json:"peers"`
|
|
DatabaseSizeBytes int64 `json:"database_size_bytes"`
|
|
LiveRoutes int `json:"live_routes"`
|
|
IPv4Routes int `json:"ipv4_routes"`
|
|
IPv6Routes int `json:"ipv6_routes"`
|
|
IPv4UpdatesPerSec float64 `json:"ipv4_updates_per_sec"`
|
|
IPv6UpdatesPerSec float64 `json:"ipv6_updates_per_sec"`
|
|
HandlerStats []HandlerStatsInfo `json:"handler_stats"`
|
|
IPv4PrefixDistribution []database.PrefixDistribution `json:"ipv4_prefix_distribution"`
|
|
IPv6PrefixDistribution []database.PrefixDistribution `json:"ipv6_prefix_distribution"`
|
|
}
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Create a 1 second timeout context for this request
|
|
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
|
|
defer cancel()
|
|
|
|
// Check if context is already cancelled
|
|
select {
|
|
case <-ctx.Done():
|
|
http.Error(w, "Request timeout", http.StatusRequestTimeout)
|
|
|
|
return
|
|
default:
|
|
}
|
|
|
|
metrics := s.streamer.GetMetrics()
|
|
|
|
// Get database stats with timeout
|
|
statsChan := make(chan database.Stats)
|
|
errChan := make(chan error)
|
|
|
|
go func() {
|
|
dbStats, err := s.db.GetStats()
|
|
if err != nil {
|
|
s.logger.Debug("Database stats query failed", "error", err)
|
|
errChan <- err
|
|
|
|
return
|
|
}
|
|
statsChan <- dbStats
|
|
}()
|
|
|
|
var dbStats database.Stats
|
|
select {
|
|
case <-ctx.Done():
|
|
s.logger.Error("Database stats timeout")
|
|
http.Error(w, "Database timeout", http.StatusRequestTimeout)
|
|
|
|
return
|
|
case err := <-errChan:
|
|
s.logger.Error("Failed to get database stats", "error", err)
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
|
|
return
|
|
case dbStats = <-statsChan:
|
|
// Success
|
|
}
|
|
|
|
uptime := time.Since(metrics.ConnectedSince).Truncate(time.Second).String()
|
|
if metrics.ConnectedSince.IsZero() {
|
|
uptime = "0s"
|
|
}
|
|
|
|
const bitsPerMegabit = 1000000.0
|
|
|
|
// Get route counts from database
|
|
ipv4Routes, ipv6Routes, err := s.db.GetLiveRouteCounts()
|
|
if err != nil {
|
|
s.logger.Warn("Failed to get live route counts", "error", err)
|
|
// Continue with zero counts
|
|
}
|
|
|
|
// Get route update metrics
|
|
routeMetrics := s.streamer.GetMetricsTracker().GetRouteMetrics()
|
|
|
|
// Get handler stats
|
|
handlerStats := s.streamer.GetHandlerStats()
|
|
handlerStatsInfo := make([]HandlerStatsInfo, 0, len(handlerStats))
|
|
const microsecondsPerMillisecond = 1000.0
|
|
for _, hs := range handlerStats {
|
|
handlerStatsInfo = append(handlerStatsInfo, HandlerStatsInfo{
|
|
Name: hs.Name,
|
|
QueueLength: hs.QueueLength,
|
|
QueueCapacity: hs.QueueCapacity,
|
|
ProcessedCount: hs.ProcessedCount,
|
|
DroppedCount: hs.DroppedCount,
|
|
AvgProcessTimeMs: float64(hs.AvgProcessTime.Microseconds()) / microsecondsPerMillisecond,
|
|
MinProcessTimeMs: float64(hs.MinProcessTime.Microseconds()) / microsecondsPerMillisecond,
|
|
MaxProcessTimeMs: float64(hs.MaxProcessTime.Microseconds()) / microsecondsPerMillisecond,
|
|
})
|
|
}
|
|
|
|
// Get memory stats
|
|
var memStats runtime.MemStats
|
|
runtime.ReadMemStats(&memStats)
|
|
|
|
stats := StatsResponse{
|
|
Uptime: uptime,
|
|
TotalMessages: metrics.TotalMessages,
|
|
TotalBytes: metrics.TotalBytes,
|
|
MessagesPerSec: metrics.MessagesPerSec,
|
|
MbitsPerSec: metrics.BitsPerSec / bitsPerMegabit,
|
|
Connected: metrics.Connected,
|
|
GoVersion: runtime.Version(),
|
|
Goroutines: runtime.NumGoroutine(),
|
|
MemoryUsage: humanize.Bytes(memStats.Alloc),
|
|
ASNs: dbStats.ASNs,
|
|
Prefixes: dbStats.Prefixes,
|
|
IPv4Prefixes: dbStats.IPv4Prefixes,
|
|
IPv6Prefixes: dbStats.IPv6Prefixes,
|
|
Peerings: dbStats.Peerings,
|
|
Peers: dbStats.Peers,
|
|
DatabaseSizeBytes: dbStats.FileSizeBytes,
|
|
LiveRoutes: dbStats.LiveRoutes,
|
|
IPv4Routes: ipv4Routes,
|
|
IPv6Routes: ipv6Routes,
|
|
IPv4UpdatesPerSec: routeMetrics.IPv4UpdatesPerSec,
|
|
IPv6UpdatesPerSec: routeMetrics.IPv6UpdatesPerSec,
|
|
HandlerStats: handlerStatsInfo,
|
|
IPv4PrefixDistribution: dbStats.IPv4PrefixDistribution,
|
|
IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution,
|
|
}
|
|
|
|
if err := writeJSONSuccess(w, stats); err != nil {
|
|
s.logger.Error("Failed to encode stats", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleStatusHTML returns a handler that serves the HTML status page
|
|
func (s *Server) handleStatusHTML() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
|
|
tmpl := templates.StatusTemplate()
|
|
if err := tmpl.Execute(w, nil); err != nil {
|
|
s.logger.Error("Failed to render template", "error", err)
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleIPLookup returns a handler that looks up AS information for an IP address
|
|
func (s *Server) handleIPLookup() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ip := chi.URLParam(r, "ip")
|
|
if ip == "" {
|
|
writeJSONError(w, http.StatusBadRequest, "IP parameter is required")
|
|
|
|
return
|
|
}
|
|
|
|
// Look up AS information for the IP
|
|
asInfo, err := s.db.GetASInfoForIP(ip)
|
|
if err != nil {
|
|
// Check if it's an invalid IP error
|
|
if errors.Is(err, database.ErrInvalidIP) {
|
|
writeJSONError(w, http.StatusBadRequest, err.Error())
|
|
} else {
|
|
// All other errors (including ErrNoRoute) are 404
|
|
writeJSONError(w, http.StatusNotFound, err.Error())
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Return successful response
|
|
if err := writeJSONSuccess(w, asInfo); err != nil {
|
|
s.logger.Error("Failed to encode AS info", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleASDetailJSON returns AS details as JSON
|
|
func (s *Server) handleASDetailJSON() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
asnStr := chi.URLParam(r, "asn")
|
|
asn, err := strconv.Atoi(asnStr)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "Invalid ASN")
|
|
|
|
return
|
|
}
|
|
|
|
asInfo, prefixes, err := s.db.GetASDetails(asn)
|
|
if err != nil {
|
|
if errors.Is(err, database.ErrNoRoute) {
|
|
writeJSONError(w, http.StatusNotFound, err.Error())
|
|
} else {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"asn": asInfo,
|
|
"ipv4_prefixes": ipv4Prefixes,
|
|
"ipv6_prefixes": ipv6Prefixes,
|
|
"total_count": len(prefixes),
|
|
}
|
|
|
|
if err := writeJSONSuccess(w, response); err != nil {
|
|
s.logger.Error("Failed to encode AS details", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handlePrefixDetailJSON returns prefix details as JSON
|
|
func (s *Server) handlePrefixDetailJSON() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
prefixParam := chi.URLParam(r, "prefix")
|
|
if prefixParam == "" {
|
|
writeJSONError(w, http.StatusBadRequest, "Prefix parameter is required")
|
|
|
|
return
|
|
}
|
|
|
|
// URL decode the prefix parameter
|
|
prefix, err := url.QueryUnescape(prefixParam)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "Invalid prefix parameter")
|
|
|
|
return
|
|
}
|
|
|
|
routes, err := s.db.GetPrefixDetails(prefix)
|
|
if err != nil {
|
|
if errors.Is(err, database.ErrNoRoute) {
|
|
writeJSONError(w, http.StatusNotFound, err.Error())
|
|
} else {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Group by origin AS
|
|
originMap := make(map[int][]database.LiveRoute)
|
|
for _, route := range routes {
|
|
originMap[route.OriginASN] = append(originMap[route.OriginASN], route)
|
|
}
|
|
|
|
response := map[string]interface{}{
|
|
"prefix": prefix,
|
|
"routes": routes,
|
|
"origins": originMap,
|
|
"peer_count": len(routes),
|
|
"origin_count": len(originMap),
|
|
}
|
|
|
|
if err := writeJSONSuccess(w, response); err != nil {
|
|
s.logger.Error("Failed to encode prefix details", "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleASDetail returns a handler that serves the AS detail HTML page
|
|
func (s *Server) handleASDetail() http.HandlerFunc {
|
|
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)
|
|
}
|
|
}
|
|
|
|
// Sort prefixes by network address
|
|
sort.Slice(ipv4Prefixes, func(i, j int) bool {
|
|
// Parse the prefixes to compare network addresses
|
|
ipI, netI, _ := net.ParseCIDR(ipv4Prefixes[i].Prefix)
|
|
ipJ, netJ, _ := net.ParseCIDR(ipv4Prefixes[j].Prefix)
|
|
|
|
// Compare by network address first
|
|
cmp := bytes.Compare(ipI.To4(), ipJ.To4())
|
|
if cmp != 0 {
|
|
return cmp < 0
|
|
}
|
|
|
|
// If network addresses are equal, compare by mask length
|
|
onesI, _ := netI.Mask.Size()
|
|
onesJ, _ := netJ.Mask.Size()
|
|
|
|
return onesI < onesJ
|
|
})
|
|
|
|
sort.Slice(ipv6Prefixes, func(i, j int) bool {
|
|
// Parse the prefixes to compare network addresses
|
|
ipI, netI, _ := net.ParseCIDR(ipv6Prefixes[i].Prefix)
|
|
ipJ, netJ, _ := net.ParseCIDR(ipv6Prefixes[j].Prefix)
|
|
|
|
// Compare by network address first
|
|
cmp := bytes.Compare(ipI.To16(), ipJ.To16())
|
|
if cmp != 0 {
|
|
return cmp < 0
|
|
}
|
|
|
|
// If network addresses are equal, compare by mask length
|
|
onesI, _ := netI.Mask.Size()
|
|
onesJ, _ := netJ.Mask.Size()
|
|
|
|
return onesI < onesJ
|
|
})
|
|
|
|
// 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, r *http.Request) {
|
|
prefixParam := chi.URLParam(r, "prefix")
|
|
if prefixParam == "" {
|
|
http.Error(w, "Prefix parameter is 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
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// Create enhanced routes with AS path handles
|
|
type ASPathEntry struct {
|
|
Number int
|
|
Handle string
|
|
}
|
|
type EnhancedRoute struct {
|
|
database.LiveRoute
|
|
ASPathWithHandle []ASPathEntry
|
|
}
|
|
|
|
enhancedRoutes := make([]EnhancedRoute, len(routes))
|
|
for i, route := range routes {
|
|
enhancedRoute := EnhancedRoute{
|
|
LiveRoute: route,
|
|
ASPathWithHandle: make([]ASPathEntry, len(route.ASPath)),
|
|
}
|
|
|
|
// Look up handle for each AS in the path
|
|
for j, asn := range route.ASPath {
|
|
handle := asinfo.GetHandle(asn)
|
|
enhancedRoute.ASPathWithHandle[j] = ASPathEntry{
|
|
Number: asn,
|
|
Handle: handle,
|
|
}
|
|
}
|
|
|
|
enhancedRoutes[i] = enhancedRoute
|
|
}
|
|
|
|
// Prepare template data
|
|
data := struct {
|
|
Prefix string
|
|
MaskLength int
|
|
IPVersion int
|
|
Routes []EnhancedRoute
|
|
Origins []*ASNInfo
|
|
PeerCount int
|
|
OriginCount int
|
|
}{
|
|
Prefix: prefix,
|
|
MaskLength: maskLength,
|
|
IPVersion: ipVersion,
|
|
Routes: enhancedRoutes,
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleIPRedirect looks up the prefix containing the IP and redirects to its detail page
|
|
func (s *Server) handleIPRedirect() http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ip := chi.URLParam(r, "ip")
|
|
if ip == "" {
|
|
http.Error(w, "IP parameter is required", http.StatusBadRequest)
|
|
|
|
return
|
|
}
|
|
|
|
// Look up AS information for the IP (which includes the prefix)
|
|
asInfo, err := s.db.GetASInfoForIP(ip)
|
|
if err != nil {
|
|
if errors.Is(err, database.ErrInvalidIP) {
|
|
http.Error(w, "Invalid IP address", http.StatusBadRequest)
|
|
} else if errors.Is(err, database.ErrNoRoute) {
|
|
http.Error(w, "No route found for this IP", http.StatusNotFound)
|
|
} else {
|
|
s.logger.Error("Failed to look up IP", "error", err)
|
|
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Redirect to the prefix detail page (URL encode the prefix)
|
|
http.Redirect(w, r, "/prefix/"+url.QueryEscape(asInfo.Prefix), http.StatusSeeOther)
|
|
}
|
|
}
|