mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
168 lines
6.2 KiB
Go
168 lines
6.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/ShukeBta/MediaStationGo/internal/service"
|
|
)
|
|
|
|
const (
|
|
licenseServerURLSetting = "license.server_url"
|
|
licenseHMACSecretSetting = "license.hmac_secret" // #nosec G101 -- setting key name, not the HMAC secret value.
|
|
licenseDeviceIDSetting = "license.device_id"
|
|
licenseDeviceNameSetting = "license.device_name"
|
|
)
|
|
|
|
type licenseActivateReq struct {
|
|
Key string `json:"key" binding:"required"`
|
|
// DeviceID is accepted for wire compatibility with older web clients but is
|
|
// intentionally ignored. Licensing binds to this MediaStationGo server
|
|
// instance, not to the browser that opened the admin page.
|
|
DeviceID string `json:"device_id"`
|
|
DeviceName string `json:"device_name"`
|
|
}
|
|
|
|
type licenseServerSignedResp struct {
|
|
Valid bool `json:"valid"`
|
|
LicenseType string `json:"license_type"`
|
|
ExpiryDate *string `json:"expiry_date"`
|
|
MaxDevices int `json:"max_devices"`
|
|
MaxUsers *int `json:"max_users"`
|
|
DaysRemaining *int `json:"days_remaining"`
|
|
NextHeartbeat string `json:"next_heartbeat"`
|
|
Signature string `json:"signature"`
|
|
SignatureAlg string `json:"signature_alg"`
|
|
LegacySignature bool `json:"-"`
|
|
}
|
|
|
|
type licenseServerStatusResp struct {
|
|
Valid bool `json:"valid"`
|
|
LicenseType *string `json:"license_type"`
|
|
ExpiryDate *string `json:"expiry_date"`
|
|
MaxDevices int `json:"max_devices"`
|
|
MaxUsers *int `json:"max_users"`
|
|
UnlimitedUsers bool `json:"unlimited_users"`
|
|
DaysRemaining *int `json:"days_remaining"`
|
|
DeviceName string `json:"device_name"`
|
|
IsActive bool `json:"is_active"`
|
|
HeartbeatRequested bool `json:"heartbeat_requested"`
|
|
}
|
|
|
|
func licenseActivateHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var req licenseActivateReq
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
client, err := newLicenseClient(c.Request.Context(), svc)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
deviceID, err := ensureLicenseDeviceID(c.Request.Context(), svc, "")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
deviceName := strings.TrimSpace(req.DeviceName)
|
|
if deviceName == "" {
|
|
deviceName = defaultLicenseDeviceName()
|
|
}
|
|
_ = svc.Repo.Setting.Set(c.Request.Context(), licenseDeviceNameSetting, deviceName)
|
|
|
|
payload := map[string]any{
|
|
"key": strings.TrimSpace(req.Key),
|
|
"fingerprint": deviceID,
|
|
"device_name": deviceName,
|
|
"instance_id": deviceID,
|
|
}
|
|
var upstream licenseServerSignedResp
|
|
if err := client.post(c.Request.Context(), "/api/v1/activate", payload, &upstream); err != nil {
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if err := client.verifySigned(&upstream); err != nil {
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
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
|
|
}
|
|
c.JSON(http.StatusOK, licenseActivationView(state))
|
|
}
|
|
}
|
|
|
|
func licenseStatusHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
state, _ := loadLicenseState(c.Request.Context(), svc)
|
|
client, err := newLicenseClient(c.Request.Context(), svc)
|
|
hasLicenseKey := strings.TrimSpace(state.LicenseKey) != ""
|
|
if err == nil && hasLicenseKey {
|
|
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) == "" {
|
|
deviceName = defaultLicenseDeviceName()
|
|
_ = svc.Repo.Setting.Set(c.Request.Context(), licenseDeviceNameSetting, deviceName)
|
|
}
|
|
var signed licenseServerSignedResp
|
|
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
|
|
if refreshed, ok, _, refreshErr := refreshLicenseServerStatus(c.Request.Context(), client, state, deviceID); refreshErr == nil && ok {
|
|
refreshed.LicenseKey = state.LicenseKey
|
|
state = refreshed
|
|
}
|
|
_ = persistLicenseState(c.Request.Context(), svc, state)
|
|
} else {
|
|
if refreshed, ok, _, getErr := refreshLicenseServerStatus(c.Request.Context(), client, state, deviceID); getErr == nil && ok {
|
|
state = refreshed
|
|
_ = persistLicenseState(c.Request.Context(), svc, state)
|
|
} else if getErr == nil {
|
|
state.Valid = false
|
|
_ = persistLicenseState(c.Request.Context(), svc, state)
|
|
}
|
|
}
|
|
}
|
|
} else if err == nil && state.Valid {
|
|
deviceID, idErr := ensureLicenseDeviceID(c.Request.Context(), svc, state.DeviceID)
|
|
if idErr == nil {
|
|
if refreshed, ok, _, getErr := refreshLicenseServerStatus(c.Request.Context(), client, state, deviceID); getErr == nil && ok {
|
|
state = refreshed
|
|
_ = persistLicenseState(c.Request.Context(), svc, state)
|
|
} else if getErr == nil {
|
|
state.Valid = false
|
|
_ = persistLicenseState(c.Request.Context(), svc, state)
|
|
}
|
|
}
|
|
}
|
|
active := state.Valid && !licenseStateExpired(state.ExpiryDate)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"active": active,
|
|
"message": licenseStatusMessage(active, err),
|
|
"max_users": licenseStatusMaxUsers(state),
|
|
"unlimited_users": state.Valid && !licenseStateExpired(state.ExpiryDate) && state.UnlimitedUsers,
|
|
"activation": licenseActivationView(state),
|
|
})
|
|
}
|
|
}
|
|
|
|
func licenseHeartbeatHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
state, err := sendLicenseHeartbeat(c.Request.Context(), svc)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, licenseActivationView(state))
|
|
}
|
|
}
|