Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require braces to be present for environment variable expansion #1304

Merged
merged 1 commit into from
Apr 5, 2016
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ https://github.com/elastic/beats/compare/v1.2.0...master[Check the HEAD diff]
- Move event preprocessor applying GeoIP to packetbeat {pull}772[772]
- Add include_fields and drop_fields as part of generic filtering {pull}1120[1120]
- The method signature of HandleFlags() was changed to allow returning an error {pull}1249[1249]
- Require braces for environment variable expansion in config files {pull}1304[1304]

*Packetbeat*
- Rename output fields in the dns package. Former flag `recursion_allowed` becomes `recursion_available`. {pull}803[803]
Expand Down
76 changes: 70 additions & 6 deletions libbeat/cfgfile/cfgfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ func ChangeDefaultCfgfileFlag(beatName string) error {

path, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return fmt.Errorf("Failed to set default config file location because the absolute path to %s could not be obtained. %v", os.Args[0], err)
return fmt.Errorf("Failed to set default config file location because "+
"the absolute path to %s could not be obtained. %v",
os.Args[0], err)
}

cliflag.DefValue = filepath.Join(path, beatName+".yml")
Expand Down Expand Up @@ -80,12 +82,12 @@ func IsTestConfig() bool {
return *testConfig
}

// expandEnv replaces ${var} or $var in config according to the values of the
// current environment variables. The replacement is case-sensitive. References
// to undefined variables are replaced by the empty string. A default value
// can be given by using the form ${var:default value}.
// expandEnv replaces ${var} in config according to the values of the current
// environment variables. The replacement is case-sensitive. References to
// undefined variables are replaced by the empty string. A default value can be
// given by using the form ${var:default value}.
func expandEnv(config []byte) []byte {
return []byte(os.Expand(string(config), func(key string) string {
return []byte(expand(string(config), func(key string) string {
keyAndDefault := strings.SplitN(key, ":", 2)
key = keyAndDefault[0]

Expand All @@ -103,3 +105,65 @@ func expandEnv(config []byte) []byte {
return v
}))
}

// The following methods were copied from the os package of the stdlib. The
// expand method was modified to only expand variables defined with braces and
// ignore $var.

// Expand replaces ${var} in the string based on the mapping function.
func expand(s string, mapping func(string) string) string {
buf := make([]byte, 0, 2*len(s))
// ${} is all ASCII, so bytes are fine for this operation.
i := 0
for j := 0; j < len(s); j++ {
if s[j] == '$' && j+2 < len(s) && s[j+1] == '{' {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only line that was changed from the os.Expand function.

buf = append(buf, s[i:j]...)
name, w := getShellName(s[j+1:])
buf = append(buf, mapping(name)...)
j += w
i = j + 1
}
}
return string(buf) + s[i:]
}

// isShellSpecialVar reports whether the character identifies a special
// shell variable such as $*.
func isShellSpecialVar(c uint8) bool {
switch c {
case '*', '#', '$', '@', '!', '?', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return true
}
return false
}

// isAlphaNum reports whether the byte is an ASCII letter, number, or underscore
func isAlphaNum(c uint8) bool {
return c == '_' || '0' <= c && c <= '9' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z'
}

// getShellName returns the name that begins the string and the number of bytes
// consumed to extract it. If the name is enclosed in {}, it's part of a ${}
// expansion and two more bytes are needed than the length of the name.
func getShellName(s string) (string, int) {
switch {
case s[0] == '{':
if len(s) > 2 && isShellSpecialVar(s[1]) && s[2] == '}' {
return s[1:2], 3
}
// Scan to closing brace
for i := 1; i < len(s); i++ {
if s[i] == '}' {
return s[1:i], i + 1
}
}
return "", 1 // Bad syntax; just eat the brace.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we silently want to ignore bad syntax?

case isShellSpecialVar(s[0]):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checking conditions in caller, this case is not possible anymore. We're always in first case s[0] == '{' => no more need for isShellSpecialVar and isAlphaNum

return s[0:1], 1
}
// Scan alphanumerics.
var i int
for i = 0; i < len(s) && isAlphaNum(s[i]); i++ {
}
return s[:i], i
}
28 changes: 19 additions & 9 deletions libbeat/cfgfile/cfgfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,31 @@ func TestExpandEnv(t *testing.T) {
in string
out string
}{
// Environment variables can be specified as ${env} or $env.
{"x$y", "xy"},
{"x${y}", "xy"},
// Environment variables can be specified as ${env} only.
{"${y}", "y"},
{"$y", "$y"},

// Environment variables are case-sensitive. Neither are replaced.
{"x$Y", "x"},
{"x${Y}", "x"},
// Environment variables are case-sensitive.
{"${Y}", ""},

// Defaults can only be specified when using braces.
// Defaults can be specified.
{"x${Z:D}", "xD"},
{"x${Z:A B C D}", "xA B C D"}, // Spaces are allowed in the default.
{"x${Z:}", "x"},

// Defaults don't work unless braces are used.
{"x$y:D", "xy:D"},
// Un-matched braces are swallowed by the Go os.Expand function.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unfortunately increases the chance of it showing up accidentally, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it does. I'll see about modify their code to remove this behavior. I didn't realize it did this until I started reading the code.

{"x${Y ${Z:Z}", "xZ"},

// Special environment variables are not replaced.
{"$*", "$*"},
{"${*}", ""},
{"$@", "$@"},
{"${@}", ""},
{"$1", "$1"},
{"${1}", ""},

{"", ""},
{"$$", "$$"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only other thing that might be useful is some sort of escaping. e.g. ${{X} becomes ${X}. Would it be tricky to implement?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or optionally escape $$ to become $, in this case one can write $${X} to get ${X}. More common escape character would be , but this one might be somehow interpreted by yaml parser of no quotes or double quotes are used.

}

for _, test := range tests {
Expand Down