233 lines
6.3 KiB
Go
233 lines
6.3 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package cache
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/armon/go-metrics"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/hashicorp/vault/command/agentproxyshared/sink"
|
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
func ProxyHandler(ctx context.Context, logger hclog.Logger, proxier Proxier, inmemSink sink.Sink, proxyVaultToken bool) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
logger.Info("received request", "method", r.Method, "path", r.URL.Path)
|
|
|
|
if !proxyVaultToken {
|
|
r.Header.Del(consts.AuthHeaderName)
|
|
}
|
|
|
|
token := r.Header.Get(consts.AuthHeaderName)
|
|
|
|
if token == "" && inmemSink != nil {
|
|
logger.Debug("using auto auth token", "method", r.Method, "path", r.URL.Path)
|
|
token = inmemSink.(sink.SinkReader).Token()
|
|
}
|
|
|
|
// Parse and reset body.
|
|
reqBody, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
logger.Error("failed to read request body")
|
|
logical.RespondError(w, http.StatusInternalServerError, errors.New("failed to read request body"))
|
|
return
|
|
}
|
|
if r.Body != nil {
|
|
r.Body.Close()
|
|
}
|
|
r.Body = io.NopCloser(bytes.NewReader(reqBody))
|
|
req := &SendRequest{
|
|
Token: token,
|
|
Request: r,
|
|
RequestBody: reqBody,
|
|
}
|
|
|
|
resp, err := proxier.Send(ctx, req)
|
|
if err != nil {
|
|
// If this is an api.Response error, don't wrap the response.
|
|
if resp != nil && resp.Response.Error() != nil {
|
|
copyHeader(w.Header(), resp.Response.Header)
|
|
w.WriteHeader(resp.Response.StatusCode)
|
|
io.Copy(w, resp.Response.Body)
|
|
metrics.IncrCounter([]string{"agent", "proxy", "client_error"}, 1)
|
|
} else {
|
|
metrics.IncrCounter([]string{"agent", "proxy", "error"}, 1)
|
|
logical.RespondError(w, http.StatusInternalServerError, fmt.Errorf("failed to get the response: %w", err))
|
|
}
|
|
return
|
|
}
|
|
|
|
err = sanitizeAutoAuthTokenResponse(ctx, logger, inmemSink, req, resp)
|
|
if err != nil {
|
|
logical.RespondError(w, http.StatusInternalServerError, fmt.Errorf("failed to process token lookup response: %w", err))
|
|
return
|
|
}
|
|
|
|
defer resp.Response.Body.Close()
|
|
|
|
metrics.IncrCounter([]string{"agent", "proxy", "success"}, 1)
|
|
if resp.CacheMeta != nil {
|
|
if resp.CacheMeta.Hit {
|
|
metrics.IncrCounter([]string{"agent", "cache", "hit"}, 1)
|
|
} else {
|
|
metrics.IncrCounter([]string{"agent", "cache", "miss"}, 1)
|
|
}
|
|
}
|
|
|
|
// Set headers
|
|
setHeaders(w, resp)
|
|
|
|
// Set response body
|
|
io.Copy(w, resp.Response.Body)
|
|
return
|
|
})
|
|
}
|
|
|
|
// setHeaders is a helper that sets the header values based on SendResponse. It
|
|
// copies over the headers from the original response and also includes any
|
|
// cache-related headers.
|
|
func setHeaders(w http.ResponseWriter, resp *SendResponse) {
|
|
// Set header values
|
|
copyHeader(w.Header(), resp.Response.Header)
|
|
if resp.CacheMeta != nil {
|
|
xCacheVal := "MISS"
|
|
|
|
if resp.CacheMeta.Hit {
|
|
xCacheVal = "HIT"
|
|
|
|
// If this is a cache hit, we also set the Age header
|
|
age := fmt.Sprintf("%.0f", resp.CacheMeta.Age.Seconds())
|
|
w.Header().Set("Age", age)
|
|
|
|
// Update the date value
|
|
w.Header().Set("Date", time.Now().Format(http.TimeFormat))
|
|
}
|
|
|
|
w.Header().Set("X-Cache", xCacheVal)
|
|
}
|
|
|
|
// Set status code
|
|
w.WriteHeader(resp.Response.StatusCode)
|
|
}
|
|
|
|
// sanitizeAutoAuthTokenResponse checks if the request was a lookup or renew
|
|
// and if the auto-auth token was used to perform lookup-self, the identifier
|
|
// of the token and its accessor same will be stripped off of the response.
|
|
func sanitizeAutoAuthTokenResponse(ctx context.Context, logger hclog.Logger, inmemSink sink.Sink, req *SendRequest, resp *SendResponse) error {
|
|
// If auto-auth token is not being used, there is nothing to do.
|
|
if inmemSink == nil {
|
|
return nil
|
|
}
|
|
autoAuthToken := inmemSink.(sink.SinkReader).Token()
|
|
|
|
// If lookup responded with non 200 status, there is nothing to do.
|
|
if resp.Response.StatusCode != http.StatusOK {
|
|
return nil
|
|
}
|
|
|
|
_, path := deriveNamespaceAndRevocationPath(req)
|
|
switch path {
|
|
case vaultPathTokenLookupSelf, vaultPathTokenRenewSelf:
|
|
if req.Token != autoAuthToken {
|
|
return nil
|
|
}
|
|
case vaultPathTokenLookup, vaultPathTokenRenew:
|
|
jsonBody := map[string]interface{}{}
|
|
if err := json.Unmarshal(req.RequestBody, &jsonBody); err != nil {
|
|
return err
|
|
}
|
|
tokenRaw, ok := jsonBody["token"]
|
|
if !ok {
|
|
// Input error will be caught by the API
|
|
return nil
|
|
}
|
|
token, ok := tokenRaw.(string)
|
|
if !ok {
|
|
// Input error will be caught by the API
|
|
return nil
|
|
}
|
|
if token != "" && token != autoAuthToken {
|
|
// Lookup is performed on the non-auto-auth token
|
|
return nil
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
logger.Info("stripping auto-auth token from the response", "method", req.Request.Method, "path", req.Request.URL.Path)
|
|
secret, err := api.ParseSecret(bytes.NewReader(resp.ResponseBody))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse token lookup response: %v", err)
|
|
}
|
|
if secret == nil {
|
|
return nil
|
|
} else if secret.Data != nil {
|
|
// lookup endpoints
|
|
if secret.Data["id"] == nil && secret.Data["accessor"] == nil {
|
|
return nil
|
|
}
|
|
delete(secret.Data, "id")
|
|
delete(secret.Data, "accessor")
|
|
} else if secret.Auth != nil {
|
|
// renew endpoints
|
|
if secret.Auth.Accessor == "" && secret.Auth.ClientToken == "" {
|
|
return nil
|
|
}
|
|
secret.Auth.Accessor = ""
|
|
secret.Auth.ClientToken = ""
|
|
} else {
|
|
// nothing to redact
|
|
return nil
|
|
}
|
|
|
|
bodyBytes, err := json.Marshal(secret)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if resp.Response.Body != nil {
|
|
resp.Response.Body.Close()
|
|
}
|
|
resp.Response.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
|
|
resp.Response.ContentLength = int64(len(bodyBytes))
|
|
|
|
// Serialize and re-read the response
|
|
var respBytes bytes.Buffer
|
|
err = resp.Response.Write(&respBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to serialize the updated response: %v", err)
|
|
}
|
|
|
|
updatedResponse, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(respBytes.Bytes())), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to deserialize the updated response: %v", err)
|
|
}
|
|
|
|
resp.Response = &api.Response{
|
|
Response: updatedResponse,
|
|
}
|
|
resp.ResponseBody = bodyBytes
|
|
|
|
return nil
|
|
}
|
|
|
|
func copyHeader(dst, src http.Header) {
|
|
for k, vv := range src {
|
|
for _, v := range vv {
|
|
dst.Add(k, v)
|
|
}
|
|
}
|
|
}
|