mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
294 lines
7.4 KiB
Go
294 lines
7.4 KiB
Go
package license
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var AccountID string
|
|
|
|
type KeygenClient struct {
|
|
AccountID string
|
|
Token string
|
|
BaseURL string
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
type APIError struct {
|
|
Operation string
|
|
StatusCode int
|
|
Body string
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
return fmt.Sprintf("keygen %s failed: status %d, response: %s", e.Operation, e.StatusCode, e.Body)
|
|
}
|
|
|
|
func (e *APIError) HasCode(code string) bool {
|
|
return e != nil && hasKeygenErrorCode([]byte(e.Body), code)
|
|
}
|
|
|
|
const defaultAPIBaseURL = "https://api.keygen.sh/v1"
|
|
|
|
func NewKeygenClient(accountID, token string) *KeygenClient {
|
|
return &KeygenClient{
|
|
AccountID: accountID,
|
|
Token: token,
|
|
BaseURL: defaultAPIBaseURL,
|
|
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (c *KeygenClient) apiURL(path string) string {
|
|
baseURL := strings.TrimRight(c.BaseURL, "/")
|
|
if baseURL == "" {
|
|
baseURL = defaultAPIBaseURL
|
|
}
|
|
return fmt.Sprintf("%s/accounts/%s/%s", baseURL, c.AccountID, strings.TrimLeft(path, "/"))
|
|
}
|
|
|
|
type ValidateResponse struct {
|
|
Meta struct {
|
|
Valid bool `json:"valid"`
|
|
Code string `json:"code"`
|
|
} `json:"meta"`
|
|
Data struct {
|
|
ID string `json:"id"`
|
|
Attributes struct {
|
|
Expiry string `json:"expiry"`
|
|
} `json:"attributes"`
|
|
} `json:"data"`
|
|
MachineID string `json:"-"`
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
type keygenErrorResponse struct {
|
|
Errors []struct {
|
|
Code string `json:"code"`
|
|
} `json:"errors"`
|
|
}
|
|
|
|
type MachineResponse struct {
|
|
Data struct {
|
|
ID string `json:"id"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
func hasKeygenErrorCode(body []byte, code string) bool {
|
|
var resp keygenErrorResponse
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return false
|
|
}
|
|
for _, item := range resp.Errors {
|
|
if strings.EqualFold(strings.TrimSpace(item.Code), code) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *KeygenClient) ValidateKeyWithFingerprint(key string, fingerprint string) (*ValidateResponse, error) {
|
|
return c.ValidateKeyWithMachine(key, fingerprint, "")
|
|
}
|
|
|
|
func (c *KeygenClient) ValidateKeyWithMachine(key, fingerprint, machineID string) (*ValidateResponse, error) {
|
|
url := c.apiURL("licenses/actions/validate-key")
|
|
|
|
meta := map[string]interface{}{
|
|
"key": key,
|
|
}
|
|
|
|
if fingerprint != "" {
|
|
meta["scope"] = map[string]interface{}{
|
|
"fingerprint": fingerprint,
|
|
}
|
|
}
|
|
if machineID != "" {
|
|
scope, _ := meta["scope"].(map[string]interface{})
|
|
if scope == nil {
|
|
scope = make(map[string]interface{})
|
|
meta["scope"] = scope
|
|
}
|
|
scope["machine"] = machineID
|
|
}
|
|
|
|
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 {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, &APIError{Operation: "validate license", StatusCode: resp.StatusCode, Body: string(body)}
|
|
}
|
|
|
|
var valResp ValidateResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&valResp); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &valResp, nil
|
|
}
|
|
|
|
func (c *KeygenClient) GetMachineID(fingerprint string) (string, error) {
|
|
machineURL := c.apiURL("machines/" + url.PathEscape(fingerprint))
|
|
req, err := http.NewRequest(http.MethodGet, machineURL, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
req.Header.Set("Accept", "application/vnd.api+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.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return "", &APIError{Operation: "retrieve machine", StatusCode: resp.StatusCode, Body: string(body)}
|
|
}
|
|
|
|
var machineResp MachineResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&machineResp); err != nil {
|
|
return "", err
|
|
}
|
|
machineID := strings.TrimSpace(machineResp.Data.ID)
|
|
if machineID == "" {
|
|
return "", fmt.Errorf("failed to retrieve machine: empty machine id")
|
|
}
|
|
return machineID, nil
|
|
}
|
|
|
|
func (c *KeygenClient) ValidateKey(key string) (*ValidateResponse, error) {
|
|
url := c.apiURL("licenses/actions/validate-key")
|
|
|
|
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 {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, &APIError{Operation: "validate license", StatusCode: resp.StatusCode, Body: string(body)}
|
|
}
|
|
|
|
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) (string, error) {
|
|
url := c.apiURL("machines")
|
|
|
|
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()
|
|
body, _ := io.ReadAll(resp.Body)
|
|
|
|
if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK {
|
|
var machineResp MachineResponse
|
|
if json.Unmarshal(body, &machineResp) == nil {
|
|
return strings.TrimSpace(machineResp.Data.ID), nil
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusUnprocessableEntity && hasKeygenErrorCode(body, "FINGERPRINT_TAKEN") {
|
|
// Machine activation is idempotent. Keygen scopes fingerprint uniqueness
|
|
// to the target license, so this means the same machine is already bound.
|
|
return "", nil
|
|
}
|
|
|
|
return "", &APIError{Operation: "activate machine", StatusCode: resp.StatusCode, Body: string(body)}
|
|
}
|