snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
218
internal/admin/handlers_test.go
Normal file
218
internal/admin/handlers_test.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestNewHandler ensures NewHandler sets up routes and proxy handling correctly.
|
||||
func TestNewHandler(t *testing.T) {
|
||||
node := ¢rifuge.Node{}
|
||||
cfg := Config{HandlerPrefix: "/prefix", WebProxyAddress: "", WebPath: ""}
|
||||
handler := NewHandler(node, nil, cfg)
|
||||
require.NotNil(t, handler)
|
||||
require.NotNil(t, handler.mux)
|
||||
|
||||
// Validate init route is registered.
|
||||
req := httptest.NewRequest("GET", "/prefix/admin/init", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
handler.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
}
|
||||
|
||||
// TestInitHandler checks the initHandler returns correct settings.
|
||||
func TestInitHandler(t *testing.T) {
|
||||
config := Config{}
|
||||
handler := &Handler{config: config}
|
||||
req := httptest.NewRequest("GET", "/admin/init", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
handler.initHandler(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var response map[string]any
|
||||
err := json.NewDecoder(resp.Body).Decode(&response)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "oss", response["edition"])
|
||||
require.Equal(t, false, response["insecure"])
|
||||
require.Equal(t, false, response["authenticated"])
|
||||
}
|
||||
|
||||
func TestInitHandler_Insecure(t *testing.T) {
|
||||
config := Config{Insecure: true}
|
||||
handler := &Handler{config: config}
|
||||
req := httptest.NewRequest("GET", "/admin/init", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
handler.initHandler(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var response map[string]any
|
||||
err := json.NewDecoder(resp.Body).Decode(&response)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "oss", response["edition"])
|
||||
require.Equal(t, true, response["insecure"])
|
||||
require.Equal(t, true, response["authenticated"]) // Since insecure on.
|
||||
}
|
||||
|
||||
// TestAuthHandler_NoPasswordOrSecret tests authHandler error when password or secret is missing.
|
||||
func TestAuthHandler_NoPasswordOrSecret(t *testing.T) {
|
||||
config := Config{Password: "", Secret: ""}
|
||||
handler := &Handler{config: config}
|
||||
req := httptest.NewRequest("POST", "/admin/auth", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
handler.authHandler(resp, req)
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
}
|
||||
|
||||
// TestAuthHandler_ValidPassword tests authHandler token generation with valid password.
|
||||
func TestAuthHandler_ValidPassword(t *testing.T) {
|
||||
config := Config{Password: "test-password", Secret: "test-secret"}
|
||||
handler := &Handler{config: config}
|
||||
form := url.Values{}
|
||||
form.Add("password", "test-password")
|
||||
req := httptest.NewRequest("POST", "/admin/auth", strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
handler.authHandler(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
|
||||
var response map[string]string
|
||||
err := json.NewDecoder(resp.Body).Decode(&response)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, response["token"])
|
||||
}
|
||||
|
||||
// TestAuthHandler_InvalidPassword tests authHandler rejection with invalid password.
|
||||
func TestAuthHandler_InvalidPassword(t *testing.T) {
|
||||
config := Config{Password: "test-password", Secret: "test-secret"}
|
||||
handler := &Handler{config: config}
|
||||
form := url.Values{}
|
||||
form.Add("password", "wrong-password")
|
||||
req := httptest.NewRequest("POST", "/admin/auth", strings.NewReader(form.Encode()))
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
handler.authHandler(resp, req)
|
||||
require.Equal(t, http.StatusBadRequest, resp.Code)
|
||||
}
|
||||
|
||||
// TestAuthHandler_Throttled ensures the /admin/auth route throttles repeated
|
||||
// failed password attempts per client IP, without blocking a valid login from
|
||||
// another IP while an attacker is brute-forcing.
|
||||
func TestAuthHandler_Throttled(t *testing.T) {
|
||||
node := ¢rifuge.Node{}
|
||||
cfg := Config{Password: "test-password", Secret: "test-secret"}
|
||||
handler := NewHandler(node, nil, cfg)
|
||||
|
||||
post := func(ip, password string) int {
|
||||
form := url.Values{}
|
||||
form.Add("password", password)
|
||||
req := httptest.NewRequest("POST", "/admin/auth", strings.NewReader(form.Encode()))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.RemoteAddr = "10.0.0.1:12345" // trusted local proxy peer, so X-Real-IP is honored.
|
||||
req.Header.Set("X-Real-IP", ip)
|
||||
resp := httptest.NewRecorder()
|
||||
handler.ServeHTTP(resp, req)
|
||||
return resp.Code
|
||||
}
|
||||
|
||||
// The default limit is 10 failures per IP per window.
|
||||
for i := 0; i < 10; i++ {
|
||||
require.Equal(t, http.StatusBadRequest, post("6.6.6.6", "wrong"), "attempt %d", i)
|
||||
}
|
||||
require.Equal(t, http.StatusTooManyRequests, post("6.6.6.6", "wrong"))
|
||||
// The attacker cannot tell a correct guess from a throttled one.
|
||||
require.Equal(t, http.StatusTooManyRequests, post("6.6.6.6", "test-password"))
|
||||
|
||||
// A valid login from a different IP still succeeds during the attack.
|
||||
require.Equal(t, http.StatusOK, post("7.7.7.7", "test-password"))
|
||||
}
|
||||
|
||||
// TestAdminSecureTokenAuth_InsecureMode tests adminSecureTokenAuth allows request in insecure mode.
|
||||
func TestAdminSecureTokenAuth_InsecureMode(t *testing.T) {
|
||||
config := Config{Insecure: true}
|
||||
handler := &Handler{config: config}
|
||||
|
||||
// Mocked handler that should be invoked
|
||||
finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
authHandler := handler.adminSecureTokenAuth(finalHandler)
|
||||
req := httptest.NewRequest("GET", "/admin/api", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
authHandler.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
}
|
||||
|
||||
// TestAdminSecureTokenAuth_MissingToken tests adminSecureTokenAuth rejection when token is missing.
|
||||
func TestAdminSecureTokenAuth_MissingToken(t *testing.T) {
|
||||
config := Config{Secret: "test-secret"}
|
||||
handler := &Handler{config: config}
|
||||
|
||||
// Mocked handler that should not be invoked
|
||||
finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
authHandler := handler.adminSecureTokenAuth(finalHandler)
|
||||
req := httptest.NewRequest("GET", "/admin/api", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
authHandler.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusUnauthorized, resp.Code)
|
||||
}
|
||||
|
||||
// TestAdminSecureTokenAuth_ValidToken tests adminSecureTokenAuth with valid token.
|
||||
func TestAdminSecureTokenAuth_ValidToken(t *testing.T) {
|
||||
config := Config{Secret: "test-secret"}
|
||||
handler := &Handler{config: config}
|
||||
|
||||
// Mocked handler that should be invoked
|
||||
finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
// Generate a valid token
|
||||
token, err := generateSecureAdminToken("test-secret")
|
||||
require.NoError(t, err)
|
||||
|
||||
authHandler := handler.adminSecureTokenAuth(finalHandler)
|
||||
req := httptest.NewRequest("GET", "/admin/api", nil)
|
||||
req.Header.Set("Authorization", "token "+token)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
authHandler.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusOK, resp.Code)
|
||||
}
|
||||
|
||||
// TestAdminSecureTokenAuth_InvalidToken tests adminSecureTokenAuth rejection with invalid token.
|
||||
func TestAdminSecureTokenAuth_InvalidToken(t *testing.T) {
|
||||
config := Config{Secret: "test-secret"}
|
||||
handler := &Handler{config: config}
|
||||
|
||||
// Mocked handler that should not be invoked.
|
||||
finalHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Fail(t, "final handler should not be invoked")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
authHandler := handler.adminSecureTokenAuth(finalHandler)
|
||||
req := httptest.NewRequest("GET", "/admin/api", nil)
|
||||
req.Header.Set("Authorization", "token invalid-token")
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
authHandler.ServeHTTP(resp, req)
|
||||
require.Equal(t, http.StatusUnauthorized, resp.Code)
|
||||
}
|
||||
Reference in New Issue
Block a user