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) {
|
||||
|
||||
Reference in New Issue
Block a user