Unify IP lookup response structure and add PTR lookups
- Always return consistent JSON structure with query and results array - Add PTR field to IPInfo for reverse DNS records - Support comma-separated IPs and hostnames in single query - Do PTR lookup for all IPs (direct, resolved from hostname, or listed) - Remove trailing dots from PTR records
This commit is contained in:
parent
cb75409647
commit
f8b7d3b773
@ -101,6 +101,7 @@ type ASInfo struct {
|
|||||||
// IPInfo represents comprehensive IP information for the /ip endpoint
|
// IPInfo represents comprehensive IP information for the /ip endpoint
|
||||||
type IPInfo struct {
|
type IPInfo struct {
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
|
PTR []string `json:"ptr,omitempty"`
|
||||||
Netblock string `json:"netblock"`
|
Netblock string `json:"netblock"`
|
||||||
MaskLength int `json:"mask_length"`
|
MaskLength int `json:"mask_length"`
|
||||||
IPVersion int `json:"ip_version"`
|
IPVersion int `json:"ip_version"`
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/routewatch/internal/database"
|
"git.eeqj.de/sneak/routewatch/internal/database"
|
||||||
@ -367,16 +368,17 @@ func (s *Server) handleIPLookup() http.HandlerFunc {
|
|||||||
return s.handleIPInfo()
|
return s.handleIPInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
// HostLookupResponse is returned when looking up a hostname.
|
// IPLookupResponse is the standard response for IP/hostname lookups.
|
||||||
type HostLookupResponse struct {
|
type IPLookupResponse struct {
|
||||||
Hostname string `json:"hostname,omitempty"`
|
Query string `json:"query"`
|
||||||
Results []*database.IPInfo `json:"results"`
|
Results []*database.IPInfo `json:"results"`
|
||||||
Errors []string `json:"errors,omitempty"`
|
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.
|
// Accepts IP addresses (single or comma-separated) and hostnames.
|
||||||
|
// Always returns the same response structure with PTR records for each IP.
|
||||||
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/hostname from URL param, falling back to client IP
|
// Get IP/hostname from URL param, falling back to client IP
|
||||||
@ -395,71 +397,49 @@ func (s *Server) handleIPInfo() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, database.ErrInvalidIP) {
|
|
||||||
writeJSONError(w, http.StatusBadRequest, err.Error())
|
|
||||||
} else {
|
|
||||||
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, 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()
|
ctx := r.Context()
|
||||||
|
response := IPLookupResponse{
|
||||||
|
Query: target,
|
||||||
|
Results: make([]*database.IPInfo, 0),
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve hostname to IP addresses
|
// Collect all IPs to look up
|
||||||
ips, err := net.DefaultResolver.LookupHost(ctx, hostname)
|
var ipsToLookup []string
|
||||||
|
|
||||||
|
// Check if target contains commas (multiple IPs)
|
||||||
|
targets := strings.Split(target, ",")
|
||||||
|
for _, t := range targets {
|
||||||
|
t = strings.TrimSpace(t)
|
||||||
|
if t == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this target is an IP address
|
||||||
|
if parsedIP := net.ParseIP(t); parsedIP != nil {
|
||||||
|
ipsToLookup = append(ipsToLookup, t)
|
||||||
|
} else {
|
||||||
|
// It's a hostname - resolve it
|
||||||
|
resolved, err := net.DefaultResolver.LookupHost(ctx, t)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSONError(w, http.StatusBadRequest, "Failed to resolve hostname: "+err.Error())
|
response.Errors = append(response.Errors, t+": "+err.Error())
|
||||||
|
|
||||||
return
|
continue
|
||||||
|
}
|
||||||
|
ipsToLookup = append(ipsToLookup, resolved...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ips) == 0 {
|
if len(ipsToLookup) == 0 {
|
||||||
writeJSONError(w, http.StatusNotFound, "No IP addresses found for hostname")
|
writeJSONError(w, http.StatusBadRequest, "No valid IPs or hostnames provided")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response := HostLookupResponse{
|
|
||||||
Hostname: hostname,
|
|
||||||
Results: make([]*database.IPInfo, 0, len(ips)),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Track ASNs that need WHOIS refresh
|
// Track ASNs that need WHOIS refresh
|
||||||
refreshASNs := make(map[int]bool)
|
refreshASNs := make(map[int]bool)
|
||||||
|
|
||||||
// Look up each resolved IP
|
// Look up each IP
|
||||||
for _, ip := range ips {
|
for _, ip := range ipsToLookup {
|
||||||
ipInfo, err := s.db.GetIPInfoContext(ctx, ip)
|
ipInfo, err := s.db.GetIPInfoContext(ctx, ip)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.Errors = append(response.Errors, ip+": "+err.Error())
|
response.Errors = append(response.Errors, ip+": "+err.Error())
|
||||||
@ -467,6 +447,16 @@ func (s *Server) lookupHostname(w http.ResponseWriter, r *http.Request, hostname
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Do PTR lookup for this IP
|
||||||
|
ptrs, err := net.DefaultResolver.LookupAddr(ctx, ip)
|
||||||
|
if err == nil && len(ptrs) > 0 {
|
||||||
|
// Remove trailing dots from PTR records
|
||||||
|
for i, ptr := range ptrs {
|
||||||
|
ptrs[i] = strings.TrimSuffix(ptr, ".")
|
||||||
|
}
|
||||||
|
ipInfo.PTR = ptrs
|
||||||
|
}
|
||||||
|
|
||||||
response.Results = append(response.Results, ipInfo)
|
response.Results = append(response.Results, ipInfo)
|
||||||
|
|
||||||
if ipInfo.NeedsWHOISRefresh {
|
if ipInfo.NeedsWHOISRefresh {
|
||||||
@ -481,15 +471,16 @@ func (s *Server) lookupHostname(w http.ResponseWriter, r *http.Request, hostname
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return response
|
// Return response (even if no results, include errors)
|
||||||
if len(response.Results) == 0 {
|
if len(response.Results) == 0 && len(response.Errors) > 0 {
|
||||||
writeJSONError(w, http.StatusNotFound, "No routes found for any resolved IP")
|
writeJSONError(w, http.StatusNotFound, "No routes found: "+response.Errors[0])
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := writeJSONSuccess(w, response); err != nil {
|
if err := writeJSONSuccess(w, response); err != nil {
|
||||||
s.logger.Error("Failed to encode hostname lookup response", "error", err)
|
s.logger.Error("Failed to encode IP lookup response", "error", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user