update

Changed files
+68 -38
internal
+8 -6
internal/api/handlers.go
··· 103 103 "discovered_at": ep.DiscoveredAt, 104 104 "last_checked": ep.LastChecked, 105 105 "status": statusToString(ep.Status), 106 - // REMOVED: "user_count": ep.UserCount, // No longer exists 107 106 } 108 107 109 - // Add IP if available 108 + // Add IPs if available 110 109 if ep.IP != "" { 111 110 response["ip"] = ep.IP 112 111 } 113 - 114 - // REMOVED: IP info extraction - no longer in Endpoint struct 115 - // IPInfo is now in separate table, joined only in PDS handlers 112 + if ep.IPv6 != "" { 113 + response["ipv6"] = ep.IPv6 114 + } 116 115 117 116 return response 118 117 } ··· 257 256 } 258 257 } 259 258 260 - // Add IP if available 259 + // Add IPs if available 261 260 if pds.IP != "" { 262 261 response["ip"] = pds.IP 262 + } 263 + if pds.IPv6 != "" { 264 + response["ipv6"] = pds.IPv6 263 265 } 264 266 265 267 // Add IP info (from ip_infos table via JOIN)
+20 -2
internal/plc/helpers.go
··· 1 1 package plc 2 2 3 - import "strings" 3 + import ( 4 + "regexp" 5 + "strings" 6 + ) 4 7 5 8 // MaxHandleLength is the maximum allowed handle length for database storage 6 9 const MaxHandleLength = 500 10 + 11 + // Handle validation regex per AT Protocol spec 12 + // Ensures proper domain format: alphanumeric labels separated by dots, TLD starts with letter 13 + var handleRegex = regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`) 7 14 8 15 // ExtractHandle safely extracts the handle from a PLC operation 9 16 func ExtractHandle(op *PLCOperation) string { ··· 29 36 } 30 37 31 38 // ValidateHandle checks if a handle is valid for database storage 32 - // Returns empty string if handle is too long 39 + // Returns empty string if handle is invalid (too long or wrong format) 33 40 func ValidateHandle(handle string) string { 41 + if handle == "" { 42 + return "" 43 + } 44 + 45 + // Check length first (faster) 34 46 if len(handle) > MaxHandleLength { 35 47 return "" 36 48 } 49 + 50 + // Validate format using regex 51 + if !handleRegex.MatchString(handle) { 52 + return "" 53 + } 54 + 37 55 return handle 38 56 } 39 57
+38 -29
internal/storage/postgres.go
··· 73 73 log.Info("Running database migrations...") 74 74 75 75 schema := ` 76 - -- Endpoints table (NO user_count, NO ip_info) 77 - CREATE TABLE IF NOT EXISTS endpoints ( 78 - id BIGSERIAL PRIMARY KEY, 79 - endpoint_type TEXT NOT NULL DEFAULT 'pds', 80 - endpoint TEXT NOT NULL, 81 - server_did TEXT, 82 - discovered_at TIMESTAMP NOT NULL, 83 - last_checked TIMESTAMP, 84 - status INTEGER DEFAULT 0, 85 - ip TEXT, 86 - ipv6 TEXT, 87 - ip_resolved_at TIMESTAMP, 88 - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 89 - UNIQUE(endpoint_type, endpoint) 90 - ); 76 + -- Endpoints table (with IPv6 support) 77 + CREATE TABLE IF NOT EXISTS endpoints ( 78 + id BIGSERIAL PRIMARY KEY, 79 + endpoint_type TEXT NOT NULL DEFAULT 'pds', 80 + endpoint TEXT NOT NULL, 81 + server_did TEXT, 82 + discovered_at TIMESTAMP NOT NULL, 83 + last_checked TIMESTAMP, 84 + status INTEGER DEFAULT 0, 85 + ip TEXT, 86 + ipv6 TEXT, 87 + ip_resolved_at TIMESTAMP, 88 + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 89 + UNIQUE(endpoint_type, endpoint) 90 + ); 91 91 92 - CREATE INDEX IF NOT EXISTS idx_endpoints_type_endpoint ON endpoints(endpoint_type, endpoint); 93 - CREATE INDEX IF NOT EXISTS idx_endpoints_status ON endpoints(status); 94 - CREATE INDEX IF NOT EXISTS idx_endpoints_type ON endpoints(endpoint_type); 95 - CREATE INDEX IF NOT EXISTS idx_endpoints_ip ON endpoints(ip); 96 - CREATE INDEX IF NOT EXISTS idx_endpoints_ipv6 ON endpoints(ipv6); 97 - CREATE INDEX IF NOT EXISTS idx_endpoints_server_did ON endpoints(server_did); 98 - CREATE INDEX IF NOT EXISTS idx_endpoints_server_did_type_discovered ON endpoints(server_did, endpoint_type, discovered_at); 92 + CREATE INDEX IF NOT EXISTS idx_endpoints_type_endpoint ON endpoints(endpoint_type, endpoint); 93 + CREATE INDEX IF NOT EXISTS idx_endpoints_status ON endpoints(status); 94 + CREATE INDEX IF NOT EXISTS idx_endpoints_type ON endpoints(endpoint_type); 95 + CREATE INDEX IF NOT EXISTS idx_endpoints_ip ON endpoints(ip); 96 + CREATE INDEX IF NOT EXISTS idx_endpoints_ipv6 ON endpoints(ipv6); 97 + CREATE INDEX IF NOT EXISTS idx_endpoints_server_did ON endpoints(server_did); 98 + CREATE INDEX IF NOT EXISTS idx_endpoints_server_did_type_discovered ON endpoints(server_did, endpoint_type, discovered_at); 99 99 100 100 -- IP infos table (IP as PRIMARY KEY) 101 101 CREATE TABLE IF NOT EXISTS ip_infos ( ··· 590 590 discovered_at, 591 591 last_checked, 592 592 status, 593 - ip 593 + ip, 594 + ipv6 594 595 FROM endpoints 595 596 WHERE endpoint_type = 'pds' 596 597 ORDER BY COALESCE(server_did, id::text), discovered_at ASC 597 598 ) 598 599 SELECT 599 - e.id, e.endpoint, e.server_did, e.discovered_at, e.last_checked, e.status, e.ip, 600 + e.id, e.endpoint, e.server_did, e.discovered_at, e.last_checked, e.status, e.ip, e.ipv6, 600 601 latest.user_count, latest.response_time, latest.version, latest.scanned_at, 601 602 i.city, i.country, i.country_code, i.asn, i.asn_org, 602 603 i.is_datacenter, i.is_vpn, i.is_crawler, i.is_tor, i.is_proxy, ··· 657 658 var items []*PDSListItem 658 659 for rows.Next() { 659 660 item := &PDSListItem{} 660 - var ip, serverDID, city, country, countryCode, asnOrg sql.NullString 661 + var ip, ipv6, serverDID, city, country, countryCode, asnOrg sql.NullString 661 662 var asn sql.NullInt32 662 663 var isDatacenter, isVPN, isCrawler, isTor, isProxy sql.NullBool 663 664 var lat, lon sql.NullFloat64 ··· 667 668 var scannedAt sql.NullTime 668 669 669 670 err := rows.Scan( 670 - &item.ID, &item.Endpoint, &serverDID, &item.DiscoveredAt, &item.LastChecked, &item.Status, &ip, 671 + &item.ID, &item.Endpoint, &serverDID, &item.DiscoveredAt, &item.LastChecked, &item.Status, &ip, &ipv6, 671 672 &userCount, &responseTime, &version, &scannedAt, 672 673 &city, &country, &countryCode, &asn, &asnOrg, 673 674 &isDatacenter, &isVPN, &isCrawler, &isTor, &isProxy, ··· 679 680 680 681 if ip.Valid { 681 682 item.IP = ip.String 683 + } 684 + if ipv6.Valid { 685 + item.IPv6 = ipv6.String 682 686 } 683 687 if serverDID.Valid { 684 688 item.ServerDID = serverDID.String ··· 734 738 e.discovered_at, 735 739 e.last_checked, 736 740 e.status, 737 - e.ip 741 + e.ip, 742 + e.ipv6 738 743 FROM endpoints e 739 744 WHERE e.endpoint = $1 AND e.endpoint_type = 'pds' 740 745 ), ··· 757 762 te.last_checked, 758 763 te.status, 759 764 te.ip, 765 + te.ipv6, 760 766 latest.user_count, 761 767 latest.response_time, 762 768 latest.version, ··· 781 787 ` 782 788 783 789 detail := &PDSDetail{} 784 - var ip, city, country, countryCode, asnOrg, serverDID sql.NullString 790 + var ip, ipv6, city, country, countryCode, asnOrg, serverDID sql.NullString 785 791 var asn sql.NullInt32 786 792 var isDatacenter, isVPN, isCrawler, isTor, isProxy sql.NullBool 787 793 var lat, lon sql.NullFloat64 ··· 795 801 var firstDiscoveredAt sql.NullTime 796 802 797 803 err := p.db.QueryRowContext(ctx, query, endpoint).Scan( 798 - &detail.ID, &detail.Endpoint, &serverDID, &detail.DiscoveredAt, &detail.LastChecked, &detail.Status, &ip, 804 + &detail.ID, &detail.Endpoint, &serverDID, &detail.DiscoveredAt, &detail.LastChecked, &detail.Status, &ip, &ipv6, 799 805 &userCount, &responseTime, &version, &serverInfoJSON, &scannedAt, 800 806 &city, &country, &countryCode, &asn, &asnOrg, 801 807 &isDatacenter, &isVPN, &isCrawler, &isTor, &isProxy, ··· 810 816 811 817 if ip.Valid { 812 818 detail.IP = ip.String 819 + } 820 + if ipv6.Valid { 821 + detail.IPv6 = ipv6.String 813 822 } 814 823 815 824 if serverDID.Valid {
+2 -1
internal/storage/types.go
··· 217 217 // From endpoints table 218 218 ID int64 219 219 Endpoint string 220 - ServerDID string // NEW: Add this 220 + ServerDID string 221 221 DiscoveredAt time.Time 222 222 LastChecked time.Time 223 223 Status int 224 224 IP string 225 + IPv6 string // NEW 225 226 226 227 // From latest endpoint_scans (via JOIN) 227 228 LatestScan *struct {