diff --git a/http/logical.go b/http/logical.go index c9e842e62..70d0535ab 100644 --- a/http/logical.go +++ b/http/logical.go @@ -96,6 +96,12 @@ func respondLogical(w http.ResponseWriter, r *http.Request, path string, resp *l return } + // Check if this is a raw response + if _, ok := resp.Data[logical.HTTPContentType]; ok { + respondRaw(w, r, path, resp) + return + } + logicalResp := &LogicalResponse{Data: resp.Data} if resp.Secret != nil { logicalResp.LeaseID = resp.Secret.LeaseID @@ -140,6 +146,58 @@ func respondLogical(w http.ResponseWriter, r *http.Request, path string, resp *l respondOk(w, httpResp) } +// respondRaw is used when the response is using HTTPContentType and HTTPRawBody +// to change the default response handling. This is only used for specific things like +// returning the CRL information on the PKI backends. +func respondRaw(w http.ResponseWriter, r *http.Request, path string, resp *logical.Response) { + // Ensure this is never a secret or auth response + if resp.Secret != nil || resp.Auth != nil { + respondError(w, http.StatusInternalServerError, nil) + return + } + + // Get the status code + statusRaw, ok := resp.Data[logical.HTTPStatusCode] + if !ok { + respondError(w, http.StatusInternalServerError, nil) + return + } + status, ok := statusRaw.(int) + if !ok { + respondError(w, http.StatusInternalServerError, nil) + return + } + + // Get the header + contentTypeRaw, ok := resp.Data[logical.HTTPContentType] + if !ok { + respondError(w, http.StatusInternalServerError, nil) + return + } + contentType, ok := contentTypeRaw.(string) + if !ok { + respondError(w, http.StatusInternalServerError, nil) + return + } + + // Get the body + bodyRaw, ok := resp.Data[logical.HTTPRawBody] + if !ok { + respondError(w, http.StatusInternalServerError, nil) + return + } + body, ok := bodyRaw.([]byte) + if !ok { + respondError(w, http.StatusInternalServerError, nil) + return + } + + // Write the response + w.Header().Set("Content-Type", contentType) + w.WriteHeader(status) + w.Write(body) +} + type LogicalResponse struct { LeaseID string `json:"lease_id"` Renewable bool `json:"renewable"` diff --git a/http/logical_test.go b/http/logical_test.go index 402d7903b..1c454e96b 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -1,6 +1,8 @@ package http import ( + "bytes" + "io" "net/http" "reflect" "testing" @@ -182,3 +184,34 @@ func TestLogical_CreateToken(t *testing.T) { t.Fatalf("should not get cookies: %#v", cookies) } } + +func TestLogical_RawHTTP(t *testing.T) { + core, _, token := vault.TestCoreUnsealed(t) + ln, addr := TestServer(t, core) + defer ln.Close() + TestServerAuth(t, addr, token) + + resp := testHttpPost(t, addr+"/v1/sys/mounts/foo", map[string]interface{}{ + "type": "http", + }) + testResponseStatus(t, resp, 204) + + // Get the raw response + resp, err := http.Get(addr + "/v1/foo/raw") + if err != nil { + t.Fatalf("err: %s", err) + } + testResponseStatus(t, resp, 200) + + // Test the headers + if resp.Header.Get("Content-Type") != "plain/text" { + t.Fatalf("Bad: %#v", resp.Header) + } + + // Get the body + body := new(bytes.Buffer) + io.Copy(body, resp.Body) + if string(body.Bytes()) != "hello world" { + t.Fatalf("Bad: %s", body.Bytes()) + } +} diff --git a/logical/response.go b/logical/response.go index 1a7037100..d87a8f7c9 100644 --- a/logical/response.go +++ b/logical/response.go @@ -1,5 +1,24 @@ package logical +const ( + // HTTPContentType can be specified in the Data field of a Response + // so that the HTTP front end can specify a custom Content-Type associated + // with the HTTPRawBody. This can only be used for non-secrets, and should + // be avoided unless absolutely necessary, such as implementing a specification. + // The value must be a string. + HTTPContentType = "http_content_type" + + // HTTPRawBody is the raw content of the HTTP body that goes with the HTTPContentType. + // This can only be specified for non-secrets, and should should be similarly + // avoided like the HTTPContentType. The value must be a byte slice. + HTTPRawBody = "http_raw_body" + + // HTTPStatusCode is the response code the HTTP body that goes with the HTTPContentType. + // This can only be specified for non-secrets, and should should be similarly + // avoided like the HTTPContentType. The value must be an integer. + HTTPStatusCode = "http_status_code" +) + // Response is a struct that stores the response of a request. // It is used to abstract the details of the higher level request protocol. type Response struct { diff --git a/vault/testing.go b/vault/testing.go index 1a650db95..5bb4946ea 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -1,6 +1,7 @@ package vault import ( + "log" "testing" "github.com/hashicorp/vault/audit" @@ -23,6 +24,9 @@ func TestCore(t *testing.T) *Core { noopBackends["noop"] = func(map[string]string) (logical.Backend, error) { return new(framework.Backend), nil } + noopBackends["http"] = func(map[string]string) (logical.Backend, error) { + return new(rawHTTP), nil + } physicalBackend := physical.NewInmem() c, err := NewCore(&CoreConfig{ @@ -89,3 +93,21 @@ func (n *noopAudit) LogRequest(a *logical.Auth, r *logical.Request) error { func (n *noopAudit) LogResponse(a *logical.Auth, r *logical.Request, re *logical.Response, err error) error { return nil } + +type rawHTTP struct{} + +func (n *rawHTTP) HandleRequest(req *logical.Request) (*logical.Response, error) { + return &logical.Response{ + Data: map[string]interface{}{ + logical.HTTPStatusCode: 200, + logical.HTTPContentType: "plain/text", + logical.HTTPRawBody: []byte("hello world"), + }, + }, nil +} + +func (n *rawHTTP) SpecialPaths() *logical.Paths { + return &logical.Paths{Unauthenticated: []string{"*"}} +} + +func (n *rawHTTP) SetLogger(l *log.Logger) {}