Add IP information API with background WHOIS fetcher
- Add /ip and /ip/{addr} JSON endpoints returning comprehensive IP info
- Include ASN, netblock, country code, org name, abuse contact, RIR data
- Extend ASN schema with WHOIS fields (country, org, abuse contact, etc)
- Create background WHOIS fetcher for rate-limited ASN info updates
- Store raw WHOIS responses for debugging and data preservation
- Queue on-demand WHOIS lookups when stale data is requested
- Refactor handleIPInfo to serve all IP endpoints consistently
This commit is contained in:
@@ -364,35 +364,66 @@ 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 s.handleIPInfo()
|
||||
}
|
||||
|
||||
// handleIPInfo returns a handler that provides comprehensive IP information.
|
||||
// Used for /ip, /ip/{addr}, and /api/v1/ip/{ip} endpoints.
|
||||
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 == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "IP parameter is required")
|
||||
ip = chi.URLParam(r, "addr")
|
||||
}
|
||||
if ip == "" {
|
||||
// Use client IP (RealIP middleware has already processed this)
|
||||
ip = extractClientIP(r)
|
||||
}
|
||||
|
||||
if ip == "" {
|
||||
writeJSONError(w, http.StatusBadRequest, "Could not determine IP address")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Look up AS information for the IP
|
||||
asInfo, err := s.db.GetASInfoForIPContext(r.Context(), ip)
|
||||
// Look up comprehensive IP information
|
||||
ipInfo, err := s.db.GetIPInfoContext(r.Context(), 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
|
||||
}
|
||||
|
||||
// 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, asInfo); err != nil {
|
||||
s.logger.Error("Failed to encode AS info", "error", err)
|
||||
if err := writeJSONSuccess(w, ipInfo); err != nil {
|
||||
s.logger.Error("Failed to encode IP info", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extractClientIP extracts the client IP from the request.
|
||||
// Works with chi's RealIP middleware which sets RemoteAddr.
|
||||
func extractClientIP(r *http.Request) string {
|
||||
// RemoteAddr is in the form "IP:port" or just "IP" for unix sockets
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
// Might be just an IP without port
|
||||
return r.RemoteAddr
|
||||
}
|
||||
|
||||
return host
|
||||
}
|
||||
|
||||
// handleASDetailJSON returns AS details as JSON
|
||||
func (s *Server) handleASDetailJSON() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -750,36 +781,6 @@ func (s *Server) handlePrefixDetail() http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePrefixLength shows a random sample of IPv4 prefixes with the specified mask length
|
||||
func (s *Server) handlePrefixLength() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -30,7 +30,13 @@ func (s *Server) setupRoutes() {
|
||||
r.Get("/prefix/{prefix}", s.handlePrefixDetail())
|
||||
r.Get("/prefixlength/{length}", s.handlePrefixLength())
|
||||
r.Get("/prefixlength6/{length}", s.handlePrefixLength6())
|
||||
r.Get("/ip/{ip}", s.handleIPRedirect())
|
||||
|
||||
// IP info JSON endpoints (replaces old /ip redirect)
|
||||
r.Route("/ip", func(r chi.Router) {
|
||||
r.Use(JSONValidationMiddleware)
|
||||
r.Get("/", s.handleIPInfo()) // Client IP
|
||||
r.Get("/{addr}", s.handleIPInfo()) // Specified IP
|
||||
})
|
||||
|
||||
// API routes
|
||||
r.Route("/api/v1", func(r chi.Router) {
|
||||
|
||||
@@ -13,13 +13,19 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// ASNFetcher is an interface for queuing ASN WHOIS lookups.
|
||||
type ASNFetcher interface {
|
||||
QueueImmediate(asn int)
|
||||
}
|
||||
|
||||
// Server provides HTTP endpoints for status monitoring
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
db database.Store
|
||||
streamer *streamer.Streamer
|
||||
logger *logger.Logger
|
||||
srv *http.Server
|
||||
router *chi.Mux
|
||||
db database.Store
|
||||
streamer *streamer.Streamer
|
||||
logger *logger.Logger
|
||||
srv *http.Server
|
||||
asnFetcher ASNFetcher
|
||||
}
|
||||
|
||||
// New creates a new HTTP server
|
||||
@@ -70,3 +76,8 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
|
||||
return s.srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// SetASNFetcher sets the ASN WHOIS fetcher for on-demand lookups.
|
||||
func (s *Server) SetASNFetcher(fetcher ASNFetcher) {
|
||||
s.asnFetcher = fetcher
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user