Files
aaaamoooo 3951134940
CI / test (push) Canceled after 0s
CI / anytls-smoke (push) Canceled after 0s
feat: Go port of the panel in panel-go/ — drop-in replacement for server.js
Pure-stdlib Go implementation of the whole control plane: same
nexusgate.json schema (unknown fields preserved verbatim, explicit
null/false tri-states modeled), same HTTP API and error strings,
embedded frontend (NG_PUBLIC_DIR override), self-implemented scrypt
auth, X25519 Reality keys, panel-hosted keyStore and heartbeat
reconciliation included. Version stays 0.6.8.

Verified: Go tests 33/33 (scrypt + Reality vectors generated from
the Node implementation, subscription goldens byte-identical),
Node suite 52/52 untouched, differential E2E against the Node
panel 50/50 including two-way DB swap. See panel-go/PARITY.md
and panel-go/INSTALL-GO.md.
2026-10-20 08:21:54 +00:00

166 lines
5.8 KiB
Go

package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"strings"
"testing"
"time"
)
func hmacHex(secret, msg string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(msg))
return hex.EncodeToString(mac.Sum(nil))
}
func headersOf(pairs ...string) http.Header {
h := http.Header{}
for i := 0; i+1 < len(pairs); i += 2 {
h.Set(pairs[i], pairs[i+1])
}
return h
}
func TestClientIdentityHeaderKind(t *testing.T) {
id := ClientIdentity(headersOf("X-Device-Id", " device-1 ", "User-Agent", "UA/1.0"), "s3cret")
if id.Kind != "header" {
t.Errorf("Kind = %q, want header", id.Kind)
}
if id.UserAgent != "UA/1.0" {
t.Errorf("UserAgent = %q", id.UserAgent)
}
if want := hmacHex("s3cret", "header:device-1"); id.Key != want {
t.Errorf("Key = %q, want %q", id.Key, want)
}
}
func TestClientIdentityFallbacks(t *testing.T) {
// x-client-id is the fallback declared identity.
id := ClientIdentity(headersOf("X-Client-Id", "client-9"), "k")
if id.Kind != "header" || id.Key != hmacHex("k", "header:client-9") {
t.Errorf("x-client-id fallback = %+v", id)
}
// Empty x-device-id falls through to x-client-id (JS || semantics).
id = ClientIdentity(headersOf("X-Device-Id", "", "X-Client-Id", "client-9"), "k")
if id.Kind != "header" || id.Key != hmacHex("k", "header:client-9") {
t.Errorf("empty x-device-id fallback = %+v", id)
}
// No declared identity -> user-agent kind.
id = ClientIdentity(headersOf("User-Agent", "Agent X"), "k")
if id.Kind != "user-agent" || id.Key != hmacHex("k", "user-agent:Agent X") {
t.Errorf("user-agent kind = %+v", id)
}
// Nothing at all -> unknown.
id = ClientIdentity(http.Header{}, "k")
if id.Kind != "unknown" || id.UserAgent != "" || id.Key != hmacHex("k", "unknown:unknown") {
t.Errorf("unknown kind = %+v", id)
}
}
func TestClientIdentityTruncation(t *testing.T) {
longDevice := strings.Repeat("d", 200)
id := ClientIdentity(headersOf("X-Device-Id", longDevice), "k")
if want := hmacHex("k", "header:"+strings.Repeat("d", 128)); id.Key != want {
t.Errorf("declared identity not truncated to 128: Key = %q want %q", id.Key, want)
}
longUA := strings.Repeat("u", 300)
id = ClientIdentity(headersOf("User-Agent", longUA), "k")
if len([]rune(id.UserAgent)) != 200 {
t.Errorf("UserAgent length = %d, want 200", len([]rune(id.UserAgent)))
}
if want := hmacHex("k", "user-agent:"+strings.Repeat("u", 200)); id.Key != want {
t.Errorf("user agent not truncated to 200 in key")
}
}
var testNow = time.Date(2026, 10, 20, 12, 0, 0, 0, time.UTC)
func isoAgo(d time.Duration) string { return ISOFromTime(testNow.Add(-d)) }
func TestActiveClientsWindow(t *testing.T) {
d := EmptyData()
d.SubscriptionClients = []*SubscriptionClient{
{CustomerID: "c1", ClientKey: "fresh", LastSeenAt: isoAgo(time.Hour)},
{CustomerID: "c1", ClientKey: "stale", LastSeenAt: isoAgo(25 * time.Hour)},
{CustomerID: "c1", ClientKey: "boundary", LastSeenAt: isoAgo(24 * time.Hour)}, // > cutoff is strict
{CustomerID: "c2", ClientKey: "other", LastSeenAt: isoAgo(time.Hour)},
{CustomerID: "c1", ClientKey: "garbage", LastSeenAt: "not-a-date"}, // NaN in JS: excluded
}
got := ActiveClients(d, "c1", testNow)
if len(got) != 1 || got[0] != "fresh" {
t.Errorf("ActiveClients = %v, want [fresh]", got)
}
}
func TestObservedIPsWindow(t *testing.T) {
d := EmptyData() // observationTtlMinutes defaults to 10
d.Observations = []*Observation{
{CustomerID: "c1", IP: "1.1.1.1", LastSeenAt: isoAgo(5 * time.Minute)},
{CustomerID: "c1", IP: "2.2.2.2", LastSeenAt: isoAgo(11 * time.Minute)},
{CustomerID: "c2", IP: "3.3.3.3", LastSeenAt: isoAgo(time.Minute)},
{CustomerID: "c1", IP: "4.4.4.4", LastSeenAt: "garbage"},
}
got := ObservedIPs(d, "c1", testNow)
if len(got) != 1 || got[0].IP != "1.1.1.1" {
t.Errorf("ObservedIPs default TTL = %v", got)
}
d.Settings.ObservationTtlMinutes = 60
got = ObservedIPs(d, "c1", testNow)
if len(got) != 2 {
t.Errorf("ObservedIPs 60m TTL = %d entries, want 2", len(got))
}
// TTL of 0 falls back to 10 like JS `|| 10`.
d.Settings.ObservationTtlMinutes = 0
got = ObservedIPs(d, "c1", testNow)
if len(got) != 1 {
t.Errorf("ObservedIPs 0 TTL fallback = %d entries, want 1", len(got))
}
}
func TestPruneAccess(t *testing.T) {
d := EmptyData()
d.SubscriptionAccess = []*SubscriptionAccess{
{CustomerID: "c1", At: isoAgo(29 * 24 * time.Hour)},
{CustomerID: "c1", At: isoAgo(31 * 24 * time.Hour)},
{CustomerID: "c1", At: "garbage"},
}
d.SubscriptionClients = []*SubscriptionClient{
{CustomerID: "c1", ClientKey: "fresh", LastSeenAt: isoAgo(23 * time.Hour)},
{CustomerID: "c1", ClientKey: "stale", LastSeenAt: isoAgo(24*time.Hour + time.Second)},
}
d.Observations = []*Observation{
{CustomerID: "c1", IP: "1.1.1.1", LastSeenAt: isoAgo(9 * time.Minute)},
{CustomerID: "c1", IP: "2.2.2.2", LastSeenAt: isoAgo(10*time.Minute + time.Second)},
}
PruneAccess(d, testNow)
if len(d.SubscriptionAccess) != 1 {
t.Errorf("access after prune = %d, want 1", len(d.SubscriptionAccess))
}
if len(d.SubscriptionClients) != 1 || d.SubscriptionClients[0].ClientKey != "fresh" {
t.Errorf("clients after prune = %+v", d.SubscriptionClients)
}
if len(d.Observations) != 1 || d.Observations[0].IP != "1.1.1.1" {
t.Errorf("observations after prune = %+v", d.Observations)
}
}
func TestPruneAccessKeepsLast10000(t *testing.T) {
d := EmptyData()
for i := 0; i < 10005; i++ {
d.SubscriptionAccess = append(d.SubscriptionAccess, &SubscriptionAccess{
CustomerID: "c1", At: isoAgo(time.Minute), ClientKey: fmt.Sprintf("k%05d", i),
})
}
PruneAccess(d, testNow)
if len(d.SubscriptionAccess) != 10000 {
t.Fatalf("access len = %d, want 10000 (slice(-10000))", len(d.SubscriptionAccess))
}
if d.SubscriptionAccess[0].ClientKey != "k00005" {
t.Errorf("first retained = %q, want k00005", d.SubscriptionAccess[0].ClientKey)
}
}