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
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user