diff --git a/internal/cloudflare/tunnel.go b/internal/cloudflare/tunnel.go index e3926d0..918e1f3 100644 --- a/internal/cloudflare/tunnel.go +++ b/internal/cloudflare/tunnel.go @@ -1,6 +1,7 @@ package cloudflare import ( + "encoding/base64" "encoding/json" "errors" "io" @@ -53,9 +54,42 @@ func IsLegacyRunTokenState(values map[string]string) bool { func ReadCredentialsFile(path string) (Credentials, error) { contents, err := os.ReadFile(path) if err != nil { - return Credentials{}, errors.New("read Cloudflare credentials file") + return Credentials{}, errors.New("invalid Cloudflare credentials file") } - return parseCredentials(contents) + if credentials, err := parseCredentials(contents); err == nil { + return credentials, nil + } + credentials, err := credentialsFromSecret(contents) + if err != nil { + return Credentials{}, errors.New("invalid Cloudflare credentials file") + } + return credentials, nil +} + +func credentialsFromSecret(contents []byte) (Credentials, error) { + var secret struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + StringData map[string]string `yaml:"stringData"` + Data map[string]string `yaml:"data"` + } + decoder := yaml.NewDecoder(strings.NewReader(string(contents))) + if err := decoder.Decode(&secret); err != nil || decoder.Decode(&struct{}{}) != io.EOF || secret.APIVersion != "v1" || secret.Kind != "Secret" { + return Credentials{}, errors.New("invalid Secret") + } + plaintext, inStringData := secret.StringData[credentialsKey] + encoded, inData := secret.Data[credentialsKey] + if inStringData == inData { + return Credentials{}, errors.New("missing Secret credentials") + } + if inStringData { + return parseCredentials([]byte(plaintext)) + } + decoded, err := base64.StdEncoding.DecodeString(encoded) + if err != nil { + return Credentials{}, errors.New("invalid Secret credentials") + } + return parseCredentials(decoded) } func NewRoute(hostname, service string) (Route, error) { diff --git a/internal/cloudflare/tunnel_test.go b/internal/cloudflare/tunnel_test.go index 2656762..59e299a 100644 --- a/internal/cloudflare/tunnel_test.go +++ b/internal/cloudflare/tunnel_test.go @@ -1,6 +1,7 @@ package cloudflare import ( + "encoding/base64" "os" "path/filepath" "testing" @@ -31,18 +32,31 @@ func TestStoredTunnelRejectsLegacyOrPartialState(t *testing.T) { func TestReadCredentialsFileRequiresKnownFields(t *testing.T) { path := filepath.Join(t.TempDir(), "credentials.json") - if err := os.WriteFile(path, []byte(`{"AccountTag":"account","TunnelSecret":"secret","TunnelID":"tunnel"}`), 0600); err != nil { - t.Fatal(err) - } - credentials, err := ReadCredentialsFile(path) - if err != nil || credentials.TunnelID != "tunnel" { - t.Fatal("valid credential file was not accepted") + raw := `{"AccountTag":"account","TunnelSecret":"secret","TunnelID":"tunnel"}` + for _, contents := range []string{ + raw, + "apiVersion: v1\nkind: Secret\nstringData:\n credentials.json: |\n " + raw + "\n", + "apiVersion: v1\nkind: Secret\ndata:\n credentials.json: " + base64.StdEncoding.EncodeToString([]byte(raw)) + "\n", + } { + if err := os.WriteFile(path, []byte(contents), 0600); err != nil { + t.Fatal(err) + } + credentials, err := ReadCredentialsFile(path) + if err != nil || credentials.TunnelID != "tunnel" { + t.Fatal("supported credential file was not accepted") + } } if err := os.WriteFile(path, []byte(`{"AccountTag":"account","TunnelSecret":"secret","TunnelID":"tunnel","extra":"value"}`), 0600); err != nil { t.Fatal(err) } - if _, err := ReadCredentialsFile(path); err == nil { - t.Fatal("credential file with unknown fields was accepted") + if _, err := ReadCredentialsFile(path); err == nil || err.Error() != "invalid Cloudflare credentials file" { + t.Fatal("credential file with unknown fields did not return a safe error") + } + if err := os.WriteFile(path, []byte("apiVersion: v1\nkind: Secret\ndata: {}\n"), 0600); err != nil { + t.Fatal(err) + } + if _, err := ReadCredentialsFile(path); err == nil || err.Error() != "invalid Cloudflare credentials file" { + t.Fatal("Secret without credentials did not return a safe error") } }