Add hostname resolution support to IP lookup endpoint
- Accept hostnames in addition to IP addresses for /ip endpoints - Resolve A and AAAA records for hostnames - Return list of results with info for each resolved IP - Include hostname in response when resolving hostnames - Report per-IP errors while still returning successful lookups
This commit is contained in:
parent
8eaf4e5f4b
commit
cb75409647
@ -367,27 +367,49 @@ func (s *Server) handleIPLookup() http.HandlerFunc {
|
|||||||
return s.handleIPInfo()
|
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.
|
// handleIPInfo returns a handler that provides comprehensive IP information.
|
||||||
// Used for /ip, /ip/{addr}, and /api/v1/ip/{ip} endpoints.
|
// 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 {
|
func (s *Server) handleIPInfo() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// Get IP from URL param, falling back to client IP
|
// Get IP/hostname from URL param, falling back to client IP
|
||||||
ip := chi.URLParam(r, "ip")
|
target := chi.URLParam(r, "ip")
|
||||||
if ip == "" {
|
if target == "" {
|
||||||
ip = chi.URLParam(r, "addr")
|
target = chi.URLParam(r, "addr")
|
||||||
}
|
}
|
||||||
if ip == "" {
|
if target == "" {
|
||||||
// Use client IP (RealIP middleware has already processed this)
|
// 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")
|
writeJSONError(w, http.StatusBadRequest, "Could not determine IP address")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up comprehensive IP information
|
// Check if target is already an IP address
|
||||||
|
if parsedIP := net.ParseIP(target); parsedIP != nil {
|
||||||
|
// Direct IP lookup
|
||||||
|
s.lookupSingleIP(w, r, target)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
ipInfo, err := s.db.GetIPInfoContext(r.Context(), ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, database.ErrInvalidIP) {
|
if errors.Is(err, database.ErrInvalidIP) {
|
||||||
@ -408,6 +430,66 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
|||||||
if err := writeJSONSuccess(w, ipInfo); err != nil {
|
if err := writeJSONSuccess(w, ipInfo); err != nil {
|
||||||
s.logger.Error("Failed to encode IP info", "error", err)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user