+8
-6
internal/api/handlers.go
+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
+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
+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
+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 {