package api //nolint:revive import ( "fmt" "io" "net/http" "strings" "testing" "time" "github.com/stretchr/testify/require" "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/test" ) const adminTestAddress = "localhost:9996" func newAdminTestAPI(t *testing.T, authManager apiAuthManager) *API { t.Helper() api := &API{ Address: adminTestAddress, ReadTimeout: conf.Duration(10 * time.Second), WriteTimeout: conf.Duration(10 * time.Second), AuthManager: authManager, Parent: &testParent{}, } require.NoError(t, api.Initialize()) t.Cleanup(api.Close) return api } func readAdminResponse(t *testing.T, client *http.Client, url string) (*http.Response, string) { t.Helper() res, err := client.Get(url) require.NoError(t, err) body, err := io.ReadAll(res.Body) require.NoError(t, err) require.NoError(t, res.Body.Close()) return res, string(body) } func TestAdminAssets(t *testing.T) { newAdminTestAPI(t, test.NilAuthManager) transport := &http.Transport{} t.Cleanup(transport.CloseIdleConnections) client := &http.Client{Transport: transport} res, body := readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/") require.Equal(t, http.StatusOK, res.StatusCode) require.Equal(t, "text/html; charset=utf-8", res.Header.Get("Content-Type")) require.Equal(t, "no-store", res.Header.Get("Cache-Control")) require.Contains(t, res.Header.Get("Content-Security-Policy"), "object-src 'none'") require.Equal(t, "nosniff", res.Header.Get("X-Content-Type-Options")) require.Contains(t, body, "MediaMTX Stream Console") require.Contains(t, body, "./app.css") require.Contains(t, body, "./app.js") require.NotContains(t, body, "https://") res, body = readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/app.css") require.Equal(t, http.StatusOK, res.StatusCode) require.Equal(t, "text/css; charset=utf-8", res.Header.Get("Content-Type")) require.Contains(t, body, "prefers-reduced-motion") require.Contains(t, body, "@media (max-width: 700px)") res, body = readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/app.js") require.Equal(t, http.StatusOK, res.StatusCode) require.Equal(t, "text/javascript; charset=utf-8", res.Header.Get("Content-Type")) require.Contains(t, body, "sourceMasked") require.Contains(t, body, "encodePathName") require.Contains(t, body, "if (source) payload.source = source") require.Contains(t, body, "redactSensitiveText") require.Contains(t, body, "state.global.webrtcAddress") require.NotContains(t, body, ".innerHTML") require.NotContains(t, body, "localStorage") res, _ = readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/missing.js") require.Equal(t, http.StatusNotFound, res.StatusCode) } func TestAdminRedirect(t *testing.T) { newAdminTestAPI(t, test.NilAuthManager) transport := &http.Transport{} t.Cleanup(transport.CloseIdleConnections) client := &http.Client{ Transport: transport, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }, } res, _ := readAdminResponse(t, client, "http://"+adminTestAddress+"/admin") require.Equal(t, http.StatusMovedPermanently, res.StatusCode) require.Equal(t, "/admin/", res.Header.Get("Location")) } func TestAdminUsesAPIAuthentication(t *testing.T) { var got *auth.Request newAdminTestAPI(t, &test.AuthManager{ AuthenticateImpl: func(req *auth.Request) (string, *auth.Error) { got = req return "", &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf("denied")} }, }) transport := &http.Transport{} t.Cleanup(transport.CloseIdleConnections) client := &http.Client{Transport: transport} res, body := readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/") require.Equal(t, http.StatusUnauthorized, res.StatusCode) require.Equal(t, `Basic realm="mediamtx"`, res.Header.Get("WWW-Authenticate")) require.NotNil(t, got) require.Equal(t, conf.AuthActionAPI, got.Action) require.True(t, got.EnableAskCredentials) require.True(t, strings.Contains(body, "authentication error")) }