|
- // Copyright 2018 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.
-
- // +build go1.8
-
- // The proxy package provides a record/replay HTTP proxy. It is designed to support
- // both an in-memory API (cloud.google.com/go/httpreplay) and a standalone server
- // (cloud.google.com/go/httpreplay/cmd/httpr).
- package proxy
-
- // See github.com/google/martian/cmd/proxy/main.go for the origin of much of this.
-
- import (
- "crypto/tls"
- "crypto/x509"
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net"
- "net/http"
- "net/url"
- "strings"
- "time"
-
- "github.com/google/martian"
- "github.com/google/martian/fifo"
- "github.com/google/martian/har"
- "github.com/google/martian/httpspec"
- "github.com/google/martian/martianlog"
- "github.com/google/martian/mitm"
- )
-
- // A Proxy is an HTTP proxy that supports recording or replaying requests.
- type Proxy struct {
- // The certificate that the proxy uses to participate in TLS.
- CACert *x509.Certificate
-
- // The URL of the proxy.
- URL *url.URL
-
- // Initial state of the client.
- Initial []byte
-
- mproxy *martian.Proxy
- filename string // for log
- logger *har.Logger // for recording only
- }
-
- // ForRecording returns a Proxy configured to record.
- func ForRecording(filename string, port int) (*Proxy, error) {
- p, err := newProxy(filename)
- if err != nil {
- return nil, err
- }
- // Configure the transport for the proxy's outgoing traffic. We MUST use
- // DialContext and not Dial. In Go 1.10, Setting Dial (but not DialContext)
- // disables HTTP2, and that gives different behavior than http.DefaultTransport.
- // (For example, GET
- // https://storage.googleapis.com/storage-library-test-bucket/gzipped-text.txt
- // with an "Accept-Encoding: gzip" header returns a Content-Length header with
- // HTTP2, but not HTTP1.)
- // We must also hide the type http.Transport from martian, because it looks for
- // http.Transport and sets the Dial field!
- p.mproxy.SetRoundTripper((*hideTransport)(&http.Transport{
- DialContext: (&net.Dialer{
- Timeout: 30 * time.Second,
- KeepAlive: 30 * time.Second,
- }).DialContext,
- TLSHandshakeTimeout: 10 * time.Second,
- ExpectContinueTimeout: time.Second,
- }))
-
- // Construct a group that performs the standard proxy stack of request/response
- // modifications.
- stack, _ := httpspec.NewStack("httpr") // second arg is an internal group that we don't need
- p.mproxy.SetRequestModifier(stack)
- p.mproxy.SetResponseModifier(stack)
-
- // Make a group for logging requests and responses.
- logGroup := fifo.NewGroup()
- skipAuth := skipLoggingByHost("accounts.google.com")
- logGroup.AddRequestModifier(skipAuth)
- logGroup.AddResponseModifier(skipAuth)
- p.logger = har.NewLogger()
- logGroup.AddRequestModifier(martian.RequestModifierFunc(
- func(req *http.Request) error { return withRedactedHeaders(req, p.logger) }))
- logGroup.AddResponseModifier(p.logger)
-
- stack.AddRequestModifier(logGroup)
- stack.AddResponseModifier(logGroup)
-
- // Ordinary debug logging.
- logger := martianlog.NewLogger()
- logger.SetDecode(true)
- stack.AddRequestModifier(logger)
- stack.AddResponseModifier(logger)
-
- if err := p.start(port); err != nil {
- return nil, err
- }
- return p, nil
- }
-
- type hideTransport http.Transport
-
- func (t *hideTransport) RoundTrip(req *http.Request) (*http.Response, error) {
- return (*http.Transport)(t).RoundTrip(req)
- }
-
- func newProxy(filename string) (*Proxy, error) {
- mproxy := martian.NewProxy()
- // Set up a man-in-the-middle configuration with a CA certificate so the proxy can
- // participate in TLS.
- x509c, priv, err := mitm.NewAuthority("cloud.google.com/go/httpreplay", "HTTPReplay Authority", time.Hour)
- if err != nil {
- return nil, err
- }
- mc, err := mitm.NewConfig(x509c, priv)
- if err != nil {
- return nil, err
- }
- mc.SetValidity(time.Hour)
- mc.SetOrganization("cloud.google.com/go/httpreplay")
- mc.SkipTLSVerify(false)
- if err != nil {
- return nil, err
- }
- mproxy.SetMITM(mc)
- return &Proxy{
- mproxy: mproxy,
- CACert: x509c,
- filename: filename,
- }, nil
- }
-
- func (p *Proxy) start(port int) error {
- l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
- if err != nil {
- return err
- }
- p.URL = &url.URL{Scheme: "http", Host: l.Addr().String()}
- go p.mproxy.Serve(l)
- return nil
- }
-
- // Transport returns an http.Transport for clients who want to talk to the proxy.
- func (p *Proxy) Transport() *http.Transport {
- caCertPool := x509.NewCertPool()
- caCertPool.AddCert(p.CACert)
- return &http.Transport{
- TLSClientConfig: &tls.Config{RootCAs: caCertPool},
- Proxy: func(*http.Request) (*url.URL, error) { return p.URL, nil },
- }
- }
-
- // Close closes the proxy. If the proxy is recording, it also writes the log.
- func (p *Proxy) Close() error {
- p.mproxy.Close()
- if p.logger != nil {
- return p.writeLog()
- }
- return nil
- }
-
- type httprFile struct {
- Initial []byte
- HAR *har.HAR
- }
-
- func (p *Proxy) writeLog() error {
- f := httprFile{
- Initial: p.Initial,
- HAR: p.logger.ExportAndReset(),
- }
- bytes, err := json.MarshalIndent(f, "", " ")
- if err != nil {
- return err
- }
- return ioutil.WriteFile(p.filename, bytes, 0600) // only accessible by owner
- }
-
- // Headers that may contain sensitive data (auth tokens, keys).
- var sensitiveHeaders = []string{
- "Authorization",
- "X-Goog-Encryption-Key", // used by Cloud Storage for customer-supplied encryption
- "X-Goog-Copy-Source-Encryption-Key", // ditto
- }
-
- // withRedactedHeaders removes sensitive header contents before calling mod.
- func withRedactedHeaders(req *http.Request, mod martian.RequestModifier) error {
- // We have to change the headers, then log, then restore them.
- replaced := map[string]string{}
- for _, h := range sensitiveHeaders {
- if v := req.Header.Get(h); v != "" {
- replaced[h] = v
- req.Header.Set(h, "REDACTED")
- }
- }
- err := mod.ModifyRequest(req)
- for h, v := range replaced {
- req.Header.Set(h, v)
- }
- return err
- }
-
- // skipLoggingByHost disables logging for traffic to a particular host.
- type skipLoggingByHost string
-
- func (s skipLoggingByHost) ModifyRequest(req *http.Request) error {
- if strings.HasPrefix(req.Host, string(s)) {
- martian.NewContext(req).SkipLogging()
- }
- return nil
- }
-
- func (s skipLoggingByHost) ModifyResponse(res *http.Response) error {
- return s.ModifyRequest(res.Request)
- }
|