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