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:
2025-07-28 03:44:19 +02:00
parent 691710bc7c
commit 2fc24bb937
4 changed files with 46 additions and 12 deletions

View File

@@ -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
}

View File

@@ -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"`
}