package adapter import ( "encoding/base64" "net/url" "strconv" "strings" "github.com/perfect-panel/server/pkg/simnet" ) // buildOmnxtSimnetConfigs is a subscription template function (registered in // Client.Build) that produces the per-user OmnXT SimNet JSON config array. // // It mirrors the Pro reference (NPanel-backend // internal/biz/public/subscription/template.go buildOmnxtSimnetConfigs): // - per-user simnet_psk / simnet_key_id are DERIVED from the user's // subscription (uuid + user_subscribe.id), never stored. // - the server PSK (key_id=0) is passed through as simnet_server_psk so the // client SDK can derive AF path/magic with the same key material. // // Template usage: {{ buildOmnxtSimnetConfigs .Proxies .UserInfo .Params | toPrettyJson }} func buildOmnxtSimnetConfigs(proxies []map[string]interface{}, userInfo User, params map[string]string) []map[string]interface{} { result := make([]map[string]interface{}, 0) proxyMode := strings.TrimSpace(params["proxy_mode"]) if proxyMode == "" { proxyMode = "global" } dnsServers := []string{"1.1.1.1"} if raw := strings.TrimSpace(params["dns_servers"]); raw != "" { parts := strings.FieldsFunc(raw, func(r rune) bool { return r == ',' || r == '\n' || r == '\r' }) parsed := make([]string, 0, len(parts)) for _, item := range parts { if item = strings.TrimSpace(item); item != "" { parsed = append(parsed, item) } } if len(parsed) > 0 { dnsServers = parsed } } // Per-user credentials derived from the subscription record (see pkg/simnet). userKeyID := simnet.DeriveKeyID(userInfo.SubscribeID) userPSK := simnet.DeriveUserPSK(userInfo.Password) for _, proxy := range proxies { if smString(proxy["Type"]) != "simnet" { continue } afEnabled := smBool(proxy["SimnetAfEnabled"]) item := map[string]interface{}{ "tag": smString(proxy["Name"]), "server_addr": smString(proxy["Server"]), "server_port": smInt(proxy["Port"]), "protocol": "simnet", "sni": smString(proxy["SNI"]), "allow_insecure": smBool(proxy["AllowInsecure"]), "simnet_psk": userPSK, "simnet_key_id": userKeyID, // Server PSK is required for AF path/magic/content-type derivation. "simnet_server_psk": smStringOrNil(proxy["SimnetPsk"]), "simnet_server_key_id": smInt(proxy["SimnetKeyID"]), "simnet_ticket_id": smStringOrNil(proxy["SimnetTicketID"]), "simnet_path": smDefaultString(smString(proxy["SimnetPath"]), "/simnet/session"), "simnet_carrier": smDefaultString(smString(proxy["SimnetCarrier"]), "h2"), "simnet_af_enabled": afEnabled, "simnet_client_max_concurrent_streams": smDefaultInt(smInt(proxy["SimnetClientMaxConcurrentStreams"]), 32), "simnet_client_max_streams_per_session": smDefaultInt(smInt(proxy["SimnetClientMaxStreamsPerSession"]), 512), "simnet_client_session_idle_timeout_secs": smDefaultInt(smInt(proxy["SimnetClientSessionIdleTimeoutSecs"]), 90), "simnet_client_max_udp_sessions": smDefaultInt(smInt(proxy["SimnetClientMaxUDPSessions"]), 64), "proxy_mode": proxyMode, "dns_servers": dnsServers, } if afEnabled { item["simnet_af_path_mode"] = smDefaultString(smString(proxy["SimnetAfPathMode"]), "api") item["simnet_af_path_prefix"] = smStringOrNil(proxy["SimnetAfPathPrefix"]) item["simnet_af_path_suffix"] = smStringOrNil(proxy["SimnetAfPathSuffix"]) item["simnet_af_magic_mode"] = smDefaultString(smString(proxy["SimnetAfMagicMode"]), "derived") item["simnet_af_response_jitter_ms"] = smDefaultInt(smInt(proxy["SimnetAfResponseJitterMs"]), 50) item["simnet_af_handshake_polymorphism"] = smBool(proxy["SimnetAfHandshakePolymorphism"]) item["simnet_af_settings_jitter"] = smBool(proxy["SimnetAfSettingsJitter"]) item["simnet_af_fake_header_injection"] = smBool(proxy["SimnetAfFakeHeaderInjection"]) } result = append(result, item) } return result } func smString(v interface{}) string { if s, ok := v.(string); ok { return s } return "" } func smStringOrNil(v interface{}) interface{} { if s, ok := v.(string); ok && s != "" { return s } return nil } func smBool(v interface{}) bool { b, ok := v.(bool) return ok && b } func smInt(v interface{}) int { switch n := v.(type) { case int: return n case int8: return int(n) case int16: return int(n) case int32: return int(n) case int64: return int(n) case uint: return int(n) case uint8: return int(n) case uint16: return int(n) case uint32: return int(n) case uint64: return int(n) case float32: return int(n) case float64: return int(n) default: return 0 } } func smDefaultString(s, def string) string { if strings.TrimSpace(s) == "" { return def } return s } func smDefaultInt(i, def int) int { if i == 0 { return def } return i } // buildOmnxtProtocolLinks wraps each simnet config into a base64 "simnet://" // link, matching the Pro reference's final delivery format (migration 02140, // template.go buildOmnxtProtocolLinks). Template usage: // // {{- range $link := buildOmnxtProtocolLinks .Proxies .UserInfo .Params }}{{ $link }} // {{- end }} func buildOmnxtProtocolLinks(proxies []map[string]interface{}, userInfo User, params map[string]string) []string { configs := buildOmnxtSimnetConfigs(proxies, userInfo, params) result := make([]string, 0, len(configs)) for _, item := range configs { serverAddr := smString(item["server_addr"]) serverPort := smInt(item["server_port"]) tag := smString(item["tag"]) if serverAddr == "" || serverPort == 0 { continue } afEnabled := smBool(item["simnet_af_enabled"]) payload := map[string]interface{}{ "protocol": "simnet", "server_addr": serverAddr, "server_port": serverPort, "sni": smString(item["sni"]), "simnet_psk": smString(item["simnet_psk"]), "simnet_key_id": smInt(item["simnet_key_id"]), "simnet_server_psk": item["simnet_server_psk"], "simnet_server_key_id": smInt(item["simnet_server_key_id"]), "simnet_ticket_id": item["simnet_ticket_id"], "simnet_path": item["simnet_path"], "simnet_carrier": smString(item["simnet_carrier"]), "simnet_af_enabled": afEnabled, "simnet_client_max_concurrent_streams": smInt(item["simnet_client_max_concurrent_streams"]), "simnet_client_max_streams_per_session": smInt(item["simnet_client_max_streams_per_session"]), "simnet_client_session_idle_timeout_secs": smInt(item["simnet_client_session_idle_timeout_secs"]), "simnet_client_max_udp_sessions": smInt(item["simnet_client_max_udp_sessions"]), "proxy_mode": item["proxy_mode"], "dns_servers": item["dns_servers"], } if afEnabled { payload["simnet_af_path_mode"] = smString(item["simnet_af_path_mode"]) payload["simnet_af_path_prefix"] = item["simnet_af_path_prefix"] payload["simnet_af_path_suffix"] = item["simnet_af_path_suffix"] payload["simnet_af_magic_mode"] = smString(item["simnet_af_magic_mode"]) payload["simnet_af_response_jitter_ms"] = smInt(item["simnet_af_response_jitter_ms"]) payload["simnet_af_handshake_polymorphism"] = smBool(item["simnet_af_handshake_polymorphism"]) payload["simnet_af_settings_jitter"] = smBool(item["simnet_af_settings_jitter"]) payload["simnet_af_fake_header_injection"] = smBool(item["simnet_af_fake_header_injection"]) } encoded := encodeProtocolPayload(payload) if encoded == "" { continue } result = append(result, "simnet://"+encoded+"#"+url.QueryEscape(tag)) } return result } // encodeProtocolPayload url-encodes a payload map and base64-encodes it, // matching the reference encodeProtocolPayload. func encodeProtocolPayload(payload map[string]interface{}) string { values := url.Values{} for key, value := range payload { switch v := value.(type) { case nil: continue case string: if strings.TrimSpace(v) != "" { values.Set(key, v) } case bool: if v { values.Set(key, "1") } case int: if v != 0 { values.Set(key, strconv.Itoa(v)) } case int32: if v != 0 { values.Set(key, strconv.FormatInt(int64(v), 10)) } case int64: if v != 0 { values.Set(key, strconv.FormatInt(v, 10)) } case []string: if len(v) > 0 { values.Set(key, strings.Join(v, ",")) } case []interface{}: items := make([]string, 0, len(v)) for _, item := range v { if s := smString(item); s != "" { items = append(items, s) } } if len(items) > 0 { values.Set(key, strings.Join(items, ",")) } default: if s := smString(v); s != "" { values.Set(key, s) } } } if len(values) == 0 { return "" } return base64.StdEncoding.EncodeToString([]byte(values.Encode())) }