diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 36c5fc4..e26f955 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -367,47 +367,129 @@ func (s *Server) handleIPLookup() http.HandlerFunc { return s.handleIPInfo() } +// HostLookupResponse is returned when looking up a hostname. +type HostLookupResponse struct { + Hostname string `json:"hostname,omitempty"` + Results []*database.IPInfo `json:"results"` + Errors []string `json:"errors,omitempty"` +} + // handleIPInfo returns a handler that provides comprehensive IP information. // Used for /ip, /ip/{addr}, and /api/v1/ip/{ip} endpoints. +// Accepts both IP addresses and hostnames. For hostnames, resolves A and AAAA records. func (s *Server) handleIPInfo() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - // Get IP from URL param, falling back to client IP - ip := chi.URLParam(r, "ip") - if ip == "" { - ip = chi.URLParam(r, "addr") + // Get IP/hostname from URL param, falling back to client IP + target := chi.URLParam(r, "ip") + if target == "" { + target = chi.URLParam(r, "addr") } - if ip == "" { + if target == "" { // Use client IP (RealIP middleware has already processed this) - ip = extractClientIP(r) + target = extractClientIP(r) } - if ip == "" { + if target == "" { writeJSONError(w, http.StatusBadRequest, "Could not determine IP address") return } - // Look up comprehensive IP information - ipInfo, err := s.db.GetIPInfoContext(r.Context(), ip) - if err != nil { - if errors.Is(err, database.ErrInvalidIP) { - writeJSONError(w, http.StatusBadRequest, err.Error()) - } else { - writeJSONError(w, http.StatusNotFound, err.Error()) - } + // Check if target is already an IP address + if parsedIP := net.ParseIP(target); parsedIP != nil { + // Direct IP lookup + s.lookupSingleIP(w, r, target) return } - // Queue WHOIS refresh if data is stale (non-blocking) - if ipInfo.NeedsWHOISRefresh && s.asnFetcher != nil { - s.asnFetcher.QueueImmediate(ipInfo.ASN) + // Target is a hostname - resolve and look up each IP + s.lookupHostname(w, r, target) + } +} + +// lookupSingleIP handles lookup for a single IP address. +func (s *Server) lookupSingleIP(w http.ResponseWriter, r *http.Request, ip string) { + ipInfo, err := s.db.GetIPInfoContext(r.Context(), ip) + if err != nil { + if errors.Is(err, database.ErrInvalidIP) { + writeJSONError(w, http.StatusBadRequest, err.Error()) + } else { + writeJSONError(w, http.StatusNotFound, err.Error()) } - // Return successful response - if err := writeJSONSuccess(w, ipInfo); err != nil { - s.logger.Error("Failed to encode IP info", "error", err) + return + } + + // Queue WHOIS refresh if data is stale (non-blocking) + if ipInfo.NeedsWHOISRefresh && s.asnFetcher != nil { + s.asnFetcher.QueueImmediate(ipInfo.ASN) + } + + // Return successful response + if err := writeJSONSuccess(w, ipInfo); err != nil { + s.logger.Error("Failed to encode IP info", "error", err) + } +} + +// lookupHostname resolves a hostname and looks up info for each IP. +func (s *Server) lookupHostname(w http.ResponseWriter, r *http.Request, hostname string) { + ctx := r.Context() + + // Resolve hostname to IP addresses + ips, err := net.DefaultResolver.LookupHost(ctx, hostname) + if err != nil { + writeJSONError(w, http.StatusBadRequest, "Failed to resolve hostname: "+err.Error()) + + return + } + + if len(ips) == 0 { + writeJSONError(w, http.StatusNotFound, "No IP addresses found for hostname") + + return + } + + response := HostLookupResponse{ + Hostname: hostname, + Results: make([]*database.IPInfo, 0, len(ips)), + } + + // Track ASNs that need WHOIS refresh + refreshASNs := make(map[int]bool) + + // Look up each resolved IP + for _, ip := range ips { + ipInfo, err := s.db.GetIPInfoContext(ctx, ip) + if err != nil { + response.Errors = append(response.Errors, ip+": "+err.Error()) + + continue } + + response.Results = append(response.Results, ipInfo) + + if ipInfo.NeedsWHOISRefresh { + refreshASNs[ipInfo.ASN] = true + } + } + + // Queue WHOIS refresh for stale ASNs (non-blocking) + if s.asnFetcher != nil { + for asn := range refreshASNs { + s.asnFetcher.QueueImmediate(asn) + } + } + + // Return response + if len(response.Results) == 0 { + writeJSONError(w, http.StatusNotFound, "No routes found for any resolved IP") + + return + } + + if err := writeJSONSuccess(w, response); err != nil { + s.logger.Error("Failed to encode hostname lookup response", "error", err) } }