Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions conf/lex.go
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,18 @@ func (lx *lexer) isVariable() bool {
return false
}

// Check if the unquoted string is a variable reference with braces
func (lx *lexer) isVariableWithBraces() bool {
if lx.start >= len(lx.input) {
return false
}
if len(lx.input) > 3 && lx.input[lx.start:lx.start+2] == "${" {
lx.start += 2
return true
}
return false
}

// lexQuotedString 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.
Expand Down Expand Up @@ -964,6 +976,14 @@ func lexString(lx *lexer) stateFn {
lx.emitString()
} else if lx.isBool() {
lx.emit(itemBool)
} else if lx.isVariableWithBraces() {
lx.emit(itemVariable)

// consume the trailing '}'
if lx.pos < len(lx.input) && lx.input[lx.pos] == '}' {
lx.next()
lx.ignore()
}
} else if lx.isVariable() {
lx.emit(itemVariable)
} else {
Expand Down
24 changes: 24 additions & 0 deletions conf/lex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,14 @@ func TestVariableValues(t *testing.T) {
}
lx = lex("foo $bar")
expect(t, lx, expectedItems)

expectedItems = []item{
{itemKey, "foo", 1, 0},
{itemVariable, "bar", 1, 8},
{itemEOF, "", 1, 0},
}
lx = lex("foo = ${bar}")
expect(t, lx, expectedItems)
}

func TestArrays(t *testing.T) {
Expand Down Expand Up @@ -711,6 +719,22 @@ func TestNestedMaps(t *testing.T) {
expect(t, lx, expectedItems)
}

func TestSimpleMapWithVariable(t *testing.T) {
expectedItems := []item{
{itemKey, "foo", 1, 0},
{itemMapStart, "", 1, 7},
{itemKey, "ip", 1, 7},
{itemVariable, "IP", 1, 12},
{itemKey, "port", 1, 17},
{itemVariable, "PORT", 1, 26},
{itemMapEnd, "", 1, 32},
{itemEOF, "", 1, 0},
}

lx := lex("foo = {ip=${IP}, port = ${PORT}}")
expect(t, lx, expectedItems)
}

func TestQuotedKeys(t *testing.T) {
expectedItems := []item{
{itemKey, "foo", 1, 0},
Expand Down
33 changes: 33 additions & 0 deletions conf/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -287,6 +288,10 @@ func (p *parser) processItem(it item, fp string) error {
setValue(it, p.popContext())
case itemString:
// FIXME(dlc) sanitize string?
err := p.checkForEmbeddedVariables(&it)
if err != nil {
return err
}
setValue(it, it.val)
case itemInteger:
lastDigit := 0
Expand Down Expand Up @@ -430,6 +435,34 @@ const pkey = "pk"
// We special case raw strings here that are bcrypt'd. This allows us not to force quoting the strings
const bcryptPrefix = "2a$"

// To match embedded variables.
var varPat = regexp.MustCompile(`\$\{[^@\s]+\}`)

// checkForEmbeddedVariable will check for embedded variables in an itemString.
// If they are found and we can look them up we will replace them in item, otherwise will error.
func (p *parser) checkForEmbeddedVariables(it *item) error {
if !strings.ContainsAny(it.val, "${") {
return nil
}
// We have some embedded variables.
for _, m := range varPat.FindAllString(it.val, -1) {
ref := m[2 : len(m)-1] // Strip leading ${ and trailing }
value, found, err := p.lookupVariable(ref)
if err != nil {
return fmt.Errorf("variable reference for '%s' on line %d could not be parsed: %s",
m, it.line, err)
}
if !found {
return fmt.Errorf("variable reference for '%s' on line %d can not be found",
m, it.line)
}
if v, ok := value.(string); ok {
it.val = strings.Replace(it.val, m, v, 1)
}
}
return nil
}

// lookupVariable will lookup a variable reference. It will use block scoping on keys
// it has seen before, with the top level scoping being the environment variables. We
// ignore array contexts and only process the map contexts..
Expand Down
105 changes: 105 additions & 0 deletions conf/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ func TestSimpleVariable(t *testing.T) {
test(t, varSample, ex)
}

var varSampleWithBraces = `
index = 22
foo = ${index}
`

func TestSimpleVariableWithBraces(t *testing.T) {
ex := map[string]any{
"index": int64(22),
"foo": int64(22),
}
test(t, varSampleWithBraces, ex)
}

var varNestedSample = `
index = 22
nest {
Expand Down Expand Up @@ -113,6 +126,14 @@ func TestMissingVariable(t *testing.T) {
if !strings.HasPrefix(err.Error(), "variable reference") {
t.Fatalf("Wanted a variable reference err, got %q\n", err)
}

_, err = Parse("foo=${index}")
if err == nil {
t.Fatalf("Expected an error for a missing variable, got none")
}
if !strings.HasPrefix(err.Error(), "variable reference") {
t.Fatalf("Wanted a variable reference err, got %q\n", err)
}
}

func TestEnvVariable(t *testing.T) {
Expand Down Expand Up @@ -166,6 +187,90 @@ func TestEnvVariableStringStartingWithNumberUsingQuotes(t *testing.T) {
test(t, fmt.Sprintf("foo = $%s", evar), ex)
}

func TestEnvVariableEmbedded(t *testing.T) {
cluster := `
cluster {
# set the variable token
TOKEN: abc
authorization {
user: user
password: "${TOKEN}"
}
routes = [ "nats://user:${TOKEN}@server.example.com:6222" ]
}`
ex := map[string]any{
"cluster": map[string]any{
"TOKEN": "abc",
"authorization": map[string]any{
"user": "user",
"password": "abc",
},
"routes": []any{
"nats://user:[email protected]:6222",
},
},
}

// don't use test() here because we want to test the Parse function without checking pedantic mode.
m, err := Parse(cluster)
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)
}
}

func TestEnvVariableEmbeddedMissing(t *testing.T) {
cluster := `
cluster {
authorization {
user: user
password: ${TOKEN}
}
}`

_, err := Parse(cluster)
if err == nil {
t.Fatalf("Expected err not being able to process embedded variable, got none")
}
}

func TestEnvVariableEmbeddedOutsideOfQuotes(t *testing.T) {
cluster := `
cluster {
# set the variable token
TOKEN: abc
authorization {
user: user
# ok
password: ${TOKEN}
}
# not ok
routes = [ nats://user:${TOKEN}@server.example.com:6222 ]
}`

_, err := Parse(cluster)
if err == nil {
t.Fatalf("Expected err not being able to process embedded variable, got none")
}
}

func TestEnvVariableEmbeddedSYS(t *testing.T) {
// https://github.com/nats-io/nats-server/pull/5544#discussion_r1641577620
cluster := `
system_account: "$SYS"
`
ex := map[string]any{
"system_account": "$SYS",
}
test(t, cluster, ex)
}

func TestBcryptVariable(t *testing.T) {
ex := map[string]any{
"password": "$2a$11$ooo",
Expand Down
Loading