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
24 changes: 24 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@ func Load() (*Config, error) {
}
}

// Expand environment variables for a pre-defined set of fields
expand := func(s string) string { return os.Expand(s, expandWithDefault) }
for i := range cfg.Tunnels {
t := &cfg.Tunnels[i]
t.Host = expand(t.Host)
t.User = expand(t.User)
t.IdentityFile = expand(t.IdentityFile)
t.LocalAddress = tunnel.StringOrInt(expand(t.LocalAddress.String()))
t.RemoteAddress = tunnel.StringOrInt(expand(t.RemoteAddress.String()))
}
Comment thread
alebeck marked this conversation as resolved.

// Create a map of tunnel names to tunnel pointers for easy lookup later
m, err := buildTunnelsMap(cfg.Tunnels)
if err != nil {
Expand Down Expand Up @@ -127,3 +138,16 @@ func specialPrefix(s string) bool {
func containsGlob(s string) bool {
return strings.ContainsAny(s, "*?[")
}

// expandWithDefault resolves an environment variable reference, supporting
// the ${VAR:-default} syntax. If the variable is unset or empty and a default
// is provided after ":-", the default value is returned.
func expandWithDefault(key string) string {
if varName, defaultVal, found := strings.Cut(key, ":-"); found {
if val := os.Getenv(varName); val != "" {
return val
}
return defaultVal
}
return os.Getenv(key)
}
81 changes: 81 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,84 @@ func TestSpecialPrefixEmpty(t *testing.T) {
t.Error(`specialPrefix("") = true, want false`)
}
}

func loadFixture(t *testing.T, path string) *Config {
orig := Path
t.Cleanup(func() { Path = orig })
Path = path
cfg, err := Load()
if err != nil {
t.Fatalf("Load() error: %v", err)
}
return cfg
}

func TestExpandEnvVars(t *testing.T) {
t.Setenv("TEST_HOST", "example.com")
t.Setenv("TEST_USER", "alice")
t.Setenv("TEST_IDENTITY", "/keys/id_ed25519")
t.Setenv("TEST_LOCAL", "9000")
t.Setenv("TEST_REMOTE", "localhost:8080")

cfg := loadFixture(t, "../../test/testdata/config/expand/vars.toml")

tun := cfg.Tunnels[0]
if tun.Host != "example.com" {
t.Errorf("Host = %q, want %q", tun.Host, "example.com")
}
if tun.User != "alice" {
t.Errorf("User = %q, want %q", tun.User, "alice")
}
if tun.IdentityFile != "/keys/id_ed25519" {
t.Errorf("IdentityFile = %q, want %q", tun.IdentityFile, "/keys/id_ed25519")
}
if tun.LocalAddress.String() != "9000" {
t.Errorf("LocalAddress = %q, want %q", tun.LocalAddress.String(), "9000")
}
if tun.RemoteAddress.String() != "localhost:8080" {
t.Errorf("RemoteAddress = %q, want %q", tun.RemoteAddress.String(), "localhost:8080")
}
}

func TestExpandDefault(t *testing.T) {
t.Setenv("TEST_SET", "real")
t.Setenv("TEST_EMPTY", "")
// TEST_UNSET is unset

cfg := loadFixture(t, "../../test/testdata/config/expand/defaults.toml")

cases := map[string]string{
"set": "real",
"empty": "fallback",
"unset": "fallback",
}
for name, want := range cases {
if got := cfg.TunnelsMap[name].Host; got != want {
t.Errorf("tunnel %q Host = %q, want %q", name, got, want)
}
}
}

func TestExpandUnsetIsEmpty(t *testing.T) {
// A referenced-but-unset variable with no default expands to ""
cfg := loadFixture(t, "../../test/testdata/config/expand/unset.toml")

if got := cfg.Tunnels[0].Host; got != "" {
t.Errorf("Host = %q, want empty string", got)
}
}

func TestExpandFieldsNotExpanded(t *testing.T) {
// Name and Group are identifiers and are intentionally not expanded
t.Setenv("TEST_VAR", "expanded")

cfg := loadFixture(t, "../../test/testdata/config/expand/literal_fields.toml")

tun := cfg.Tunnels[0]
if tun.Name != "tun_$TEST_VAR" {
t.Errorf("Name = %q, want it left literal", tun.Name)
}
if tun.Group != "grp_$TEST_VAR" {
t.Errorf("Group = %q, want it left literal", tun.Group)
}
}
Comment thread
alebeck marked this conversation as resolved.
12 changes: 12 additions & 0 deletions test/testdata/config/expand/defaults.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ${VAR:-fallback} syntax: TEST_SET is set, TEST_EMPTY is empty, TEST_UNSET is unset
[[tunnels]]
name = "set"
host = "${TEST_SET:-fallback}"

[[tunnels]]
name = "empty"
host = "${TEST_EMPTY:-fallback}"

[[tunnels]]
name = "unset"
host = "${TEST_UNSET:-fallback}"
5 changes: 5 additions & 0 deletions test/testdata/config/expand/literal_fields.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# name and group are identifiers and are NOT expanded, so $TEST_VAR stays literal
[[tunnels]]
name = "tun_$TEST_VAR"
group = "grp_$TEST_VAR"
host = "example.com"
4 changes: 4 additions & 0 deletions test/testdata/config/expand/unset.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# TEST_NOPE is unset and has no default, so it should expand to an empty string
[[tunnels]]
name = "tun"
host = "${TEST_NOPE}"
8 changes: 8 additions & 0 deletions test/testdata/config/expand/vars.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Expanded against TEST_HOST, TEST_USER, TEST_IDENTITY, TEST_LOCAL, TEST_REMOTE
[[tunnels]]
name = "tun"
host = "${TEST_HOST}"
user = "$TEST_USER"
identity = "${TEST_IDENTITY}"
local = "${TEST_LOCAL}"
remote = "${TEST_REMOTE}"