296 lines
12 KiB
Go
296 lines
12 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/base64"
|
||
|
|
"net/http"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/identity"
|
||
|
|
"data-terminal-cloud/internal/saas"
|
||
|
|
)
|
||
|
|
|
||
|
|
func ListOpsFirmwareSigningKeysHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items, err := service.ListOpsFirmwareSigningKeys(r.Context(), principal, tenantID)
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func RegisterOpsFirmwareSigningKeyHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var input struct {
|
||
|
|
KeyID string `json:"key_id"`
|
||
|
|
PublicKey string `json:"public_key_base64"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &input, "固件签名密钥参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
input.KeyID, input.PublicKey = strings.TrimSpace(input.KeyID), strings.TrimSpace(input.PublicKey)
|
||
|
|
decoded, decodeErr := base64.StdEncoding.DecodeString(input.PublicKey)
|
||
|
|
if input.KeyID == "" || len([]rune(input.KeyID)) > 120 || decodeErr != nil || len(decoded) != 32 || base64.StdEncoding.EncodeToString(decoded) != input.PublicKey {
|
||
|
|
writeValidation(w, r, "固件签名密钥参数不正确", FieldError{Field: "key", Message: "密钥编号和标准Base64编码的32字节Ed25519公钥均为必填"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err := service.RegisterOpsFirmwareSigningKey(r.Context(), principal, tenantID, RequestID(r.Context()), input.KeyID, input.PublicKey); err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusCreated, map[string]any{"key_id": input.KeyID, "status": "ACTIVE"})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func RevokeOpsFirmwareSigningKeyHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var input struct {
|
||
|
|
KeyID string `json:"key_id"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &input, "固件签名密钥吊销参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
input.KeyID = strings.TrimSpace(input.KeyID)
|
||
|
|
if input.KeyID == "" || len([]rune(input.KeyID)) > 120 {
|
||
|
|
writeValidation(w, r, "固件签名密钥吊销参数不正确", FieldError{Field: "key_id", Message: "必须为1至120个字符"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.RevokeOpsFirmwareSigningKey(r.Context(), principal, tenantID, RequestID(r.Context()), input.KeyID)
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ListOpsFirmwareReleasesHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
status := strings.ToUpper(strings.TrimSpace(r.URL.Query().Get("status")))
|
||
|
|
if status != "" && status != "UPLOADING" && status != "READY" && status != "PAUSED" && status != "RETIRED" {
|
||
|
|
writeValidation(w, r, "固件查询参数不正确", FieldError{Field: "status", Message: "必须为UPLOADING、READY、PAUSED或RETIRED"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items, err := service.ListOpsFirmwareReleases(r.Context(), principal, tenantID, strings.TrimSpace(r.URL.Query().Get("model")), status)
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func PrepareOpsFirmwareReleaseHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var input struct {
|
||
|
|
Model string `json:"model"`
|
||
|
|
HardwareRevision string `json:"hardware_revision"`
|
||
|
|
Version string `json:"version"`
|
||
|
|
ExpectedSize int64 `json:"expected_size"`
|
||
|
|
ExpectedDigest string `json:"expected_digest"`
|
||
|
|
Signature string `json:"signature"`
|
||
|
|
SigningKeyID string `json:"signing_key_id"`
|
||
|
|
MinimumSecurityVersion string `json:"minimum_security_version"`
|
||
|
|
ReleaseNotes string `json:"release_notes"`
|
||
|
|
ReleaseChannel string `json:"release_channel"`
|
||
|
|
Mandatory bool `json:"mandatory"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &input, "固件版本参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
input.Model, input.HardwareRevision, input.Version = strings.TrimSpace(input.Model), strings.TrimSpace(input.HardwareRevision), strings.TrimSpace(input.Version)
|
||
|
|
input.ExpectedDigest, input.Signature, input.SigningKeyID = strings.ToLower(strings.TrimSpace(input.ExpectedDigest)), strings.TrimSpace(input.Signature), strings.TrimSpace(input.SigningKeyID)
|
||
|
|
input.ReleaseChannel = strings.ToUpper(strings.TrimSpace(input.ReleaseChannel))
|
||
|
|
if input.ReleaseChannel == "" {
|
||
|
|
input.ReleaseChannel = "STABLE"
|
||
|
|
}
|
||
|
|
if input.Model == "" || len([]rune(input.Model)) > 120 || len([]rune(input.HardwareRevision)) > 80 || input.Version == "" || len([]rune(input.Version)) > 80 || input.ExpectedSize < 1 || input.ExpectedSize > 268435456 || !sha256Pattern.MatchString(input.ExpectedDigest) || input.Signature == "" || input.SigningKeyID == "" || (input.ReleaseChannel != "STABLE" && input.ReleaseChannel != "BETA" && input.ReleaseChannel != "INTERNAL") || len([]rune(input.ReleaseNotes)) > 2000 {
|
||
|
|
writeValidation(w, r, "固件版本参数不正确", FieldError{Field: "firmware", Message: "型号、版本、1至256MB大小、SHA-256、签名、密钥编号和合法发布通道均为必填"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.PrepareOpsFirmwareRelease(r.Context(), principal, tenantID, RequestID(r.Context()), saas.PrepareFirmwareInput{Model: input.Model, HardwareRevision: input.HardwareRevision, Version: input.Version, ExpectedSize: input.ExpectedSize, ExpectedDigest: input.ExpectedDigest, Signature: input.Signature, SigningKeyID: input.SigningKeyID, MinimumSecurityVersion: strings.TrimSpace(input.MinimumSecurityVersion), ReleaseNotes: strings.TrimSpace(input.ReleaseNotes), ReleaseChannel: input.ReleaseChannel, Mandatory: input.Mandatory})
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusCreated, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func OpsFirmwareReleaseActionHandler(service *saas.Service, action string) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if !validUUID(r.PathValue("id")) {
|
||
|
|
writeValidation(w, r, "固件版本编号不正确", FieldError{Field: "id", Message: "必须是UUID"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var item saas.FirmwareRelease
|
||
|
|
var err error
|
||
|
|
switch action {
|
||
|
|
case "finalize":
|
||
|
|
item, err = service.FinalizeOpsFirmwareRelease(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
case "upload-session":
|
||
|
|
item, err = service.RenewOpsFirmwareUpload(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
case "pause":
|
||
|
|
item, err = service.PauseOpsFirmwareRelease(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
case "resume":
|
||
|
|
item, err = service.ResumeOpsFirmwareRelease(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
case "retire":
|
||
|
|
item, err = service.RetireOpsFirmwareRelease(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
default:
|
||
|
|
WriteError(w, r, http.StatusNotFound, "RESOURCE_NOT_FOUND", "请求的操作不存在")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func DownloadOpsFirmwareReleaseHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if !validUUID(r.PathValue("id")) {
|
||
|
|
writeValidation(w, r, "固件版本编号不正确", FieldError{Field: "id", Message: "必须是UUID"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.OpsFirmwareDownload(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func ListOpsFirmwareRolloutsHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
items, err := service.ListOpsFirmwareRollouts(r.Context(), principal, tenantID)
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func StartOpsFirmwareRolloutHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
var input struct {
|
||
|
|
ReleaseID string `json:"release_id"`
|
||
|
|
Name string `json:"name"`
|
||
|
|
TargetPercentage int `json:"target_percentage"`
|
||
|
|
DeviceIDs []string `json:"device_ids"`
|
||
|
|
}
|
||
|
|
if !decodeWebJSON(w, r, &input, "OTA升级批次参数不正确") {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
input.ReleaseID, input.Name = strings.TrimSpace(input.ReleaseID), strings.TrimSpace(input.Name)
|
||
|
|
validDevices := len(input.DeviceIDs) > 0 && len(input.DeviceIDs) <= 10000
|
||
|
|
for _, deviceID := range input.DeviceIDs {
|
||
|
|
validDevices = validDevices && validUUID(deviceID)
|
||
|
|
}
|
||
|
|
if !validUUID(input.ReleaseID) || input.Name == "" || len([]rune(input.Name)) > 160 || input.TargetPercentage < 1 || input.TargetPercentage > 100 || !validDevices {
|
||
|
|
writeValidation(w, r, "OTA升级批次参数不正确", FieldError{Field: "rollout", Message: "版本、1至160字符批次名称、1至100灰度比例和有效目标设备均为必填"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.StartOpsFirmwareRollout(r.Context(), principal, tenantID, input.ReleaseID, RequestID(r.Context()), input.Name, input.TargetPercentage, input.DeviceIDs)
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusAccepted, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func GetOpsFirmwareRolloutHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if !validUUID(r.PathValue("id")) {
|
||
|
|
writeValidation(w, r, "OTA升级批次编号不正确", FieldError{Field: "id", Message: "必须是UUID"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.GetOpsFirmwareRollout(r.Context(), principal, tenantID, r.PathValue("id"))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func PauseOpsFirmwareRolloutHandler(service *saas.Service) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
principal, tenantID, ok := opsFirmwarePrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if !validUUID(r.PathValue("id")) {
|
||
|
|
writeValidation(w, r, "OTA升级批次编号不正确", FieldError{Field: "id", Message: "必须是UUID"})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
item, err := service.PauseOpsFirmwareRollout(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()))
|
||
|
|
if err != nil {
|
||
|
|
writeSaaSError(w, r, err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
WriteJSON(w, r, http.StatusOK, item)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func opsFirmwarePrincipal(w http.ResponseWriter, r *http.Request) (identity.Principal, string, bool) {
|
||
|
|
principal, ok := webPrincipal(w, r)
|
||
|
|
if !ok {
|
||
|
|
return identity.Principal{}, "", false
|
||
|
|
}
|
||
|
|
tenantID := strings.TrimSpace(r.Header.Get("X-Tenant-Id"))
|
||
|
|
if !validUUID(tenantID) {
|
||
|
|
writeValidation(w, r, "运维固件请求参数不正确", FieldError{Field: "X-Tenant-Id", Message: "必须提供有效的客户组织UUID"})
|
||
|
|
return identity.Principal{}, "", false
|
||
|
|
}
|
||
|
|
return principal, tenantID, true
|
||
|
|
}
|