|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- // 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
-
- import (
- "bytes"
- "fmt"
- "io/ioutil"
- "net/http"
- "strconv"
- "sync"
-
- "github.com/google/martian"
- )
-
- // Replacement for the HAR logging that comes with martian. HAR is not designed for
- // replay. In particular, response bodies are interpreted (e.g. decompressed), and we
- // just want them to be stored literally. This isn't something we can fix in martian: it
- // is required in the HAR spec (http://www.softwareishard.com/blog/har-12-spec/#content).
-
- // LogVersion is the current version of the log format. It can be used to
- // support changes to the format over time, so newer code can read older files.
- const LogVersion = "0.2"
-
- // A Log is a record of HTTP interactions, suitable for replay. It can be serialized to JSON.
- type Log struct {
- Initial []byte // initial data for replay
- Version string // version of this log format
- Converter *Converter
- Entries []*Entry
- }
-
- // An Entry single request-response pair.
- type Entry struct {
- ID string // unique ID
- Request *Request
- Response *Response
- }
-
- // A Request represents an http.Request in the log.
- type Request struct {
- Method string // http.Request.Method
- URL string // http.Request.URL, as a string
- Header http.Header // http.Request.Header
- // We need to understand multipart bodies because the boundaries are
- // generated randomly, so we can't just compare the entire bodies for equality.
- MediaType string // the media type part of the Content-Type header
- BodyParts [][]byte // http.Request.Body, read to completion and split for multipart
- Trailer http.Header `json:",omitempty"` // http.Request.Trailer
- }
-
- // A Response represents an http.Response in the log.
- type Response struct {
- StatusCode int // http.Response.StatusCode
- Proto string // http.Response.Proto
- ProtoMajor int // http.Response.ProtoMajor
- ProtoMinor int // http.Response.ProtoMinor
- Header http.Header // http.Response.Header
- Body []byte // http.Response.Body, read to completion
- Trailer http.Header `json:",omitempty"` // http.Response.Trailer
- }
-
- // A Logger maintains a request-response log.
- type Logger struct {
- mu sync.Mutex
- entries map[string]*Entry // from ID
- log *Log
- }
-
- // newLogger creates a new logger.
- func newLogger() *Logger {
- return &Logger{
- log: &Log{
- Version: LogVersion,
- Converter: defaultConverter(),
- },
- entries: map[string]*Entry{},
- }
- }
-
- // ModifyRequest logs requests.
- func (l *Logger) ModifyRequest(req *http.Request) error {
- if req.Method == "CONNECT" {
- return nil
- }
- ctx := martian.NewContext(req)
- if ctx.SkippingLogging() {
- return nil
- }
- lreq, err := l.log.Converter.convertRequest(req)
- if err != nil {
- return err
- }
- id := ctx.ID()
- entry := &Entry{ID: id, Request: lreq}
-
- l.mu.Lock()
- defer l.mu.Unlock()
-
- if _, ok := l.entries[id]; ok {
- panic(fmt.Sprintf("proxy: duplicate request ID: %s", id))
- }
- l.entries[id] = entry
- l.log.Entries = append(l.log.Entries, entry)
- return nil
- }
-
- // ModifyResponse logs responses.
- func (l *Logger) ModifyResponse(res *http.Response) error {
- ctx := martian.NewContext(res.Request)
- if ctx.SkippingLogging() {
- return nil
- }
- id := ctx.ID()
- lres, err := l.log.Converter.convertResponse(res)
- if err != nil {
- return err
- }
-
- l.mu.Lock()
- defer l.mu.Unlock()
-
- if e, ok := l.entries[id]; ok {
- e.Response = lres
- }
- // Ignore the response if we haven't seen the request.
- return nil
- }
-
- // Extract returns the Log and removes it. The Logger is not usable
- // after this call.
- func (l *Logger) Extract() *Log {
- l.mu.Lock()
- defer l.mu.Unlock()
- r := l.log
- l.log = nil
- l.entries = nil
- return r
- }
-
- func toHTTPResponse(lr *Response, req *http.Request) *http.Response {
- res := &http.Response{
- StatusCode: lr.StatusCode,
- Proto: lr.Proto,
- ProtoMajor: lr.ProtoMajor,
- ProtoMinor: lr.ProtoMinor,
- Header: lr.Header,
- Body: ioutil.NopCloser(bytes.NewReader(lr.Body)),
- ContentLength: int64(len(lr.Body)),
- }
- res.Request = req
- // For HEAD, set ContentLength to the value of the Content-Length header, or -1
- // if there isn't one.
- if req.Method == "HEAD" {
- res.ContentLength = -1
- if c := res.Header["Content-Length"]; len(c) == 1 {
- if c64, err := strconv.ParseInt(c[0], 10, 64); err == nil {
- res.ContentLength = c64
- }
- }
- }
- return res
- }
|