From f504aca90fa16f6c507dddcfceb707ab1571e761 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 19 Apr 2015 13:17:25 -0700 Subject: [PATCH 1/3] http: split testing methods --- http/testing.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/http/testing.go b/http/testing.go index 1a2950eda9e8..5c6f9800328b 100644 --- a/http/testing.go +++ b/http/testing.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/vault/vault" ) -func TestServer(t *testing.T, core *vault.Core) (net.Listener, string) { +func TestListener(t *testing.T) (net.Listener, string) { fail := func(format string, args ...interface{}) { panic(fmt.Sprintf(format, args...)) } @@ -24,7 +24,10 @@ func TestServer(t *testing.T, core *vault.Core) (net.Listener, string) { fail("err: %s", err) } addr := "http://" + ln.Addr().String() + return ln, addr +} +func TestServerWithListener(t *testing.T, ln net.Listener, addr string, core *vault.Core) { // Create a muxer to handle our requests so that we can authenticate // for tests. mux := http.NewServeMux() @@ -36,7 +39,11 @@ func TestServer(t *testing.T, core *vault.Core) (net.Listener, string) { Handler: mux, } go server.Serve(ln) +} +func TestServer(t *testing.T, core *vault.Core) (net.Listener, string) { + ln, addr := TestListener(t) + TestServerWithListener(t, ln, addr, core) return ln, addr } From 92dadc4dca9070ca8fdbd66b5d647a6b4d70c9bf Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 19 Apr 2015 13:18:09 -0700 Subject: [PATCH 2/3] http: support standby redirects --- http/handler.go | 50 ++++++++++++++++++++++++++++++- http/logical.go | 2 +- http/logical_test.go | 71 ++++++++++++++++++++++++++++++++++++++++++++ http/sys_lease.go | 2 +- http/sys_policy.go | 8 ++--- 5 files changed, 126 insertions(+), 7 deletions(-) diff --git a/http/handler.go b/http/handler.go index 212c7affb68b..1951ba5b2d18 100644 --- a/http/handler.go +++ b/http/handler.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "net/url" "strings" "github.com/hashicorp/vault/logical" @@ -62,8 +63,12 @@ func parseRequest(r *http.Request, out interface{}) error { // request is a helper to perform a request and properly exit in the // case of an error. -func request(core *vault.Core, w http.ResponseWriter, r *logical.Request) (*logical.Response, bool) { +func request(core *vault.Core, w http.ResponseWriter, reqURL *url.URL, r *logical.Request) (*logical.Response, bool) { resp, err := core.HandleRequest(r) + if err == vault.ErrStandby { + respondStandby(core, w, reqURL) + return resp, false + } if respondCommon(w, resp) { return resp, false } @@ -75,6 +80,49 @@ func request(core *vault.Core, w http.ResponseWriter, r *logical.Request) (*logi return resp, true } +// respondStandby is used to trigger a redirect in the case that this Vault is currently a hot standby +func respondStandby(core *vault.Core, w http.ResponseWriter, reqURL *url.URL) { + // Request the leader address + _, advertise, err := core.Leader() + if err != nil { + respondError(w, http.StatusInternalServerError, err) + return + } + + // If there is no leader, generate a 503 error + if advertise == "" { + err = fmt.Errorf("no active Vault instance found") + respondError(w, http.StatusServiceUnavailable, err) + return + } + + // Parse the advertise location + advertiseURL, err := url.Parse(advertise) + if err != nil { + respondError(w, http.StatusInternalServerError, err) + return + } + + // Generate a redirect URL + redirectURL := url.URL{ + Scheme: advertiseURL.Scheme, + Host: advertiseURL.Host, + Path: reqURL.Path, + RawQuery: reqURL.RawQuery, + } + + // Ensure there is a scheme, default to https + if redirectURL.Scheme == "" { + redirectURL.Scheme = "https" + } + + // If we have an address, redirect! We use a 307 code + // because we don't actually know if its permanent and + // the request method should be preserved. + w.Header().Set("Location", redirectURL.String()) + w.WriteHeader(307) +} + // requestAuth adds the token to the logical.Request if it exists. func requestAuth(r *http.Request, req *logical.Request) *logical.Request { // Attach the cookie value as the token if we have it diff --git a/http/logical.go b/http/logical.go index 9a6946fcbab4..52e04c3f8f0e 100644 --- a/http/logical.go +++ b/http/logical.go @@ -56,7 +56,7 @@ func handleLogical(core *vault.Core) http.Handler { // Make the internal request. We attach the connection info // as well in case this is an authentication request that requires // it. Vault core handles stripping this if we need to. - resp, ok := request(core, w, requestAuth(r, &logical.Request{ + resp, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ Operation: op, Path: path, Data: req, diff --git a/http/logical_test.go b/http/logical_test.go index e1bcc08fdb21..feffa8c82a0c 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/hashicorp/vault/physical" "github.com/hashicorp/vault/vault" ) @@ -66,3 +67,73 @@ func TestLogical_noExist(t *testing.T) { } testResponseStatus(t, resp, 404) } + +func TestLogical_StandbyRedirect(t *testing.T) { + ln1, addr1 := TestListener(t) + defer ln1.Close() + ln2, addr2 := TestListener(t) + defer ln2.Close() + + // Create an HA Vault + inm := physical.NewInmemHA() + conf := &vault.CoreConfig{Physical: inm, AdvertiseAddr: addr1} + core1, err := vault.NewCore(conf) + if err != nil { + t.Fatalf("err: %v", err) + } + key, root := vault.TestCoreInit(t, core1) + if _, err := core1.Unseal(vault.TestKeyCopy(key)); err != nil { + t.Fatalf("unseal err: %s", err) + } + + // Create a second HA Vault + conf2 := &vault.CoreConfig{Physical: inm, AdvertiseAddr: addr2} + core2, err := vault.NewCore(conf2) + if err != nil { + t.Fatalf("err: %v", err) + } + if _, err := core2.Unseal(vault.TestKeyCopy(key)); err != nil { + t.Fatalf("unseal err: %s", err) + } + + TestServerWithListener(t, ln1, addr1, core1) + TestServerWithListener(t, ln2, addr2, core2) + TestServerAuth(t, addr1, root) + + // WRITE to STANDBY + resp := testHttpPut(t, addr2+"/v1/secret/foo", map[string]interface{}{ + "data": "bar", + }) + testResponseStatus(t, resp, 307) + + //// READ to standby + resp, err = http.Get(addr2 + "/v1/auth/token/lookup-self") + if err != nil { + t.Fatalf("err: %s", err) + } + + var actual map[string]interface{} + expected := map[string]interface{}{ + "renewable": false, + "lease_duration": float64(0), + "data": map[string]interface{}{ + "meta": nil, + "num_uses": float64(0), + "path": "auth/token/root", + "policies": []interface{}{"root"}, + "display_name": "root", + "id": root, + }, + "auth": nil, + } + testResponseStatus(t, resp, 200) + testResponseBody(t, resp, &actual) + delete(actual, "lease_id") + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad: %#v %#v", actual, expected) + } + + //// DELETE to standby + resp = testHttpDelete(t, addr2+"/v1/secret/foo") + testResponseStatus(t, resp, 307) +} diff --git a/http/sys_lease.go b/http/sys_lease.go index 4e1e1c22a157..216ccd342c77 100644 --- a/http/sys_lease.go +++ b/http/sys_lease.go @@ -37,7 +37,7 @@ func handleSysRenew(core *vault.Core) http.Handler { } } - resp, ok := request(core, w, requestAuth(r, &logical.Request{ + resp, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ Operation: logical.WriteOperation, Path: "sys/renew/" + path, Data: map[string]interface{}{ diff --git a/http/sys_policy.go b/http/sys_policy.go index aec0022c5fc6..8da5e8fe1eb8 100644 --- a/http/sys_policy.go +++ b/http/sys_policy.go @@ -15,7 +15,7 @@ func handleSysListPolicies(core *vault.Core) http.Handler { return } - resp, ok := request(core, w, requestAuth(r, &logical.Request{ + resp, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ Operation: logical.ReadOperation, Path: "sys/policy", })) @@ -64,7 +64,7 @@ func handleSysDeletePolicy(core *vault.Core, w http.ResponseWriter, r *http.Requ return } - _, ok := request(core, w, requestAuth(r, &logical.Request{ + _, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ Operation: logical.DeleteOperation, Path: "sys/policy/" + path, })) @@ -88,7 +88,7 @@ func handleSysReadPolicy(core *vault.Core, w http.ResponseWriter, r *http.Reques return } - resp, ok := request(core, w, requestAuth(r, &logical.Request{ + resp, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ Operation: logical.ReadOperation, Path: "sys/policy/" + path, })) @@ -119,7 +119,7 @@ func handleSysWritePolicy(core *vault.Core, w http.ResponseWriter, r *http.Reque return } - _, ok := request(core, w, requestAuth(r, &logical.Request{ + _, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ Operation: logical.WriteOperation, Path: "sys/policy/" + path, Data: map[string]interface{}{ From 273da85e85c776a8c1919c59c16f072e7af30b75 Mon Sep 17 00:00:00 2001 From: Armon Dadgar Date: Sun, 19 Apr 2015 14:36:50 -0700 Subject: [PATCH 3/3] http: pass raw request through --- http/handler.go | 4 ++-- http/logical.go | 2 +- http/sys_lease.go | 2 +- http/sys_policy.go | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/http/handler.go b/http/handler.go index 1951ba5b2d18..6bbbe20e8008 100644 --- a/http/handler.go +++ b/http/handler.go @@ -63,10 +63,10 @@ func parseRequest(r *http.Request, out interface{}) error { // request is a helper to perform a request and properly exit in the // case of an error. -func request(core *vault.Core, w http.ResponseWriter, reqURL *url.URL, r *logical.Request) (*logical.Response, bool) { +func request(core *vault.Core, w http.ResponseWriter, rawReq *http.Request, r *logical.Request) (*logical.Response, bool) { resp, err := core.HandleRequest(r) if err == vault.ErrStandby { - respondStandby(core, w, reqURL) + respondStandby(core, w, rawReq.URL) return resp, false } if respondCommon(w, resp) { diff --git a/http/logical.go b/http/logical.go index 52e04c3f8f0e..c7904a9129cf 100644 --- a/http/logical.go +++ b/http/logical.go @@ -56,7 +56,7 @@ func handleLogical(core *vault.Core) http.Handler { // Make the internal request. We attach the connection info // as well in case this is an authentication request that requires // it. Vault core handles stripping this if we need to. - resp, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ + resp, ok := request(core, w, r, requestAuth(r, &logical.Request{ Operation: op, Path: path, Data: req, diff --git a/http/sys_lease.go b/http/sys_lease.go index 216ccd342c77..19ddccb03b8f 100644 --- a/http/sys_lease.go +++ b/http/sys_lease.go @@ -37,7 +37,7 @@ func handleSysRenew(core *vault.Core) http.Handler { } } - resp, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ + resp, ok := request(core, w, r, requestAuth(r, &logical.Request{ Operation: logical.WriteOperation, Path: "sys/renew/" + path, Data: map[string]interface{}{ diff --git a/http/sys_policy.go b/http/sys_policy.go index 8da5e8fe1eb8..db010e0a5975 100644 --- a/http/sys_policy.go +++ b/http/sys_policy.go @@ -15,7 +15,7 @@ func handleSysListPolicies(core *vault.Core) http.Handler { return } - resp, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ + resp, ok := request(core, w, r, requestAuth(r, &logical.Request{ Operation: logical.ReadOperation, Path: "sys/policy", })) @@ -64,7 +64,7 @@ func handleSysDeletePolicy(core *vault.Core, w http.ResponseWriter, r *http.Requ return } - _, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ + _, ok := request(core, w, r, requestAuth(r, &logical.Request{ Operation: logical.DeleteOperation, Path: "sys/policy/" + path, })) @@ -88,7 +88,7 @@ func handleSysReadPolicy(core *vault.Core, w http.ResponseWriter, r *http.Reques return } - resp, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ + resp, ok := request(core, w, r, requestAuth(r, &logical.Request{ Operation: logical.ReadOperation, Path: "sys/policy/" + path, })) @@ -119,7 +119,7 @@ func handleSysWritePolicy(core *vault.Core, w http.ResponseWriter, r *http.Reque return } - _, ok := request(core, w, r.URL, requestAuth(r, &logical.Request{ + _, ok := request(core, w, r, requestAuth(r, &logical.Request{ Operation: logical.WriteOperation, Path: "sys/policy/" + path, Data: map[string]interface{}{