Files
ttstd-infinite-canvas/backend/internal/hostupdate/server.go

102 lines
3.0 KiB
Go

package hostupdate
import (
"crypto/subtle"
"encoding/json"
"errors"
"io"
"net/http"
"strings"
)
type Server struct {
manager *Manager
token string
}
func NewServer(manager *Manager, token string) (*Server, error) {
if manager == nil {
return nil, errors.New("更新管理器不能为空")
}
if len(strings.TrimSpace(token)) < 32 {
return nil, errors.New("CANVAS_UPDATER_TOKEN 至少需要 32 个字符")
}
return &Server{manager: manager, token: strings.TrimSpace(token)}, nil
}
func (s *Server) Handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /v1/status", func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, s.manager.Snapshot()) })
mux.HandleFunc("POST /v1/check", func(w http.ResponseWriter, r *http.Request) {
status, err := s.manager.Check(r.Context())
if err != nil {
writeError(w, http.StatusBadGateway, err, status)
return
}
writeJSON(w, http.StatusOK, status)
})
mux.HandleFunc("POST /v1/update", func(w http.ResponseWriter, r *http.Request) {
var request StartRequest
if err := decodeJSON(r, &request); err != nil {
writeError(w, http.StatusBadRequest, err, nil)
return
}
status, err := s.manager.StartUpdate(request.TargetVersion)
if err != nil {
writeError(w, http.StatusConflict, err, status)
return
}
writeJSON(w, http.StatusAccepted, status)
})
mux.HandleFunc("POST /v1/rollback", func(w http.ResponseWriter, r *http.Request) {
var request RollbackRequest
if err := decodeJSON(r, &request); err != nil {
writeError(w, http.StatusBadRequest, err, nil)
return
}
status, err := s.manager.StartRollback(request.Reason)
if err != nil {
writeError(w, http.StatusConflict, err, status)
return
}
writeJSON(w, http.StatusAccepted, status)
})
return s.authorize(mux)
}
func (s *Server) authorize(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
provided := strings.TrimSpace(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "))
if len(provided) != len(s.token) || subtle.ConstantTimeCompare([]byte(provided), []byte(s.token)) != 1 {
writeError(w, http.StatusUnauthorized, errors.New("更新器认证失败"), nil)
return
}
w.Header().Set("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}
func decodeJSON(r *http.Request, target any) error {
defer r.Body.Close()
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return err
}
return nil
}
func writeError(w http.ResponseWriter, status int, err error, data any) {
writeJSONStatus(w, status, map[string]any{"error": err.Error(), "data": data})
}
func writeJSON(w http.ResponseWriter, status int, value any) {
writeJSONStatus(w, status, value)
}
func writeJSONStatus(w http.ResponseWriter, status int, value any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(value)
}