blob: 233148e847ed512d88fea111f5db17648c32540d [file] [log] [blame]
// 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.
// Package proxy 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"
"sync"
"time"
"github.com/google/martian"
"github.com/google/martian/fifo"
"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 *Logger // for recording only
ignoreHeaders map[string]bool // headers the user has asked to ignore
}
// 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
}
// 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 = newLogger()
logGroup.AddRequestModifier(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
}
var (
configOnce sync.Once
cert *x509.Certificate
config *mitm.Config
configErr error
)
func newProxy(filename string) (*Proxy, error) {
configOnce.Do(func() {
// 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", 100*time.Hour)
if err != nil {
configErr = err
return
}
cert = x509c
config, configErr = mitm.NewConfig(x509c, priv)
if config != nil {
config.SetValidity(100 * time.Hour)
config.SetOrganization("cloud.google.com/go/httpreplay")
config.SkipTLSVerify(false)
}
})
if configErr != nil {
return nil, configErr
}
mproxy := martian.NewProxy()
mproxy.SetMITM(config)
return &Proxy{
mproxy: mproxy,
CACert: cert,
filename: filename,
ignoreHeaders: map[string]bool{},
}, 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 },
}
}
// RemoveRequestHeaders will remove request headers matching patterns from the log,
// and skip matching them. Pattern is taken literally except for *, which matches any
// sequence of characters.
//
// This only needs to be called during recording; the patterns will be saved to the
// log for replay.
func (p *Proxy) RemoveRequestHeaders(patterns []string) {
for _, pat := range patterns {
p.logger.log.Converter.registerRemoveRequestHeaders(pat)
}
}
// ClearHeaders will replace matching headers with CLEARED.
//
// This only needs to be called during recording; the patterns will be saved to the
// log for replay.
func (p *Proxy) ClearHeaders(patterns []string) {
for _, pat := range patterns {
p.logger.log.Converter.registerClearHeaders(pat)
}
}
// RemoveQueryParams will remove query parameters matching patterns from the request
// URL before logging, and skip matching them. Pattern is taken literally except for
// *, which matches any sequence of characters.
//
// This only needs to be called during recording; the patterns will be saved to the
// log for replay.
func (p *Proxy) RemoveQueryParams(patterns []string) {
for _, pat := range patterns {
p.logger.log.Converter.registerRemoveParams(pat)
}
}
// ClearQueryParams will replace matching query params in the request URL with CLEARED.
//
// This only needs to be called during recording; the patterns will be saved to the
// log for replay.
func (p *Proxy) ClearQueryParams(patterns []string) {
for _, pat := range patterns {
p.logger.log.Converter.registerClearParams(pat)
}
}
// IgnoreHeader will cause h to be ignored during matching on replay.
// Deprecated: use RemoveRequestHeaders instead.
func (p *Proxy) IgnoreHeader(h string) {
p.ignoreHeaders[http.CanonicalHeaderKey(h)] = true
}
// 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
}
func (p *Proxy) writeLog() error {
lg := p.logger.Extract()
lg.Initial = p.Initial
bytes, err := json.MarshalIndent(lg, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(p.filename, bytes, 0600) // only accessible by owner
}
// 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)
}