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.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func goldenSubData() *Data {
|
||||
entryServer := &Server{ID: "srv_entry", PublicAddress: "entry.example.com", TLSDomain: "tls.example.com"}
|
||||
v6Server := &Server{ID: "srv_v6", PublicAddress: "v6.example.com", TLSDomain: "tls.example.com"}
|
||||
art := func(pub string) map[string]json.RawMessage {
|
||||
raw, _ := marshalJSON(pub)
|
||||
return map[string]json.RawMessage{"realityPublicKey": raw}
|
||||
}
|
||||
nodes := []*Node{
|
||||
{ID: "nod_rf", Name: "RF-Reality", ServerID: "srv_entry", Protocol: "vless-reality-vision",
|
||||
Topology: "direct", NetworkMode: "ipv4", Port: 23456, Status: "active",
|
||||
Reality: &RealityConfig{KeyID: "reality-nod_rf", ServerName: "www.amd.com", DestPort: 443},
|
||||
Artifacts: art("FIXEDPBKRfKey0123456789abcdefghijklmnopqrstu")},
|
||||
{ID: "nod_ss", Name: "SS-Direct", ServerID: "srv_entry", Protocol: "shadowsocks-2022-aes128",
|
||||
Topology: "direct", NetworkMode: "ipv4", Port: 23458, Status: "active",
|
||||
SSServerKey: StrPtr("CwwNDg8QERITFBUWFxgZGg=="), Artifacts: map[string]json.RawMessage{}},
|
||||
{ID: "nod_hy", Name: "HY2", ServerID: "srv_entry", Protocol: "hysteria2",
|
||||
Topology: "direct", NetworkMode: "ipv4", Port: 23459, Status: "active", Artifacts: map[string]json.RawMessage{}},
|
||||
{ID: "nod_any", Name: "AnyTLS", ServerID: "srv_entry", Protocol: "anytls",
|
||||
Topology: "direct", NetworkMode: "ipv4", Port: 23460, Status: "active", Artifacts: map[string]json.RawMessage{}},
|
||||
{ID: "nod_ws", Name: "WS-TLS", ServerID: "srv_entry", Protocol: "vless-ws-tls",
|
||||
Topology: "direct", NetworkMode: "ipv4", Port: 23461, Status: "active",
|
||||
WSPath: StrPtr("/wspath123"), Artifacts: map[string]json.RawMessage{}},
|
||||
{ID: "nod_sk", Name: "Socks", ServerID: "srv_entry", Protocol: "socks5-auth",
|
||||
Topology: "direct", NetworkMode: "ipv4", Port: 23462, Status: "active", Artifacts: map[string]json.RawMessage{}},
|
||||
{ID: "nod_rv", Name: "RV-Plain", ServerID: "srv_v6", Protocol: "vless-reality",
|
||||
Topology: "direct", NetworkMode: "ipv4", Port: 23463, Status: "active",
|
||||
Reality: &RealityConfig{KeyID: "reality-nod_rv", ServerName: "www.apple.com", DestPort: 443},
|
||||
Artifacts: art("FIXEDPBKRvKey0123456789abcdefghijklmnopqrstu")},
|
||||
}
|
||||
nc := func(nodeID, clientID, relayPassword, shortID string) *NodeClient {
|
||||
return &NodeClient{
|
||||
NodeID: nodeID, CustomerID: "cus_1", Status: "active",
|
||||
Credentials: Credentials{ClientID: clientID, RelayPassword: relayPassword, ShortID: shortID},
|
||||
}
|
||||
}
|
||||
clients := []*NodeClient{
|
||||
nc("nod_rf", "11111111-2222-3333-4444-000000000001", "unused", "abcd0001ef01"),
|
||||
nc("nod_ss", "11111111-2222-3333-4444-000000000003", "FRYXGBkaGxwdHh8gISIjJA==", "abcd0003ef01"),
|
||||
nc("nod_hy", "11111111-2222-3333-4444-000000000004", "HB0eHyAhIiMkJSYnKCkqKywtLi8wMTIz", "abcd0004ef01"),
|
||||
nc("nod_any", "11111111-2222-3333-4444-000000000005", "IyQlJicoKSorLC0uLzAxMjM0NTY3ODk6", "abcd0005ef01"),
|
||||
nc("nod_ws", "11111111-2222-3333-4444-000000000006", "unused", "abcd0006ef01"),
|
||||
nc("nod_sk", "11111111-2222-3333-4444-000000000007", "MTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdI", "abcd0007ef01"),
|
||||
nc("nod_rv", "11111111-2222-3333-4444-000000000008", "unused", "abcd0008ef01"),
|
||||
}
|
||||
return &Data{
|
||||
Servers: []*Server{entryServer, v6Server},
|
||||
Nodes: nodes,
|
||||
NodeClients: clients,
|
||||
Customers: []*Customer{{ID: "cus_1"}},
|
||||
}
|
||||
}
|
||||
|
||||
type goldenSubs struct {
|
||||
Entries []SubEntry `json:"entries"`
|
||||
Formats map[string]struct {
|
||||
Body string `json:"body"`
|
||||
ContentType string `json:"contentType"`
|
||||
} `json:"formats"`
|
||||
}
|
||||
|
||||
func loadGoldenSubs(t *testing.T) goldenSubs {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile("testdata/golden_subscriptions.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var g goldenSubs
|
||||
if err := unmarshalJSONUseNumber(raw, &g); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func TestGoldenEntries(t *testing.T) {
|
||||
g := loadGoldenSubs(t)
|
||||
got := Entries(goldenSubData(), "cus_1")
|
||||
if !reflect.DeepEqual(got, g.Entries) {
|
||||
t.Fatalf("entries mismatch:\n got: %+v\nwant: %+v", got, g.Entries)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenFormats(t *testing.T) {
|
||||
g := loadGoldenSubs(t)
|
||||
d := goldenSubData()
|
||||
customer := &Customer{ID: "cus_1"}
|
||||
cases := []struct {
|
||||
key string
|
||||
format string
|
||||
ua string
|
||||
}{
|
||||
{"raw", "raw", ""},
|
||||
{"base64", "base64", ""},
|
||||
{"clash", "clash", ""},
|
||||
{"clash-smart", "clash-smart", ""},
|
||||
{"singbox", "singbox", ""},
|
||||
{"surge", "surge", ""},
|
||||
{"auto-clash-ua", "auto", "ClashForAndroid/2.0"},
|
||||
{"auto-plain-ua", "auto", "Mozilla/5.0"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
want, ok := g.Formats[c.key]
|
||||
if !ok {
|
||||
t.Fatalf("golden missing format %s", c.key)
|
||||
}
|
||||
res, ok := FormatSubscription(d, customer, c.format, c.ua)
|
||||
if !ok || res == nil {
|
||||
t.Fatalf("format %s: FormatSubscription returned null", c.key)
|
||||
}
|
||||
if res.ContentType != want.ContentType {
|
||||
t.Errorf("format %s contentType: got %q want %q", c.key, res.ContentType, want.ContentType)
|
||||
}
|
||||
if res.Body != want.Body {
|
||||
t.Errorf("format %s body mismatch:\n got: %q\nwant: %q", c.key, res.Body, want.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user