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
Changes from 2 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
@@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of doing url.URL lets just pass the full http.Request object through. We don't know when we might need it in the future and it actually makes it easier to call as a client: w, r is pretty esa.

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
2 changes: 1 addition & 1 deletion http/logical.go
Original file line number Diff line number Diff line change
@@ -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,
71 changes: 71 additions & 0 deletions http/logical_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion http/sys_lease.go
Original file line number Diff line number Diff line change
@@ -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{}{
8 changes: 4 additions & 4 deletions http/sys_policy.go
Original file line number Diff line number Diff line change
@@ -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{}{
9 changes: 8 additions & 1 deletion http/testing.go
Original file line number Diff line number Diff line change
@@ -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
}