初版功能完成
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"remlink/internal/protocol"
|
||||
)
|
||||
|
||||
const maxRequestBody = 1 << 20
|
||||
|
||||
// Handler exposes the exact v1 public Bootstrap routes.
|
||||
func Handler(service *Service) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /api/v1/server/info", func(writer http.ResponseWriter, request *http.Request) {
|
||||
writeJSON(writer, http.StatusOK, service.ServerInfo())
|
||||
})
|
||||
mux.HandleFunc("POST /api/v1/bootstrap/register", func(writer http.ResponseWriter, request *http.Request) {
|
||||
var input RegisterRequest
|
||||
if err := decodeJSON(writer, request, &input); err != nil {
|
||||
writeAPIError(writer, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||||
return
|
||||
}
|
||||
output, err := service.Register(request.Context(), input)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, ErrJoinTokenInvalid):
|
||||
writeAPIError(writer, http.StatusUnauthorized, string(protocol.ErrorJoinTokenInvalid), err.Error())
|
||||
case errors.Is(err, ErrNodeConflict):
|
||||
writeAPIError(writer, http.StatusConflict, "NODE_CONFLICT", err.Error())
|
||||
default:
|
||||
writeAPIError(writer, http.StatusBadRequest, "REGISTRATION_FAILED", err.Error())
|
||||
}
|
||||
return
|
||||
}
|
||||
writeJSON(writer, http.StatusCreated, output)
|
||||
})
|
||||
mux.HandleFunc("POST /api/v1/bootstrap/config", func(writer http.ResponseWriter, request *http.Request) {
|
||||
var input ConfigRequest
|
||||
if err := decodeJSON(writer, request, &input); err != nil {
|
||||
writeAPIError(writer, http.StatusBadRequest, "INVALID_REQUEST", err.Error())
|
||||
return
|
||||
}
|
||||
output, err := service.Config(request.Context(), input)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNodeAuthFailed) {
|
||||
writeAPIError(writer, http.StatusUnauthorized, string(protocol.ErrorNodeAuthFailed), err.Error())
|
||||
return
|
||||
}
|
||||
writeAPIError(writer, http.StatusInternalServerError, "CONFIG_FAILED", err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(writer, http.StatusOK, output)
|
||||
})
|
||||
return securityHeaders(mux)
|
||||
}
|
||||
|
||||
func decodeJSON(writer http.ResponseWriter, request *http.Request, destination any) error {
|
||||
if contentType := request.Header.Get("Content-Type"); contentType != "" &&
|
||||
!strings.HasPrefix(strings.ToLower(contentType), "application/json") {
|
||||
return errors.New("Content-Type must be application/json")
|
||||
}
|
||||
request.Body = http.MaxBytesReader(writer, request.Body, maxRequestBody)
|
||||
decoder := json.NewDecoder(request.Body)
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(destination); err != nil {
|
||||
return fmt.Errorf("decode JSON: %w", err)
|
||||
}
|
||||
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
|
||||
return errors.New("request body must contain one JSON object")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type apiErrorEnvelope struct {
|
||||
Error apiError `json:"error"`
|
||||
}
|
||||
|
||||
type apiError struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func writeAPIError(writer http.ResponseWriter, status int, code, message string) {
|
||||
writeJSONStatus(writer, status, apiErrorEnvelope{Error: apiError{Code: code, Message: message}})
|
||||
}
|
||||
|
||||
func writeJSON(writer http.ResponseWriter, status int, value any) {
|
||||
writeJSONStatus(writer, status, value)
|
||||
}
|
||||
|
||||
func writeJSONStatus(writer http.ResponseWriter, status int, value any) {
|
||||
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
writer.WriteHeader(status)
|
||||
_ = json.NewEncoder(writer).Encode(value)
|
||||
}
|
||||
|
||||
func securityHeaders(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
writer.Header().Set("Cache-Control", "no-store")
|
||||
writer.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
next.ServeHTTP(writer, request)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user