mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-05 01:26:37 +08:00
49ab2915ee
* fix: increase updateTunnel timeout to 120s Editing tunnel entry nodes triggers forward sync to all entry nodes. If nodes are offline or many forwards exist, the sync can exceed the default 30s timeout. Match the timeout used by other heavy operations like batchDeleteTunnels. * docs: add commercial white-label design spec * docs: add commercial white-label implementation plan * feat: add license activation endpoint and authorization check for commercial config keys * feat: add frontend api and update site config state for license * feat: conditionally hide flvx footer brand * feat: ui settings for commercial white-label and license activation * fix: add missing licenseActivateRequest and fix GetConfig in handler.go * docs: add keygen.sh license integration design spec * docs: add keygen.sh integration implementation plan * feat: add machine fingerprint generation * feat: add keygen.sh api client * feat: integrate keygen into license activation endpoint * feat: add periodic license validation job * fix: remove accidentally leaked dash kernel test codes that caused compilation failures * fix: correct keygen validation scope and binding logic * feat: hardcode Keygen.sh account ID * fix: relax strict validation matching after successful machine activation
184 lines
4.7 KiB
Go
184 lines
4.7 KiB
Go
package license
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type KeygenClient struct {
|
|
AccountID string
|
|
Token string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
func NewKeygenClient(accountID, token string) *KeygenClient {
|
|
return &KeygenClient{
|
|
AccountID: accountID,
|
|
Token: token,
|
|
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
|
}
|
|
}
|
|
|
|
type ValidateResponse struct {
|
|
Meta struct {
|
|
Valid bool `json:"valid"`
|
|
Code string `json:"code"`
|
|
} `json:"meta"`
|
|
Data struct {
|
|
ID string `json:"id"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type ActivateMachineRequest struct {
|
|
Data struct {
|
|
Type string `json:"type"`
|
|
Attributes struct {
|
|
Fingerprint string `json:"fingerprint"`
|
|
} `json:"attributes"`
|
|
Relationships struct {
|
|
License struct {
|
|
Data struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id"`
|
|
} `json:"data"`
|
|
} `json:"license"`
|
|
} `json:"relationships"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
func (c *KeygenClient) ValidateKeyWithFingerprint(key string, fingerprint string) (*ValidateResponse, error) {
|
|
url := fmt.Sprintf("https://api.keygen.sh/v1/accounts/%s/licenses/actions/validate-key", c.AccountID)
|
|
|
|
meta := map[string]interface{}{
|
|
"key": key,
|
|
}
|
|
|
|
if fingerprint != "" {
|
|
meta["scope"] = map[string]interface{}{
|
|
"fingerprint": fingerprint,
|
|
}
|
|
}
|
|
|
|
reqBody := map[string]interface{}{
|
|
"meta": meta,
|
|
}
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
|
|
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
if c.Token != "" {
|
|
if !strings.HasPrefix(c.Token, "Bearer ") && !strings.HasPrefix(c.Token, "License ") {
|
|
req.Header.Set("Authorization", "License "+c.Token)
|
|
} else {
|
|
req.Header.Set("Authorization", c.Token)
|
|
}
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("keygen api error: status %d", resp.StatusCode)
|
|
}
|
|
|
|
var valResp ValidateResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&valResp); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &valResp, nil
|
|
}
|
|
|
|
func (c *KeygenClient) ValidateKey(key string) (*ValidateResponse, error) {
|
|
url := fmt.Sprintf("https://api.keygen.sh/v1/accounts/%s/licenses/actions/validate-key", c.AccountID)
|
|
|
|
reqBody := map[string]interface{}{
|
|
"meta": map[string]string{
|
|
"key": key,
|
|
},
|
|
}
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
|
|
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
if c.Token != "" {
|
|
if !strings.HasPrefix(c.Token, "Bearer ") && !strings.HasPrefix(c.Token, "License ") {
|
|
req.Header.Set("Authorization", "License "+c.Token)
|
|
} else {
|
|
req.Header.Set("Authorization", c.Token)
|
|
}
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return nil, fmt.Errorf("keygen api error: status %d", resp.StatusCode)
|
|
}
|
|
|
|
var valResp ValidateResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&valResp); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &valResp, nil
|
|
}
|
|
|
|
func (c *KeygenClient) ActivateMachine(licenseID, fingerprint string) error {
|
|
url := fmt.Sprintf("https://api.keygen.sh/v1/accounts/%s/machines", c.AccountID)
|
|
|
|
var reqBody ActivateMachineRequest
|
|
reqBody.Data.Type = "machines"
|
|
reqBody.Data.Attributes.Fingerprint = fingerprint
|
|
reqBody.Data.Relationships.License.Data.Type = "licenses"
|
|
reqBody.Data.Relationships.License.Data.ID = licenseID
|
|
|
|
bodyBytes, _ := json.Marshal(reqBody)
|
|
|
|
req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(bodyBytes))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
if c.Token != "" {
|
|
if !strings.HasPrefix(c.Token, "Bearer ") && !strings.HasPrefix(c.Token, "License ") {
|
|
req.Header.Set("Authorization", "License "+c.Token)
|
|
} else {
|
|
req.Header.Set("Authorization", c.Token)
|
|
}
|
|
}
|
|
|
|
resp, err := c.HTTPClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK {
|
|
return nil
|
|
}
|
|
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
if resp.StatusCode == http.StatusConflict || resp.StatusCode == http.StatusUnprocessableEntity {
|
|
if strings.Contains(string(body), "FINGERPRINT_TAKEN") || strings.Contains(string(body), "MACHINE_LIMIT_EXCEEDED") {
|
|
// Machine already registered to this license or limit reached because it's already us.
|
|
// The subsequent ValidateKey check will determine if the existing machine is actually us.
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("failed to activate machine: status %d, response: %s", resp.StatusCode, string(body))
|
|
} |