open-vault/http/handler.go

365 lines
11 KiB
Go
Raw Normal View History

2015-03-12 06:05:16 +00:00
package http
import (
"encoding/json"
"fmt"
"io"
2015-03-12 06:05:16 +00:00
"net/http"
2015-04-19 20:18:09 +00:00
"net/url"
2015-04-03 05:21:33 +00:00
"strings"
2015-03-12 06:05:16 +00:00
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/helper/duration"
"github.com/hashicorp/vault/helper/jsonutil"
2015-03-29 23:14:54 +00:00
"github.com/hashicorp/vault/logical"
2015-03-12 06:05:16 +00:00
"github.com/hashicorp/vault/vault"
)
const (
// AuthHeaderName is the name of the header containing the token.
AuthHeaderName = "X-Vault-Token"
// WrapHeaderName is the name of the header containing a directive to wrap the
// response.
WrapTTLHeaderName = "X-Vault-Wrap-TTL"
// NoRequestForwardingHeaderName is the name of the header telling Vault
// not to use request forwarding
NoRequestForwardingHeaderName = "X-Vault-No-Request-Forwarding"
)
2015-03-12 06:05:16 +00:00
// Handler returns an http.Handler for the API. This can be used on
// its own to mount the Vault API within another web server.
func Handler(core *vault.Core) http.Handler {
2015-04-03 05:21:33 +00:00
// Create the muxer to handle the actual endpoints
2015-03-12 06:05:16 +00:00
mux := http.NewServeMux()
2015-03-12 19:37:41 +00:00
mux.Handle("/v1/sys/init", handleSysInit(core))
2015-03-12 17:47:31 +00:00
mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core))
mux.Handle("/v1/sys/seal", handleSysSeal(core))
mux.Handle("/v1/sys/step-down", handleSysStepDown(core))
2015-03-12 17:47:31 +00:00
mux.Handle("/v1/sys/unseal", handleSysUnseal(core))
mux.Handle("/v1/sys/renew", handleRequestForwarding(core, handleLogical(core, false, nil)))
mux.Handle("/v1/sys/renew/", handleRequestForwarding(core, handleLogical(core, false, nil)))
2015-04-20 18:59:24 +00:00
mux.Handle("/v1/sys/leader", handleSysLeader(core))
2015-04-23 18:53:31 +00:00
mux.Handle("/v1/sys/health", handleSysHealth(core))
mux.Handle("/v1/sys/generate-root/attempt", handleRequestForwarding(core, handleSysGenerateRootAttempt(core)))
mux.Handle("/v1/sys/generate-root/update", handleRequestForwarding(core, handleSysGenerateRootUpdate(core)))
mux.Handle("/v1/sys/rekey/init", handleRequestForwarding(core, handleSysRekeyInit(core, false)))
mux.Handle("/v1/sys/rekey/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, false)))
mux.Handle("/v1/sys/rekey-recovery-key/init", handleRequestForwarding(core, handleSysRekeyInit(core, true)))
mux.Handle("/v1/sys/rekey-recovery-key/update", handleRequestForwarding(core, handleSysRekeyUpdate(core, true)))
2016-09-29 04:01:28 +00:00
mux.Handle("/v1/sys/wrapping/lookup", handleRequestForwarding(core, handleLogical(core, false, wrappingVerificationFunc)))
mux.Handle("/v1/sys/wrapping/rewrap", handleRequestForwarding(core, handleLogical(core, false, wrappingVerificationFunc)))
mux.Handle("/v1/sys/wrapping/unwrap", handleRequestForwarding(core, handleLogical(core, false, wrappingVerificationFunc)))
mux.Handle("/v1/sys/capabilities-self", handleRequestForwarding(core, handleLogical(core, true, nil)))
mux.Handle("/v1/sys/", handleRequestForwarding(core, handleLogical(core, true, nil)))
mux.Handle("/v1/", handleRequestForwarding(core, handleLogical(core, false, nil)))
2015-04-03 05:21:33 +00:00
// Wrap the handler in another handler to trigger all help paths.
handler := handleHelpHandler(mux, core)
return handler
}
2016-09-29 04:01:28 +00:00
// A lookup on a token that is about to expire returns nil, which means by the
// time we can validate a wrapping token lookup will return nil since it will
// be revoked after the call. So we have to do the validation here.
func wrappingVerificationFunc(core *vault.Core, req *logical.Request) error {
if req == nil {
2016-03-18 03:01:28 +00:00
return fmt.Errorf("invalid request")
}
2016-09-29 04:01:28 +00:00
var token string
if req.Data != nil && req.Data["token"] != nil {
if tokenStr, ok := req.Data["token"].(string); !ok {
return fmt.Errorf("could not decode token in request body")
} else if tokenStr == "" {
return fmt.Errorf("empty token in request body")
} else {
token = tokenStr
}
} else {
token = req.ClientToken
}
valid, err := core.ValidateWrappingToken(token)
if err != nil {
return fmt.Errorf("error validating wrapping token: %v", err)
}
if !valid {
return fmt.Errorf("wrapping token is not valid or does not exist")
}
return nil
}
2015-04-03 05:21:33 +00:00
// stripPrefix is a helper to strip a prefix from the path. It will
// return false from the second return value if it the prefix doesn't exist.
func stripPrefix(prefix, path string) (string, bool) {
if !strings.HasPrefix(path, prefix) {
return "", false
}
path = path[len(prefix):]
if path == "" {
return "", false
}
return path, true
2015-03-12 06:05:16 +00:00
}
func parseRequest(r *http.Request, out interface{}) error {
err := jsonutil.DecodeJSONFromReader(r.Body, out)
if err != nil && err != io.EOF {
return fmt.Errorf("Failed to parse JSON input: %s", err)
}
return err
2015-03-12 06:05:16 +00:00
}
// handleRequestForwarding determines whether to forward a request or not,
// falling back on the older behavior of redirecting the client
func handleRequestForwarding(core *vault.Core, handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get(vault.IntNoForwardingHeaderName) != "" {
handler.ServeHTTP(w, r)
return
}
if r.Header.Get(NoRequestForwardingHeaderName) != "" {
// Forwarding explicitly disabled, fall back to previous behavior
2016-08-19 20:45:17 +00:00
core.Logger().Trace("http/handleRequestForwarding: forwarding disabled by client request")
handler.ServeHTTP(w, r)
return
}
// Note: in an HA setup, this call will also ensure that connections to
// the leader are set up, as that happens once the advertised cluster
// values are read during this function
isLeader, leaderAddr, err := core.Leader()
if err != nil {
if err == vault.ErrHANotEnabled {
// Standalone node, serve request normally
handler.ServeHTTP(w, r)
return
}
// Some internal error occurred
respondError(w, http.StatusInternalServerError, err)
return
}
if isLeader {
// No forwarding needed, we're leader
handler.ServeHTTP(w, r)
return
}
if leaderAddr == "" {
respondError(w, http.StatusInternalServerError, fmt.Errorf("node not active but active node not found"))
return
}
// Attempt forwarding the request. If we cannot forward -- perhaps it's
// been disabled on the active node -- this will return with an
// ErrCannotForward and we simply fall back
statusCode, header, retBytes, err := core.ForwardRequest(r)
if err != nil {
if err == vault.ErrCannotForward {
2016-08-19 20:45:17 +00:00
core.Logger().Trace("http/handleRequestForwarding: cannot forward (possibly disabled on active node), falling back")
} else {
2016-08-19 20:45:17 +00:00
core.Logger().Error("http/handleRequestForwarding: error forwarding request", "error", err)
}
// Fall back to redirection
handler.ServeHTTP(w, r)
return
}
if header != nil {
for k, v := range header {
for _, j := range v {
w.Header().Add(k, j)
}
}
}
2016-08-19 15:03:53 +00:00
w.WriteHeader(statusCode)
w.Write(retBytes)
return
})
}
2015-04-08 18:19:03 +00:00
// request is a helper to perform a request and properly exit in the
// case of an error.
2015-04-19 21:36:50 +00:00
func request(core *vault.Core, w http.ResponseWriter, rawReq *http.Request, r *logical.Request) (*logical.Response, bool) {
2015-04-08 18:19:03 +00:00
resp, err := core.HandleRequest(r)
if errwrap.Contains(err, vault.ErrStandby.Error()) {
2015-04-19 21:36:50 +00:00
respondStandby(core, w, rawReq.URL)
2015-04-19 20:18:09 +00:00
return resp, false
}
if respondErrorCommon(w, resp, err) {
2015-04-08 18:19:03 +00:00
return resp, false
}
return resp, true
}
2015-04-19 20:18:09 +00:00
// 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
_, redirectAddr, err := core.Leader()
2015-04-19 20:18:09 +00:00
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
// If there is no leader, generate a 503 error
if redirectAddr == "" {
2015-04-19 20:18:09 +00:00
err = fmt.Errorf("no active Vault instance found")
respondError(w, http.StatusServiceUnavailable, err)
return
}
// Parse the redirect location
redirectURL, err := url.Parse(redirectAddr)
2015-04-19 20:18:09 +00:00
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
// Generate a redirect URL
finalURL := url.URL{
Scheme: redirectURL.Scheme,
Host: redirectURL.Host,
2015-04-19 20:18:09 +00:00
Path: reqURL.Path,
RawQuery: reqURL.RawQuery,
}
// Ensure there is a scheme, default to https
if finalURL.Scheme == "" {
finalURL.Scheme = "https"
2015-04-19 20:18:09 +00:00
}
// 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", finalURL.String())
2015-04-19 20:18:09 +00:00
w.WriteHeader(307)
}
2015-03-29 23:14:54 +00:00
// requestAuth adds the token to the logical.Request if it exists.
func requestAuth(core *vault.Core, r *http.Request, req *logical.Request) *logical.Request {
// Attach the header value if we have it
if v := r.Header.Get(AuthHeaderName); v != "" {
req.ClientToken = v
// Also attach the accessor if we have it. This doesn't fail if it
// doesn't exist because the request may be to an unauthenticated
// endpoint/login endpoint where a bad current token doesn't matter, or
// a token from a Vault version pre-accessors.
te, err := core.LookupToken(v)
if err == nil && te != nil {
req.ClientTokenAccessor = te.Accessor
}
}
2015-03-29 23:14:54 +00:00
return req
}
// requestWrapTTL adds the WrapTTL value to the logical.Request if it
// exists.
func requestWrapTTL(r *http.Request, req *logical.Request) (*logical.Request, error) {
// First try for the header value
wrapTTL := r.Header.Get(WrapTTLHeaderName)
if wrapTTL == "" {
return req, nil
}
// If it has an allowed suffix parse as a duration string
dur, err := duration.ParseDurationSecond(wrapTTL)
if err != nil {
return req, err
}
if int64(dur) < 0 {
return req, fmt.Errorf("requested wrap ttl cannot be negative")
}
req.WrapTTL = dur
return req, nil
}
2015-03-12 06:05:16 +00:00
func respondError(w http.ResponseWriter, status int, err error) {
2015-05-19 07:59:19 +00:00
// Adjust status code when sealed
if errwrap.Contains(err, vault.ErrSealed.Error()) {
2015-05-19 07:59:19 +00:00
status = http.StatusServiceUnavailable
}
// Allow HTTPCoded error passthrough to specify a code
if t, ok := err.(logical.HTTPCodedError); ok {
status = t.Code()
}
2015-03-12 06:05:16 +00:00
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(status)
resp := &ErrorResponse{Errors: make([]string, 0, 1)}
if err != nil {
resp.Errors = append(resp.Errors, err.Error())
}
enc := json.NewEncoder(w)
enc.Encode(resp)
}
func respondErrorCommon(w http.ResponseWriter, resp *logical.Response, err error) bool {
// If there are no errors return
if err == nil && (resp == nil || !resp.IsError()) {
return false
}
// Start out with internal server error since in most of these cases there
// won't be a response so this won't be overridden
statusCode := http.StatusInternalServerError
// If we actually have a response, start out with bad request
if resp != nil {
statusCode = http.StatusBadRequest
}
// Now, check the error itself; if it has a specific logical error, set the
// appropriate code
if err != nil {
switch {
case errwrap.ContainsType(err, new(vault.StatusBadRequest)):
statusCode = http.StatusBadRequest
case errwrap.Contains(err, logical.ErrPermissionDenied.Error()):
statusCode = http.StatusForbidden
case errwrap.Contains(err, logical.ErrUnsupportedOperation.Error()):
statusCode = http.StatusMethodNotAllowed
case errwrap.Contains(err, logical.ErrUnsupportedPath.Error()):
statusCode = http.StatusNotFound
case errwrap.Contains(err, logical.ErrInvalidRequest.Error()):
statusCode = http.StatusBadRequest
}
}
if resp != nil && resp.IsError() {
err = fmt.Errorf("%s", resp.Data["error"].(string))
}
respondError(w, statusCode, err)
return true
}
2015-03-12 06:05:16 +00:00
func respondOk(w http.ResponseWriter, body interface{}) {
w.Header().Add("Content-Type", "application/json")
if body == nil {
w.WriteHeader(http.StatusNoContent)
} else {
w.WriteHeader(http.StatusOK)
enc := json.NewEncoder(w)
enc.Encode(body)
}
}
type ErrorResponse struct {
Errors []string `json:"errors"`
}