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

HTTP Server should handle standby redirects #16

Merged
merged 3 commits into from
Apr 19, 2015
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
50 changes: 49 additions & 1 deletion http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"

"github.com/hashicorp/vault/logical"
Expand Down Expand Up @@ -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, rawReq *http.Request, r *logical.Request) (*logical.Response, bool) {
resp, err := core.HandleRequest(r)
if err == vault.ErrStandby {
respondStandby(core, w, rawReq.URL)
return resp, false
}
if respondCommon(w, resp) {
return resp, false
}
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion http/logical.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, requestAuth(r, &logical.Request{
Operation: op,
Path: path,
Data: req,
Expand Down
71 changes: 71 additions & 0 deletions http/logical_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

"github.com/hashicorp/vault/physical"
"github.com/hashicorp/vault/vault"
)

Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion http/sys_lease.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, requestAuth(r, &logical.Request{
Operation: logical.WriteOperation,
Path: "sys/renew/" + path,
Data: map[string]interface{}{
Expand Down
8 changes: 4 additions & 4 deletions http/sys_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, requestAuth(r, &logical.Request{
Operation: logical.ReadOperation,
Path: "sys/policy",
}))
Expand Down Expand Up @@ -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, requestAuth(r, &logical.Request{
Operation: logical.DeleteOperation,
Path: "sys/policy/" + path,
}))
Expand All @@ -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, requestAuth(r, &logical.Request{
Operation: logical.ReadOperation,
Path: "sys/policy/" + path,
}))
Expand Down Expand Up @@ -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, requestAuth(r, &logical.Request{
Operation: logical.WriteOperation,
Path: "sys/policy/" + path,
Data: map[string]interface{}{
Expand Down
9 changes: 8 additions & 1 deletion http/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...))
}
Expand All @@ -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()
Expand All @@ -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
}

Expand Down