package services // opsx-licence 客户端:activate / verify / heartbeat。 // 默认 Base = https://licence.rlwj.cn/api;product_code = lokistack。 // 响应若带 Ed25519 sig 则验签(公钥与 OpsxShell 同源)。 import ( "bytes" "crypto/ed25519" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" "lokistack/config" ) const ( licenceProductCode = "lokistack" licenceHTTPTimeout = 15 * time.Second // 与 OpsxShell / opsx-licence OFFLINE_LICENSE_SEED 派生公钥一致 offlineLicensePublicKeyHex = "108375d485c58a1a738f93c2cbb838f76331de981ee5da13d90a1f280672131d" offlineToleranceSecs = int64(30 * 86400) weeklyVerifySecs = int64(7 * 86400) hmacDeprecationNotice = "当前为旧版本地 HMAC 许可码,将逐步停用。请联系发卡方换发 LIC- / LICOFF-(opsx-licence)授权码;迁移窗口内旧码仍有效,不会静默失效。" ) type licenceRemoteInfo struct { Valid bool LicenseKey string ProductCode string Features []string Status string ExpiresAtUnix int64 MaxDevices int PlanType string InGrace bool GraceDaysLeft int ServerTime int64 ActivationToken string Message string } func licenceAPIBases() []string { primary := strings.TrimRight(config.GetConfig().LicenseAPIBase, "/") bases := []string{primary} // 仅当主节点不是 l123 时把 l123 作备灾(网络失败才切) backup := "https://l123.91ai.io/api" if primary != backup && !strings.Contains(primary, "l123.91ai.io") { bases = append(bases, backup) } return bases } func isOpsxLicenseKey(key string) bool { k := strings.TrimSpace(key) return strings.HasPrefix(k, "LIC-") || strings.HasPrefix(k, "LICOFF-") } func isOfflineLicenseKey(key string) bool { return strings.HasPrefix(strings.TrimSpace(key), "LICOFF-") } func licenceHTTPClient() *http.Client { return &http.Client{Timeout: licenceHTTPTimeout} } func postLicenceJSON(path string, body any) (map[string]any, error) { payload, err := json.Marshal(body) if err != nil { return nil, err } var lastErr error for _, base := range licenceAPIBases() { url := base + path req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(payload)) if err != nil { lastErr = err continue } req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "LokiStack/1") resp, err := licenceHTTPClient().Do(req) if err != nil { lastErr = fmt.Errorf("授权服务不可达: %w", err) continue } raw, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) _ = resp.Body.Close() if readErr != nil { lastErr = readErr continue } var envelope map[string]any if err := json.Unmarshal(raw, &envelope); err != nil { lastErr = fmt.Errorf("授权服务响应无法解析 (%s)", resp.Status) continue } return envelope, nil } if lastErr == nil { lastErr = fmt.Errorf("无可用授权节点") } return nil, lastErr } func envelopeCode(m map[string]any) int { switch v := m["code"].(type) { case float64: return int(v) case int: return v default: return 0 } } func envelopeMessage(m map[string]any) string { if s, ok := m["message"].(string); ok { return s } return "" } func envelopeData(m map[string]any) map[string]any { if d, ok := m["data"].(map[string]any); ok { return d } return m } func jsonString(m map[string]any, keys ...string) string { for _, k := range keys { if s, ok := m[k].(string); ok && s != "" { return s } } return "" } func jsonInt(m map[string]any, keys ...string) int { for _, k := range keys { switch v := m[k].(type) { case float64: return int(v) case int: return v case json.Number: n, _ := v.Int64() return int(n) } } return 0 } func jsonInt64(m map[string]any, keys ...string) int64 { for _, k := range keys { switch v := m[k].(type) { case float64: return int64(v) case int64: return v case int: return int64(v) } } return 0 } func jsonBool(m map[string]any, keys ...string) bool { for _, k := range keys { if b, ok := m[k].(bool); ok { return b } } return false } func parseRemoteInfo(data map[string]any) licenceRemoteInfo { features := []string{} if arr, ok := data["features"].([]any); ok { for _, item := range arr { if s, ok := item.(string); ok { features = append(features, s) } } } return licenceRemoteInfo{ Valid: jsonBool(data, "valid"), LicenseKey: jsonString(data, "license_key", "licenseKey"), ProductCode: jsonString(data, "product_code", "productCode"), Features: features, Status: jsonString(data, "status"), ExpiresAtUnix: jsonInt64(data, "expires_at_unix", "expiresAtUnix"), MaxDevices: jsonInt(data, "max_devices", "maxDevices"), PlanType: jsonString(data, "plan_type", "planType"), InGrace: jsonBool(data, "in_grace", "inGrace"), GraceDaysLeft: jsonInt(data, "grace_days_left", "graceDaysLeft"), ServerTime: jsonInt64(data, "server_time", "serverTime"), ActivationToken: jsonString(data, "activation_token", "activationToken"), Message: jsonString(data, "message"), } } func verifyLicenceResponseSig(licenseKey string, data map[string]any) error { sigHex := jsonString(data, "sig") if sigHex == "" { // 开发环境可能未配 OFFLINE_LICENSE_SEED;生产应有签名 return nil } pub, err := hex.DecodeString(offlineLicensePublicKeyHex) if err != nil || len(pub) != ed25519.PublicKeySize { return fmt.Errorf("离线验签公钥配置错误") } sig, err := hex.DecodeString(sigHex) if err != nil { return fmt.Errorf("授权响应签名无效") } valid := jsonBool(data, "valid") status := jsonString(data, "status") expiresUnix := jsonInt64(data, "expires_at_unix", "expiresAtUnix") serverTime := jsonInt64(data, "server_time", "serverTime") inGrace := jsonBool(data, "in_grace", "inGrace") maxDevices := jsonInt(data, "max_devices", "maxDevices") planType := jsonString(data, "plan_type", "planType") msg := "v1|" + licenseKey + "|" + strconv.FormatBool(valid) + "|" + status + "|" + strconv.FormatInt(expiresUnix, 10) + "|" + strconv.FormatInt(serverTime, 10) + "|" + strconv.FormatBool(inGrace) + "|" + strconv.Itoa(maxDevices) + "|" + planType if !ed25519.Verify(ed25519.PublicKey(pub), []byte(msg), sig) { return fmt.Errorf("授权响应签名校验失败(疑似假服务器)") } return nil } func remoteActivateLicence(licenseKey, deviceID string) (*licenceRemoteInfo, error) { body := map[string]string{ "license_key": licenseKey, "device_id": deviceID, "device_name": "LokiStack", } env, err := postLicenceJSON("/v1/license/activate", body) if err != nil { return nil, err } if envelopeCode(env) != 0 { msg := envelopeMessage(env) if msg == "" { msg = "激活失败" } return nil, fmt.Errorf("%s", msg) } data := envelopeData(env) if err := verifyLicenceResponseSig(licenseKey, data); err != nil { return nil, err } info := parseRemoteInfo(data) if info.LicenseKey == "" { info.LicenseKey = licenseKey } if !info.Valid && info.Status == "" { // activate payload 可能不带 valid,视为成功 info.Valid = true } return &info, nil } func remoteVerifyLicence(licenseKey, deviceID string) (*licenceRemoteInfo, error) { body := map[string]string{ "license_key": licenseKey, "device_id": deviceID, } env, err := postLicenceJSON("/v1/license/verify", body) if err != nil { return nil, fmt.Errorf("network:%w", err) } // verify 可能是裸对象或 envelope var data map[string]any if _, ok := env["valid"]; ok { data = env } else if envelopeCode(env) == 0 { data = envelopeData(env) } else if c := envelopeCode(env); c != 0 { msg := envelopeMessage(env) if msg == "" { msg = "验证失败" } return nil, fmt.Errorf("rejected:%s", msg) } else { data = env } info := parseRemoteInfo(data) if info.LicenseKey == "" { info.LicenseKey = licenseKey } if !info.Valid { msg := info.Message if msg == "" { msg = envelopeMessage(env) } if msg == "" { msg = "许可证无效" } return nil, fmt.Errorf("rejected:%s", msg) } if err := verifyLicenceResponseSig(licenseKey, data); err != nil { return nil, fmt.Errorf("rejected:%v", err) } return &info, nil } func remoteHeartbeatLicence(licenseKey, deviceID string) error { body := map[string]string{ "license_key": licenseKey, "device_id": deviceID, } env, err := postLicenceJSON("/v1/license/heartbeat", body) if err != nil { return err } if envelopeCode(env) != 0 { return fmt.Errorf("%s", envelopeMessage(env)) } return nil }