219 lines
7.5 KiB
Go
219 lines
7.5 KiB
Go
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)
|
|
}
|