mirror of
https://github.com/juanfont/headscale.git
synced 2026-08-07 07:48:44 +09:00
fe76fe0f57
CreateUser, ListUsers (with filters), RenameUser, DeleteUser over the state layer; unknown users now 404. HTTP-parity tests via the generated client.
146 lines
4.2 KiB
Go
146 lines
4.2 KiB
Go
package apiv1
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
oas "github.com/juanfont/headscale/gen/api/v1"
|
|
"github.com/juanfont/headscale/hscontrol/db"
|
|
"github.com/juanfont/headscale/hscontrol/state"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/rs/zerolog/log"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const problemContentType = "application/problem+json"
|
|
|
|
// apiError builds an RFC 7807 problem response with the given HTTP status and
|
|
// detail. Handlers return it so expected errors render as problem documents
|
|
// with the correct status code (ogen encodes [oas.ErrorStatusCode] directly,
|
|
// without going through [errorHandler]).
|
|
func apiError(status int, detail string) *oas.ErrorStatusCode {
|
|
return &oas.ErrorStatusCode{
|
|
StatusCode: status,
|
|
Response: oas.Problem{
|
|
Title: oas.NewOptString(http.StatusText(status)),
|
|
//nolint:gosec // G115: status is an HTTP status code, always within int32.
|
|
Status: oas.NewOptInt32(int32(status)),
|
|
Detail: oas.NewOptString(detail),
|
|
},
|
|
}
|
|
}
|
|
|
|
func notFound(
|
|
detail string,
|
|
) *oas.ErrorStatusCode {
|
|
return apiError(http.StatusNotFound, detail)
|
|
}
|
|
|
|
func badRequest(
|
|
detail string,
|
|
) *oas.ErrorStatusCode {
|
|
return apiError(http.StatusBadRequest, detail)
|
|
}
|
|
|
|
func internalError(detail string) *oas.ErrorStatusCode {
|
|
return apiError(http.StatusInternalServerError, detail)
|
|
}
|
|
|
|
// mapStateError classifies an error returned by the state layer into an HTTP
|
|
// problem response. Not-found sentinels become 404; everything else is a 500.
|
|
// Handlers that need a different status (e.g. validation 400) build the problem
|
|
// explicitly with [badRequest] rather than routing through here.
|
|
func mapStateError(err error) *oas.ErrorStatusCode {
|
|
switch {
|
|
case errors.Is(err, gorm.ErrRecordNotFound),
|
|
errors.Is(err, state.ErrNodeNotFound),
|
|
errors.Is(err, db.ErrUserNotFound):
|
|
return notFound(err.Error())
|
|
case errors.Is(err, types.ErrPolicyUpdateIsDisabled):
|
|
return badRequest(err.Error())
|
|
default:
|
|
return internalError(err.Error())
|
|
}
|
|
}
|
|
|
|
// NewError converts an error that ogen raises outside a handler return value —
|
|
// failed authentication, request decoding — into a typed problem response. ogen
|
|
// calls it from the generated security/decoding paths; the resulting
|
|
// [oas.ErrorStatusCode] is then encoded as application/problem+json.
|
|
func (s *Server) NewError(_ context.Context, err error) *oas.ErrorStatusCode {
|
|
return classify(err)
|
|
}
|
|
|
|
// errorHandler renders problems for the remaining ogen error path: a plain
|
|
// error returned from a handler. Expected errors are returned as
|
|
// [oas.ErrorStatusCode] (encoded directly by ogen), so this is the safety net
|
|
// for anything else.
|
|
func errorHandler(
|
|
_ context.Context,
|
|
w http.ResponseWriter,
|
|
_ *http.Request,
|
|
err error,
|
|
) {
|
|
writeProblem(w, classify(err))
|
|
}
|
|
|
|
// classify maps an arbitrary error to a problem response. An already-typed
|
|
// [oas.ErrorStatusCode] passes through; ogen's framework errors carry their own
|
|
// HTTP status via Code() (security -> 401, decode -> 400); everything else is
|
|
// classified by [mapStateError].
|
|
func classify(err error) *oas.ErrorStatusCode {
|
|
var esc *oas.ErrorStatusCode
|
|
if errors.As(err, &esc) {
|
|
return esc
|
|
}
|
|
|
|
var coder interface{ Code() int }
|
|
if errors.As(err, &coder) {
|
|
return apiError(coder.Code(), err.Error())
|
|
}
|
|
|
|
return mapStateError(err)
|
|
}
|
|
|
|
func writeProblem(w http.ResponseWriter, esc *oas.ErrorStatusCode) {
|
|
w.Header().Set("Content-Type", problemContentType)
|
|
w.WriteHeader(esc.StatusCode)
|
|
|
|
p := esc.Response
|
|
|
|
status := esc.StatusCode
|
|
if v, ok := p.Status.Get(); ok {
|
|
status = int(v)
|
|
}
|
|
|
|
body := problemJSON{
|
|
Title: p.Title.Or(""),
|
|
Status: status,
|
|
Detail: p.Detail.Or(""),
|
|
}
|
|
if v, ok := p.Type.Get(); ok {
|
|
body.Type = v.String()
|
|
}
|
|
|
|
if v, ok := p.Instance.Get(); ok {
|
|
body.Instance = v.String()
|
|
}
|
|
|
|
err := json.NewEncoder(w).Encode(body)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("writing problem response failed")
|
|
}
|
|
}
|
|
|
|
// problemJSON mirrors [oas.Problem] for hand-written encoding in
|
|
// [errorHandler]; ogen's own encoder is package-private.
|
|
type problemJSON struct {
|
|
Type string `json:"type,omitempty"`
|
|
Title string `json:"title,omitempty"`
|
|
Status int `json:"status,omitempty"`
|
|
Detail string `json:"detail,omitempty"`
|
|
Instance string `json:"instance,omitempty"`
|
|
}
|