Files
upstream-centrifugo/internal/admin/handlers.go
upstream-mirror b352a9a30d
Some checks failed
test / Lint (push) Failing after 3s
test / Test with Go 1.26.8 (push) Failing after 2s
snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:17:21 +05:00

202 lines
6.1 KiB
Go

package admin
import (
"encoding/json"
"net/http"
"strings"
"time"
"github.com/centrifugal/centrifugo/v6/internal/api"
"github.com/centrifugal/centrifugo/v6/internal/configtypes"
"github.com/centrifugal/centrifugo/v6/internal/middleware"
"github.com/centrifugal/centrifugo/v6/internal/reverseproxy"
"github.com/centrifugal/centrifugo/v6/internal/tools"
"github.com/centrifugal/centrifugo/v6/internal/webui"
"github.com/centrifugal/centrifuge"
"github.com/gorilla/securecookie"
"github.com/rs/zerolog/log"
)
type Config = configtypes.Admin
// Handler handles admin web UI endpoints.
type Handler struct {
mux *http.ServeMux
node *centrifuge.Node
config configtypes.Admin
}
// NewHandler creates new Handler.
func NewHandler(n *centrifuge.Node, apiExecutor *api.Executor, c Config) *Handler {
h := &Handler{
node: n,
config: c,
}
mux := http.NewServeMux()
prefix := strings.TrimRight(h.config.HandlerPrefix, "/")
mux.Handle(prefix+"/admin/init", http.HandlerFunc(h.initHandler))
// Throttle repeated failed password attempts per client IP to slow brute-force
// of admin.password. Only failures count and each IP is tracked independently,
// so a valid login is never throttled - including while another source attacks.
//
// This is best-effort, in-process protection only. The admin auth endpoint is
// intended to be reachable by trusted operators, and Centrifugo recommends
// restricting access to it at the infrastructure level (firewall rules, a
// private network, an authenticating reverse proxy, etc.). The throttle here
// raises the bar for brute-force but is not a substitute for that: the primary
// protection of the admin endpoint must be done at the infrastructure level.
authThrottle := middleware.NewAuthThrottle(10, time.Minute, nil)
mux.Handle(prefix+"/admin/auth", middleware.Post(authThrottle.Middleware(http.HandlerFunc(h.authHandler))))
mux.Handle(prefix+"/admin/api", middleware.Post(h.adminSecureTokenAuth(api.NewHandler(n, apiExecutor, api.Config{}).OldRoute())))
webPrefix := prefix + "/"
if c.WebProxyAddress != "" {
log.Info().Str("address", c.WebProxyAddress).Msg("using admin web reverse proxy")
proxy, err := reverseproxy.New(c.WebProxyAddress)
if err != nil {
log.Fatal().Err(err).Msg("error creating admin web reverse proxy")
}
mux.HandleFunc(webPrefix, reverseproxy.ProxyRequestHandler(proxy))
} else if c.WebPath != "" {
mux.Handle(webPrefix, http.StripPrefix(webPrefix, http.FileServer(http.Dir(c.WebPath))))
} else {
mux.Handle(webPrefix, http.StripPrefix(webPrefix, http.FileServer(webui.FS)))
}
h.mux = mux
return h
}
func (s *Handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(rw, r)
}
func (s *Handler) adminSecureTokenAuth(h http.Handler) http.Handler {
secret := s.config.Secret
insecure := s.config.Insecure
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if insecure {
h.ServeHTTP(w, r)
return
}
if secret == "" {
log.Error().Msg("no admin secret key found in configuration")
w.WriteHeader(http.StatusUnauthorized)
return
}
var token string
authorization := r.Header.Get("Authorization")
if authorization != "" {
parts := strings.Fields(authorization)
if len(parts) != 2 {
w.WriteHeader(http.StatusUnauthorized)
return
}
authMethod := strings.ToLower(parts[0])
if authMethod != "token" {
w.WriteHeader(http.StatusUnauthorized)
return
}
token = parts[1]
}
if token == "" || !checkSecureAdminToken(secret, token) {
w.WriteHeader(http.StatusUnauthorized)
return
}
h.ServeHTTP(w, r)
})
}
// initHandler allows to get admin web interface settings.
func (s *Handler) initHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := map[string]any{
"insecure": s.config.Insecure,
"edition": "oss",
}
secret := s.config.Secret
authenticated := false
if s.config.Insecure {
authenticated = true
} else if secret != "" {
var token string
authorization := r.Header.Get("Authorization")
if authorization != "" {
parts := strings.Fields(authorization)
if len(parts) == 2 && strings.ToLower(parts[0]) == "token" {
token = parts[1]
}
}
if token != "" && checkSecureAdminToken(secret, token) {
authenticated = true
}
}
resp["authenticated"] = authenticated
_ = json.NewEncoder(w).Encode(resp)
}
// authHandler allows to get admin web interface token.
func (s *Handler) authHandler(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1024*1024) // 1MB limit.
formPassword := r.FormValue("password")
password := s.config.Password
secret := s.config.Secret
if password == "" || secret == "" {
log.Error().Msg("admin_password and admin_secret must be set in configuration")
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
if tools.SecureCompareString(formPassword, password) {
w.Header().Set("Content-Type", "application/json")
token, err := generateSecureAdminToken(secret)
if err != nil {
log.Error().Msgf("error generating admin token: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
resp := map[string]string{
"token": token,
}
_ = json.NewEncoder(w).Encode(resp)
return
}
http.Error(w, "Bad Request", http.StatusBadRequest)
}
const (
// AdminTokenKey is a key for admin authorization token.
secureAdminTokenKey = "token"
// AdminTokenValue is a value for secure admin authorization token.
secureAdminTokenValue = "authorized"
)
// generateSecureAdminToken generates admin authentication token.
func generateSecureAdminToken(secret string) (string, error) {
s := securecookie.New([]byte(secret), nil)
return s.Encode(secureAdminTokenKey, secureAdminTokenValue)
}
// checkSecureAdminToken checks admin connection token which Centrifugo returns after admin login.
func checkSecureAdminToken(secret string, token string) bool {
s := securecookie.New([]byte(secret), nil)
var val string
err := s.Decode(secureAdminTokenKey, token, &val)
if err != nil {
return false
}
if val != secureAdminTokenValue {
return false
}
return true
}