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) { authCalls := 0 newAdminTestAPI(t, &test.AuthManager{ AuthenticateImpl: func(_ *auth.Request) (string, *auth.Error) { authCalls++ 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.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, `id="login-form"`) require.Contains(t, body, `id="logout-button"`) require.Contains(t, body, "./app.css") require.Contains(t, body, "./list-tools.js") require.Contains(t, body, "./app.js") require.NotContains(t, body, "https://") require.Zero(t, authCalls) 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.Contains(t, body, "sessionStorage") require.Contains(t, body, "Authorization") require.NotContains(t, body, ".innerHTML") require.NotContains(t, body, "localStorage") res, body = readAdminResponse(t, client, "http://"+adminTestAddress+"/admin/list-tools.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, "fetchAllPages") require.Contains(t, body, "API 返回了无效的分页数据") require.Contains(t, body, "concurrency") require.NotContains(t, body, ".innerHTML") 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 TestAdminAPIUsesAuthentication(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+"/v3/info") 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")) } func TestAdminAPIAcceptsBasicCredentials(t *testing.T) { newAdminTestAPI(t, &test.AuthManager{ AuthenticateImpl: func(req *auth.Request) (string, *auth.Error) { require.Equal(t, conf.AuthActionAPI, req.Action) require.Equal(t, "test-admin", req.Credentials.User) require.Equal(t, "test-password", req.Credentials.Pass) return req.Credentials.User, nil }, }) transport := &http.Transport{} t.Cleanup(transport.CloseIdleConnections) client := &http.Client{Transport: transport} req, err := http.NewRequest(http.MethodGet, "http://"+adminTestAddress+"/v3/info", nil) require.NoError(t, err) req.SetBasicAuth("test-admin", "test-password") res, err := client.Do(req) require.NoError(t, err) require.NoError(t, res.Body.Close()) require.Equal(t, http.StatusOK, res.StatusCode) }