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/agent/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) } } }