94 lines
3.1 KiB
Go
94 lines
3.1 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"crypto/hmac"
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/hex"
|
||
|
|
"encoding/json"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"log/slog"
|
||
|
|
"net/http"
|
||
|
|
"strconv"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"data-terminal-cloud/internal/diagnostics"
|
||
|
|
)
|
||
|
|
|
||
|
|
const (
|
||
|
|
HeaderPlatformTimestamp = "X-Platform-Timestamp"
|
||
|
|
HeaderPlatformSignature = "X-Platform-Signature"
|
||
|
|
)
|
||
|
|
|
||
|
|
type PlatformHealthProcessor interface {
|
||
|
|
IngestPlatformHealth(context.Context, diagnostics.PlatformHealthInput, []byte, time.Time) (diagnostics.PlatformHealthReceipt, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
func PlatformHealthCallbackHandler(secret []byte, maxSkew time.Duration, processor PlatformHealthProcessor) http.Handler {
|
||
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
|
|
r.Body = http.MaxBytesReader(w, r.Body, 64*1024)
|
||
|
|
rawBody, err := io.ReadAll(r.Body)
|
||
|
|
if err != nil {
|
||
|
|
WriteError(w, r, http.StatusBadRequest, "INVALID_PLATFORM_HEALTH_REPORT", "服务器健康报告无法读取")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
now := time.Now().UTC()
|
||
|
|
if !verifyPlatformHealthSignature(secret, r.Header.Get(HeaderPlatformTimestamp), r.Header.Get(HeaderPlatformSignature), rawBody, now, maxSkew) {
|
||
|
|
WriteError(w, r, http.StatusUnauthorized, "PLATFORM_HEALTH_AUTHENTICATION_FAILED", "服务器健康报告身份验证失败")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
decoder := json.NewDecoder(bytes.NewReader(rawBody))
|
||
|
|
decoder.DisallowUnknownFields()
|
||
|
|
var input diagnostics.PlatformHealthInput
|
||
|
|
if err = decoder.Decode(&input); err != nil || ensureJSONEOF(decoder) != nil {
|
||
|
|
WriteError(w, r, http.StatusBadRequest, "INVALID_PLATFORM_HEALTH_REPORT", "服务器健康报告字段不正确")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
receipt, err := processor.IngestPlatformHealth(r.Context(), input, rawBody, now)
|
||
|
|
if err != nil {
|
||
|
|
if errors.Is(err, diagnostics.ErrInvalid) {
|
||
|
|
WriteError(w, r, http.StatusBadRequest, "INVALID_PLATFORM_HEALTH_REPORT", "服务器健康报告字段不正确")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
slog.ErrorContext(r.Context(), "platform health callback processing failed", "error", err)
|
||
|
|
WriteAPIError(w, r, http.StatusServiceUnavailable, Error{Code: "PLATFORM_HEALTH_UNAVAILABLE", Message: "服务器健康报告暂时无法处理", Retryable: true})
|
||
|
|
return
|
||
|
|
}
|
||
|
|
status := http.StatusCreated
|
||
|
|
if receipt.Duplicate {
|
||
|
|
status = http.StatusOK
|
||
|
|
}
|
||
|
|
w.Header().Set("Cache-Control", "no-store")
|
||
|
|
WriteJSON(w, r, status, receipt)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func verifyPlatformHealthSignature(secret []byte, timestamp, signature string, body []byte, now time.Time, maxSkew time.Duration) bool {
|
||
|
|
if len(secret) < 32 || maxSkew <= 0 || timestamp == "" || !strings.HasPrefix(signature, "sha256=") {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
epoch, err := strconv.ParseInt(timestamp, 10, 64)
|
||
|
|
if err != nil || absDuration(now.Sub(time.Unix(epoch, 0))) > maxSkew {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
provided, err := hex.DecodeString(strings.TrimPrefix(signature, "sha256="))
|
||
|
|
if err != nil {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
mac := hmac.New(sha256.New, secret)
|
||
|
|
_, _ = mac.Write([]byte(timestamp))
|
||
|
|
_, _ = mac.Write([]byte("."))
|
||
|
|
_, _ = mac.Write(body)
|
||
|
|
return hmac.Equal(provided, mac.Sum(nil))
|
||
|
|
}
|
||
|
|
|
||
|
|
func absDuration(value time.Duration) time.Duration {
|
||
|
|
if value < 0 {
|
||
|
|
return -value
|
||
|
|
}
|
||
|
|
return value
|
||
|
|
}
|