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"
|
"database/sql"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
@ -31,6 +32,14 @@ const (
|
|||||||
maxIPv4 = 0xFFFFFFFF
|
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.
|
// Database manages the SQLite database connection and operations.
|
||||||
type Database struct {
|
type Database struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
@ -574,7 +583,7 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
|||||||
// Parse the IP to validate it
|
// Parse the IP to validate it
|
||||||
parsedIP := net.ParseIP(ip)
|
parsedIP := net.ParseIP(ip)
|
||||||
if parsedIP == nil {
|
if parsedIP == nil {
|
||||||
return nil, fmt.Errorf("invalid IP address: %s", ip)
|
return nil, fmt.Errorf("%w: %s", ErrInvalidIP, ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine IP version
|
// Determine IP version
|
||||||
@ -590,7 +599,7 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
|||||||
ipUint := ipToUint32(ipv4)
|
ipUint := ipToUint32(ipv4)
|
||||||
|
|
||||||
query := `
|
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
|
FROM live_routes lr
|
||||||
LEFT JOIN asns a ON a.number = lr.origin_asn
|
LEFT JOIN asns a ON a.number = lr.origin_asn
|
||||||
WHERE lr.ip_version = ? AND lr.v4_ip_start <= ? AND lr.v4_ip_end >= ?
|
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 prefix string
|
||||||
var maskLength, originASN int
|
var maskLength, originASN int
|
||||||
|
var lastUpdated time.Time
|
||||||
var handle, description sql.NullString
|
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 != nil {
|
||||||
if err == sql.ErrNoRows {
|
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)
|
return nil, fmt.Errorf("failed to query routes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
age := time.Since(lastUpdated).Round(time.Second).String()
|
||||||
|
|
||||||
return &ASInfo{
|
return &ASInfo{
|
||||||
ASN: originASN,
|
ASN: originASN,
|
||||||
Handle: handle.String,
|
Handle: handle.String,
|
||||||
Description: description.String,
|
Description: description.String,
|
||||||
Prefix: prefix,
|
Prefix: prefix,
|
||||||
|
LastUpdated: lastUpdated,
|
||||||
|
Age: age,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// For IPv6, use the original method since we don't have range optimization
|
// For IPv6, use the original method since we don't have range optimization
|
||||||
query := `
|
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
|
FROM live_routes lr
|
||||||
LEFT JOIN asns a ON a.number = lr.origin_asn
|
LEFT JOIN asns a ON a.number = lr.origin_asn
|
||||||
WHERE lr.ip_version = ?
|
WHERE lr.ip_version = ?
|
||||||
@ -639,6 +654,7 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
|||||||
prefix string
|
prefix string
|
||||||
maskLength int
|
maskLength int
|
||||||
originASN int
|
originASN int
|
||||||
|
lastUpdated time.Time
|
||||||
handle sql.NullString
|
handle sql.NullString
|
||||||
description sql.NullString
|
description sql.NullString
|
||||||
}
|
}
|
||||||
@ -647,9 +663,10 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var prefix string
|
var prefix string
|
||||||
var maskLength, originASN int
|
var maskLength, originASN int
|
||||||
|
var lastUpdated time.Time
|
||||||
var handle, description sql.NullString
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,6 +681,7 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
|||||||
bestMatch.prefix = prefix
|
bestMatch.prefix = prefix
|
||||||
bestMatch.maskLength = maskLength
|
bestMatch.maskLength = maskLength
|
||||||
bestMatch.originASN = originASN
|
bestMatch.originASN = originASN
|
||||||
|
bestMatch.lastUpdated = lastUpdated
|
||||||
bestMatch.handle = handle
|
bestMatch.handle = handle
|
||||||
bestMatch.description = description
|
bestMatch.description = description
|
||||||
bestMaskLength = maskLength
|
bestMaskLength = maskLength
|
||||||
@ -671,14 +689,18 @@ func (d *Database) GetASInfoForIP(ip string) (*ASInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bestMaskLength == -1 {
|
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{
|
return &ASInfo{
|
||||||
ASN: bestMatch.originASN,
|
ASN: bestMatch.originASN,
|
||||||
Handle: bestMatch.handle.String,
|
Handle: bestMatch.handle.String,
|
||||||
Description: bestMatch.description.String,
|
Description: bestMatch.description.String,
|
||||||
Prefix: bestMatch.prefix,
|
Prefix: bestMatch.prefix,
|
||||||
|
LastUpdated: bestMatch.lastUpdated,
|
||||||
|
Age: age,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,8 +70,10 @@ type PrefixDistribution struct {
|
|||||||
|
|
||||||
// ASInfo represents AS information for an IP lookup
|
// ASInfo represents AS information for an IP lookup
|
||||||
type ASInfo struct {
|
type ASInfo struct {
|
||||||
ASN int `json:"asn"`
|
ASN int `json:"asn"`
|
||||||
Handle string `json:"handle"`
|
Handle string `json:"handle"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Prefix string `json:"prefix"`
|
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
|
// GetASInfoForIP mock implementation
|
||||||
func (m *mockStore) GetASInfoForIP(ip string) (*database.ASInfo, error) {
|
func (m *mockStore) GetASInfoForIP(ip string) (*database.ASInfo, error) {
|
||||||
// Simple mock - return a test AS
|
// Simple mock - return a test AS
|
||||||
|
now := time.Now()
|
||||||
return &database.ASInfo{
|
return &database.ASInfo{
|
||||||
ASN: 15169,
|
ASN: 15169,
|
||||||
Handle: "GOOGLE",
|
Handle: "GOOGLE",
|
||||||
Description: "Google LLC",
|
Description: "Google LLC",
|
||||||
Prefix: "8.8.8.0/24",
|
Prefix: "8.8.8.0/24",
|
||||||
|
LastUpdated: now.Add(-5 * time.Minute),
|
||||||
|
Age: "5m0s",
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ package server
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@ -439,7 +440,13 @@ func (s *Server) handleIPLookup() http.HandlerFunc {
|
|||||||
// Look up AS information for the IP
|
// Look up AS information for the IP
|
||||||
asInfo, err := s.db.GetASInfoForIP(ip)
|
asInfo, err := s.db.GetASInfoForIP(ip)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user