From df2364af26816d11c494052a3576d7e6380672c5 Mon Sep 17 00:00:00 2001 From: Waldemar Quevedo Date: Wed, 5 Sep 2018 16:20:30 -0700 Subject: [PATCH] Add -t pedantic config check to the server Signed-off-by: Waldemar Quevedo --- conf/parse.go | 119 ++++- main.go | 4 + server/config_check_test.go | 519 +++++++++++++++++++ server/configs/include_bad_conf_check_a.conf | 6 + server/configs/include_bad_conf_check_b.conf | 4 + server/configs/include_conf_check_a.conf | 6 + server/configs/include_conf_check_b.conf | 3 + server/configs/include_conf_check_c.conf | 5 + server/opts.go | 311 +++++++++-- 9 files changed, 901 insertions(+), 76 deletions(-) create mode 100644 server/config_check_test.go create mode 100644 server/configs/include_bad_conf_check_a.conf create mode 100644 server/configs/include_bad_conf_check_b.conf create mode 100644 server/configs/include_conf_check_a.conf create mode 100644 server/configs/include_conf_check_b.conf create mode 100644 server/configs/include_conf_check_c.conf diff --git a/conf/parse.go b/conf/parse.go index 09205ae0..91cd17b3 100644 --- a/conf/parse.go +++ b/conf/parse.go @@ -51,13 +51,16 @@ type parser struct { // The config file path, empty by default. fp string + + // pedantic reports error when configuration is not correct. + pedantic bool } // Parse will return a map of keys to interface{}, although concrete types // underly them. The values supported are string, bool, int64, float64, DateTime. // Arrays and nested Maps are also supported. func Parse(data string) (map[string]interface{}, error) { - p, err := parse(data, "") + p, err := parse(data, "", false) if err != nil { return nil, err } @@ -71,20 +74,58 @@ func ParseFile(fp string) (map[string]interface{}, error) { return nil, fmt.Errorf("error opening config file: %v", err) } - p, err := parse(string(data), filepath.Dir(fp)) + p, err := parse(string(data), fp, false) if err != nil { return nil, err } return p.mapping, nil } -func parse(data, fp string) (p *parser, err error) { +func ParseFileWithChecks(fp string) (map[string]interface{}, error) { + data, err := ioutil.ReadFile(fp) + if err != nil { + return nil, fmt.Errorf("error opening config file: %v", err) + } + + p, err := parse(string(data), fp, true) + if err != nil { + return nil, err + } + + return p.mapping, nil +} + +type token struct { + item item + value interface{} + usedVariable bool + sourceFile string +} + +func (t *token) Value() interface{} { + return t.value +} + +func (t *token) Line() int { + return t.item.line +} + +func (t *token) IsUsedVariable() bool { + return t.usedVariable +} + +func (t *token) SourceFile() string { + return t.sourceFile +} + +func parse(data, fp string, pedantic bool) (p *parser, err error) { p = &parser{ - mapping: make(map[string]interface{}), - lx: lex(data), - ctxs: make([]interface{}, 0, 4), - keys: make([]string, 0, 4), - fp: fp, + mapping: make(map[string]interface{}), + lx: lex(data), + ctxs: make([]interface{}, 0, 4), + keys: make([]string, 0, 4), + fp: filepath.Dir(fp), + pedantic: pedantic, } p.pushContext(p.mapping) @@ -93,7 +134,7 @@ func parse(data, fp string) (p *parser, err error) { if it.typ == itemEOF { break } - if err := p.processItem(it); err != nil { + if err := p.processItem(it, fp); err != nil { return nil, err } } @@ -135,7 +176,15 @@ func (p *parser) popKey() string { return last } -func (p *parser) processItem(it item) error { +func (p *parser) processItem(it item, fp string) error { + setValue := func(it item, v interface{}) { + if p.pedantic { + p.setValue(&token{it, v, false, fp}) + } else { + p.setValue(v) + } + } + switch it.typ { case itemError: return fmt.Errorf("Parse error on line %d: '%s'", it.line, it.val) @@ -145,9 +194,10 @@ func (p *parser) processItem(it item) error { newCtx := make(map[string]interface{}) p.pushContext(newCtx) case itemMapEnd: - p.setValue(p.popContext()) + setValue(it, p.popContext()) case itemString: - p.setValue(it.val) // FIXME(dlc) sanitize string? + // FIXME(dlc) sanitize string? + setValue(it, it.val) case itemInteger: lastDigit := 0 for _, r := range it.val { @@ -167,21 +217,22 @@ func (p *parser) processItem(it item) error { } // Process a suffix suffix := strings.ToLower(strings.TrimSpace(it.val[lastDigit:])) + switch suffix { case "": - p.setValue(num) + setValue(it, num) case "k": - p.setValue(num * 1000) + setValue(it, num*1000) case "kb": - p.setValue(num * 1024) + setValue(it, num*1024) case "m": - p.setValue(num * 1000 * 1000) + setValue(it, num*1000*1000) case "mb": - p.setValue(num * 1024 * 1024) + setValue(it, num*1024*1024) case "g": - p.setValue(num * 1000 * 1000 * 1000) + setValue(it, num*1000*1000*1000) case "gb": - p.setValue(num * 1024 * 1024 * 1024) + setValue(it, num*1024*1024*1024) } case itemFloat: num, err := strconv.ParseFloat(it.val, 64) @@ -192,39 +243,55 @@ func (p *parser) processItem(it item) error { } return fmt.Errorf("Expected float, but got '%s'.", it.val) } - p.setValue(num) + setValue(it, num) case itemBool: switch strings.ToLower(it.val) { case "true", "yes", "on": - p.setValue(true) + setValue(it, true) case "false", "no", "off": - p.setValue(false) + setValue(it, false) default: return fmt.Errorf("Expected boolean value, but got '%s'.", it.val) } + case itemDatetime: dt, err := time.Parse("2006-01-02T15:04:05Z", it.val) if err != nil { return fmt.Errorf( "Expected Zulu formatted DateTime, but got '%s'.", it.val) } - p.setValue(dt) + setValue(it, dt) case itemArrayStart: var array = make([]interface{}, 0) p.pushContext(array) case itemArrayEnd: array := p.ctx p.popContext() - p.setValue(array) + setValue(it, array) case itemVariable: if value, ok := p.lookupVariable(it.val); ok { - p.setValue(value) + switch tk := value.(type) { + case *token: + // Mark that the variable was used. + tk.usedVariable = true + p.setValue(tk) + default: + p.setValue(value) + } } else { return fmt.Errorf("Variable reference for '%s' on line %d can not be found.", it.val, it.line) } case itemInclude: - m, err := ParseFile(filepath.Join(p.fp, it.val)) + var ( + m map[string]interface{} + err error + ) + if p.pedantic { + m, err = ParseFileWithChecks(filepath.Join(p.fp, it.val)) + } else { + m, err = ParseFile(filepath.Join(p.fp, it.val)) + } if err != nil { return fmt.Errorf("Error parsing include file '%s', %v.", it.val, err) } diff --git a/main.go b/main.go index 4553336c..ac78e829 100644 --- a/main.go +++ b/main.go @@ -33,6 +33,7 @@ Server Options: -c, --config Configuration file -sl,--signal [=] Send signal to gnatsd process (stop, quit, reopen, reload) --client_advertise Client URL to advertise to other servers + -t Test configuration and exit Logging Options: -l, --log File to redirect log output @@ -87,6 +88,9 @@ func main() { server.PrintTLSHelpAndDie) if err != nil { server.PrintAndDie(err.Error()) + } else if opts.CheckConfig { + fmt.Fprintf(os.Stderr, "configuration file %s test is successful\n", opts.ConfigFile) + os.Exit(0) } // Create the server with appropriate options. diff --git a/server/config_check_test.go b/server/config_check_test.go new file mode 100644 index 00000000..3bfcea45 --- /dev/null +++ b/server/config_check_test.go @@ -0,0 +1,519 @@ +// Copyright 2018 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "errors" + "fmt" + "os" + "testing" +) + +func TestConfigCheck(t *testing.T) { + tests := []struct { + // name is the name of the test. + name string + + // config is content of the configuration file. + config string + + // defaultErr is the error we get pedantic checks are not enabled. + defaultErr error + + // pedanticErr is the error we get when pedantic checks are enabled. + pedanticErr error + + // errorLine is the location of the error. + errorLine int + }{ + { + name: "when unknown field is used at top level", + config: ` + monitor = "127.0.0.1:4442" + `, + defaultErr: nil, + pedanticErr: errors.New(`unknown field "monitor"`), + errorLine: 2, + }, + { + name: "when default permissions are used at top level", + config: ` + "default_permissions" { + publish = ["_SANDBOX.>"] + subscribe = ["_SANDBOX.>"] + } + `, + defaultErr: nil, + pedanticErr: errors.New(`unknown field "default_permissions"`), + + // NOTE: line number is '5' because it is where the map definition ends. + errorLine: 5, + }, + { + name: "when authorization config is empty", + config: ` + authorization = { + } + `, + defaultErr: nil, + pedanticErr: nil, + }, + { + name: "when authorization config has unknown fields", + config: ` + authorization = { + foo = "bar" + } + `, + defaultErr: nil, + pedanticErr: errors.New(`unknown field "foo"`), + errorLine: 3, + }, + { + name: "when authorization config has unknown fields", + config: ` + port = 4222 + + authorization = { + user = "hello" + foo = "bar" + password = "world" + } + + `, + defaultErr: nil, + pedanticErr: errors.New(`unknown field "foo"`), + errorLine: 6, + }, + { + name: "when user authorization config has unknown fields", + config: ` + authorization = { + users = [ + { + user = "foo" + pass = "bar" + token = "quux" + } + ] + } + `, + defaultErr: nil, + pedanticErr: errors.New(`unknown field "token"`), + errorLine: 7, + }, + { + name: "when user authorization permissions config has unknown fields", + config: ` + authorization { + permissions { + subscribe = {} + inboxes = {} + publish = {} + } + } + `, + defaultErr: errors.New(`Unknown field inboxes parsing permissions`), + pedanticErr: errors.New(`unknown field "inboxes"`), + errorLine: 5, + }, + { + name: "when user authorization permissions config has unknown fields within allow or deny", + config: ` + authorization { + permissions { + subscribe = { + allow = ["hello", "world"] + deny = ["foo", "bar"] + denied = "_INBOX.>" + } + publish = {} + } + } + `, + defaultErr: errors.New(`Unknown field name "denied" parsing subject permissions, only 'allow' or 'deny' are permitted`), + pedanticErr: errors.New(`unknown field "denied"`), + errorLine: 7, + }, + { + name: "when user authorization permissions config has unknown fields within allow or deny", + config: ` + authorization { + permissions { + publish = { + allow = ["hello", "world"] + deny = ["foo", "bar"] + allowed = "_INBOX.>" + } + subscribe = {} + } + } + `, + defaultErr: errors.New(`Unknown field name "allowed" parsing subject permissions, only 'allow' or 'deny' are permitted`), + pedanticErr: errors.New(`unknown field "allowed"`), + errorLine: 7, + }, + { + name: "when user authorization permissions config has unknown fields using arrays", + config: ` + authorization { + + default_permissions { + subscribe = ["a"] + publish = ["b"] + inboxes = ["c"] + } + + users = [ + { + user = "foo" + pass = "bar" + } + ] + } + `, + defaultErr: errors.New(`Unknown field inboxes parsing permissions`), + pedanticErr: errors.New(`unknown field "inboxes"`), + errorLine: 7, + }, + { + name: "when user authorization permissions config has unknown fields using strings", + config: ` + authorization { + + default_permissions { + subscribe = "a" + requests = "b" + publish = "c" + } + + users = [ + { + user = "foo" + pass = "bar" + } + ] + } + `, + defaultErr: errors.New(`Unknown field requests parsing permissions`), + pedanticErr: errors.New(`unknown field "requests"`), + errorLine: 6, + }, + { + name: "when user authorization permissions config is empty", + config: ` + authorization = { + users = [ + { + user = "foo", pass = "bar", permissions = { + } + } + ] + } + `, + defaultErr: nil, + pedanticErr: nil, + }, + { + name: "when unknown permissions are included in config", + config: ` + authorization = { + users = [ + { + user = "foo", pass = "bar", permissions { + inboxes = true + } + } + ] + } + `, + defaultErr: errors.New(`Unknown field inboxes parsing permissions`), + pedanticErr: errors.New(`unknown field "inboxes"`), + errorLine: 6, + }, + { + name: "when clustering config is empty", + config: ` + cluster = { + } + `, + + defaultErr: nil, + pedanticErr: nil, + }, + { + name: "when unknown option is in clustering config", + config: ` + # NATS Server Configuration + port = 4222 + + cluster = { + + port = 6222 + + foo = "bar" + + authorization { + user = "hello" + pass = "world" + } + + } + `, + + defaultErr: nil, + pedanticErr: errors.New(`unknown field "foo"`), + errorLine: 9, + }, + { + name: "when unknown option is in clustering authorization config", + config: ` + cluster = { + authorization { + foo = "bar" + } + } + `, + + defaultErr: nil, + pedanticErr: errors.New(`unknown field "foo"`), + errorLine: 4, + }, + { + name: "when unknown option is in clustering authorization permissions config", + config: ` + cluster = { + authorization { + user = "foo" + pass = "bar" + permissions = { + hello = "world" + } + } + } + `, + defaultErr: errors.New(`Unknown field hello parsing permissions`), + pedanticErr: errors.New(`unknown field "hello"`), + errorLine: 7, + }, + { + name: "when unknown option is in tls config", + config: ` + tls = { + hello = "world" + } + `, + defaultErr: errors.New(`error parsing tls config, unknown field ["hello"]`), + pedanticErr: errors.New(`unknown field "hello"`), + errorLine: 3, + }, + { + name: "when unknown option is in cluster tls config", + config: ` + cluster { + tls = { + foo = "bar" + } + } + `, + // Backwards compatibility: also report error by default even if pedantic checks disabled. + defaultErr: errors.New(`error parsing tls config, unknown field ["foo"]`), + pedanticErr: errors.New(`unknown field "foo"`), + errorLine: 4, + }, + { + name: "when using cipher suites in the TLS config", + config: ` + tls = { + cipher_suites: [ + "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", + "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" + ] + preferences = [] + } + `, + defaultErr: errors.New(`error parsing tls config, unknown field ["preferences"]`), + pedanticErr: errors.New(`unknown field "preferences"`), + errorLine: 7, + }, + { + name: "when using curve preferences in the TLS config", + config: ` + tls = { + curve_preferences: [ + "CurveP256", + "CurveP384", + "CurveP521" + ] + suites = [] + } + `, + defaultErr: errors.New(`error parsing tls config, unknown field ["suites"]`), + pedanticErr: errors.New(`unknown field "suites"`), + errorLine: 8, + }, + { + name: "when unknown option is in cluster config with defined routes", + config: ` + cluster { + port = 6222 + routes = [ + nats://127.0.0.1:6222 + ] + peers = [] + } + `, + defaultErr: nil, + pedanticErr: errors.New(`unknown field "peers"`), + errorLine: 7, + }, + { + name: "when used as variable in authorization block it should not be considered as unknown field", + config: ` + # listen: 127.0.0.1:-1 + listen: 127.0.0.1:4222 + + authorization { + # Superuser can do anything. + super_user = { + publish = ">" + subscribe = ">" + } + + # Can do requests on foo or bar, and subscribe to anything + # that is a response to an _INBOX. + # + # Notice that authorization filters can be singletons or arrays. + req_pub_user = { + publish = ["req.foo", "req.bar"] + subscribe = "_INBOX.>" + } + + # Setup a default user that can subscribe to anything, but has + # no publish capabilities. + default_user = { + subscribe = "PUBLIC.>" + } + + unused = "hello" + + # Default permissions if none presented. e.g. susan below. + default_permissions: $default_user + + # Users listed with persmissions. + users = [ + {user: alice, password: foo, permissions: $super_user} + {user: bob, password: bar, permissions: $req_pub_user} + {user: susan, password: baz} + ] + } + `, + defaultErr: nil, + pedanticErr: errors.New(`unknown field "unused"`), + errorLine: 27, + }, + { + name: "when used as variable in top level config it should not be considered as unknown field", + config: ` + monitoring_port = 8222 + + http_port = $monitoring_port + + port = 4222 + `, + defaultErr: nil, + pedanticErr: nil, + }, + { + name: "when used as variable in cluster config it should not be considered as unknown field", + config: ` + cluster { + clustering_port = 6222 + port = $clustering_port + } + `, + defaultErr: nil, + pedanticErr: nil, + }, + } + + checkConfig := func(config string, pedantic bool) error { + opts := &Options{ + CheckConfig: pedantic, + } + return opts.ProcessConfigFile(config) + } + + checkErr := func(t *testing.T, err, expectedErr error) { + t.Helper() + switch { + case err == nil && expectedErr == nil: + // OK + case err != nil && expectedErr == nil: + t.Errorf("Unexpected error after processing config: %s", err) + case err == nil && expectedErr != nil: + t.Errorf("Expected %q error after processing invalid config but got nothing", expectedErr) + } + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + conf := createConfFile(t, []byte(test.config)) + defer os.Remove(conf) + + t.Run("with pedantic check enabled", func(t *testing.T) { + err := checkConfig(conf, true) + expectedErr := test.pedanticErr + if err != nil && expectedErr != nil { + msg := fmt.Sprintf("%s in %s:%d", expectedErr.Error(), conf, test.errorLine) + if err.Error() != msg { + t.Errorf("Expected %q, got %q", msg, err.Error()) + } + } + checkErr(t, err, test.pedanticErr) + }) + + t.Run("with pedantic check disabled", func(t *testing.T) { + err := checkConfig(conf, false) + expectedErr := test.defaultErr + if err != nil && expectedErr != nil && err.Error() != expectedErr.Error() { + t.Errorf("Expected %q, got %q", expectedErr.Error(), err.Error()) + } + checkErr(t, err, test.defaultErr) + }) + }) + } +} + +func TestConfigCheckIncludes(t *testing.T) { + // Check happy path first using pedantic mode. + opts := &Options{ + CheckConfig: true, + } + err := opts.ProcessConfigFile("./configs/include_conf_check_a.conf") + if err != nil { + t.Errorf("Unexpected error processing include files with configuration check enabled: %s", err) + } + + opts = &Options{ + CheckConfig: true, + } + err = opts.ProcessConfigFile("./configs/include_bad_conf_check_a.conf") + if err == nil { + t.Errorf("Expected error processing include files with configuration check enabled: %s", err) + } + expectedErr := errors.New(`unknown field "monitoring_port" in configs/include_bad_conf_check_b.conf:2`) + if err != nil && expectedErr != nil && err.Error() != expectedErr.Error() { + t.Errorf("Expected %q, got %q", expectedErr.Error(), err.Error()) + } +} diff --git a/server/configs/include_bad_conf_check_a.conf b/server/configs/include_bad_conf_check_a.conf new file mode 100644 index 00000000..a0973f43 --- /dev/null +++ b/server/configs/include_bad_conf_check_a.conf @@ -0,0 +1,6 @@ + +port = 4222 + +include "include_bad_conf_check_b.conf" + +# http_port = $monitoring_port diff --git a/server/configs/include_bad_conf_check_b.conf b/server/configs/include_bad_conf_check_b.conf new file mode 100644 index 00000000..c8719c07 --- /dev/null +++ b/server/configs/include_bad_conf_check_b.conf @@ -0,0 +1,4 @@ + +monitoring_port = 8222 + +include "include_conf_check_c.conf" diff --git a/server/configs/include_conf_check_a.conf b/server/configs/include_conf_check_a.conf new file mode 100644 index 00000000..0911bbe8 --- /dev/null +++ b/server/configs/include_conf_check_a.conf @@ -0,0 +1,6 @@ + +port = 4222 + +include "include_conf_check_b.conf" + +http_port = $monitoring_port diff --git a/server/configs/include_conf_check_b.conf b/server/configs/include_conf_check_b.conf new file mode 100644 index 00000000..e175af9e --- /dev/null +++ b/server/configs/include_conf_check_b.conf @@ -0,0 +1,3 @@ +monitoring_port = 8222 + +include "include_conf_check_c.conf" diff --git a/server/configs/include_conf_check_c.conf b/server/configs/include_conf_check_c.conf new file mode 100644 index 00000000..8e61ebb9 --- /dev/null +++ b/server/configs/include_conf_check_c.conf @@ -0,0 +1,5 @@ + +authorization { + user = "foo" + pass = "bar" +} diff --git a/server/opts.go b/server/opts.go index 2b4722a6..9f29f9da 100644 --- a/server/opts.go +++ b/server/opts.go @@ -96,6 +96,9 @@ type Options struct { CustomClientAuthentication Authentication `json:"-"` CustomRouterAuthentication Authentication `json:"-"` + + // CheckConfig enables pedantic configuration file syntax checks. + CheckConfig bool `json:"-"` } // Clone performs a deep copy of the Options struct, returning a new clone @@ -186,6 +189,27 @@ e.g. Available cipher suites include: ` +type token interface { + Value() interface{} + Line() int + IsUsedVariable() bool + SourceFile() string +} + +type unknownConfigFieldErr struct { + field string + token token + configFile string +} + +func (e *unknownConfigFieldErr) Error() string { + msg := fmt.Sprintf("unknown field %q", e.field) + if e.token != nil { + return msg + fmt.Sprintf(" in %s:%d", e.configFile, e.token.Line()) + } + return msg +} + // ProcessConfigFile processes a configuration file. // FIXME(dlc): Hacky func ProcessConfigFile(configFile string) (*Options, error) { @@ -196,6 +220,18 @@ func ProcessConfigFile(configFile string) (*Options, error) { return opts, nil } +// unwrapValue can be used to get the token and value from an item +// to be able to report the line number in case of an incorrect +// configuration. +func unwrapValue(v interface{}) (token, interface{}) { + switch tk := v.(type) { + case token: + return tk, tk.Value() + default: + return nil, v + } +} + // ProcessConfigFile updates the Options structure with options // present in the given configuration file. // This version is convenient if one wants to set some default @@ -216,12 +252,24 @@ func (o *Options) ProcessConfigFile(configFile string) error { return nil } - m, err := conf.ParseFile(configFile) + var ( + m map[string]interface{} + tk token + pedantic bool = o.CheckConfig + err error + ) + if pedantic { + m, err = conf.ParseFileWithChecks(configFile) + } else { + m, err = conf.ParseFile(configFile) + } if err != nil { return err } - for k, v := range m { + // When pedantic checks are enabled then need to unwrap + // to get the value along with reported error line. + tk, v = unwrapValue(v) switch strings.ToLower(k) { case "listen": hp, err := parseListen(v) @@ -243,8 +291,12 @@ func (o *Options) ProcessConfigFile(configFile string) error { case "logtime": o.Logtime = v.(bool) case "authorization": - am := v.(map[string]interface{}) - auth, err := parseAuthorization(am) + var auth *authorization + if pedantic { + auth, err = parseAuthorization(tk, o) + } else { + auth, err = parseAuthorization(v, o) + } if err != nil { return err } @@ -288,8 +340,13 @@ func (o *Options) ProcessConfigFile(configFile string) error { case "https_port": o.HTTPSPort = int(v.(int64)) case "cluster": - cm := v.(map[string]interface{}) - if err := parseCluster(cm, o); err != nil { + var err error + if pedantic { + err = parseCluster(tk, o) + } else { + err = parseCluster(v, o) + } + if err != nil { return err } case "logfile", "log_file": @@ -319,8 +376,15 @@ func (o *Options) ProcessConfigFile(configFile string) error { case "ping_max": o.MaxPingsOut = int(v.(int64)) case "tls": - tlsm := v.(map[string]interface{}) - tc, err := parseTLS(tlsm) + var ( + tc *TLSConfigOpts + err error + ) + if pedantic { + tc, err = parseTLS(tk, o) + } else { + tc, err = parseTLS(v, o) + } if err != nil { return err } @@ -342,6 +406,14 @@ func (o *Options) ProcessConfigFile(configFile string) error { o.WriteDeadline = time.Duration(v.(int64)) * time.Second fmt.Printf("WARNING: write_deadline should be converted to a duration\n") } + default: + if pedantic && tk != nil && !tk.IsUsedVariable() { + return &unknownConfigFieldErr{ + field: k, + token: tk, + configFile: tk.SourceFile(), + } + } } } return nil @@ -375,8 +447,17 @@ func parseListen(v interface{}) (*hostPort, error) { } // parseCluster will parse the cluster config. -func parseCluster(cm map[string]interface{}, opts *Options) error { +func parseCluster(v interface{}, opts *Options) error { + var ( + cm map[string]interface{} + tk token + pedantic bool = opts.CheckConfig + ) + _, v = unwrapValue(v) + cm = v.(map[string]interface{}) for mk, mv := range cm { + // Again, unwrap token value if line check is required. + tk, mv = unwrapValue(mv) switch strings.ToLower(mk) { case "listen": hp, err := parseListen(mv) @@ -390,8 +471,15 @@ func parseCluster(cm map[string]interface{}, opts *Options) error { case "host", "net": opts.Cluster.Host = mv.(string) case "authorization": - am := mv.(map[string]interface{}) - auth, err := parseAuthorization(am) + var ( + auth *authorization + err error + ) + if pedantic { + auth, err = parseAuthorization(tk, opts) + } else { + auth, err = parseAuthorization(mv, opts) + } if err != nil { return err } @@ -409,6 +497,7 @@ func parseCluster(cm map[string]interface{}, opts *Options) error { ra := mv.([]interface{}) opts.Routes = make([]*url.URL, 0, len(ra)) for _, r := range ra { + _, r = unwrapValue(r) routeURL := r.(string) url, err := url.Parse(routeURL) if err != nil { @@ -417,8 +506,15 @@ func parseCluster(cm map[string]interface{}, opts *Options) error { opts.Routes = append(opts.Routes, url) } case "tls": - tlsm := mv.(map[string]interface{}) - tc, err := parseTLS(tlsm) + var ( + tc *TLSConfigOpts + err error + ) + if pedantic { + tc, err = parseTLS(tk, opts) + } else { + tc, err = parseTLS(mv, opts) + } if err != nil { return err } @@ -442,12 +538,20 @@ func parseCluster(cm map[string]interface{}, opts *Options) error { if !ok { return fmt.Errorf("Expected permissions to be a map/struct, got %+v", mv) } - perms, err := parseUserPermissions(pm) + perms, err := parseUserPermissions(pm, opts) if err != nil { return err } // This will possibly override permissions that were define in auth block setClusterPermissions(&opts.Cluster, perms) + default: + if pedantic && tk != nil && !tk.IsUsedVariable() { + return &unknownConfigFieldErr{ + field: mk, + token: tk, + configFile: tk.SourceFile(), + } + } } } return nil @@ -468,9 +572,19 @@ func setClusterPermissions(opts *ClusterOpts, perms *Permissions) { } // Helper function to parse Authorization configs. -func parseAuthorization(am map[string]interface{}) (*authorization, error) { - auth := &authorization{} +func parseAuthorization(v interface{}, opts *Options) (*authorization, error) { + var ( + am map[string]interface{} + tk token + pedantic bool = opts.CheckConfig + auth *authorization = &authorization{} + ) + + // Unwrap value first if pedantic config check enabled. + _, v = unwrapValue(v) + am = v.(map[string]interface{}) for mk, mv := range am { + tk, mv = unwrapValue(mv) switch strings.ToLower(mk) { case "user", "username": auth.user = mv.(string) @@ -488,22 +602,43 @@ func parseAuthorization(am map[string]interface{}) (*authorization, error) { } auth.timeout = at case "users": - nkeys, users, err := parseUsers(mv) + var ( + users []*User + err error + nkeys []*NkeyUser + ) + if pedantic { + nkeys, users, err = parseUsers(tk, opts) + } else { + nkeys, users, err = parseUsers(mv, opts) + } if err != nil { return nil, err } auth.users = users auth.nkeys = nkeys case "default_permission", "default_permissions", "permissions": - pm, ok := mv.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("Expected default permissions to be a map/struct, got %+v", mv) + var ( + permissions *Permissions + err error + ) + if pedantic { + permissions, err = parseUserPermissions(tk, opts) + } else { + permissions, err = parseUserPermissions(mv, opts) } - permissions, err := parseUserPermissions(pm) if err != nil { return nil, err } auth.defaultPermissions = permissions + default: + if pedantic && tk != nil && !tk.IsUsedVariable() { + return nil, &unknownConfigFieldErr{ + field: mk, + token: tk, + configFile: tk.SourceFile(), + } + } } // Now check for permission defaults with multiple users, etc. @@ -520,25 +655,39 @@ func parseAuthorization(am map[string]interface{}) (*authorization, error) { } // Helper function to parse multiple users array with optional permissions. -func parseUsers(mv interface{}) ([]*NkeyUser, []*User, error) { +func parseUsers(mv interface{}, opts *Options) ([]*NkeyUser, []*User, error) { + var ( + tk token + pedantic bool = opts.CheckConfig + users []*User = []*User{} + keys []*NkeyUser + ) + _, mv = unwrapValue(mv) + // Make sure we have an array uv, ok := mv.([]interface{}) if !ok { return nil, nil, fmt.Errorf("Expected users field to be an array, got %v", mv) } - var users []*User - var keys []*NkeyUser for _, u := range uv { + _, u = unwrapValue(u) + // Check its a map/struct um, ok := u.(map[string]interface{}) if !ok { return nil, nil, fmt.Errorf("Expected user entry to be a map/struct, got %v", u) } - var perms *Permissions - var err error - user := &User{} - nkey := &NkeyUser{} + + var ( + user *User = &User{} + nkey *NkeyUser = &NkeyUser{} + perms *Permissions + err error + ) for k, v := range um { + // Also needs to unwrap first + tk, v = unwrapValue(v) + switch strings.ToLower(k) { case "nkey": nkey.Nkey = v.(string) @@ -547,14 +696,22 @@ func parseUsers(mv interface{}) ([]*NkeyUser, []*User, error) { case "pass", "password": user.Password = v.(string) case "permission", "permissions", "authorization": - pm, ok := v.(map[string]interface{}) - if !ok { - return nil, nil, fmt.Errorf("Expected user permissions to be a map/struct, got %+v", v) + if pedantic { + perms, err = parseUserPermissions(tk, opts) + } else { + perms, err = parseUserPermissions(v, opts) } - perms, err = parseUserPermissions(pm) if err != nil { return nil, nil, err } + default: + if pedantic && tk != nil && !tk.IsUsedVariable() { + return nil, nil, &unknownConfigFieldErr{ + field: k, + token: tk, + configFile: tk.SourceFile(), + } + } } } // Place perms if we have them. @@ -588,38 +745,58 @@ func parseUsers(mv interface{}) ([]*NkeyUser, []*User, error) { } // Helper function to parse user/account permissions -func parseUserPermissions(pm map[string]interface{}) (*Permissions, error) { - p := &Permissions{} +func parseUserPermissions(mv interface{}, opts *Options) (*Permissions, error) { + var ( + tk token + pedantic bool = opts.CheckConfig + p *Permissions = &Permissions{} + ) + _, mv = unwrapValue(mv) + pm, ok := mv.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("Expected permissions to be a map/struct, got %+v", mv) + } for k, v := range pm { + tk, v = unwrapValue(v) + switch strings.ToLower(k) { // For routes: // Import is Publish // Export is Subscribe case "pub", "publish", "import": - perms, err := parseVariablePermissions(v) + perms, err := parseVariablePermissions(v, opts) + if err != nil { return nil, err } p.Publish = perms case "sub", "subscribe", "export": - perms, err := parseVariablePermissions(v) + perms, err := parseVariablePermissions(v, opts) + if err != nil { return nil, err } p.Subscribe = perms default: + if pedantic && tk != nil && !tk.IsUsedVariable() { + return nil, &unknownConfigFieldErr{ + field: k, + token: tk, + configFile: tk.SourceFile(), + } + } return nil, fmt.Errorf("Unknown field %s parsing permissions", k) } } return p, nil } -// Tope level parser for authorization configurations. -func parseVariablePermissions(v interface{}) (*SubjectPermission, error) { - switch v.(type) { +// Top level parser for authorization configurations. +func parseVariablePermissions(v interface{}, opts *Options) (*SubjectPermission, error) { + switch vv := v.(type) { case map[string]interface{}: // New style with allow and/or deny properties. - return parseSubjectPermission(v.(map[string]interface{})) + return parseSubjectPermission(vv, opts) default: // Old style return parseOldPermissionStyle(v) @@ -629,13 +806,15 @@ func parseVariablePermissions(v interface{}) (*SubjectPermission, error) { // Helper function to parse subject singeltons and/or arrays func parseSubjects(v interface{}) ([]string, error) { var subjects []string - switch v.(type) { + switch vv := v.(type) { case string: - subjects = append(subjects, v.(string)) + subjects = append(subjects, vv) case []string: - subjects = v.([]string) + subjects = vv case []interface{}: - for _, i := range v.([]interface{}) { + for _, i := range vv { + _, i = unwrapValue(i) + subject, ok := i.(string) if !ok { return nil, fmt.Errorf("Subject in permissions array cannot be cast to string") @@ -661,14 +840,15 @@ func parseOldPermissionStyle(v interface{}) (*SubjectPermission, error) { } // Helper function to parse new style authorization into a SubjectPermission with Allow and Deny. -func parseSubjectPermission(m map[string]interface{}) (*SubjectPermission, error) { +func parseSubjectPermission(v interface{}, opts *Options) (*SubjectPermission, error) { + m := v.(map[string]interface{}) if len(m) == 0 { return nil, nil } - p := &SubjectPermission{} - + pedantic := opts.CheckConfig for k, v := range m { + tk, v := unwrapValue(v) switch strings.ToLower(k) { case "allow": subjects, err := parseSubjects(v) @@ -683,6 +863,13 @@ func parseSubjectPermission(m map[string]interface{}) (*SubjectPermission, error } p.Deny = subjects default: + if pedantic && tk != nil && !tk.IsUsedVariable() { + return nil, &unknownConfigFieldErr{ + field: k, + token: tk, + configFile: tk.SourceFile(), + } + } return nil, fmt.Errorf("Unknown field name %q parsing subject permissions, only 'allow' or 'deny' are permitted", k) } } @@ -713,7 +900,6 @@ func PrintTLSHelpAndDie() { } func parseCipher(cipherName string) (uint16, error) { - cipher, exists := cipherMap[cipherName] if !exists { return 0, fmt.Errorf("Unrecognized cipher %s", cipherName) @@ -731,9 +917,17 @@ func parseCurvePreferences(curveName string) (tls.CurveID, error) { } // Helper function to parse TLS configs. -func parseTLS(tlsm map[string]interface{}) (*TLSConfigOpts, error) { - tc := TLSConfigOpts{} +func parseTLS(v interface{}, opts *Options) (*TLSConfigOpts, error) { + var ( + tlsm map[string]interface{} + tk token + tc TLSConfigOpts = TLSConfigOpts{} + pedantic bool = opts.CheckConfig + ) + _, v = unwrapValue(v) + tlsm = v.(map[string]interface{}) for mk, mv := range tlsm { + tk, mv = unwrapValue(mv) switch strings.ToLower(mk) { case "cert_file": certFile, ok := mv.(string) @@ -766,6 +960,7 @@ func parseTLS(tlsm map[string]interface{}) (*TLSConfigOpts, error) { } tc.Ciphers = make([]uint16, 0, len(ra)) for _, r := range ra { + _, r = unwrapValue(r) cipher, err := parseCipher(r.(string)) if err != nil { return nil, err @@ -779,6 +974,7 @@ func parseTLS(tlsm map[string]interface{}) (*TLSConfigOpts, error) { } tc.CurvePreferences = make([]tls.CurveID, 0, len(ra)) for _, r := range ra { + _, r = unwrapValue(r) cps, err := parseCurvePreferences(r.(string)) if err != nil { return nil, err @@ -795,6 +991,14 @@ func parseTLS(tlsm map[string]interface{}) (*TLSConfigOpts, error) { } tc.Timeout = at default: + if pedantic && tk != nil && !tk.IsUsedVariable() { + return nil, &unknownConfigFieldErr{ + field: mk, + token: tk, + configFile: tk.SourceFile(), + } + } + return nil, fmt.Errorf("error parsing tls config, unknown field [%q]", mk) } } @@ -1134,6 +1338,7 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp, fs.IntVar(&opts.HTTPSPort, "https_port", 0, "HTTPS Port for /varz, /connz endpoints.") fs.StringVar(&configFile, "c", "", "Configuration file.") fs.StringVar(&configFile, "config", "", "Configuration file.") + fs.BoolVar(&opts.CheckConfig, "t", false, "Check configuration and exit.") fs.StringVar(&signal, "sl", "", "Send signal to gnatsd process (stop, quit, reopen, reload)") fs.StringVar(&signal, "signal", "", "Send signal to gnatsd process (stop, quit, reopen, reload)") fs.StringVar(&opts.PidFile, "P", "", "File to store process pid.") @@ -1216,12 +1421,18 @@ func ConfigureOptions(fs *flag.FlagSet, args []string, printVersion, printHelp, // This will update the options with values from the config file. if err := opts.ProcessConfigFile(configFile); err != nil { return nil, err + } else if opts.CheckConfig { + // Report configuration file syntax test was successful and exit. + return opts, nil } + // Call this again to override config file options with options from command line. // Note: We don't need to check error here since if there was an error, it would // have been caught the first time this function was called (after setting up the // flags). fs.Parse(args) + } else if opts.CheckConfig { + return nil, fmt.Errorf("must specify [-c, --config] option to check configuration file syntax") } // Special handling of some flags