You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

176 lines
4.8 KiB

  1. // Copyright 2018 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package proxy
  15. import (
  16. "bytes"
  17. "fmt"
  18. "io/ioutil"
  19. "net/http"
  20. "strconv"
  21. "sync"
  22. "github.com/google/martian"
  23. )
  24. // Replacement for the HAR logging that comes with martian. HAR is not designed for
  25. // replay. In particular, response bodies are interpreted (e.g. decompressed), and we
  26. // just want them to be stored literally. This isn't something we can fix in martian: it
  27. // is required in the HAR spec (http://www.softwareishard.com/blog/har-12-spec/#content).
  28. // LogVersion is the current version of the log format. It can be used to
  29. // support changes to the format over time, so newer code can read older files.
  30. const LogVersion = "0.2"
  31. // A Log is a record of HTTP interactions, suitable for replay. It can be serialized to JSON.
  32. type Log struct {
  33. Initial []byte // initial data for replay
  34. Version string // version of this log format
  35. Converter *Converter
  36. Entries []*Entry
  37. }
  38. // An Entry single request-response pair.
  39. type Entry struct {
  40. ID string // unique ID
  41. Request *Request
  42. Response *Response
  43. }
  44. // A Request represents an http.Request in the log.
  45. type Request struct {
  46. Method string // http.Request.Method
  47. URL string // http.Request.URL, as a string
  48. Header http.Header // http.Request.Header
  49. // We need to understand multipart bodies because the boundaries are
  50. // generated randomly, so we can't just compare the entire bodies for equality.
  51. MediaType string // the media type part of the Content-Type header
  52. BodyParts [][]byte // http.Request.Body, read to completion and split for multipart
  53. Trailer http.Header `json:",omitempty"` // http.Request.Trailer
  54. }
  55. // A Response represents an http.Response in the log.
  56. type Response struct {
  57. StatusCode int // http.Response.StatusCode
  58. Proto string // http.Response.Proto
  59. ProtoMajor int // http.Response.ProtoMajor
  60. ProtoMinor int // http.Response.ProtoMinor
  61. Header http.Header // http.Response.Header
  62. Body []byte // http.Response.Body, read to completion
  63. Trailer http.Header `json:",omitempty"` // http.Response.Trailer
  64. }
  65. // A Logger maintains a request-response log.
  66. type Logger struct {
  67. mu sync.Mutex
  68. entries map[string]*Entry // from ID
  69. log *Log
  70. }
  71. // newLogger creates a new logger.
  72. func newLogger() *Logger {
  73. return &Logger{
  74. log: &Log{
  75. Version: LogVersion,
  76. Converter: defaultConverter(),
  77. },
  78. entries: map[string]*Entry{},
  79. }
  80. }
  81. // ModifyRequest logs requests.
  82. func (l *Logger) ModifyRequest(req *http.Request) error {
  83. if req.Method == "CONNECT" {
  84. return nil
  85. }
  86. ctx := martian.NewContext(req)
  87. if ctx.SkippingLogging() {
  88. return nil
  89. }
  90. lreq, err := l.log.Converter.convertRequest(req)
  91. if err != nil {
  92. return err
  93. }
  94. id := ctx.ID()
  95. entry := &Entry{ID: id, Request: lreq}
  96. l.mu.Lock()
  97. defer l.mu.Unlock()
  98. if _, ok := l.entries[id]; ok {
  99. panic(fmt.Sprintf("proxy: duplicate request ID: %s", id))
  100. }
  101. l.entries[id] = entry
  102. l.log.Entries = append(l.log.Entries, entry)
  103. return nil
  104. }
  105. // ModifyResponse logs responses.
  106. func (l *Logger) ModifyResponse(res *http.Response) error {
  107. ctx := martian.NewContext(res.Request)
  108. if ctx.SkippingLogging() {
  109. return nil
  110. }
  111. id := ctx.ID()
  112. lres, err := l.log.Converter.convertResponse(res)
  113. if err != nil {
  114. return err
  115. }
  116. l.mu.Lock()
  117. defer l.mu.Unlock()
  118. if e, ok := l.entries[id]; ok {
  119. e.Response = lres
  120. }
  121. // Ignore the response if we haven't seen the request.
  122. return nil
  123. }
  124. // Extract returns the Log and removes it. The Logger is not usable
  125. // after this call.
  126. func (l *Logger) Extract() *Log {
  127. l.mu.Lock()
  128. defer l.mu.Unlock()
  129. r := l.log
  130. l.log = nil
  131. l.entries = nil
  132. return r
  133. }
  134. func toHTTPResponse(lr *Response, req *http.Request) *http.Response {
  135. res := &http.Response{
  136. StatusCode: lr.StatusCode,
  137. Proto: lr.Proto,
  138. ProtoMajor: lr.ProtoMajor,
  139. ProtoMinor: lr.ProtoMinor,
  140. Header: lr.Header,
  141. Body: ioutil.NopCloser(bytes.NewReader(lr.Body)),
  142. ContentLength: int64(len(lr.Body)),
  143. }
  144. res.Request = req
  145. // For HEAD, set ContentLength to the value of the Content-Length header, or -1
  146. // if there isn't one.
  147. if req.Method == "HEAD" {
  148. res.ContentLength = -1
  149. if c := res.Header["Content-Length"]; len(c) == 1 {
  150. if c64, err := strconv.ParseInt(c[0], 10, 64); err == nil {
  151. res.ContentLength = c64
  152. }
  153. }
  154. }
  155. return res
  156. }