mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 10:04:55 +08:00
fix(api): 补齐 bat-api 控制与后端 RPC
This commit is contained in:
+171
-2
@@ -1,6 +1,21 @@
|
||||
package api
|
||||
|
||||
import "net/http"
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"bat-api/internal/backendrpc"
|
||||
)
|
||||
|
||||
const adminControlMaxBodyBytes = 1024
|
||||
|
||||
type adminControlRequest struct {
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
@@ -10,7 +25,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
body := AdminIndexResponse{
|
||||
Service: "bat-api",
|
||||
Panel: "admin",
|
||||
Status: "reserved",
|
||||
Status: "available",
|
||||
Links: []string{
|
||||
"/healthz",
|
||||
"/readyz",
|
||||
@@ -19,6 +34,15 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
"/v1/resources",
|
||||
"/openapi.yaml",
|
||||
},
|
||||
Controls: []string{
|
||||
"/admin/control/reload",
|
||||
"/admin/control/refresh",
|
||||
"/admin/control/restart",
|
||||
"/admin/control/sync",
|
||||
"/admin/control/verify",
|
||||
"/admin/control/repair",
|
||||
"/admin/control/catalog-refresh",
|
||||
},
|
||||
}
|
||||
if r.Method == http.MethodHead {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
@@ -27,3 +51,148 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeNoStoreJSON(w, http.StatusOK, body)
|
||||
}
|
||||
|
||||
func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
|
||||
return
|
||||
}
|
||||
if !s.requireAdminToken(w, r) {
|
||||
return
|
||||
}
|
||||
action := strings.TrimPrefix(r.URL.Path, "/admin/control/")
|
||||
if action == "" || strings.Contains(action, "/") {
|
||||
writeErrorJSON(w, http.StatusNotFound, "control_not_found", "unknown control action")
|
||||
return
|
||||
}
|
||||
request, ok := decodeAdminControlRequest(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
backend, ok := s.backend.(ControlBackend)
|
||||
if !ok || backend == nil {
|
||||
writeErrorJSON(w, http.StatusServiceUnavailable, "control_backend_unavailable", "Rust bat control backend is unavailable")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
method string
|
||||
result any
|
||||
err error
|
||||
)
|
||||
switch action {
|
||||
case "reload":
|
||||
if request.Force {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "reload does not accept force")
|
||||
return
|
||||
}
|
||||
method = "daemon.reload"
|
||||
result, err = backend.DaemonReload(r.Context())
|
||||
case "refresh":
|
||||
method = "daemon.refresh"
|
||||
result, err = backend.DaemonRefresh(r.Context(), request.Force)
|
||||
case "restart":
|
||||
if request.Force {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "restart does not accept force")
|
||||
return
|
||||
}
|
||||
method = "daemon.restart"
|
||||
result, err = backend.DaemonRestart(r.Context())
|
||||
case "sync":
|
||||
method = "resource.sync"
|
||||
result, err = backend.ResourceSync(r.Context(), request.Force)
|
||||
case "verify":
|
||||
if request.Force {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "verify does not accept force")
|
||||
return
|
||||
}
|
||||
method = "resource.verify"
|
||||
result, err = backend.ResourceVerify(r.Context())
|
||||
case "repair":
|
||||
if request.Force {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "repair does not accept force")
|
||||
return
|
||||
}
|
||||
method = "resource.repair"
|
||||
result, err = backend.ResourceRepair(r.Context())
|
||||
case "catalog-refresh":
|
||||
method = "catalog.refresh"
|
||||
result, err = backend.CatalogRefresh(r.Context(), request.Force)
|
||||
case "stop", "clean-stable":
|
||||
writeErrorJSON(w, http.StatusForbidden, "control_not_allowed", "control action is not exposed by bat-api")
|
||||
return
|
||||
default:
|
||||
writeErrorJSON(w, http.StatusNotFound, "control_not_found", "unknown control action")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
s.writeControlBackendError(w, action, err)
|
||||
return
|
||||
}
|
||||
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
|
||||
Service: "bat-api",
|
||||
Action: action,
|
||||
RPCMethod: method,
|
||||
Status: "accepted",
|
||||
Result: result,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) requireAdminToken(w http.ResponseWriter, r *http.Request) bool {
|
||||
if s.cfg.AuthToken == "" {
|
||||
writeErrorJSON(w, http.StatusForbidden, "admin_auth_required", "admin controls require BAT_API_AUTH_TOKEN")
|
||||
return false
|
||||
}
|
||||
if !constantTimeTokenEqual(s.requestToken(r), s.cfg.AuthToken) {
|
||||
w.Header().Set("WWW-Authenticate", `Bearer realm="bat-api-admin"`)
|
||||
writeErrorJSON(w, http.StatusUnauthorized, "unauthorized", "missing or invalid access token")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func decodeAdminControlRequest(w http.ResponseWriter, r *http.Request) (adminControlRequest, bool) {
|
||||
var request adminControlRequest
|
||||
if r.Body == nil {
|
||||
return request, true
|
||||
}
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, adminControlMaxBodyBytes))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
return request, true
|
||||
}
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "control request must be a JSON object with an optional force boolean")
|
||||
return adminControlRequest{}, false
|
||||
}
|
||||
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
||||
writeErrorJSON(w, http.StatusBadRequest, "invalid_control_params", "control request must contain exactly one JSON object")
|
||||
return adminControlRequest{}, false
|
||||
}
|
||||
return request, true
|
||||
}
|
||||
|
||||
func (s *Server) writeControlBackendError(w http.ResponseWriter, action string, err error) {
|
||||
status := http.StatusBadGateway
|
||||
code := "control_backend_failed"
|
||||
message := "Rust bat rejected the control request"
|
||||
switch {
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
status = http.StatusGatewayTimeout
|
||||
code = "control_backend_timeout"
|
||||
message = "Rust bat control request timed out"
|
||||
case errors.Is(err, context.Canceled):
|
||||
status = http.StatusRequestTimeout
|
||||
code = "control_request_canceled"
|
||||
message = "control request was canceled"
|
||||
default:
|
||||
var apiErr *backendrpc.APIError
|
||||
if errors.As(err, &apiErr) && apiErr.Kind == "not_implemented" {
|
||||
status = http.StatusNotImplemented
|
||||
code = "control_not_implemented"
|
||||
message = "Rust bat does not implement this control action"
|
||||
}
|
||||
}
|
||||
s.logger.Printf("bat-api control action=%s error=%v", action, err)
|
||||
writeErrorJSON(w, status, code, message)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user