package conf import ( "crypto/rand" "encoding/base64" "io" "os" "testing" "time" "github.com/bluenviron/gortsplib/v4" "github.com/stretchr/testify/require" "golang.org/x/crypto/nacl/secretbox" "github.com/bluenviron/mediamtx/internal/logger" ) func createTempFile(byts []byte) (string, error) { tmpf, err := os.CreateTemp(os.TempDir(), "rtsp-") if err != nil { return "", err } defer tmpf.Close() _, err = tmpf.Write(byts) if err != nil { return "", err } return tmpf.Name(), nil } func TestConfFromFile(t *testing.T) { func() { tmpf, err := createTempFile([]byte("logLevel: debug\n" + "paths:\n" + " cam1:\n" + " runOnDemandStartTimeout: 5s\n")) require.NoError(t, err) defer os.Remove(tmpf) conf, confPath, err := Load(tmpf, nil) require.NoError(t, err) require.Equal(t, tmpf, confPath) require.Equal(t, LogLevel(logger.Debug), conf.LogLevel) pa, ok := conf.Paths["cam1"] require.Equal(t, true, ok) require.Equal(t, &Path{ Name: "cam1", Source: "publisher", SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), SourceOnDemandCloseAfter: 10 * StringDuration(time.Second), RecordPath: "./recordings/%path/%Y-%m-%d_%H-%M-%S-%f", RecordFormat: RecordFormatFMP4, RecordPartDuration: StringDuration(1 * time.Second), RecordSegmentDuration: 3600000000000, RecordDeleteAfter: 86400000000000, OverridePublisher: true, RPICameraWidth: 1920, RPICameraHeight: 1080, RPICameraContrast: 1, RPICameraSaturation: 1, RPICameraSharpness: 1, RPICameraExposure: "normal", RPICameraAWB: "auto", RPICameraAWBGains: []float64{0, 0}, RPICameraDenoise: "off", RPICameraMetering: "centre", RPICameraFPS: 30, RPICameraAfMode: "continuous", RPICameraAfRange: "normal", RPICameraAfSpeed: "normal", RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX", RPICameraCodec: "auto", RPICameraIDRPeriod: 60, RPICameraBitrate: 5000000, RPICameraProfile: "main", RPICameraLevel: "4.1", RunOnDemandStartTimeout: 5 * StringDuration(time.Second), RunOnDemandCloseAfter: 10 * StringDuration(time.Second), }, pa) }() func() { tmpf, err := createTempFile([]byte(``)) require.NoError(t, err) defer os.Remove(tmpf) _, _, err = Load(tmpf, nil) require.NoError(t, err) }() func() { tmpf, err := createTempFile([]byte(`paths:`)) require.NoError(t, err) defer os.Remove(tmpf) _, _, err = Load(tmpf, nil) require.NoError(t, err) }() func() { tmpf, err := createTempFile([]byte( "paths:\n" + " mypath:\n")) require.NoError(t, err) defer os.Remove(tmpf) _, _, err = Load(tmpf, nil) require.NoError(t, err) }() } func TestConfFromFileAndEnv(t *testing.T) { // global parameter t.Setenv("RTSP_PROTOCOLS", "tcp") // path parameter t.Setenv("MTX_PATHS_CAM1_SOURCE", "rtsp://testing") // deprecated global parameter t.Setenv("MTX_RTMPDISABLE", "yes") // deprecated path parameter t.Setenv("MTX_PATHS_CAM2_DISABLEPUBLISHEROVERRIDE", "yes") tmpf, err := createTempFile([]byte("{}")) require.NoError(t, err) defer os.Remove(tmpf) conf, confPath, err := Load(tmpf, nil) require.NoError(t, err) require.Equal(t, tmpf, confPath) require.Equal(t, Protocols{Protocol(gortsplib.TransportTCP): {}}, conf.Protocols) require.Equal(t, false, conf.RTMP) pa, ok := conf.Paths["cam1"] require.Equal(t, true, ok) require.Equal(t, "rtsp://testing", pa.Source) pa, ok = conf.Paths["cam2"] require.Equal(t, true, ok) require.Equal(t, false, pa.OverridePublisher) } func TestConfFromEnvOnly(t *testing.T) { t.Setenv("MTX_PATHS_CAM1_SOURCE", "rtsp://testing") conf, confPath, err := Load("", nil) require.NoError(t, err) require.Equal(t, "", confPath) pa, ok := conf.Paths["cam1"] require.Equal(t, true, ok) require.Equal(t, "rtsp://testing", pa.Source) } func TestConfEncryption(t *testing.T) { key := "testing123testin" plaintext := "paths:\n" + " path1:\n" + " path2:\n" encryptedConf := func() string { var secretKey [32]byte copy(secretKey[:], key) var nonce [24]byte _, err := io.ReadFull(rand.Reader, nonce[:]) require.NoError(t, err) encrypted := secretbox.Seal(nonce[:], []byte(plaintext), &nonce, &secretKey) return base64.StdEncoding.EncodeToString(encrypted) }() t.Setenv("RTSP_CONFKEY", key) tmpf, err := createTempFile([]byte(encryptedConf)) require.NoError(t, err) defer os.Remove(tmpf) conf, confPath, err := Load(tmpf, nil) require.NoError(t, err) require.Equal(t, tmpf, confPath) _, ok := conf.Paths["path1"] require.Equal(t, true, ok) _, ok = conf.Paths["path2"] require.Equal(t, true, ok) } func TestConfDeprecatedAuth(t *testing.T) { tmpf, err := createTempFile([]byte( "paths:\n" + " cam:\n" + " readUser: myuser\n" + " readPass: mypass\n")) require.NoError(t, err) defer os.Remove(tmpf) conf, _, err := Load(tmpf, nil) require.NoError(t, err) require.Equal(t, AuthInternalUsers{ { User: "any", Permissions: []AuthInternalUserPermission{ { Action: AuthActionPlayback, }, }, }, { User: "any", IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")}, Permissions: []AuthInternalUserPermission{ { Action: AuthActionAPI, }, { Action: AuthActionMetrics, }, { Action: AuthActionPprof, }, }, }, { User: "any", IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")}, Permissions: []AuthInternalUserPermission{ { Action: AuthActionPublish, Path: "cam", }, }, }, { User: "myuser", Pass: "mypass", IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")}, Permissions: []AuthInternalUserPermission{ { Action: AuthActionRead, Path: "cam", }, }, }, }, conf.AuthInternalUsers) } func TestConfErrors(t *testing.T) { for _, ca := range []struct { name string conf string err string }{ { "duplicate parameter", "paths:\n" + "paths:\n", "yaml: unmarshal errors:\n line 2: key \"paths\" already set in map", }, { "non existent parameter 1", `invalid: param`, "json: unknown field \"invalid\"", }, { "invalid readTimeout", "readTimeout: 0s\n", "'readTimeout' must be greater than zero", }, { "invalid writeTimeout", "writeTimeout: 0s\n", "'writeTimeout' must be greater than zero", }, { "invalid writeQueueSize", "writeQueueSize: 1001\n", "'writeQueueSize' must be a power of two", }, { "invalid udpMaxPayloadSize", "udpMaxPayloadSize: 5000\n", "'udpMaxPayloadSize' must be less than 1472", }, { "invalid strict encryption 1", "encryption: strict\n" + "protocols: [udp]\n", "strict encryption can't be used with the UDP transport protocol", }, { "invalid strict encryption 2", "encryption: strict\n" + "protocols: [multicast]\n", "strict encryption can't be used with the UDP-multicast transport protocol", }, { "invalid ICE server", "webrtcICEServers: [testing]\n", "invalid ICE server: 'testing'", }, { "non existent parameter 2", "paths:\n" + " mypath:\n" + " invalid: parameter\n", "json: unknown field \"invalid\"", }, { "invalid path name", "paths:\n" + " '':\n" + " source: publisher\n", "invalid path name '': cannot be empty", }, { "double raspberry pi camera", "paths:\n" + " cam1:\n" + " source: rpiCamera\n" + " cam2:\n" + " source: rpiCamera\n", "'rpiCamera' with same camera ID 0 is used as source in two paths, 'cam2' and 'cam1'", }, { "invalid srt publish passphrase", "paths:\n" + " mypath:\n" + " srtPublishPassphrase: a\n", `invalid 'srtPublishPassphrase': must be between 10 and 79 characters`, }, { "invalid srt read passphrase", "paths:\n" + " mypath:\n" + " srtReadPassphrase: a\n", `invalid 'readRTPassphrase': must be between 10 and 79 characters`, }, { "all_others aliases", "paths:\n" + " all:\n" + " all_others:\n", `all_others, all and '~^.*$' are aliases`, }, { "all_others aliases", "paths:\n" + " all_others:\n" + " ~^.*$:\n", `all_others, all and '~^.*$' are aliases`, }, { "playback", "playback: yes\n" + "paths:\n" + " my_path:\n" + " recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S", `record path './recordings/%path/%Y-%m-%d_%H-%M-%S' is missing one of the` + ` mandatory elements for the playback server to work: %Y %m %d %H %M %S %f`, }, { "jwt claim key empty", "authMethod: jwt\n" + "authJWTJWKS: https://not-real.com\n" + "authJWTClaimKey: \"\"", "'authJWTClaimKey' is empty", }, } { t.Run(ca.name, func(t *testing.T) { tmpf, err := createTempFile([]byte(ca.conf)) require.NoError(t, err) defer os.Remove(tmpf) _, _, err = Load(tmpf, nil) require.EqualError(t, err, ca.err) }) } } func TestSampleConfFile(t *testing.T) { func() { conf1, confPath1, err := Load("../../mediamtx.yml", nil) require.NoError(t, err) require.Equal(t, "../../mediamtx.yml", confPath1) conf1.Paths = make(map[string]*Path) conf1.OptionalPaths = nil conf2, confPath2, err := Load("", nil) require.NoError(t, err) require.Equal(t, "", confPath2) require.Equal(t, conf1, conf2) }() func() { conf1, confPath1, err := Load("../../mediamtx.yml", nil) require.NoError(t, err) require.Equal(t, "../../mediamtx.yml", confPath1) tmpf, err := createTempFile([]byte("paths:\n all_others:")) require.NoError(t, err) defer os.Remove(tmpf) conf2, confPath2, err := Load(tmpf, nil) require.NoError(t, err) require.Equal(t, tmpf, confPath2) require.Equal(t, conf1.Paths, conf2.Paths) }() } // needed due to https://github.com/golang/go/issues/21092 func TestConfOverrideDefaultSlices(t *testing.T) { tmpf, err := createTempFile([]byte( "authInternalUsers:\n" + " - user: user1\n" + " - user: user2\n" + "authHTTPExclude:\n" + " - path: ''\n")) require.NoError(t, err) defer os.Remove(tmpf) conf, _, err := Load(tmpf, nil) require.NoError(t, err) require.Equal(t, AuthInternalUsers{ { User: "user1", }, { User: "user2", }, }, conf.AuthInternalUsers) require.Equal(t, AuthInternalUserPermissions{ {}, }, conf.AuthHTTPExclude) }