From 1f540a2aaf861dd2dc3438db50d60f59d5514477 Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Mon, 27 Sep 2021 14:59:27 +0200 Subject: [PATCH] return error in case of non-existent parameters in the configuration file --- internal/conf/conf.go | 53 ++++++++++++++++++++- internal/conf/conf_test.go | 98 ++++++++++++++++++++++++++++---------- internal/conf/protocol.go | 4 +- internal/core/api.go | 91 ++++++++++++++++++----------------- 4 files changed, 175 insertions(+), 71 deletions(-) diff --git a/internal/conf/conf.go b/internal/conf/conf.go index 3acf0f9f..17bcb547 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "os" + "reflect" "time" "github.com/aler9/gortsplib/pkg/headers" @@ -72,6 +73,7 @@ func loadFromFile(fpath string, conf *Conf) (bool, error) { m2[k.(string)] = convert(v) } return m2 + case []interface{}: a2 := make([]interface{}, len(x)) for i, v := range x { @@ -79,10 +81,59 @@ func loadFromFile(fpath string, conf *Conf) (bool, error) { } return a2 } + return i } temp = convert(temp) + // check for non-existent parameters + var checkNonExistentFields func(what interface{}, ref interface{}) error + checkNonExistentFields = func(what interface{}, ref interface{}) error { + if what == nil { + return nil + } + + ma, ok := what.(map[string]interface{}) + if !ok { + return fmt.Errorf("not a map") + } + + for k, v := range ma { + fi := func() reflect.Type { + rr := reflect.TypeOf(ref) + for i := 0; i < rr.NumField(); i++ { + f := rr.Field(i) + if f.Tag.Get("json") == k { + return f.Type + } + } + return nil + }() + if fi == nil { + return fmt.Errorf("non-existent parameter: '%s'", k) + } + + if fi == reflect.TypeOf(map[string]*PathConf{}) && v != nil { + ma2, ok := v.(map[string]interface{}) + if !ok { + return fmt.Errorf("parameter %s is not a map", k) + } + + for k2, v2 := range ma2 { + err := checkNonExistentFields(v2, reflect.Zero(fi.Elem().Elem()).Interface()) + if err != nil { + return fmt.Errorf("parameter %s, key %s: %s", k, k2, err) + } + } + } + } + return nil + } + err = checkNonExistentFields(temp, Conf{}) + if err != nil { + return true, err + } + // convert the generic map into JSON byts, err = json.Marshal(temp) if err != nil { @@ -170,7 +221,7 @@ func Load(fpath string) (*Conf, bool, error) { return conf, found, nil } -// CheckAndFillMissing checks the configuration for errors and fills missing fields. +// CheckAndFillMissing checks the configuration for errors and fills missing parameters. func (conf *Conf) CheckAndFillMissing() error { if conf.LogLevel == 0 { conf.LogLevel = LogLevel(logger.Info) diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 98370236..fce4c100 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -29,27 +29,57 @@ func writeTempFile(byts []byte) (string, error) { } func TestConfFromFile(t *testing.T) { - tmpf, err := writeTempFile([]byte(` -paths: - cam1: - runOnDemandStartTimeout: 5s -`)) - require.NoError(t, err) - defer os.Remove(tmpf) + func() { + tmpf, err := writeTempFile([]byte( + "paths:\n" + + " cam1:\n" + + " runOnDemandStartTimeout: 5s\n")) + require.NoError(t, err) + defer os.Remove(tmpf) - conf, hasFile, err := Load(tmpf) - require.NoError(t, err) - require.Equal(t, true, hasFile) + conf, hasFile, err := Load(tmpf) + require.NoError(t, err) + require.Equal(t, true, hasFile) - pa, ok := conf.Paths["cam1"] - require.Equal(t, true, ok) - require.Equal(t, &PathConf{ - Source: "publisher", - SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), - SourceOnDemandCloseAfter: 10 * StringDuration(time.Second), - RunOnDemandStartTimeout: 5 * StringDuration(time.Second), - RunOnDemandCloseAfter: 10 * StringDuration(time.Second), - }, pa) + pa, ok := conf.Paths["cam1"] + require.Equal(t, true, ok) + require.Equal(t, &PathConf{ + Source: "publisher", + SourceOnDemandStartTimeout: 10 * StringDuration(time.Second), + SourceOnDemandCloseAfter: 10 * StringDuration(time.Second), + RunOnDemandStartTimeout: 5 * StringDuration(time.Second), + RunOnDemandCloseAfter: 10 * StringDuration(time.Second), + }, pa) + }() + + func() { + tmpf, err := writeTempFile([]byte(``)) + require.NoError(t, err) + defer os.Remove(tmpf) + + _, _, err = Load(tmpf) + require.NoError(t, err) + }() + + func() { + tmpf, err := writeTempFile([]byte(`paths:`)) + require.NoError(t, err) + defer os.Remove(tmpf) + + _, _, err = Load(tmpf) + require.NoError(t, err) + }() + + func() { + tmpf, err := writeTempFile([]byte( + "paths:\n" + + " mypath:\n")) + require.NoError(t, err) + defer os.Remove(tmpf) + + _, _, err = Load(tmpf) + require.NoError(t, err) + }() } func TestConfFromFileAndEnv(t *testing.T) { @@ -96,11 +126,9 @@ func TestConfFromEnvOnly(t *testing.T) { func TestConfEncryption(t *testing.T) { key := "testing123testin" - plaintext := ` -paths: - path1: - path2: -` + plaintext := "paths:\n" + + " path1:\n" + + " path2:\n" encryptedConf := func() string { var secretKey [32]byte @@ -132,3 +160,25 @@ paths: _, ok = conf.Paths["path2"] require.Equal(t, true, ok) } + +func TestConfErrorNonExistentParameter(t *testing.T) { + func() { + tmpf, err := writeTempFile([]byte(`invalid: param`)) + require.NoError(t, err) + defer os.Remove(tmpf) + + _, _, err = Load(tmpf) + require.Equal(t, "non-existent parameter: 'invalid'", err.Error()) + }() + + func() { + tmpf, err := writeTempFile([]byte("paths:\n" + + " mypath:\n" + + " invalid: parameter\n")) + require.NoError(t, err) + defer os.Remove(tmpf) + + _, _, err = Load(tmpf) + require.Equal(t, "parameter paths, key mypath: non-existent parameter: 'invalid'", err.Error()) + }() +} diff --git a/internal/conf/protocol.go b/internal/conf/protocol.go index 173d18bb..83dc015c 100644 --- a/internal/conf/protocol.go +++ b/internal/conf/protocol.go @@ -21,6 +21,7 @@ type Protocols map[Protocol]struct{} // MarshalJSON marshals a Protocols into JSON. func (d Protocols) MarshalJSON() ([]byte, error) { out := make([]string, len(d)) + i := 0 for p := range d { var v string @@ -36,7 +37,8 @@ func (d Protocols) MarshalJSON() ([]byte, error) { v = "tcp" } - out = append(out, v) + out[i] = v + i++ } return json.Marshal(out) diff --git a/internal/core/api.go b/internal/core/api.go index 53702322..d002fe14 100644 --- a/internal/core/api.go +++ b/internal/core/api.go @@ -39,49 +39,49 @@ func fillStruct(dest interface{}, source interface{}) { } func cloneStruct(dest interface{}, source interface{}) { - enc, _ := json.Marshal(dest) - _ = json.Unmarshal(enc, source) + enc, _ := json.Marshal(source) + _ = json.Unmarshal(enc, dest) } func loadConfData(ctx *gin.Context) (interface{}, error) { var in struct { // general - LogLevel *string `json:"logLevel"` - LogDestinations *[]string `json:"logDestinations"` - LogFile *string `json:"logFile"` - ReadTimeout *conf.StringDuration `json:"readTimeout"` - WriteTimeout *conf.StringDuration `json:"writeTimeout"` - ReadBufferCount *int `json:"readBufferCount"` - API *bool `json:"api"` - APIAddress *string `json:"apiAddress"` - Metrics *bool `json:"metrics"` - MetricsAddress *string `json:"metricsAddress"` - PPROF *bool `json:"pprof"` - PPROFAddress *string `json:"pprofAddress"` - RunOnConnect *string `json:"runOnConnect"` - RunOnConnectRestart *bool `json:"runOnConnectRestart"` + LogLevel *conf.LogLevel `json:"logLevel"` + LogDestinations *conf.LogDestinations `json:"logDestinations"` + LogFile *string `json:"logFile"` + ReadTimeout *conf.StringDuration `json:"readTimeout"` + WriteTimeout *conf.StringDuration `json:"writeTimeout"` + ReadBufferCount *int `json:"readBufferCount"` + API *bool `json:"api"` + APIAddress *string `json:"apiAddress"` + Metrics *bool `json:"metrics"` + MetricsAddress *string `json:"metricsAddress"` + PPROF *bool `json:"pprof"` + PPROFAddress *string `json:"pprofAddress"` + RunOnConnect *string `json:"runOnConnect"` + RunOnConnectRestart *bool `json:"runOnConnectRestart"` - // rtsp - RTSPDisable *bool `json:"rtspDisable"` - Protocols *[]string `json:"protocols"` - Encryption *string `json:"encryption"` - RTSPAddress *string `json:"rtspAddress"` - RTSPSAddress *string `json:"rtspsAddress"` - RTPAddress *string `json:"rtpAddress"` - RTCPAddress *string `json:"rtcpAddress"` - MulticastIPRange *string `json:"multicastIPRange"` - MulticastRTPPort *int `json:"multicastRTPPort"` - MulticastRTCPPort *int `json:"multicastRTCPPort"` - ServerKey *string `json:"serverKey"` - ServerCert *string `json:"serverCert"` - AuthMethods *[]string `json:"authMethods"` - ReadBufferSize *int `json:"readBufferSize"` + // RTSP + RTSPDisable *bool `json:"rtspDisable"` + Protocols *conf.Protocols `json:"protocols"` + Encryption *conf.Encryption `json:"encryption"` + RTSPAddress *string `json:"rtspAddress"` + RTSPSAddress *string `json:"rtspsAddress"` + RTPAddress *string `json:"rtpAddress"` + RTCPAddress *string `json:"rtcpAddress"` + MulticastIPRange *string `json:"multicastIPRange"` + MulticastRTPPort *int `json:"multicastRTPPort"` + MulticastRTCPPort *int `json:"multicastRTCPPort"` + ServerKey *string `json:"serverKey"` + ServerCert *string `json:"serverCert"` + AuthMethods *conf.AuthMethods `json:"authMethods"` + ReadBufferSize *int `json:"readBufferSize"` - // rtmp + // RTMP RTMPDisable *bool `json:"rtmpDisable"` RTMPAddress *string `json:"rtmpAddress"` - // hls + // HLS HLSDisable *bool `json:"hlsDisable"` HLSAddress *string `json:"hlsAddress"` HLSAlwaysRemux *bool `json:"hlsAlwaysRemux"` @@ -101,7 +101,7 @@ func loadConfPathData(ctx *gin.Context) (interface{}, error) { var in struct { // source Source *string `json:"source"` - SourceProtocol *string `json:"sourceProtocol"` + SourceProtocol *conf.SourceProtocol `json:"sourceProtocol"` SourceAnyPortEnable *bool `json:"sourceAnyPortEnable"` SourceFingerprint *string `json:"sourceFingerprint"` SourceOnDemand *bool `json:"sourceOnDemand"` @@ -112,12 +112,12 @@ func loadConfPathData(ctx *gin.Context) (interface{}, error) { Fallback *string `json:"fallback"` // authentication - PublishUser *string `json:"publishUser"` - PublishPass *string `json:"publishPass"` - PublishIPs *[]string `json:"publishIPs"` - ReadUser *string `json:"readUser"` - ReadPass *string `json:"readPass"` - ReadIPs *[]string `json:"readIPs"` + PublishUser *string `json:"publishUser"` + PublishPass *string `json:"publishPass"` + PublishIPs *conf.IPsOrNets `json:"publishIPs"` + ReadUser *string `json:"readUser"` + ReadPass *string `json:"readPass"` + ReadIPs *conf.IPsOrNets `json:"readIPs"` // custom commands RunOnInit *string `json:"runOnInit"` @@ -363,7 +363,7 @@ func (a *api) onConfigSet(ctx *gin.Context) { defer a.mutex.Unlock() var newConf conf.Conf - cloneStruct(a.conf, &newConf) + cloneStruct(&newConf, a.conf) fillStruct(&newConf, in) err = newConf.CheckAndFillMissing() @@ -399,15 +399,16 @@ func (a *api) onConfigPathsAdd(ctx *gin.Context) { defer a.mutex.Unlock() var newConf conf.Conf - cloneStruct(a.conf, &newConf) + cloneStruct(&newConf, a.conf) if _, ok := newConf.Paths[name]; ok { ctx.AbortWithStatus(http.StatusBadRequest) return } - newConfPath := &conf.PathConf{} + newConfPath := &conf.PathConf{} fillStruct(newConfPath, in) + newConf.Paths[name] = newConfPath err = newConf.CheckAndFillMissing() @@ -443,7 +444,7 @@ func (a *api) onConfigPathsEdit(ctx *gin.Context) { defer a.mutex.Unlock() var newConf conf.Conf - cloneStruct(a.conf, &newConf) + cloneStruct(&newConf, a.conf) newConfPath, ok := newConf.Paths[name] if !ok { @@ -480,7 +481,7 @@ func (a *api) onConfigPathsDelete(ctx *gin.Context) { defer a.mutex.Unlock() var newConf conf.Conf - cloneStruct(a.conf, &newConf) + cloneStruct(&newConf, a.conf) if _, ok := newConf.Paths[name]; !ok { ctx.AbortWithStatus(http.StatusBadRequest)