mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-05 21:06:38 +08:00
Backfill license usage and restore bot proxy fallbacks
This commit is contained in:
+25
-13
@@ -98,6 +98,7 @@ func licenseActivateHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
state := licenseStateFromSigned(upstream, deviceID, deviceName)
|
state := licenseStateFromSigned(upstream, deviceID, deviceName)
|
||||||
|
state.LicenseKey = strings.TrimSpace(req.Key)
|
||||||
if err := persistLicenseState(c.Request.Context(), svc, state); err != nil {
|
if err := persistLicenseState(c.Request.Context(), svc, state); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -111,7 +112,7 @@ func licenseStatusHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
state, _ := loadLicenseState(c.Request.Context(), svc)
|
state, _ := loadLicenseState(c.Request.Context(), svc)
|
||||||
client, err := newLicenseClient(c.Request.Context(), svc)
|
client, err := newLicenseClient(c.Request.Context(), svc)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
deviceID, idErr := ensureLicenseDeviceID(c.Request.Context(), svc, "")
|
deviceID, idErr := ensureLicenseDeviceID(c.Request.Context(), svc, state.DeviceID)
|
||||||
if idErr == nil {
|
if idErr == nil {
|
||||||
deviceName, _ := svc.Repo.Setting.Get(c.Request.Context(), licenseDeviceNameSetting)
|
deviceName, _ := svc.Repo.Setting.Get(c.Request.Context(), licenseDeviceNameSetting)
|
||||||
if strings.TrimSpace(deviceName) == "" {
|
if strings.TrimSpace(deviceName) == "" {
|
||||||
@@ -119,12 +120,10 @@ func licenseStatusHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
_ = svc.Repo.Setting.Set(c.Request.Context(), licenseDeviceNameSetting, deviceName)
|
_ = svc.Repo.Setting.Set(c.Request.Context(), licenseDeviceNameSetting, deviceName)
|
||||||
}
|
}
|
||||||
var signed licenseServerSignedResp
|
var signed licenseServerSignedResp
|
||||||
if heartbeatErr := client.post(c.Request.Context(), "/api/v1/heartbeat", map[string]any{
|
if heartbeatErr := client.post(c.Request.Context(), "/api/v1/heartbeat", licenseHeartbeatPayload(state, deviceID, deviceName), &signed); heartbeatErr == nil && client.verifySigned(&signed) == nil {
|
||||||
"fingerprint": deviceID,
|
nextState := licenseStateFromSigned(signed, deviceID, deviceName)
|
||||||
"instance_id": deviceID,
|
nextState.LicenseKey = state.LicenseKey
|
||||||
"device_name": deviceName,
|
state = nextState
|
||||||
}, &signed); heartbeatErr == nil && client.verifySigned(&signed) == nil {
|
|
||||||
state = licenseStateFromSigned(signed, deviceID, deviceName)
|
|
||||||
_ = persistLicenseState(c.Request.Context(), svc, state)
|
_ = persistLicenseState(c.Request.Context(), svc, state)
|
||||||
} else {
|
} else {
|
||||||
var upstream licenseServerStatusResp
|
var upstream licenseServerStatusResp
|
||||||
@@ -156,7 +155,8 @@ func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
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 {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
@@ -167,11 +167,7 @@ func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
_ = svc.Repo.Setting.Set(c.Request.Context(), licenseDeviceNameSetting, deviceName)
|
_ = svc.Repo.Setting.Set(c.Request.Context(), licenseDeviceNameSetting, deviceName)
|
||||||
}
|
}
|
||||||
var upstream licenseServerSignedResp
|
var upstream licenseServerSignedResp
|
||||||
if err := client.post(c.Request.Context(), "/api/v1/heartbeat", map[string]any{
|
if err := client.post(c.Request.Context(), "/api/v1/heartbeat", licenseHeartbeatPayload(oldState, deviceID, deviceName), &upstream); err != nil {
|
||||||
"fingerprint": deviceID,
|
|
||||||
"instance_id": deviceID,
|
|
||||||
"device_name": deviceName,
|
|
||||||
}, &upstream); err != nil {
|
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -180,6 +176,7 @@ func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
state := licenseStateFromSigned(upstream, deviceID, deviceName)
|
state := licenseStateFromSigned(upstream, deviceID, deviceName)
|
||||||
|
state.LicenseKey = oldState.LicenseKey
|
||||||
if err := persistLicenseState(c.Request.Context(), svc, state); err != nil {
|
if err := persistLicenseState(c.Request.Context(), svc, state); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
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 {
|
func licenseStatusMaxUsers(state service.LicenseActivationState) any {
|
||||||
active := state.Valid && !licenseStateExpired(state.ExpiryDate)
|
active := state.Valid && !licenseStateExpired(state.ExpiryDate)
|
||||||
if active {
|
if active {
|
||||||
@@ -415,6 +424,9 @@ func persistLicenseState(ctx context.Context, svc *service.Container, state serv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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))
|
return svc.Repo.Setting.Set(ctx, service.LicenseSettingActivation, string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -77,3 +77,16 @@ func TestApplyLicenseStatusReflectsUnlimitedUsers(t *testing.T) {
|
|||||||
t.Fatalf("unlimited status should clear previous finite user limit: %+v", state)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const (
|
|||||||
|
|
||||||
type LicenseActivationState struct {
|
type LicenseActivationState struct {
|
||||||
Valid bool `json:"valid"`
|
Valid bool `json:"valid"`
|
||||||
|
LicenseKey string `json:"license_key,omitempty"`
|
||||||
LicenseType string `json:"license_type,omitempty"`
|
LicenseType string `json:"license_type,omitempty"`
|
||||||
ExpiryDate string `json:"expiry_date,omitempty"`
|
ExpiryDate string `json:"expiry_date,omitempty"`
|
||||||
MaxDevices int `json:"max_devices,omitempty"`
|
MaxDevices int `json:"max_devices,omitempty"`
|
||||||
|
|||||||
@@ -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 {
|
func telegramHTTPClients(timeout time.Duration, cfg map[string]string) []*http.Client {
|
||||||
clients := []*http.Client{}
|
clients := []*http.Client{}
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
|
for _, proxyRaw := range telegramProxyCandidates(cfg) {
|
||||||
addProxy := func(proxyURL *url.URL) {
|
proxyURL, err := normalizeProxyURL(proxyRaw, "http")
|
||||||
if proxyURL == nil {
|
if err != nil || proxyURL == nil {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
key := proxyURL.String()
|
key := proxyURL.String()
|
||||||
if seen[key] {
|
if seen[key] {
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
seen[key] = true
|
seen[key] = true
|
||||||
transport := NewExternalTransport()
|
transport := NewExternalTransport()
|
||||||
transport.Proxy = http.ProxyURL(proxyURL)
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
|
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 := NewExternalTransport()
|
||||||
transport.Proxy = nil
|
|
||||||
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
|
clients = append(clients, &http.Client{Timeout: timeout, Transport: transport})
|
||||||
return clients
|
return clients
|
||||||
}
|
}
|
||||||
|
|
||||||
func telegramExplicitProxyCandidates(cfg map[string]string) []string {
|
func telegramProxyCandidates(cfg map[string]string) []string {
|
||||||
out := []string{}
|
out := []string{}
|
||||||
for _, value := range []string{
|
for _, value := range []string{
|
||||||
cfg["proxy_url"],
|
cfg["proxy_url"],
|
||||||
@@ -103,48 +82,22 @@ func telegramExplicitProxyCandidates(cfg map[string]string) []string {
|
|||||||
out = append(out, value)
|
out = append(out, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func telegramProxyCandidates(cfg map[string]string) []string {
|
|
||||||
out := telegramExplicitProxyCandidates(cfg)
|
|
||||||
if len(out) > 0 {
|
if len(out) > 0 {
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
return telegramFallbackProxyCandidates()
|
for _, value := range []string{
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
"http://127.0.0.1:10808",
|
"http://127.0.0.1:10808",
|
||||||
"http://127.0.0.1:10809",
|
"http://127.0.0.1:10809",
|
||||||
"http://127.0.0.1:7890",
|
"http://127.0.0.1:7890",
|
||||||
"http://127.0.0.1:7891",
|
"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)
|
||||||
}
|
}
|
||||||
}
|
return out
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func telegramPostForm(ctx context.Context, cfg map[string]string, method string, form url.Values, timeout time.Duration) error {
|
func telegramPostForm(ctx context.Context, cfg map[string]string, method string, form url.Values, timeout time.Duration) error {
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ func telegramCommandNames(commands []telegramBotCommand) map[string]bool {
|
|||||||
func TestTelegramProxyCandidatesDefaultLocalFallbacks(t *testing.T) {
|
func TestTelegramProxyCandidatesDefaultLocalFallbacks(t *testing.T) {
|
||||||
got := telegramProxyCandidates(map[string]string{})
|
got := telegramProxyCandidates(map[string]string{})
|
||||||
joined := strings.Join(got, ",")
|
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) {
|
if !strings.Contains(joined, want) {
|
||||||
t.Fatalf("default proxy candidates %q missing %q", joined, want)
|
t.Fatalf("default proxy candidates %q missing %q", joined, want)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user