Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ keys remain all-access.

- Expiring or deleting a non-existent pre-auth key now returns an error instead of silently succeeding [#3324](https://github.com/juanfont/headscale/pull/3324)
- Improve systemd service file hardening [#3341](https://github.com/juanfont/headscale/pull/3341)
- Fix interactive OIDC login when the confirmation page is reloaded by an ad
blocker, back navigation, or pull-to-refresh. The confirmation page now has
its own URL, keeping single-use authorization codes out of reloads
[#3448](https://github.com/juanfont/headscale/pull/3448)
- Headscale now requires Go 1.27 to build
- Fix extra-records filewatcher hanging on shutdown after the watched file is deleted, and leaking the watcher when setup fails [#3437](https://github.com/juanfont/headscale/pull/3437)

Expand Down
1 change: 1 addition & 0 deletions hscontrol/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ func (h *Headscale) createRouter(apiV1Mux, apiV2Mux http.Handler) *chi.Mux {

if provider, ok := h.authProvider.(*AuthProviderOIDC); ok {
r.Get("/oidc/callback", provider.OIDCCallbackHandler)
r.Get("/register/confirm/{auth_id}", provider.RegisterConfirmGetHandler)
r.Post("/register/confirm/{auth_id}", provider.RegisterConfirmHandler)
}

Expand Down
18 changes: 14 additions & 4 deletions hscontrol/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,23 @@ func httpError(w http.ResponseWriter, err error) {
// an actionable message derived from the HTTP status code.
func httpUserError(w http.ResponseWriter, err error) {
code := http.StatusInternalServerError
userMsg := ""

if herr, ok := errors.AsType[HTTPError](err); ok {
if herr.Code != 0 {
code = herr.Code
}

userMsg = herr.UserMsg

log.Error().Err(herr.Err).Int("code", code).Msgf("user msg: %s", herr.Msg)
} else {
log.Error().Err(err).Int("code", code).Msg("http internal server error")
}

userMsg := userMessageForStatusCode(code)
if userMsg == "" {
userMsg = userMessageForStatusCode(code)
}

w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(code)
Expand Down Expand Up @@ -83,9 +88,10 @@ func userMessageForStatusCode(code int) string {

// HTTPError represents an error that is surfaced to the user via web.
type HTTPError struct {
Code int // HTTP response code to send to client; 0 means 500
Msg string // Response body to send to client
Err error // Detailed error to log on the server
Code int // HTTP response code to send to client; 0 means 500
Msg string // Response body to send to non-browser clients
Err error // Detailed error to log on the server
UserMsg string // Optional safe message for browser-facing error pages
}

func (e HTTPError) Error() string { return fmt.Sprintf("http error[%d]: %s, %s", e.Code, e.Msg, e.Err) }
Expand All @@ -96,6 +102,10 @@ func NewHTTPError(code int, msg string, err error) HTTPError {
return HTTPError{Code: code, Msg: msg, Err: err}
}

func newHTTPUserError(code int, msg, userMsg string, err error) HTTPError {
return HTTPError{Code: code, Msg: msg, Err: err, UserMsg: userMsg}
}

var errMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed, "method not allowed", nil)

var ErrRegisterMethodCLIDoesNotSupportExpire = errors.New(
Expand Down
12 changes: 12 additions & 0 deletions hscontrol/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,18 @@ func TestHttpUserError(t *testing.T) {
wantContains: "Your session has expired. Please try again.",
wantNotContain: "login session expired",
},
{
name: "gone_with_user_message_renders_specific_guidance",
err: newHTTPUserError(
http.StatusGone,
"registration link already used or expired",
"This link has already been used or has expired.",
nil,
),
wantCode: http.StatusGone,
wantContains: "This link has already been used or has expired.",
wantNotContain: "registration link already used or expired",
},
{
name: "bad_request_renders_generic_retry",
err: NewHTTPError(http.StatusBadRequest, "state not found", nil),
Expand Down
Loading
Loading