mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
Add support for 'include' to configuration files
This commit is contained in:
3
conf/includes/passwords.conf
Normal file
3
conf/includes/passwords.conf
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Just foo & bar for testing
|
||||||
|
ALICE_PASS: $2a$10$UHR6GhotWhpLsKtVP0/i6.Nh9.fuY73cWjLoJjb2sKT8KISBcUW5q
|
||||||
|
BOB_PASS: $2a$11$dZM98SpGeI7dCFFGSpt.JObQcix8YHml4TBUZoge9R1uxnMIln5ly
|
||||||
8
conf/includes/users.conf
Normal file
8
conf/includes/users.conf
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Users configuration
|
||||||
|
|
||||||
|
include ./passwords.conf;
|
||||||
|
|
||||||
|
users = [
|
||||||
|
{user: alice, password: $ALICE_PASS}
|
||||||
|
{user: bob, password: $BOB_PASS}
|
||||||
|
]
|
||||||
119
conf/lex.go
119
conf/lex.go
@@ -17,6 +17,7 @@ package conf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
@@ -40,6 +41,7 @@ const (
|
|||||||
itemMapEnd
|
itemMapEnd
|
||||||
itemCommentStart
|
itemCommentStart
|
||||||
itemVariable
|
itemVariable
|
||||||
|
itemInclude
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -288,11 +290,118 @@ func lexQuotedKey(lx *lexer) stateFn {
|
|||||||
return lexQuotedKey
|
return lexQuotedKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keyCheckKeyword will check for reserved keywords as the key value when the key is
|
||||||
|
// separated with a space.
|
||||||
|
func (lx *lexer) keyCheckKeyword(fallThrough, push stateFn) stateFn {
|
||||||
|
key := strings.ToLower(lx.input[lx.start:lx.pos])
|
||||||
|
switch key {
|
||||||
|
case "include":
|
||||||
|
lx.ignore()
|
||||||
|
if push != nil {
|
||||||
|
lx.push(push)
|
||||||
|
}
|
||||||
|
return lexIncludeStart
|
||||||
|
}
|
||||||
|
lx.emit(itemKey)
|
||||||
|
return fallThrough
|
||||||
|
}
|
||||||
|
|
||||||
|
// lexIncludeStart will consume the whitespace til the start of the value.
|
||||||
|
func lexIncludeStart(lx *lexer) stateFn {
|
||||||
|
r := lx.next()
|
||||||
|
if isWhitespace(r) {
|
||||||
|
return lexSkip(lx, lexIncludeStart)
|
||||||
|
}
|
||||||
|
lx.backup()
|
||||||
|
return lexInclude
|
||||||
|
}
|
||||||
|
|
||||||
|
// lexIncludeQuotedString consumes the inner contents of a string. It assumes that the
|
||||||
|
// beginning '"' has already been consumed and ignored. It will not interpret any
|
||||||
|
// internal contents.
|
||||||
|
func lexIncludeQuotedString(lx *lexer) stateFn {
|
||||||
|
r := lx.next()
|
||||||
|
switch {
|
||||||
|
case r == sqStringEnd:
|
||||||
|
lx.backup()
|
||||||
|
lx.emit(itemInclude)
|
||||||
|
lx.next()
|
||||||
|
lx.ignore()
|
||||||
|
return lx.pop()
|
||||||
|
}
|
||||||
|
return lexIncludeQuotedString
|
||||||
|
}
|
||||||
|
|
||||||
|
// lexIncludeDubQuotedString consumes the inner contents of a string. It assumes that the
|
||||||
|
// beginning '"' has already been consumed and ignored. It will not interpret any
|
||||||
|
// internal contents.
|
||||||
|
func lexIncludeDubQuotedString(lx *lexer) stateFn {
|
||||||
|
r := lx.next()
|
||||||
|
switch {
|
||||||
|
case r == dqStringEnd:
|
||||||
|
lx.backup()
|
||||||
|
lx.emit(itemInclude)
|
||||||
|
lx.next()
|
||||||
|
lx.ignore()
|
||||||
|
return lx.pop()
|
||||||
|
}
|
||||||
|
return lexIncludeDubQuotedString
|
||||||
|
}
|
||||||
|
|
||||||
|
// lexIncludeString consumes the inner contents of a raw string.
|
||||||
|
func lexIncludeString(lx *lexer) stateFn {
|
||||||
|
r := lx.next()
|
||||||
|
switch {
|
||||||
|
case isNL(r) || r == eof || r == optValTerm || r == mapEnd || isWhitespace(r):
|
||||||
|
lx.backup()
|
||||||
|
lx.emit(itemInclude)
|
||||||
|
return lx.pop()
|
||||||
|
case r == sqStringEnd:
|
||||||
|
lx.backup()
|
||||||
|
lx.emit(itemInclude)
|
||||||
|
lx.next()
|
||||||
|
lx.ignore()
|
||||||
|
return lx.pop()
|
||||||
|
}
|
||||||
|
return lexIncludeString
|
||||||
|
}
|
||||||
|
|
||||||
|
// lexInclude will consume the include value.
|
||||||
|
func lexInclude(lx *lexer) stateFn {
|
||||||
|
r := lx.next()
|
||||||
|
switch {
|
||||||
|
case r == sqStringStart:
|
||||||
|
lx.ignore() // ignore the " or '
|
||||||
|
return lexIncludeQuotedString
|
||||||
|
case r == dqStringStart:
|
||||||
|
lx.ignore() // ignore the " or '
|
||||||
|
return lexIncludeDubQuotedString
|
||||||
|
case r == arrayStart:
|
||||||
|
return lx.errorf("Expected include value but found start of an array")
|
||||||
|
case r == mapStart:
|
||||||
|
return lx.errorf("Expected include value but found start of a map")
|
||||||
|
case r == blockStart:
|
||||||
|
return lx.errorf("Expected include value but found start of a block")
|
||||||
|
case unicode.IsDigit(r), r == '-':
|
||||||
|
return lx.errorf("Expected include value but found start of a number")
|
||||||
|
case r == '\\':
|
||||||
|
return lx.errorf("Expected include value but found escape sequence")
|
||||||
|
case isNL(r):
|
||||||
|
return lx.errorf("Expected include value but found new line")
|
||||||
|
}
|
||||||
|
lx.backup()
|
||||||
|
return lexIncludeString
|
||||||
|
}
|
||||||
|
|
||||||
// lexKey consumes the text of a key. Assumes that the first character (which
|
// lexKey consumes the text of a key. Assumes that the first character (which
|
||||||
// is not whitespace) has already been consumed.
|
// is not whitespace) has already been consumed.
|
||||||
func lexKey(lx *lexer) stateFn {
|
func lexKey(lx *lexer) stateFn {
|
||||||
r := lx.peek()
|
r := lx.peek()
|
||||||
if unicode.IsSpace(r) || isKeySeparator(r) || r == eof {
|
if unicode.IsSpace(r) {
|
||||||
|
// Spaces signal we could be looking at a keyword, e.g. include.
|
||||||
|
// Keywords will eat the keyword and set the appropriate return stateFn.
|
||||||
|
return lx.keyCheckKeyword(lexKeyEnd, nil)
|
||||||
|
} else if isKeySeparator(r) || r == eof {
|
||||||
lx.emit(itemKey)
|
lx.emit(itemKey)
|
||||||
return lexKeyEnd
|
return lexKeyEnd
|
||||||
}
|
}
|
||||||
@@ -492,7 +601,11 @@ func lexMapDubQuotedKey(lx *lexer) stateFn {
|
|||||||
// is not whitespace) has already been consumed.
|
// is not whitespace) has already been consumed.
|
||||||
func lexMapKey(lx *lexer) stateFn {
|
func lexMapKey(lx *lexer) stateFn {
|
||||||
r := lx.peek()
|
r := lx.peek()
|
||||||
if unicode.IsSpace(r) || isKeySeparator(r) {
|
if unicode.IsSpace(r) {
|
||||||
|
// Spaces signal we could be looking at a keyword, e.g. include.
|
||||||
|
// Keywords will eat the keyword and set the appropriate return stateFn.
|
||||||
|
return lx.keyCheckKeyword(lexMapKeyEnd, lexMapValueEnd)
|
||||||
|
} else if isKeySeparator(r) {
|
||||||
lx.emit(itemKey)
|
lx.emit(itemKey)
|
||||||
return lexMapKeyEnd
|
return lexMapKeyEnd
|
||||||
}
|
}
|
||||||
@@ -953,6 +1066,8 @@ func (itype itemType) String() string {
|
|||||||
return "CommentStart"
|
return "CommentStart"
|
||||||
case itemVariable:
|
case itemVariable:
|
||||||
return "Variable"
|
return "Variable"
|
||||||
|
case itemInclude:
|
||||||
|
return "Include"
|
||||||
}
|
}
|
||||||
panic(fmt.Sprintf("BUG: Unknown type '%s'.", itype.String()))
|
panic(fmt.Sprintf("BUG: Unknown type '%s'.", itype.String()))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -842,3 +842,40 @@ func TestArrayOfMaps(t *testing.T) {
|
|||||||
lx := lex(arrayOfMaps)
|
lx := lex(arrayOfMaps)
|
||||||
expect(t, lx, expectedItems)
|
expect(t, lx, expectedItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInclude(t *testing.T) {
|
||||||
|
expectedItems := []item{
|
||||||
|
{itemInclude, "users.conf", 1},
|
||||||
|
{itemEOF, "", 1},
|
||||||
|
}
|
||||||
|
lx := lex("include \"users.conf\"")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
|
||||||
|
lx = lex("include 'users.conf'")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
|
||||||
|
lx = lex("include users.conf")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMapInclude(t *testing.T) {
|
||||||
|
expectedItems := []item{
|
||||||
|
{itemKey, "foo", 1},
|
||||||
|
{itemMapStart, "", 1},
|
||||||
|
{itemInclude, "users.conf", 1},
|
||||||
|
{itemMapEnd, "", 1},
|
||||||
|
{itemEOF, "", 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
lx := lex("foo { include users.conf }")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
|
||||||
|
lx = lex("foo {include users.conf}")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
|
||||||
|
lx = lex("foo { include 'users.conf' }")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
|
||||||
|
lx = lex("foo { include \"users.conf\"}")
|
||||||
|
expect(t, lx, expectedItems)
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ package conf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -35,25 +37,43 @@ type parser struct {
|
|||||||
|
|
||||||
// Keys stack
|
// Keys stack
|
||||||
keys []string
|
keys []string
|
||||||
|
|
||||||
|
// The config file path, empty by default.
|
||||||
|
fp string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse will return a map of keys to interface{}, although concrete types
|
// Parse will return a map of keys to interface{}, although concrete types
|
||||||
// underly them. The values supported are string, bool, int64, float64, DateTime.
|
// underly them. The values supported are string, bool, int64, float64, DateTime.
|
||||||
// Arrays and nested Maps are also supported.
|
// Arrays and nested Maps are also supported.
|
||||||
func Parse(data string) (map[string]interface{}, error) {
|
func Parse(data string) (map[string]interface{}, error) {
|
||||||
p, err := parse(data)
|
p, err := parse(data, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return p.mapping, nil
|
return p.mapping, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parse(data string) (p *parser, err error) {
|
// ParseFile is a helper to open file, etc. and parse the contents.
|
||||||
|
func ParseFile(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), filepath.Dir(fp))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return p.mapping, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(data, fp string) (p *parser, err error) {
|
||||||
p = &parser{
|
p = &parser{
|
||||||
mapping: make(map[string]interface{}),
|
mapping: make(map[string]interface{}),
|
||||||
lx: lex(data),
|
lx: lex(data),
|
||||||
ctxs: make([]interface{}, 0, 4),
|
ctxs: make([]interface{}, 0, 4),
|
||||||
keys: make([]string, 0, 4),
|
keys: make([]string, 0, 4),
|
||||||
|
fp: fp,
|
||||||
}
|
}
|
||||||
p.pushContext(p.mapping)
|
p.pushContext(p.mapping)
|
||||||
|
|
||||||
@@ -152,7 +172,6 @@ func (p *parser) processItem(it item) error {
|
|||||||
case "gb":
|
case "gb":
|
||||||
p.setValue(num * 1024 * 1024 * 1024)
|
p.setValue(num * 1024 * 1024 * 1024)
|
||||||
}
|
}
|
||||||
|
|
||||||
case itemFloat:
|
case itemFloat:
|
||||||
num, err := strconv.ParseFloat(it.val, 64)
|
num, err := strconv.ParseFloat(it.val, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -193,6 +212,15 @@ func (p *parser) processItem(it item) error {
|
|||||||
return fmt.Errorf("Variable reference for '%s' on line %d can not be found.",
|
return fmt.Errorf("Variable reference for '%s' on line %d can not be found.",
|
||||||
it.val, it.line)
|
it.val, it.line)
|
||||||
}
|
}
|
||||||
|
case itemInclude:
|
||||||
|
m, err := ParseFile(filepath.Join(p.fp, it.val))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error parsing include file '%s', %v.", it.val, err)
|
||||||
|
}
|
||||||
|
for k, v := range m {
|
||||||
|
p.pushKey(k)
|
||||||
|
p.setValue(v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -231,3 +231,34 @@ func TestSample5(t *testing.T) {
|
|||||||
}
|
}
|
||||||
test(t, sample5, ex)
|
test(t, sample5, ex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIncludes(t *testing.T) {
|
||||||
|
ex := map[string]interface{}{
|
||||||
|
"listen": "127.0.0.1:4222",
|
||||||
|
"authorization": map[string]interface{}{
|
||||||
|
"ALICE_PASS": "$2a$10$UHR6GhotWhpLsKtVP0/i6.Nh9.fuY73cWjLoJjb2sKT8KISBcUW5q",
|
||||||
|
"BOB_PASS": "$2a$11$dZM98SpGeI7dCFFGSpt.JObQcix8YHml4TBUZoge9R1uxnMIln5ly",
|
||||||
|
"users": []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"user": "alice",
|
||||||
|
"password": "$2a$10$UHR6GhotWhpLsKtVP0/i6.Nh9.fuY73cWjLoJjb2sKT8KISBcUW5q"},
|
||||||
|
map[string]interface{}{
|
||||||
|
"user": "bob",
|
||||||
|
"password": "$2a$11$dZM98SpGeI7dCFFGSpt.JObQcix8YHml4TBUZoge9R1uxnMIln5ly"},
|
||||||
|
},
|
||||||
|
"timeout": float64(0.5),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := ParseFile("simple.conf")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Received err: %v\n", err)
|
||||||
|
}
|
||||||
|
if m == nil {
|
||||||
|
t.Fatal("Received nil map")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(m, ex) {
|
||||||
|
t.Fatalf("Not Equal:\nReceived: '%+v'\nExpected: '%+v'\n", m, ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
8
conf/simple.conf
Normal file
8
conf/simple.conf
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Copyright 2016 Apcera Inc. All rights reserved.
|
||||||
|
|
||||||
|
listen: 127.0.0.1:4222
|
||||||
|
|
||||||
|
authorization {
|
||||||
|
include 'includes/users.conf' # Pull in from file
|
||||||
|
timeout: 0.5
|
||||||
|
}
|
||||||
@@ -130,12 +130,7 @@ func ProcessConfigFile(configFile string) (*Options, error) {
|
|||||||
return opts, nil
|
return opts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(configFile)
|
m, err := conf.ParseFile(configFile)
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error opening config file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m, err := conf.Parse(string(data))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,33 +3,8 @@
|
|||||||
listen: 127.0.0.1:2442
|
listen: 127.0.0.1:2442
|
||||||
|
|
||||||
authorization {
|
authorization {
|
||||||
# Our role based permissions.
|
# Authorizations
|
||||||
|
include "auths.conf"
|
||||||
# Admin can do anything.
|
|
||||||
ADMIN = {
|
|
||||||
publish = ">"
|
|
||||||
subscribe = ">"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Can do requests on req.foo or req.bar, and subscribe to anything
|
|
||||||
# that is a response, e.g. _INBOX.*
|
|
||||||
#
|
|
||||||
# Notice that authorization filters can be singletons or arrays.
|
|
||||||
REQUESTOR = {
|
|
||||||
publish = ["req.foo", "req.bar"]
|
|
||||||
subscribe = "_INBOX.*"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Default permissions if none presented. e.g. Joe below.
|
|
||||||
DEFAULT_PERMISSIONS = {
|
|
||||||
publish = "SANDBOX.*"
|
|
||||||
subscribe = ["PUBLIC.>", "_INBOX.>"]
|
|
||||||
}
|
|
||||||
|
|
||||||
# This is to benchmark pub performance.
|
|
||||||
BENCH = {
|
|
||||||
publish = "a"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Just foo for testing
|
# Just foo for testing
|
||||||
PASS: $2a$10$UHR6GhotWhpLsKtVP0/i6.Nh9.fuY73cWjLoJjb2sKT8KISBcUW5q
|
PASS: $2a$10$UHR6GhotWhpLsKtVP0/i6.Nh9.fuY73cWjLoJjb2sKT8KISBcUW5q
|
||||||
|
|||||||
@@ -87,5 +87,4 @@ func TestUserAuthorizationProto(t *testing.T) {
|
|||||||
expectResult(t, c, permErrRe)
|
expectResult(t, c, permErrRe)
|
||||||
|
|
||||||
c.Close()
|
c.Close()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user