// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package api import ( crand "crypto/rand" "crypto/tls" "crypto/x509" "fmt" "net" "net/http" "net/url" "os" "path" "path/filepath" "reflect" "runtime" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil/retry" ) type configCallback func(c *Config) func makeClient(t *testing.T) (*Client, *testutil.TestServer) { return makeClientWithConfig(t, nil, nil) } func makeClientWithoutConnect(t *testing.T) (*Client, *testutil.TestServer) { return makeClientWithConfig(t, nil, func(serverConfig *testutil.TestServerConfig) { serverConfig.Connect = nil }) } func makeACLClient(t *testing.T) (*Client, *testutil.TestServer) { return makeClientWithConfig(t, func(clientConfig *Config) { clientConfig.Token = "root" }, func(serverConfig *testutil.TestServerConfig) { serverConfig.PrimaryDatacenter = "dc1" serverConfig.ACL.Tokens.InitialManagement = "root" serverConfig.ACL.Tokens.Agent = "root" serverConfig.ACL.Enabled = true serverConfig.ACL.DefaultPolicy = "deny" }) } // Makes a client with Audit enabled, it requires ACLs func makeAuditClient(t *testing.T) (*Client, *testutil.TestServer) { return makeClientWithConfig(t, func(clientConfig *Config) { clientConfig.Token = "root" }, func(serverConfig *testutil.TestServerConfig) { serverConfig.PrimaryDatacenter = "dc1" serverConfig.ACL.Tokens.InitialManagement = "root" serverConfig.ACL.Tokens.Agent = "root" serverConfig.ACL.Enabled = true serverConfig.ACL.DefaultPolicy = "deny" serverConfig.Audit = &testutil.TestAuditConfig{ Enabled: true, } }) } func makeNonBootstrappedACLClient(t *testing.T, defaultPolicy string) (*Client, *testutil.TestServer) { return makeClientWithConfig(t, func(clientConfig *Config) { clientConfig.Token = "" }, func(serverConfig *testutil.TestServerConfig) { serverConfig.PrimaryDatacenter = "dc1" serverConfig.ACL.Enabled = true serverConfig.ACL.DefaultPolicy = defaultPolicy serverConfig.Bootstrap = true }) } func makeClientWithCA(t *testing.T) (*Client, *testutil.TestServer) { return makeClientWithConfig(t, func(c *Config) { c.TLSConfig = TLSConfig{ Address: "consul.test", CAFile: "../test/client_certs/rootca.crt", CertFile: "../test/client_certs/client.crt", KeyFile: "../test/client_certs/client.key", } }, func(c *testutil.TestServerConfig) { c.CAFile = "../test/client_certs/rootca.crt" c.CertFile = "../test/client_certs/server.crt" c.KeyFile = "../test/client_certs/server.key" }) } func makeClientWithConfig( t *testing.T, cb1 configCallback, cb2 testutil.ServerConfigCallback) (*Client, *testutil.TestServer) { // Skip test when -short flag provided; any tests that create a test // server will take at least 100ms which is undesirable for -short if testing.Short() { t.Skip("too slow for testing.Short") } // Make client config conf := DefaultConfig() if cb1 != nil { cb1(conf) } // Create server var server *testutil.TestServer var err error retry.RunWith(retry.ThreeTimes(), t, func(r *retry.R) { server, err = testutil.NewTestServerConfigT(t, cb2) if err != nil { r.Fatalf("Failed to start server: %v", err.Error()) } }) if server.Config.Bootstrap { server.WaitForLeader(t) } connectEnabled := server.Config.Connect["enabled"] if enabled, ok := connectEnabled.(bool); ok && server.Config.Server && enabled { server.WaitForActiveCARoot(t) } conf.Address = server.HTTPAddr // Create client client, err := NewClient(conf) if err != nil { server.Stop() t.Fatalf("err: %v", err) } return client, server } func testKey() string { buf := make([]byte, 16) if _, err := crand.Read(buf); err != nil { panic(fmt.Errorf("Failed to read random bytes: %v", err)) } return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x", buf[0:4], buf[4:6], buf[6:8], buf[8:10], buf[10:16]) } func testNodeServiceCheckRegistrations(t *testing.T, client *Client, datacenter string) { t.Helper() registrations := map[string]*CatalogRegistration{ "Node foo": { Datacenter: datacenter, Node: "foo", ID: "e0155642-135d-4739-9853-a1ee6c9f945b", Address: "127.0.0.2", TaggedAddresses: map[string]string{ "lan": "127.0.0.2", "wan": "198.18.0.2", }, NodeMeta: map[string]string{ "env": "production", "os": "linux", }, Checks: HealthChecks{ &HealthCheck{ Node: "foo", CheckID: "foo:alive", Name: "foo-liveness", Status: HealthPassing, Notes: "foo is alive and well", }, &HealthCheck{ Node: "foo", CheckID: "foo:ssh", Name: "foo-remote-ssh", Status: HealthPassing, Notes: "foo has ssh access", }, }, Locality: &Locality{Region: "us-west-1", Zone: "us-west-1a"}, }, "Service redis v1 on foo": { Datacenter: datacenter, Node: "foo", SkipNodeUpdate: true, Service: &AgentService{ Kind: ServiceKindTypical, ID: "redisV1", Service: "redis", Tags: []string{"v1"}, Meta: map[string]string{"version": "1"}, Port: 1234, Address: "198.18.1.2", Locality: &Locality{Region: "us-west-1", Zone: "us-west-1a"}, }, Checks: HealthChecks{ &HealthCheck{ Node: "foo", CheckID: "foo:redisV1", Name: "redis-liveness", Status: HealthPassing, Notes: "redis v1 is alive and well", ServiceID: "redisV1", ServiceName: "redis", }, }, }, "Service redis v2 on foo": { Datacenter: datacenter, Node: "foo", SkipNodeUpdate: true, Service: &AgentService{ Kind: ServiceKindTypical, ID: "redisV2", Service: "redis", Tags: []string{"v2"}, Meta: map[string]string{"version": "2"}, Port: 1235, Address: "198.18.1.2", }, Checks: HealthChecks{ &HealthCheck{ Node: "foo", CheckID: "foo:redisV2", Name: "redis-v2-liveness", Status: HealthPassing, Notes: "redis v2 is alive and well", ServiceID: "redisV2", ServiceName: "redis", }, }, }, "Node bar": { Datacenter: datacenter, Node: "bar", ID: "c6e7a976-8f4f-44b5-bdd3-631be7e8ecac", Address: "127.0.0.3", TaggedAddresses: map[string]string{ "lan": "127.0.0.3", "wan": "198.18.0.3", }, NodeMeta: map[string]string{ "env": "production", "os": "windows", }, Checks: HealthChecks{ &HealthCheck{ Node: "bar", CheckID: "bar:alive", Name: "bar-liveness", Status: HealthPassing, Notes: "bar is alive and well", }, }, }, "Service redis v1 on bar": { Datacenter: datacenter, Node: "bar", SkipNodeUpdate: true, Service: &AgentService{ Kind: ServiceKindTypical, ID: "redisV1", Service: "redis", Tags: []string{"v1"}, Meta: map[string]string{"version": "1"}, Port: 1234, Address: "198.18.1.3", }, Checks: HealthChecks{ &HealthCheck{ Node: "bar", CheckID: "bar:redisV1", Name: "redis-liveness", Status: HealthPassing, Notes: "redis v1 is alive and well", ServiceID: "redisV1", ServiceName: "redis", }, }, }, "Service web v1 on bar": { Datacenter: datacenter, Node: "bar", SkipNodeUpdate: true, Service: &AgentService{ Kind: ServiceKindTypical, ID: "webV1", Service: "web", Tags: []string{"v1", "connect"}, Meta: map[string]string{"version": "1", "connect": "enabled"}, Port: 443, Address: "198.18.1.4", Connect: &AgentServiceConnect{Native: true}, }, Checks: HealthChecks{ &HealthCheck{ Node: "bar", CheckID: "bar:web:v1", Name: "web-v1-liveness", Status: HealthPassing, Notes: "web connect v1 is alive and well", ServiceID: "webV1", ServiceName: "web", }, }, }, "Node baz": { Datacenter: datacenter, Node: "baz", ID: "12f96b27-a7b0-47bd-add7-044a2bfc7bfb", Address: "127.0.0.4", TaggedAddresses: map[string]string{ "lan": "127.0.0.4", }, NodeMeta: map[string]string{ "env": "qa", "os": "linux", }, Checks: HealthChecks{ &HealthCheck{ Node: "baz", CheckID: "baz:alive", Name: "baz-liveness", Status: HealthPassing, Notes: "baz is alive and well", }, &HealthCheck{ Node: "baz", CheckID: "baz:ssh", Name: "baz-remote-ssh", Status: HealthPassing, Notes: "baz has ssh access", }, }, }, "Service web v1 on baz": { Datacenter: datacenter, Node: "baz", SkipNodeUpdate: true, Service: &AgentService{ Kind: ServiceKindTypical, ID: "webV1", Service: "web", Tags: []string{"v1", "connect"}, Meta: map[string]string{"version": "1", "connect": "enabled"}, Port: 443, Address: "198.18.1.4", Connect: &AgentServiceConnect{Native: true}, }, Checks: HealthChecks{ &HealthCheck{ Node: "baz", CheckID: "baz:web:v1", Name: "web-v1-liveness", Status: HealthPassing, Notes: "web connect v1 is alive and well", ServiceID: "webV1", ServiceName: "web", }, }, }, "Service web v2 on baz": { Datacenter: datacenter, Node: "baz", SkipNodeUpdate: true, Service: &AgentService{ Kind: ServiceKindTypical, ID: "webV2", Service: "web", Tags: []string{"v2", "connect"}, Meta: map[string]string{"version": "2", "connect": "enabled"}, Port: 8443, Address: "198.18.1.4", Connect: &AgentServiceConnect{Native: true}, }, Checks: HealthChecks{ &HealthCheck{ Node: "baz", CheckID: "baz:web:v2", Name: "web-v2-liveness", Status: HealthPassing, Notes: "web connect v2 is alive and well", ServiceID: "webV2", ServiceName: "web", }, }, }, "Service critical on baz": { Datacenter: datacenter, Node: "baz", SkipNodeUpdate: true, Service: &AgentService{ Kind: ServiceKindTypical, ID: "criticalV2", Service: "critical", Tags: []string{"v2"}, Meta: map[string]string{"version": "2"}, Port: 8080, Address: "198.18.1.4", }, Checks: HealthChecks{ &HealthCheck{ Node: "baz", CheckID: "baz:critical:v2", Name: "critical-v2-liveness", Status: HealthCritical, Notes: "critical v2 is in the critical state", ServiceID: "criticalV2", ServiceName: "critical", }, }, }, "Service warning on baz": { Datacenter: datacenter, Node: "baz", SkipNodeUpdate: true, Service: &AgentService{ Kind: ServiceKindTypical, ID: "warningV2", Service: "warning", Tags: []string{"v2"}, Meta: map[string]string{"version": "2"}, Port: 8081, Address: "198.18.1.4", }, Checks: HealthChecks{ &HealthCheck{ Node: "baz", CheckID: "baz:warning:v2", Name: "warning-v2-liveness", Status: HealthWarning, Notes: "warning v2 is in the warning state", ServiceID: "warningV2", ServiceName: "warning", }, }, }, } catalog := client.Catalog() for name, reg := range registrations { _, err := catalog.Register(reg, nil) require.NoError(t, err, "Failed catalog registration for %q: %v", name, err) } } func TestAPI_DefaultConfig_env(t *testing.T) { // t.Parallel() // DO NOT ENABLE !!! // do not enable t.Parallel for this test since it modifies global state // (environment) which has non-deterministic effects on the other tests // which derive their default configuration from the environment // if this test is failing because of expired certificates // use the procedure in test/CA-GENERATION.md addr := "1.2.3.4:5678" token := "abcd1234" auth := "username:password" os.Setenv(HTTPAddrEnvName, addr) defer os.Setenv(HTTPAddrEnvName, "") os.Setenv(HTTPTokenEnvName, token) defer os.Setenv(HTTPTokenEnvName, "") os.Setenv(HTTPAuthEnvName, auth) defer os.Setenv(HTTPAuthEnvName, "") os.Setenv(HTTPSSLEnvName, "1") defer os.Setenv(HTTPSSLEnvName, "") os.Setenv(HTTPCAFile, "ca.pem") defer os.Setenv(HTTPCAFile, "") os.Setenv(HTTPCAPath, "certs/") defer os.Setenv(HTTPCAPath, "") os.Setenv(HTTPClientCert, "client.crt") defer os.Setenv(HTTPClientCert, "") os.Setenv(HTTPClientKey, "client.key") defer os.Setenv(HTTPClientKey, "") os.Setenv(HTTPTLSServerName, "consul.test") defer os.Setenv(HTTPTLSServerName, "") os.Setenv(HTTPSSLVerifyEnvName, "0") defer os.Setenv(HTTPSSLVerifyEnvName, "") for i, config := range []*Config{ DefaultConfig(), DefaultConfigWithLogger(testutil.Logger(t)), DefaultNonPooledConfig(), } { if config.Address != addr { t.Errorf("expected %q to be %q", config.Address, addr) } if config.Token != token { t.Errorf("expected %q to be %q", config.Token, token) } if config.HttpAuth == nil { t.Fatalf("expected HttpAuth to be enabled") } if config.HttpAuth.Username != "username" { t.Errorf("expected %q to be %q", config.HttpAuth.Username, "username") } if config.HttpAuth.Password != "password" { t.Errorf("expected %q to be %q", config.HttpAuth.Password, "password") } if config.Scheme != "https" { t.Errorf("expected %q to be %q", config.Scheme, "https") } if config.TLSConfig.CAFile != "ca.pem" { t.Errorf("expected %q to be %q", config.TLSConfig.CAFile, "ca.pem") } if config.TLSConfig.CAPath != "certs/" { t.Errorf("expected %q to be %q", config.TLSConfig.CAPath, "certs/") } if config.TLSConfig.CertFile != "client.crt" { t.Errorf("expected %q to be %q", config.TLSConfig.CertFile, "client.crt") } if config.TLSConfig.KeyFile != "client.key" { t.Errorf("expected %q to be %q", config.TLSConfig.KeyFile, "client.key") } if config.TLSConfig.Address != "consul.test" { t.Errorf("expected %q to be %q", config.TLSConfig.Address, "consul.test") } if !config.TLSConfig.InsecureSkipVerify { t.Errorf("expected SSL verification to be off") } // Use keep alives as a check for whether pooling is on or off. if pooled := i != 2; pooled { if config.Transport.DisableKeepAlives != false { t.Errorf("expected keep alives to be enabled") } } else { if config.Transport.DisableKeepAlives != true { t.Errorf("expected keep alives to be disabled") } } } } func TestAPI_SetupTLSConfig(t *testing.T) { // if this test is failing because of expired certificates // use the procedure in test/CA-GENERATION.md t.Parallel() // A default config should result in a clean default client config. tlsConfig := &TLSConfig{} cc, err := SetupTLSConfig(tlsConfig) if err != nil { t.Fatalf("err: %v", err) } expected := &tls.Config{RootCAs: cc.RootCAs} if !reflect.DeepEqual(cc, expected) { t.Fatalf("bad: \n%v, \n%v", cc, expected) } // Try some address variations with and without ports. tlsConfig.Address = "127.0.0.1" cc, err = SetupTLSConfig(tlsConfig) if err != nil { t.Fatalf("err: %v", err) } expected.ServerName = "127.0.0.1" if !reflect.DeepEqual(cc, expected) { t.Fatalf("bad: %v", cc) } tlsConfig.Address = "127.0.0.1:80" cc, err = SetupTLSConfig(tlsConfig) if err != nil { t.Fatalf("err: %v", err) } expected.ServerName = "127.0.0.1" if !reflect.DeepEqual(cc, expected) { t.Fatalf("bad: %v", cc) } tlsConfig.Address = "demo.consul.io:80" cc, err = SetupTLSConfig(tlsConfig) if err != nil { t.Fatalf("err: %v", err) } expected.ServerName = "demo.consul.io" if !reflect.DeepEqual(cc, expected) { t.Fatalf("bad: %v", cc) } tlsConfig.Address = "[2001:db8:a0b:12f0::1]" cc, err = SetupTLSConfig(tlsConfig) if err != nil { t.Fatalf("err: %v", err) } expected.ServerName = "[2001:db8:a0b:12f0::1]" if !reflect.DeepEqual(cc, expected) { t.Fatalf("bad: %v", cc) } tlsConfig.Address = "[2001:db8:a0b:12f0::1]:80" cc, err = SetupTLSConfig(tlsConfig) if err != nil { t.Fatalf("err: %v", err) } expected.ServerName = "2001:db8:a0b:12f0::1" if !reflect.DeepEqual(cc, expected) { t.Fatalf("bad: %v", cc) } // Skip verification. tlsConfig.InsecureSkipVerify = true cc, err = SetupTLSConfig(tlsConfig) if err != nil { t.Fatalf("err: %v", err) } expected.InsecureSkipVerify = true if !reflect.DeepEqual(cc, expected) { t.Fatalf("bad: %v", cc) } // Make a new config that hits all the file parsers. tlsConfig = &TLSConfig{ CertFile: "../test/hostname/Alice.crt", KeyFile: "../test/hostname/Alice.key", CAFile: "../test/hostname/CertAuth.crt", } cc, err = SetupTLSConfig(tlsConfig) if err != nil { t.Fatalf("err: %v", err) } if len(cc.Certificates) != 1 { t.Fatalf("missing certificate: %v", cc.Certificates) } if cc.RootCAs == nil { t.Fatalf("didn't load root CAs") } // Use a directory to load the certs instead cc, err = SetupTLSConfig(&TLSConfig{ CAPath: "../test/ca_path", }) if err != nil { t.Fatalf("err: %v", err) } expectedCaPoolByDir := getExpectedCaPoolByDir(t) assertDeepEqual(t, expectedCaPoolByDir, cc.RootCAs, cmpCertPool) // Load certs in-memory certPEM, err := os.ReadFile("../test/hostname/Alice.crt") if err != nil { t.Fatalf("err: %v", err) } keyPEM, err := os.ReadFile("../test/hostname/Alice.key") if err != nil { t.Fatalf("err: %v", err) } caPEM, err := os.ReadFile("../test/hostname/CertAuth.crt") if err != nil { t.Fatalf("err: %v", err) } // Setup config with in-memory certs cc, err = SetupTLSConfig(&TLSConfig{ CertPEM: certPEM, KeyPEM: keyPEM, CAPem: caPEM, }) if err != nil { t.Fatalf("err: %v", err) } if len(cc.Certificates) != 1 { t.Fatalf("missing certificate: %v", cc.Certificates) } if cc.RootCAs == nil { t.Fatalf("didn't load root CAs") } } func TestAPI_ClientTLSOptions(t *testing.T) { t.Parallel() // Start a server that verifies incoming HTTPS connections _, srvVerify := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) { conf.CAFile = "../test/client_certs/rootca.crt" conf.CertFile = "../test/client_certs/server.crt" conf.KeyFile = "../test/client_certs/server.key" conf.VerifyIncomingHTTPS = true }) defer srvVerify.Stop() // Start a server without VerifyIncomingHTTPS _, srvNoVerify := makeClientWithConfig(t, nil, func(conf *testutil.TestServerConfig) { conf.CAFile = "../test/client_certs/rootca.crt" conf.CertFile = "../test/client_certs/server.crt" conf.KeyFile = "../test/client_certs/server.key" conf.VerifyIncomingHTTPS = false }) defer srvNoVerify.Stop() // Client without a cert t.Run("client without cert, validation", func(t *testing.T) { client, err := NewClient(&Config{ Address: srvVerify.HTTPSAddr, Scheme: "https", TLSConfig: TLSConfig{ Address: "consul.test", CAFile: "../test/client_certs/rootca.crt", }, }) if err != nil { t.Fatal(err) } // Should fail _, err = client.Agent().Self() // Check for one of the possible cert error messages // See https://cs.opensource.google/go/go/+/62a994837a57a7d0c58bb364b580a389488446c9 if err == nil || !(strings.Contains(err.Error(), "tls: bad certificate") || strings.Contains(err.Error(), "tls: certificate required")) { t.Fatalf("expected tls certificate error, but got '%v'", err) } }) // Client with a valid cert t.Run("client with cert, validation", func(t *testing.T) { client, err := NewClient(&Config{ Address: srvVerify.HTTPSAddr, Scheme: "https", TLSConfig: TLSConfig{ Address: "consul.test", CAFile: "../test/client_certs/rootca.crt", CertFile: "../test/client_certs/client.crt", KeyFile: "../test/client_certs/client.key", }, }) if err != nil { t.Fatal(err) } // Should succeed _, err = client.Agent().Self() if err != nil { t.Fatal(err) } }) // Client without a cert t.Run("client without cert, no validation", func(t *testing.T) { client, err := NewClient(&Config{ Address: srvNoVerify.HTTPSAddr, Scheme: "https", TLSConfig: TLSConfig{ Address: "consul.test", CAFile: "../test/client_certs/rootca.crt", }, }) if err != nil { t.Fatal(err) } // Should succeed _, err = client.Agent().Self() if err != nil { t.Fatal(err) } }) // Client with a valid cert t.Run("client with cert, no validation", func(t *testing.T) { client, err := NewClient(&Config{ Address: srvNoVerify.HTTPSAddr, Scheme: "https", TLSConfig: TLSConfig{ Address: "consul.test", CAFile: "../test/client_certs/rootca.crt", CertFile: "../test/client_certs/client.crt", KeyFile: "../test/client_certs/client.key", }, }) if err != nil { t.Fatal(err) } // Should succeed _, err = client.Agent().Self() if err != nil { t.Fatal(err) } }) } func TestAPI_SetQueryOptions(t *testing.T) { t.Parallel() c, s := makeClient(t) defer s.Stop() r := c.newRequest("GET", "/v1/kv/foo") q := &QueryOptions{ Namespace: "operator", Partition: "asdf", Datacenter: "foo", Peer: "dc10", AllowStale: true, RequireConsistent: true, WaitIndex: 1000, WaitTime: 100 * time.Second, Token: "12345", Near: "nodex", LocalOnly: true, } r.setQueryOptions(q) if r.params.Get("ns") != "operator" { t.Fatalf("bad: %v", r.params) } if r.params.Get("partition") != "asdf" { t.Fatalf("bad: %v", r.params) } if r.params.Get("peer") != "dc10" { t.Fatalf("bad: %v", r.params) } if r.params.Get("dc") != "foo" { t.Fatalf("bad: %v", r.params) } if _, ok := r.params["stale"]; !ok { t.Fatalf("bad: %v", r.params) } if _, ok := r.params["consistent"]; !ok { t.Fatalf("bad: %v", r.params) } if r.params.Get("index") != "1000" { t.Fatalf("bad: %v", r.params) } if r.params.Get("wait") != "100000ms" { t.Fatalf("bad: %v", r.params) } if r.header.Get("X-Consul-Token") != "12345" { t.Fatalf("bad: %v", r.header) } if r.params.Get("near") != "nodex" { t.Fatalf("bad: %v", r.params) } if r.params.Get("local-only") != "true" { t.Fatalf("bad: %v", r.params) } assert.Equal(t, "", r.header.Get("Cache-Control")) r = c.newRequest("GET", "/v1/kv/foo") q = &QueryOptions{ UseCache: true, MaxAge: 30 * time.Second, StaleIfError: 345678 * time.Millisecond, // Fractional seconds should be rounded } r.setQueryOptions(q) _, ok := r.params["cached"] assert.True(t, ok) assert.Equal(t, "max-age=30, stale-if-error=346", r.header.Get("Cache-Control")) } func TestAPI_SetWriteOptions(t *testing.T) { t.Parallel() c, s := makeClient(t) defer s.Stop() r := c.newRequest("GET", "/v1/kv/foo") q := &WriteOptions{ Namespace: "operator", Partition: "asdf", Datacenter: "foo", Token: "23456", } r.setWriteOptions(q) if r.params.Get("ns") != "operator" { t.Fatalf("bad: %v", r.params) } if r.params.Get("partition") != "asdf" { t.Fatalf("bad: %v", r.params) } if r.params.Get("dc") != "foo" { t.Fatalf("bad: %v", r.params) } if r.header.Get("X-Consul-Token") != "23456" { t.Fatalf("bad: %v", r.header) } } func TestAPI_Headers(t *testing.T) { t.Parallel() var request *http.Request c, s := makeClientWithConfig(t, func(c *Config) { transport := http.DefaultTransport.(*http.Transport).Clone() transport.Proxy = func(r *http.Request) (*url.URL, error) { // Keep track of the last request sent request = r return nil, nil } c.Transport = transport }, nil) defer s.Stop() if len(c.Headers()) != 0 { t.Fatalf("expected headers to be empty: %v", c.Headers()) } c.AddHeader("Hello", "World") r := c.newRequest("GET", "/v1/kv/foo") if r.header.Get("Hello") != "World" { t.Fatalf("Hello header not set : %v", r.header) } c.SetHeaders(http.Header{ "Auth": []string{"Token"}, }) r = c.newRequest("GET", "/v1/kv/foo") if r.header.Get("Hello") != "" { t.Fatalf("Hello header should not be set: %v", r.header) } if r.header.Get("Auth") != "Token" { t.Fatalf("Auth header not set: %v", r.header) } kv := c.KV() _, err := kv.Put(&KVPair{Key: "test-headers", Value: []byte("foo")}, nil) require.NoError(t, err) require.Equal(t, "application/octet-stream", request.Header.Get("Content-Type")) _, _, err = kv.Get("test-headers", nil) require.NoError(t, err) require.Equal(t, "", request.Header.Get("Content-Type")) _, err = kv.Delete("test-headers", nil) require.NoError(t, err) require.Equal(t, "", request.Header.Get("Content-Type")) err = c.Snapshot().Restore(nil, strings.NewReader("foo")) require.Error(t, err) require.Equal(t, "application/octet-stream", request.Header.Get("Content-Type")) _, _, err = c.Event().Fire(&UserEvent{ Name: "test", Payload: []byte("foo"), }, nil) require.NoError(t, err) require.Equal(t, "application/octet-stream", request.Header.Get("Content-Type")) } func TestAPI_Deprecated(t *testing.T) { t.Parallel() c, s := makeClientWithConfig(t, func(c *Config) { transport := http.DefaultTransport.(*http.Transport).Clone() c.Transport = transport }, nil) defer s.Stop() // Rules translation functionality was completely removed in Consul 1.15. _, err := c.ACL().RulesTranslate(strings.NewReader(` agent "" { policy = "read" } `)) require.Error(t, err) _, err = c.ACL().RulesTranslateToken("") require.Error(t, err) } func TestAPI_RequestToHTTP(t *testing.T) { t.Parallel() c, s := makeClient(t) defer s.Stop() r := c.newRequest("DELETE", "/v1/kv/foo") q := &QueryOptions{ Datacenter: "foo", } r.setQueryOptions(q) req, err := r.toHTTP() if err != nil { t.Fatalf("err: %v", err) } if req.Method != "DELETE" { t.Fatalf("bad: %v", req) } if req.URL.RequestURI() != "/v1/kv/foo?dc=foo" { t.Fatalf("bad: %v", req) } } func TestAPI_ParseQueryMeta(t *testing.T) { t.Parallel() resp := &http.Response{ Header: make(map[string][]string), } resp.Header.Set("X-Consul-Index", "12345") resp.Header.Set("X-Consul-LastContact", "80") resp.Header.Set("X-Consul-KnownLeader", "true") resp.Header.Set("X-Consul-Translate-Addresses", "true") resp.Header.Set("X-Consul-Default-ACL-Policy", "deny") resp.Header.Set("X-Consul-Results-Filtered-By-ACLs", "true") qm := &QueryMeta{} if err := parseQueryMeta(resp, qm); err != nil { t.Fatalf("err: %v", err) } if qm.LastIndex != 12345 { t.Fatalf("Bad: %v", qm) } if qm.LastContact != 80*time.Millisecond { t.Fatalf("Bad: %v", qm) } if !qm.KnownLeader { t.Fatalf("Bad: %v", qm) } if !qm.AddressTranslationEnabled { t.Fatalf("Bad: %v", qm) } if qm.DefaultACLPolicy != "deny" { t.Fatalf("Bad: %v", qm) } if !qm.ResultsFilteredByACLs { t.Fatalf("Bad: %v", qm) } } func TestAPI_UnixSocket(t *testing.T) { t.Parallel() if runtime.GOOS == "windows" { t.SkipNow() } tempDir := testutil.TempDir(t, "consul") socket := filepath.Join(tempDir, "test.sock") c, s := makeClientWithConfig(t, func(c *Config) { c.Address = "unix://" + socket }, func(c *testutil.TestServerConfig) { c.Addresses = &testutil.TestAddressConfig{ HTTP: "unix://" + socket, } }) defer s.Stop() agent := c.Agent() info, err := agent.Self() if err != nil { t.Fatalf("err: %s", err) } if info["Config"]["NodeName"].(string) == "" { t.Fatalf("bad: %v", info) } } func TestAPI_durToMsec(t *testing.T) { t.Parallel() if ms := durToMsec(0); ms != "0ms" { t.Fatalf("bad: %s", ms) } if ms := durToMsec(time.Millisecond); ms != "1ms" { t.Fatalf("bad: %s", ms) } if ms := durToMsec(time.Microsecond); ms != "1ms" { t.Fatalf("bad: %s", ms) } if ms := durToMsec(5 * time.Millisecond); ms != "5ms" { t.Fatalf("bad: %s", ms) } } func TestAPI_IsRetryableError(t *testing.T) { t.Parallel() if IsRetryableError(nil) { t.Fatal("should not be a retryable error") } if IsRetryableError(fmt.Errorf("not the error you are looking for")) { t.Fatal("should not be a retryable error") } if !IsRetryableError(fmt.Errorf(serverError)) { t.Fatal("should be a retryable error") } if !IsRetryableError(&net.OpError{Err: fmt.Errorf("network conn error")}) { t.Fatal("should be a retryable error") } } func TestAPI_GenerateEnv(t *testing.T) { t.Parallel() c := &Config{ Address: "127.0.0.1:8500", Token: "test", TokenFile: "test.file", Scheme: "http", TLSConfig: TLSConfig{ CAFile: "", CAPath: "", CertFile: "", KeyFile: "", Address: "", InsecureSkipVerify: true, }, } expected := []string{ "CONSUL_HTTP_ADDR=127.0.0.1:8500", "CONSUL_HTTP_TOKEN=test", "CONSUL_HTTP_TOKEN_FILE=test.file", "CONSUL_HTTP_SSL=false", "CONSUL_CACERT=", "CONSUL_CAPATH=", "CONSUL_CLIENT_CERT=", "CONSUL_CLIENT_KEY=", "CONSUL_TLS_SERVER_NAME=", "CONSUL_HTTP_SSL_VERIFY=false", "CONSUL_HTTP_AUTH=", } require.Equal(t, expected, c.GenerateEnv()) } func TestAPI_GenerateEnvHTTPS(t *testing.T) { t.Parallel() c := &Config{ Address: "127.0.0.1:8500", Token: "test", TokenFile: "test.file", Scheme: "https", TLSConfig: TLSConfig{ CAFile: "/var/consul/ca.crt", CAPath: "/var/consul/ca.dir", CertFile: "/var/consul/server.crt", KeyFile: "/var/consul/ssl/server.key", Address: "127.0.0.1:8500", InsecureSkipVerify: false, }, HttpAuth: &HttpBasicAuth{ Username: "user", Password: "password", }, } expected := []string{ "CONSUL_HTTP_ADDR=127.0.0.1:8500", "CONSUL_HTTP_TOKEN=test", "CONSUL_HTTP_TOKEN_FILE=test.file", "CONSUL_HTTP_SSL=true", "CONSUL_CACERT=/var/consul/ca.crt", "CONSUL_CAPATH=/var/consul/ca.dir", "CONSUL_CLIENT_CERT=/var/consul/server.crt", "CONSUL_CLIENT_KEY=/var/consul/ssl/server.key", "CONSUL_TLS_SERVER_NAME=127.0.0.1:8500", "CONSUL_HTTP_SSL_VERIFY=true", "CONSUL_HTTP_AUTH=user:password", } require.Equal(t, expected, c.GenerateEnv()) } // TestAPI_PrefixPath() validates that Config.Address is split into // Config.Address and Config.PathPrefix as expected. If we want to add end to // end testing in the future this will require configuring and running an // API gateway / reverse proxy (e.g. nginx) func TestAPI_PrefixPath(t *testing.T) { t.Parallel() cases := []struct { name string addr string expectAddr string expectPrefix string }{ { name: "with http and prefix", addr: "http://reverse.proxy.com/consul/path/prefix", expectAddr: "reverse.proxy.com", expectPrefix: "/consul/path/prefix", }, { name: "with https and prefix", addr: "https://reverse.proxy.com/consul/path/prefix", expectAddr: "reverse.proxy.com", expectPrefix: "/consul/path/prefix", }, { name: "with http and no prefix", addr: "http://localhost", expectAddr: "localhost", expectPrefix: "", }, { name: "with https and no prefix", addr: "https://localhost", expectAddr: "localhost", expectPrefix: "", }, { name: "no scheme and no prefix", addr: "localhost", expectAddr: "localhost", expectPrefix: "", }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { c := &Config{Address: tc.addr} client, err := NewClient(c) require.NoError(t, err) require.Equal(t, tc.expectAddr, client.config.Address) require.Equal(t, tc.expectPrefix, client.config.PathPrefix) }) } } func getExpectedCaPoolByDir(t *testing.T) *x509.CertPool { pool := x509.NewCertPool() entries, err := os.ReadDir("../test/ca_path") require.NoError(t, err) for _, entry := range entries { filename := path.Join("../test/ca_path", entry.Name()) data, err := os.ReadFile(filename) require.NoError(t, err) if !pool.AppendCertsFromPEM(data) { t.Fatalf("could not add test ca %s to pool", filename) } } return pool } // lazyCerts has a func field which can't be compared. var cmpCertPool = cmp.Options{ cmpopts.IgnoreFields(x509.CertPool{}, "lazyCerts"), cmp.AllowUnexported(x509.CertPool{}), } func assertDeepEqual(t *testing.T, x, y interface{}, opts ...cmp.Option) { t.Helper() if diff := cmp.Diff(x, y, opts...); diff != "" { t.Fatalf("assertion failed: values are not equal\n--- expected\n+++ actual\n%v", diff) } }