Files
hi-server/pkg/simnet/simnet.go
T
shanshanzhong147 634b5a7bd0 feat(simnet): add SimNet protocol end-to-end support
- model: SimNet protocol fields + NormalizeSimnet; type+port protocol uniqueness
- server config: OmnXT runtime config delivery via compatible() simnet case
- credentials: derive per-user psk/key_id from subscription (pkg/simnet), no new table
- subscription: adapter buildOmnxtSimnetConfigs + base64 buildOmnxtProtocolLinks + OmnXT SimNet application (migration 02161)
- UA gating: hide experimental protocols from non first-party clients (download + JSON node-list)
- admin: normalize simnet on create/update and on GET responses
- tests: 21 simnet unit tests; full suite green
2026-07-27 00:17:02 -07:00

113 lines
3.2 KiB
Go

// Package simnet holds shared, stateless helpers for the simnet protocol.
//
// The per-user simnet credentials are DERIVED from the existing user_subscribe
// record (id + uuid), not stored in a dedicated table. This mirrors the Pro
// reference (NPanel-backend internal/data/delivery_node.go deriveSimnetUserPSK
// and internal/biz/public/subscription/template.go key_id derivation), and is
// required for wire compatibility with OmnXT / SlagClient:
//
// - key_id MUST equal the user_subscribe.id (the node's StaticKeyResolver /
// assembler identifies the user by this id in the handshake frame header).
// - psk is the user's subscription UUID normalized to lowercase 32-hex.
package simnet
import (
"encoding/hex"
"strings"
)
// serverKeyID is the reserved key id for the server-side PSK (AF derivation).
const serverKeyID = 0
// DeriveKeyID maps a user_subscribe.id to the simnet key_id.
// It matches the reference: subscribeID % (2^31-1), avoiding 0 which collides
// with the server key id.
func DeriveKeyID(subscribeID int64) int {
keyID := int(subscribeID % (1<<31 - 1))
if keyID == serverKeyID {
keyID = 1
}
return keyID
}
// DeriveUserPSK normalizes a subscription UUID/password into the per-user PSK.
// - canonical UUID -> lowercase hex without dashes (32 chars)
// - already 32-char ASCII hex -> lowercased
// - otherwise -> hex-encoded bytes of the raw value
func DeriveUserPSK(value string) string {
trimmed := strings.TrimSpace(value)
if isCanonicalUUID(trimmed) {
return strings.ToLower(strings.ReplaceAll(trimmed, "-", ""))
}
if len(trimmed) == 32 && isASCIIHex(trimmed) {
return strings.ToLower(trimmed)
}
return hex.EncodeToString([]byte(trimmed))
}
func isCanonicalUUID(value string) bool {
if len(value) != 36 {
return false
}
for idx, ch := range value {
switch idx {
case 8, 13, 18, 23:
if ch != '-' {
return false
}
default:
if !isASCIIHexRune(ch) {
return false
}
}
}
return true
}
func isASCIIHex(value string) bool {
if value == "" {
return false
}
for _, ch := range value {
if !isASCIIHexRune(ch) {
return false
}
}
return true
}
func isASCIIHexRune(ch rune) bool {
return (ch >= '0' && ch <= '9') ||
(ch >= 'a' && ch <= 'f') ||
(ch >= 'A' && ch <= 'F')
}
// officialClientKeywords are User-Agent substrings of first-party clients/SDKs
// allowed to receive experimental protocols. Mirrors the Pro reference
// (internal/biz/public/subscribe/subscribe.go officialClientKeywords).
var officialClientKeywords = []string{"omnxt", "slag/", "slaglab"}
// experimentalProtocols must be hidden from non-official clients.
var experimentalProtocols = map[string]struct{}{
"simnet": {},
}
// ClientSupportsExperimental reports whether the client UA is a first-party
// client permitted to receive experimental protocols (simnet).
func ClientSupportsExperimental(userAgent string) bool {
ua := strings.ToLower(userAgent)
for _, kw := range officialClientKeywords {
if strings.Contains(ua, kw) {
return true
}
}
return false
}
// IsExperimentalProtocol reports whether a protocol type is experimental
// (hidden from non-official clients).
func IsExperimentalProtocol(protocol string) bool {
_, ok := experimentalProtocols[strings.ToLower(strings.TrimSpace(protocol))]
return ok
}