| // Copyright 2019 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package proxy |
| |
| import ( |
| "bytes" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "regexp" |
| "strings" |
| ) |
| |
| // A converter converts HTTP requests and responses to the Request and Response types |
| // of this package, while removing or redacting information. |
| type converter struct { |
| // These all apply to both headers and trailers. |
| redactHeaders []*regexp.Regexp // replace matching headers with "REDACTED" |
| removeRequestHeaders []*regexp.Regexp // remove matching headers in requests |
| removeResponseHeaders []*regexp.Regexp // remove matching headers in responses |
| } |
| |
| var defaultRemoveRequestHeaders = []string{ |
| "Authorization", |
| "Proxy-Authorization", |
| "Connection", |
| "Date", |
| "Host", |
| "Transfer-Encoding", |
| "Via", |
| "X-Forwarded-*", |
| "X-Cloud-Trace-Context", // OpenCensus traces have a random ID |
| "X-Goog-Api-Client", // can differ for, e.g., different Go versions |
| } |
| |
| var defaultRemoveBothHeaders = []string{ |
| // GFEs scrub X-Google- and X-GFE- headers from requests and responses. |
| // Drop them from recordings made by users inside Google. |
| // http://g3doc/gfe/g3doc/gfe3/design/http_filters/google_header_filter |
| // (internal Google documentation). |
| "X-Google-*", |
| "X-Gfe-*", |
| } |
| |
| func defaultConverter() *converter { |
| c := &converter{ |
| // X-Goog-...Encryption-Key used by Cloud Storage for customer-supplied encryption. |
| // We don't want to record the secret, but we do want to preserve the existence |
| // of the header to verify that it was sent. |
| redactHeaders: []*regexp.Regexp{pattern("X-Goog-*Encryption-Key")}, |
| } |
| for _, h := range defaultRemoveRequestHeaders { |
| c.removeRequestHeaders = append(c.removeRequestHeaders, pattern(h)) |
| } |
| for _, h := range defaultRemoveBothHeaders { |
| c.removeRequestHeaders = append(c.removeRequestHeaders, pattern(h)) |
| c.removeResponseHeaders = append(c.removeResponseHeaders, pattern(h)) |
| } |
| return c |
| } |
| |
| // Convert a pattern into a regexp. |
| // A pattern is like a literal regexp anchored on both ends, with only one |
| // non-literal character: "*", which matches zero or more characters. |
| func pattern(p string) *regexp.Regexp { |
| q := regexp.QuoteMeta(p) |
| q = "^" + strings.Replace(q, `\*`, `.*`, -1) + "$" |
| // q must be a legal regexp. |
| return regexp.MustCompile(q) |
| } |
| |
| func (c *converter) convertRequest(req *http.Request) (*Request, error) { |
| data, err := snapshotBody(&req.Body) |
| if err != nil { |
| return nil, err |
| } |
| return &Request{ |
| Method: req.Method, |
| URL: req.URL.String(), |
| Proto: req.Proto, |
| Header: scrubHeaders(req.Header, c.redactHeaders, c.removeRequestHeaders), |
| Body: data, |
| Trailer: scrubHeaders(req.Trailer, c.redactHeaders, c.removeRequestHeaders), |
| }, nil |
| } |
| |
| func (c *converter) convertResponse(res *http.Response) (*Response, error) { |
| data, err := snapshotBody(&res.Body) |
| if err != nil { |
| return nil, err |
| } |
| return &Response{ |
| StatusCode: res.StatusCode, |
| Proto: res.Proto, |
| ProtoMajor: res.ProtoMajor, |
| ProtoMinor: res.ProtoMinor, |
| Header: scrubHeaders(res.Header, c.redactHeaders, c.removeResponseHeaders), |
| Body: data, |
| Trailer: scrubHeaders(res.Trailer, c.redactHeaders, c.removeResponseHeaders), |
| }, nil |
| } |
| |
| func snapshotBody(body *io.ReadCloser) ([]byte, error) { |
| data, err := ioutil.ReadAll(*body) |
| if err != nil { |
| return nil, err |
| } |
| (*body).Close() |
| *body = ioutil.NopCloser(bytes.NewReader(data)) |
| return data, nil |
| } |
| |
| // Copy headers, redacting some and removing others. |
| func scrubHeaders(hs http.Header, redact, remove []*regexp.Regexp) http.Header { |
| rh := http.Header{} |
| for k, v := range hs { |
| switch { |
| case match(k, redact): |
| rh.Set(k, "REDACTED") |
| case match(k, remove): |
| // skip |
| default: |
| rh[k] = v |
| } |
| } |
| return rh |
| } |
| |
| func match(s string, res []*regexp.Regexp) bool { |
| for _, re := range res { |
| if re.MatchString(s) { |
| return true |
| } |
| } |
| return false |
| } |