From 691710bc7cef87d1907c0f25c4d2ed04c96ce298 Mon Sep 17 00:00:00 2001 From: sneak Date: Mon, 28 Jul 2025 03:31:53 +0200 Subject: [PATCH] Add /api/v1/ip/ endpoint for IP to AS lookups - Add handleIPLookup handler that uses GetASInfoForIP - Create writeJSONError and writeJSONSuccess helper functions - Refactor all JSON error responses to use the helpers - Add GetASInfoForIP to Store interface - Add mock implementation for tests - Fix all linter warnings --- internal/database/interface.go | 3 + internal/routewatch/app_integration_test.go | 11 +++ internal/server/server.go | 83 +++++++++++++-------- 3 files changed, 67 insertions(+), 30 deletions(-) diff --git a/internal/database/interface.go b/internal/database/interface.go index 42bf5d7..98e1d2d 100644 --- a/internal/database/interface.go +++ b/internal/database/interface.go @@ -44,6 +44,9 @@ type Store interface { GetPrefixDistribution() (ipv4 []PrefixDistribution, ipv6 []PrefixDistribution, err error) GetLiveRouteCounts() (ipv4Count, ipv6Count int, err error) + // IP lookup operations + GetASInfoForIP(ip string) (*ASInfo, error) + // Lifecycle Close() error } diff --git a/internal/routewatch/app_integration_test.go b/internal/routewatch/app_integration_test.go index 69c6893..dd79558 100644 --- a/internal/routewatch/app_integration_test.go +++ b/internal/routewatch/app_integration_test.go @@ -187,6 +187,17 @@ func (m *mockStore) GetLiveRouteCounts() (ipv4Count, ipv6Count int, err error) { return m.RouteCount / 2, m.RouteCount / 2, nil } +// GetASInfoForIP mock implementation +func (m *mockStore) GetASInfoForIP(ip string) (*database.ASInfo, error) { + // Simple mock - return a test AS + return &database.ASInfo{ + ASN: 15169, + Handle: "GOOGLE", + Description: "Google LLC", + Prefix: "8.8.8.0/24", + }, nil +} + func TestRouteWatchLiveFeed(t *testing.T) { // Create mock database diff --git a/internal/server/server.go b/internal/server/server.go index e82c037..7641935 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -61,6 +61,7 @@ func (s *Server) setupRoutes() { // API routes r.Route("/api/v1", func(r chi.Router) { r.Get("/stats", s.handleStats()) + r.Get("/ip/{ip}", s.handleIPLookup()) }) s.router = r @@ -109,6 +110,29 @@ func (s *Server) handleRoot() http.HandlerFunc { } } +// 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 @@ -164,28 +188,12 @@ func (s *Server) handleStatusJSON() http.HandlerFunc { select { case <-ctx.Done(): s.logger.Error("Database stats timeout in status.json") - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusRequestTimeout) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "msg": "Database timeout", - "code": http.StatusRequestTimeout, - }, - }) + writeJSONError(w, http.StatusRequestTimeout, "Database timeout") return case err := <-errChan: s.logger.Error("Failed to get database stats", "error", err) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(map[string]interface{}{ - "status": "error", - "error": map[string]interface{}{ - "msg": err.Error(), - "code": http.StatusInternalServerError, - }, - }) + writeJSONError(w, http.StatusInternalServerError, err.Error()) return case dbStats = <-statsChan: @@ -239,12 +247,7 @@ func (s *Server) handleStatusJSON() http.HandlerFunc { IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution, } - w.Header().Set("Content-Type", "application/json") - response := map[string]interface{}{ - "status": "ok", - "data": stats, - } - if err := json.NewEncoder(w).Encode(response); err != nil { + if err := writeJSONSuccess(w, stats); err != nil { s.logger.Error("Failed to encode stats", "error", err) } } @@ -404,12 +407,7 @@ func (s *Server) handleStats() http.HandlerFunc { IPv6PrefixDistribution: dbStats.IPv6PrefixDistribution, } - w.Header().Set("Content-Type", "application/json") - response := map[string]interface{}{ - "status": "ok", - "data": stats, - } - if err := json.NewEncoder(w).Encode(response); err != nil { + if err := writeJSONSuccess(w, stats); err != nil { s.logger.Error("Failed to encode stats", "error", err) } } @@ -427,3 +425,28 @@ func (s *Server) handleStatusHTML() http.HandlerFunc { } } } + +// 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 { + 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) + } + } +}