From fb362e1fec347fa8d7592dc16053d7715a60c796 Mon Sep 17 00:00:00 2001 From: ShukeBta <272197458+ShukeBta@users.noreply.github.com> Date: Wed, 17 Jun 2026 22:22:46 +0800 Subject: [PATCH] Backfill license usage and restore bot proxy fallbacks --- internal/handler/license.go | 38 +++++++++----- internal/handler/license_test.go | 13 +++++ internal/service/license.go | 1 + internal/service/telegram_api.go | 75 +++++---------------------- internal/service/telegram_api_test.go | 2 +- 5 files changed, 54 insertions(+), 75 deletions(-) diff --git a/internal/handler/license.go b/internal/handler/license.go index ad9ef31..a4a74cd 100644 --- a/internal/handler/license.go +++ b/internal/handler/license.go @@ -98,6 +98,7 @@ func licenseActivateHandler(svc *service.Container) gin.HandlerFunc { return } state := licenseStateFromSigned(upstream, deviceID, deviceName) + state.LicenseKey = strings.TrimSpace(req.Key) if err := persistLicenseState(c.Request.Context(), svc, state); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -111,7 +112,7 @@ func licenseStatusHandler(svc *service.Container) gin.HandlerFunc { state, _ := loadLicenseState(c.Request.Context(), svc) client, err := newLicenseClient(c.Request.Context(), svc) if err == nil { - deviceID, idErr := ensureLicenseDeviceID(c.Request.Context(), svc, "") + deviceID, idErr := ensureLicenseDeviceID(c.Request.Context(), svc, state.DeviceID) if idErr == nil { deviceName, _ := svc.Repo.Setting.Get(c.Request.Context(), licenseDeviceNameSetting) if strings.TrimSpace(deviceName) == "" { @@ -119,12 +120,10 @@ func licenseStatusHandler(svc *service.Container) gin.HandlerFunc { _ = svc.Repo.Setting.Set(c.Request.Context(), licenseDeviceNameSetting, deviceName) } var signed licenseServerSignedResp - if heartbeatErr := client.post(c.Request.Context(), "/api/v1/heartbeat", map[string]any{ - "fingerprint": deviceID, - "instance_id": deviceID, - "device_name": deviceName, - }, &signed); heartbeatErr == nil && client.verifySigned(&signed) == nil { - state = licenseStateFromSigned(signed, deviceID, deviceName) + if heartbeatErr := client.post(c.Request.Context(), "/api/v1/heartbeat", licenseHeartbeatPayload(state, deviceID, deviceName), &signed); heartbeatErr == nil && client.verifySigned(&signed) == nil { + nextState := licenseStateFromSigned(signed, deviceID, deviceName) + nextState.LicenseKey = state.LicenseKey + state = nextState _ = persistLicenseState(c.Request.Context(), svc, state) } else { var upstream licenseServerStatusResp @@ -156,7 +155,8 @@ func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - deviceID, err := ensureLicenseDeviceID(c.Request.Context(), svc, "") + oldState, _ := loadLicenseState(c.Request.Context(), svc) + deviceID, err := ensureLicenseDeviceID(c.Request.Context(), svc, oldState.DeviceID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -167,11 +167,7 @@ func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc { _ = svc.Repo.Setting.Set(c.Request.Context(), licenseDeviceNameSetting, deviceName) } var upstream licenseServerSignedResp - if err := client.post(c.Request.Context(), "/api/v1/heartbeat", map[string]any{ - "fingerprint": deviceID, - "instance_id": deviceID, - "device_name": deviceName, - }, &upstream); err != nil { + if err := client.post(c.Request.Context(), "/api/v1/heartbeat", licenseHeartbeatPayload(oldState, deviceID, deviceName), &upstream); err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } @@ -180,6 +176,7 @@ func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc { return } state := licenseStateFromSigned(upstream, deviceID, deviceName) + state.LicenseKey = oldState.LicenseKey if err := persistLicenseState(c.Request.Context(), svc, state); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -373,6 +370,18 @@ func licenseStateFromSigned(resp licenseServerSignedResp, deviceID, deviceName s } } +func licenseHeartbeatPayload(state service.LicenseActivationState, deviceID, deviceName string) map[string]any { + payload := map[string]any{ + "fingerprint": deviceID, + "instance_id": deviceID, + "device_name": deviceName, + } + if key := strings.TrimSpace(state.LicenseKey); key != "" { + payload["key"] = key + } + return payload +} + func licenseStatusMaxUsers(state service.LicenseActivationState) any { active := state.Valid && !licenseStateExpired(state.ExpiryDate) if active { @@ -415,6 +424,9 @@ func persistLicenseState(ctx context.Context, svc *service.Container, state serv if err != nil { return err } + if strings.TrimSpace(state.DeviceID) != "" { + _ = svc.Repo.Setting.Set(ctx, licenseDeviceIDSetting, strings.TrimSpace(state.DeviceID)) + } return svc.Repo.Setting.Set(ctx, service.LicenseSettingActivation, string(data)) } diff --git a/internal/handler/license_test.go b/internal/handler/license_test.go index c20a699..9157efe 100644 --- a/internal/handler/license_test.go +++ b/internal/handler/license_test.go @@ -77,3 +77,16 @@ func TestApplyLicenseStatusReflectsUnlimitedUsers(t *testing.T) { t.Fatalf("unlimited status should clear previous finite user limit: %+v", state) } } + +func TestLicenseHeartbeatPayloadIncludesStoredLicenseKey(t *testing.T) { + payload := licenseHeartbeatPayload(service.LicenseActivationState{ + LicenseKey: "MS-ABCD-EFGH-JKLM-NPQR", + }, "device-1", "NAS") + + if payload["fingerprint"] != "device-1" || payload["instance_id"] != "device-1" || payload["device_name"] != "NAS" { + t.Fatalf("heartbeat identity payload is wrong: %#v", payload) + } + if payload["key"] != "MS-ABCD-EFGH-JKLM-NPQR" { + t.Fatalf("heartbeat should include stored license key for server-side backfill: %#v", payload) + } +} diff --git a/internal/service/license.go b/internal/service/license.go index 94c0003..63eb145 100644 --- a/internal/service/license.go +++ b/internal/service/license.go @@ -18,6 +18,7 @@ const ( type LicenseActivationState struct { Valid bool `json:"valid"` + LicenseKey string `json:"license_key,omitempty"` LicenseType string `json:"license_type,omitempty"` ExpiryDate string `json:"expiry_date,omitempty"` MaxDevices int `json:"max_devices,omitempty"` diff --git a/internal/service/telegram_api.go b/internal/service/telegram_api.go index e7570fe..d1c591f 100644 --- a/internal/service/telegram_api.go +++ b/internal/service/telegram_api.go @@ -53,47 +53,26 @@ func telegramHTTPClient(timeout time.Duration, cfg map[string]string) *http.Clie func telegramHTTPClients(timeout time.Duration, cfg map[string]string) []*http.Client { clients := []*http.Client{} seen := map[string]bool{} - - addProxy := func(proxyURL *url.URL) { - if proxyURL == nil { - return + for _, proxyRaw := range telegramProxyCandidates(cfg) { + proxyURL, err := normalizeProxyURL(proxyRaw, "http") + if err != nil || proxyURL == nil { + continue } key := proxyURL.String() if seen[key] { - return + continue } seen[key] = true transport := NewExternalTransport() transport.Proxy = http.ProxyURL(proxyURL) clients = append(clients, &http.Client{Timeout: timeout, Transport: transport}) } - - for _, proxyRaw := range telegramExplicitProxyCandidates(cfg) { - proxyURL, err := normalizeProxyURL(proxyRaw, "http") - if err != nil || proxyURL == nil { - continue - } - addProxy(proxyURL) - } - if proxyURL, err := telegramAutoProxyURL(cfg); err == nil { - addProxy(proxyURL) - } - if telegramAPIBaseURL(cfg) == defaultTelegramAPIBaseURL { - for _, proxyRaw := range telegramFallbackProxyCandidates() { - proxyURL, err := normalizeProxyURL(proxyRaw, "http") - if err != nil || proxyURL == nil { - continue - } - addProxy(proxyURL) - } - } transport := NewExternalTransport() - transport.Proxy = nil clients = append(clients, &http.Client{Timeout: timeout, Transport: transport}) return clients } -func telegramExplicitProxyCandidates(cfg map[string]string) []string { +func telegramProxyCandidates(cfg map[string]string) []string { out := []string{} for _, value := range []string{ cfg["proxy_url"], @@ -103,48 +82,22 @@ func telegramExplicitProxyCandidates(cfg map[string]string) []string { out = append(out, value) } } - return out -} - -func telegramProxyCandidates(cfg map[string]string) []string { - out := telegramExplicitProxyCandidates(cfg) if len(out) > 0 { return out } - return telegramFallbackProxyCandidates() -} - -func telegramFallbackProxyCandidates() []string { - // Common local proxy ports used by Clash / v2rayN / v2rayA. Docker on Linux - // reaches host services through host.docker.internal (when extra_hosts is - // configured) or the bridge gateway 172.17.0.1. These are only fallbacks: - // explicit channel/env proxy and environment/system proxy are tried first. - return []string{ - "http://host.docker.internal:20171", - "socks5://host.docker.internal:20170", - "http://172.17.0.1:20171", - "socks5://172.17.0.1:20170", - "http://host.docker.internal:7890", - "http://host.docker.internal:10808", - "http://172.17.0.1:7890", - "http://172.17.0.1:10808", + for _, value := range []string{ "http://127.0.0.1:10808", "http://127.0.0.1:10809", "http://127.0.0.1:7890", "http://127.0.0.1:7891", + "http://host.docker.internal:7890", + "http://host.docker.internal:10808", + "http://172.17.0.1:7890", + "http://172.17.0.1:10808", + } { + out = append(out, value) } -} - -func telegramAutoProxyURL(cfg map[string]string) (*url.URL, error) { - base := telegramAPIBaseURL(cfg) - if _, err := url.ParseRequestURI(base); err != nil { - return nil, err - } - req, err := http.NewRequest(http.MethodGet, base, nil) - if err != nil { - return nil, err - } - return ProxyFromEnvironmentOrSystem(req) + return out } func telegramPostForm(ctx context.Context, cfg map[string]string, method string, form url.Values, timeout time.Duration) error { diff --git a/internal/service/telegram_api_test.go b/internal/service/telegram_api_test.go index 10f8647..70aa189 100644 --- a/internal/service/telegram_api_test.go +++ b/internal/service/telegram_api_test.go @@ -242,7 +242,7 @@ func telegramCommandNames(commands []telegramBotCommand) map[string]bool { func TestTelegramProxyCandidatesDefaultLocalFallbacks(t *testing.T) { got := telegramProxyCandidates(map[string]string{}) joined := strings.Join(got, ",") - for _, want := range []string{"host.docker.internal:20171", "socks5://172.17.0.1:20170", "127.0.0.1:10808", "172.17.0.1:7890"} { + for _, want := range []string{"127.0.0.1:10808", "127.0.0.1:7890", "host.docker.internal:7890", "172.17.0.1:7890"} { if !strings.Contains(joined, want) { t.Fatalf("default proxy candidates %q missing %q", joined, want) }