Add route age information to IP lookup API
- Add last_updated timestamp and age fields to ASInfo - Include route's last_updated time from live_routes table - Calculate and display age as human-readable duration - Update both IPv4 and IPv6 queries to fetch timestamp - Fix error handling to return 400 for invalid IPs
This commit is contained in:
parent
691710bc7c
commit
2fc24bb937
@ -5,6 +5,7 @@ import (
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
@ -31,6 +32,14 @@ const (
|
||||
maxIPv4 = 0xFFFFFFFF
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
// ErrInvalidIP is returned when an IP address is malformed
|
||||
ErrInvalidIP = errors.New("invalid IP address")
|
||||
// ErrNoRoute is returned when no route is found for an IP
|
||||
ErrNoRoute = errors.New("no route found")
|
||||
)
|
||||
|
||||
// Database manages the SQLite database connection and operations.
|
||||
type Database struct {
|
||||
db *sql.DB
|
||||
@ -574,7 +583,7 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
||||
// Parse the IP to validate it
|
||||
parsedIP := net.ParseIP(ip)
|
||||
if parsedIP == nil {
|
||||
return nil, fmt.Errorf("invalid IP address: %s", ip)
|
||||
return nil, fmt.Errorf("%w: %s", ErrInvalidIP, ip)
|
||||
}
|
||||
|
||||
// Determine IP version
|
||||
@ -590,7 +599,7 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
||||
ipUint := ipToUint32(ipv4)
|
||||
|
||||
query := `
|
||||
SELECT DISTINCT lr.prefix, lr.mask_length, lr.origin_asn, a.handle, a.description
|
||||
SELECT DISTINCT lr.prefix, lr.mask_length, lr.origin_asn, lr.last_updated, a.handle, a.description
|
||||
FROM live_routes lr
|
||||
LEFT JOIN asns a ON a.number = lr.origin_asn
|
||||
WHERE lr.ip_version = ? AND lr.v4_ip_start <= ? AND lr.v4_ip_end >= ?
|
||||
@ -600,28 +609,34 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
||||
|
||||
var prefix string
|
||||
var maskLength, originASN int
|
||||
var lastUpdated time.Time
|
||||
var handle, description sql.NullString
|
||||
|
||||
err := d.db.QueryRow(query, ipVersionV4, ipUint, ipUint).Scan(&prefix, &maskLength, &originASN, &handle, &description)
|
||||
err := d.db.QueryRow(query, ipVersionV4, ipUint, ipUint).Scan(
|
||||
&prefix, &maskLength, &originASN, &lastUpdated, &handle, &description)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("no route found for IP %s", ip)
|
||||
return nil, fmt.Errorf("%w for IP %s", ErrNoRoute, ip)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("failed to query routes: %w", err)
|
||||
}
|
||||
|
||||
age := time.Since(lastUpdated).Round(time.Second).String()
|
||||
|
||||
return &ASInfo{
|
||||
ASN: originASN,
|
||||
Handle: handle.String,
|
||||
Description: description.String,
|
||||
Prefix: prefix,
|
||||
LastUpdated: lastUpdated,
|
||||
Age: age,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// For IPv6, use the original method since we don't have range optimization
|
||||
query := `
|
||||
SELECT DISTINCT lr.prefix, lr.mask_length, lr.origin_asn, a.handle, a.description
|
||||
SELECT DISTINCT lr.prefix, lr.mask_length, lr.origin_asn, lr.last_updated, a.handle, a.description
|
||||
FROM live_routes lr
|
||||
LEFT JOIN asns a ON a.number = lr.origin_asn
|
||||
WHERE lr.ip_version = ?
|
||||
@ -639,6 +654,7 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
||||
prefix string
|
||||
maskLength int
|
||||
originASN int
|
||||
lastUpdated time.Time
|
||||
handle sql.NullString
|
||||
description sql.NullString
|
||||
}
|
||||
@ -647,9 +663,10 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
||||
for rows.Next() {
|
||||
var prefix string
|
||||
var maskLength, originASN int
|
||||
var lastUpdated time.Time
|
||||
var handle, description sql.NullString
|
||||
|
||||
if err := rows.Scan(&prefix, &maskLength, &originASN, &handle, &description); err != nil {
|
||||
if err := rows.Scan(&prefix, &maskLength, &originASN, &lastUpdated, &handle, &description); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -664,6 +681,7 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
||||
bestMatch.prefix = prefix
|
||||
bestMatch.maskLength = maskLength
|
||||
bestMatch.originASN = originASN
|
||||
bestMatch.lastUpdated = lastUpdated
|
||||
bestMatch.handle = handle
|
||||
bestMatch.description = description
|
||||
bestMaskLength = maskLength
|
||||
@ -671,14 +689,18 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
||||
}
|
||||
|
||||
if bestMaskLength == -1 {
|
||||
return nil, fmt.Errorf("no route found for IP %s", ip)
|
||||
return nil, fmt.Errorf("%w for IP %s", ErrNoRoute, ip)
|
||||
}
|
||||
|
||||
age := time.Since(bestMatch.lastUpdated).Round(time.Second).String()
|
||||
|
||||
return &ASInfo{
|
||||
ASN: bestMatch.originASN,
|
||||
Handle: bestMatch.handle.String,
|
||||
Description: bestMatch.description.String,
|
||||
Prefix: bestMatch.prefix,
|
||||
LastUpdated: bestMatch.lastUpdated,
|
||||
Age: age,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -70,8 +70,10 @@ type PrefixDistribution struct {
|
||||
|
||||
// ASInfo represents AS information for an IP lookup
|
||||
type ASInfo struct {
|
||||
ASN int `json:"asn"`
|
||||
Handle string `json:"handle"`
|
||||
Description string `json:"description"`
|
||||
Prefix string `json:"prefix"`
|
||||
ASN int `json:"asn"`
|
||||
Handle string `json:"handle"`
|
||||
Description string `json:"description"`
|
||||
Prefix string `json:"prefix"`
|
||||
LastUpdated time.Time `json:"last_updated"`
|
||||
Age string `json:"age"`
|
||||
}
|
||||
|
@ -190,11 +190,14 @@ func (m *mockStore) GetLiveRouteCounts() (ipv4Count, ipv6Count int, err error) {
|
||||
// GetASInfoForIP mock implementation
|
||||
func (m *mockStore) GetASInfoForIP(ip string) (*database.ASInfo, error) {
|
||||
// Simple mock - return a test AS
|
||||
now := time.Now()
|
||||
return &database.ASInfo{
|
||||
ASN: 15169,
|
||||
Handle: "GOOGLE",
|
||||
Description: "Google LLC",
|
||||
Prefix: "8.8.8.0/24",
|
||||
LastUpdated: now.Add(-5 * time.Minute),
|
||||
Age: "5m0s",
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
@ -439,7 +440,13 @@ func (s *Server) handleIPLookup() http.HandlerFunc {
|
||||
// Look up AS information for the IP
|
||||
asInfo, err := s.db.GetASInfoForIP(ip)
|
||||
if err != nil {
|
||||
writeJSONError(w, http.StatusNotFound, err.Error())
|
||||
// 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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user