github.com/google/martian/v3@v3.3.3/har/har.go (about)

     1  // Copyright 2015 Google Inc. All rights reserved.
     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  
    15  // Package har collects HTTP requests and responses and stores them in HAR format.
    16  //
    17  // For more information on HAR, see:
    18  // https://w3c.github.io/web-performance/specs/HAR/Overview.html
    19  package har
    20  
    21  import (
    22  	"bytes"
    23  	"encoding/base64"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"io/ioutil"
    28  	"mime"
    29  	"mime/multipart"
    30  	"net/http"
    31  	"net/url"
    32  	"strings"
    33  	"sync"
    34  	"time"
    35  	"unicode/utf8"
    36  
    37  	"github.com/google/martian/v3"
    38  	"github.com/google/martian/v3/log"
    39  	"github.com/google/martian/v3/messageview"
    40  	"github.com/google/martian/v3/proxyutil"
    41  )
    42  
    43  // Logger maintains request and response log entries.
    44  type Logger struct {
    45  	bodyLogging     func(*http.Response) bool
    46  	postDataLogging func(*http.Request) bool
    47  
    48  	creator *Creator
    49  
    50  	mu      sync.Mutex
    51  	entries map[string]*Entry
    52  	tail    *Entry
    53  }
    54  
    55  // HAR is the top level object of a HAR log.
    56  type HAR struct {
    57  	Log *Log `json:"log"`
    58  }
    59  
    60  // Log is the HAR HTTP request and response log.
    61  type Log struct {
    62  	// Version number of the HAR format.
    63  	Version string `json:"version"`
    64  	// Creator holds information about the log creator application.
    65  	Creator *Creator `json:"creator"`
    66  	// Entries is a list containing requests and responses.
    67  	Entries []*Entry `json:"entries"`
    68  }
    69  
    70  // Creator is the program responsible for generating the log. Martian, in this case.
    71  type Creator struct {
    72  	// Name of the log creator application.
    73  	Name string `json:"name"`
    74  	// Version of the log creator application.
    75  	Version string `json:"version"`
    76  }
    77  
    78  // Entry is a individual log entry for a request or response.
    79  type Entry struct {
    80  	// ID is the unique ID for the entry.
    81  	ID string `json:"_id"`
    82  	// StartedDateTime is the date and time stamp of the request start (ISO 8601).
    83  	StartedDateTime time.Time `json:"startedDateTime"`
    84  	// Time is the total elapsed time of the request in milliseconds.
    85  	Time int64 `json:"time"`
    86  	// Request contains the detailed information about the request.
    87  	Request *Request `json:"request"`
    88  	// Response contains the detailed information about the response.
    89  	Response *Response `json:"response,omitempty"`
    90  	// Cache contains information about a request coming from browser cache.
    91  	Cache *Cache `json:"cache"`
    92  	// Timings describes various phases within request-response round trip. All
    93  	// times are specified in milliseconds.
    94  	Timings *Timings `json:"timings"`
    95  	next    *Entry
    96  }
    97  
    98  // Request holds data about an individual HTTP request.
    99  type Request struct {
   100  	// Method is the request method (GET, POST, ...).
   101  	Method string `json:"method"`
   102  	// URL is the absolute URL of the request (fragments are not included).
   103  	URL string `json:"url"`
   104  	// HTTPVersion is the Request HTTP version (HTTP/1.1).
   105  	HTTPVersion string `json:"httpVersion"`
   106  	// Cookies is a list of cookies.
   107  	Cookies []Cookie `json:"cookies"`
   108  	// Headers is a list of headers.
   109  	Headers []Header `json:"headers"`
   110  	// QueryString is a list of query parameters.
   111  	QueryString []QueryString `json:"queryString"`
   112  	// PostData is the posted data information.
   113  	PostData *PostData `json:"postData,omitempty"`
   114  	// HeaderSize is the Total number of bytes from the start of the HTTP request
   115  	// message until (and including) the double CLRF before the body. Set to -1
   116  	// if the info is not available.
   117  	HeadersSize int64 `json:"headersSize"`
   118  	// BodySize is the size of the request body (POST data payload) in bytes. Set
   119  	// to -1 if the info is not available.
   120  	BodySize int64 `json:"bodySize"`
   121  }
   122  
   123  // Response holds data about an individual HTTP response.
   124  type Response struct {
   125  	// Status is the response status code.
   126  	Status int `json:"status"`
   127  	// StatusText is the response status description.
   128  	StatusText string `json:"statusText"`
   129  	// HTTPVersion is the Response HTTP version (HTTP/1.1).
   130  	HTTPVersion string `json:"httpVersion"`
   131  	// Cookies is a list of cookies.
   132  	Cookies []Cookie `json:"cookies"`
   133  	// Headers is a list of headers.
   134  	Headers []Header `json:"headers"`
   135  	// Content contains the details of the response body.
   136  	Content *Content `json:"content"`
   137  	// RedirectURL is the target URL from the Location response header.
   138  	RedirectURL string `json:"redirectURL"`
   139  	// HeadersSize is the total number of bytes from the start of the HTTP
   140  	// request message until (and including) the double CLRF before the body.
   141  	// Set to -1 if the info is not available.
   142  	HeadersSize int64 `json:"headersSize"`
   143  	// BodySize is the size of the request body (POST data payload) in bytes. Set
   144  	// to -1 if the info is not available.
   145  	BodySize int64 `json:"bodySize"`
   146  }
   147  
   148  // Cache contains information about a request coming from browser cache.
   149  type Cache struct {
   150  	// Has no fields as they are not supported, but HAR requires the "cache"
   151  	// object to exist.
   152  }
   153  
   154  // Timings describes various phases within request-response round trip. All
   155  // times are specified in milliseconds
   156  type Timings struct {
   157  	// Send is the time required to send HTTP request to the server.
   158  	Send int64 `json:"send"`
   159  	// Wait is the time spent waiting for a response from the server.
   160  	Wait int64 `json:"wait"`
   161  	// Receive is the time required to read entire response from server or cache.
   162  	Receive int64 `json:"receive"`
   163  }
   164  
   165  // Cookie is the data about a cookie on a request or response.
   166  type Cookie struct {
   167  	// Name is the cookie name.
   168  	Name string `json:"name"`
   169  	// Value is the cookie value.
   170  	Value string `json:"value"`
   171  	// Path is the path pertaining to the cookie.
   172  	Path string `json:"path,omitempty"`
   173  	// Domain is the host of the cookie.
   174  	Domain string `json:"domain,omitempty"`
   175  	// Expires contains cookie expiration time.
   176  	Expires time.Time `json:"-"`
   177  	// Expires8601 contains cookie expiration time in ISO 8601 format.
   178  	Expires8601 string `json:"expires,omitempty"`
   179  	// HTTPOnly is set to true if the cookie is HTTP only, false otherwise.
   180  	HTTPOnly bool `json:"httpOnly,omitempty"`
   181  	// Secure is set to true if the cookie was transmitted over SSL, false
   182  	// otherwise.
   183  	Secure bool `json:"secure,omitempty"`
   184  }
   185  
   186  // Header is an HTTP request or response header.
   187  type Header struct {
   188  	// Name is the header name.
   189  	Name string `json:"name"`
   190  	// Value is the header value.
   191  	Value string `json:"value"`
   192  }
   193  
   194  // QueryString is a query string parameter on a request.
   195  type QueryString struct {
   196  	// Name is the query parameter name.
   197  	Name string `json:"name"`
   198  	// Value is the query parameter value.
   199  	Value string `json:"value"`
   200  }
   201  
   202  // PostData describes posted data on a request.
   203  type PostData struct {
   204  	// MimeType is the MIME type of the posted data.
   205  	MimeType string `json:"mimeType"`
   206  	// Params is a list of posted parameters (in case of URL encoded parameters).
   207  	Params []Param `json:"params"`
   208  	// Text contains the posted data. Although its type is string, it may contain
   209  	// binary data.
   210  	Text string `json:"text"`
   211  }
   212  
   213  // pdBinary is the JSON representation of binary PostData.
   214  type pdBinary struct {
   215  	MimeType string `json:"mimeType"`
   216  	// Params is a list of posted parameters (in case of URL encoded parameters).
   217  	Params   []Param `json:"params"`
   218  	Text     []byte  `json:"text"`
   219  	Encoding string  `json:"encoding"`
   220  }
   221  
   222  // MarshalJSON returns a JSON representation of binary PostData.
   223  func (p *PostData) MarshalJSON() ([]byte, error) {
   224  	if utf8.ValidString(p.Text) {
   225  		type noMethod PostData // avoid infinite recursion
   226  		return json.Marshal((*noMethod)(p))
   227  	}
   228  	return json.Marshal(pdBinary{
   229  		MimeType: p.MimeType,
   230  		Params:   p.Params,
   231  		Text:     []byte(p.Text),
   232  		Encoding: "base64",
   233  	})
   234  }
   235  
   236  // UnmarshalJSON populates PostData based on the []byte representation of
   237  // the binary PostData.
   238  func (p *PostData) UnmarshalJSON(data []byte) error {
   239  	if bytes.Equal(data, []byte("null")) { // conform to json.Unmarshaler spec
   240  		return nil
   241  	}
   242  	var enc struct {
   243  		Encoding string `json:"encoding"`
   244  	}
   245  	if err := json.Unmarshal(data, &enc); err != nil {
   246  		return err
   247  	}
   248  	if enc.Encoding != "base64" {
   249  		type noMethod PostData // avoid infinite recursion
   250  		return json.Unmarshal(data, (*noMethod)(p))
   251  	}
   252  	var pb pdBinary
   253  	if err := json.Unmarshal(data, &pb); err != nil {
   254  		return err
   255  	}
   256  	p.MimeType = pb.MimeType
   257  	p.Params = pb.Params
   258  	p.Text = string(pb.Text)
   259  	return nil
   260  }
   261  
   262  // Param describes an individual posted parameter.
   263  type Param struct {
   264  	// Name of the posted parameter.
   265  	Name string `json:"name"`
   266  	// Value of the posted parameter.
   267  	Value string `json:"value,omitempty"`
   268  	// Filename of a posted file.
   269  	Filename string `json:"fileName,omitempty"`
   270  	// ContentType is the content type of a posted file.
   271  	ContentType string `json:"contentType,omitempty"`
   272  }
   273  
   274  // Content describes details about response content.
   275  type Content struct {
   276  	// Size is the length of the returned content in bytes. Should be equal to
   277  	// response.bodySize if there is no compression and bigger when the content
   278  	// has been compressed.
   279  	Size int64 `json:"size"`
   280  	// MimeType is the MIME type of the response text (value of the Content-Type
   281  	// response header).
   282  	MimeType string `json:"mimeType"`
   283  	// Text contains the response body sent from the server or loaded from the
   284  	// browser cache. This field is populated with fully decoded version of the
   285  	// respose body.
   286  	Text []byte `json:"text,omitempty"`
   287  	// The desired encoding to use for the text field when encoding to JSON.
   288  	Encoding string `json:"encoding,omitempty"`
   289  }
   290  
   291  // For marshaling Content to and from json. This works around the json library's
   292  // default conversion of []byte to base64 encoded string.
   293  type contentJSON struct {
   294  	Size     int64  `json:"size"`
   295  	MimeType string `json:"mimeType"`
   296  
   297  	// Text contains the response body sent from the server or loaded from the
   298  	// browser cache. This field is populated with textual content only. The text
   299  	// field is either HTTP decoded text or a encoded (e.g. "base64")
   300  	// representation of the response body. Leave out this field if the
   301  	// information is not available.
   302  	Text string `json:"text,omitempty"`
   303  
   304  	// Encoding used for response text field e.g "base64". Leave out this field
   305  	// if the text field is HTTP decoded (decompressed & unchunked), than
   306  	// trans-coded from its original character set into UTF-8.
   307  	Encoding string `json:"encoding,omitempty"`
   308  }
   309  
   310  // MarshalJSON marshals the byte slice into json after encoding based on c.Encoding.
   311  func (c Content) MarshalJSON() ([]byte, error) {
   312  	var txt string
   313  	switch c.Encoding {
   314  	case "base64":
   315  		txt = base64.StdEncoding.EncodeToString(c.Text)
   316  	case "":
   317  		txt = string(c.Text)
   318  	default:
   319  		return nil, fmt.Errorf("unsupported encoding for Content.Text: %s", c.Encoding)
   320  	}
   321  
   322  	cj := contentJSON{
   323  		Size:     c.Size,
   324  		MimeType: c.MimeType,
   325  		Text:     txt,
   326  		Encoding: c.Encoding,
   327  	}
   328  	return json.Marshal(cj)
   329  }
   330  
   331  // UnmarshalJSON unmarshals the bytes slice into Content.
   332  func (c *Content) UnmarshalJSON(data []byte) error {
   333  	var cj contentJSON
   334  	if err := json.Unmarshal(data, &cj); err != nil {
   335  		return err
   336  	}
   337  
   338  	var txt []byte
   339  	var err error
   340  	switch cj.Encoding {
   341  	case "base64":
   342  		txt, err = base64.StdEncoding.DecodeString(cj.Text)
   343  		if err != nil {
   344  			return fmt.Errorf("failed to decode base64-encoded Content.Text: %v", err)
   345  		}
   346  	case "":
   347  		txt = []byte(cj.Text)
   348  	default:
   349  		return fmt.Errorf("unsupported encoding for Content.Text: %s", cj.Encoding)
   350  	}
   351  
   352  	c.Size = cj.Size
   353  	c.MimeType = cj.MimeType
   354  	c.Text = txt
   355  	c.Encoding = cj.Encoding
   356  	return nil
   357  }
   358  
   359  // Option is a configurable setting for the logger.
   360  type Option func(l *Logger)
   361  
   362  // PostDataLogging returns an option that configures request post data logging.
   363  func PostDataLogging(enabled bool) Option {
   364  	return func(l *Logger) {
   365  		l.postDataLogging = func(*http.Request) bool {
   366  			return enabled
   367  		}
   368  	}
   369  }
   370  
   371  // PostDataLoggingForContentTypes returns an option that logs request bodies based
   372  // on opting in to the Content-Type of the request.
   373  func PostDataLoggingForContentTypes(cts ...string) Option {
   374  	return func(l *Logger) {
   375  		l.postDataLogging = func(req *http.Request) bool {
   376  			rct := req.Header.Get("Content-Type")
   377  
   378  			for _, ct := range cts {
   379  				if strings.HasPrefix(strings.ToLower(rct), strings.ToLower(ct)) {
   380  					return true
   381  				}
   382  			}
   383  
   384  			return false
   385  		}
   386  	}
   387  }
   388  
   389  // SkipPostDataLoggingForContentTypes returns an option that logs request bodies based
   390  // on opting out of the Content-Type of the request.
   391  func SkipPostDataLoggingForContentTypes(cts ...string) Option {
   392  	return func(l *Logger) {
   393  		l.postDataLogging = func(req *http.Request) bool {
   394  			rct := req.Header.Get("Content-Type")
   395  
   396  			for _, ct := range cts {
   397  				if strings.HasPrefix(strings.ToLower(rct), strings.ToLower(ct)) {
   398  					return false
   399  				}
   400  			}
   401  
   402  			return true
   403  		}
   404  	}
   405  }
   406  
   407  // BodyLogging returns an option that configures response body logging.
   408  func BodyLogging(enabled bool) Option {
   409  	return func(l *Logger) {
   410  		l.bodyLogging = func(*http.Response) bool {
   411  			return enabled
   412  		}
   413  	}
   414  }
   415  
   416  // BodyLoggingForContentTypes returns an option that logs response bodies based
   417  // on opting in to the Content-Type of the response.
   418  func BodyLoggingForContentTypes(cts ...string) Option {
   419  	return func(l *Logger) {
   420  		l.bodyLogging = func(res *http.Response) bool {
   421  			rct := res.Header.Get("Content-Type")
   422  
   423  			for _, ct := range cts {
   424  				if strings.HasPrefix(strings.ToLower(rct), strings.ToLower(ct)) {
   425  					return true
   426  				}
   427  			}
   428  
   429  			return false
   430  		}
   431  	}
   432  }
   433  
   434  // SkipBodyLoggingForContentTypes returns an option that logs response bodies based
   435  // on opting out of the Content-Type of the response.
   436  func SkipBodyLoggingForContentTypes(cts ...string) Option {
   437  	return func(l *Logger) {
   438  		l.bodyLogging = func(res *http.Response) bool {
   439  			rct := res.Header.Get("Content-Type")
   440  
   441  			for _, ct := range cts {
   442  				if strings.HasPrefix(strings.ToLower(rct), strings.ToLower(ct)) {
   443  					return false
   444  				}
   445  			}
   446  
   447  			return true
   448  		}
   449  	}
   450  }
   451  
   452  // NewLogger returns a HAR logger. The returned
   453  // logger logs all request post data and response bodies by default.
   454  func NewLogger() *Logger {
   455  	l := &Logger{
   456  		creator: &Creator{
   457  			Name:    "martian proxy",
   458  			Version: "2.0.0",
   459  		},
   460  		entries: make(map[string]*Entry),
   461  	}
   462  	l.SetOption(BodyLogging(true))
   463  	l.SetOption(PostDataLogging(true))
   464  	return l
   465  }
   466  
   467  // SetOption sets configurable options on the logger.
   468  func (l *Logger) SetOption(opts ...Option) {
   469  	for _, opt := range opts {
   470  		opt(l)
   471  	}
   472  }
   473  
   474  // ModifyRequest logs requests.
   475  func (l *Logger) ModifyRequest(req *http.Request) error {
   476  	ctx := martian.NewContext(req)
   477  	if ctx.SkippingLogging() {
   478  		return nil
   479  	}
   480  
   481  	id := ctx.ID()
   482  
   483  	return l.RecordRequest(id, req)
   484  }
   485  
   486  // RecordRequest logs the HTTP request with the given ID. The ID should be unique
   487  // per request/response pair.
   488  func (l *Logger) RecordRequest(id string, req *http.Request) error {
   489  	hreq, err := NewRequest(req, l.postDataLogging(req))
   490  	if err != nil {
   491  		return err
   492  	}
   493  
   494  	entry := &Entry{
   495  		ID:              id,
   496  		StartedDateTime: time.Now().UTC(),
   497  		Request:         hreq,
   498  		Cache:           &Cache{},
   499  		Timings:         &Timings{},
   500  	}
   501  
   502  	l.mu.Lock()
   503  	defer l.mu.Unlock()
   504  
   505  	if _, exists := l.entries[id]; exists {
   506  		return fmt.Errorf("Duplicate request ID: %s", id)
   507  	}
   508  	l.entries[id] = entry
   509  	if l.tail == nil {
   510  		l.tail = entry
   511  	}
   512  	entry.next = l.tail.next
   513  	l.tail.next = entry
   514  	l.tail = entry
   515  
   516  	return nil
   517  }
   518  
   519  // NewRequest constructs and returns a Request from req. If withBody is true,
   520  // req.Body is read to EOF and replaced with a copy in a bytes.Buffer. An error
   521  // is returned (and req.Body may be in an intermediate state) if an error is
   522  // returned from req.Body.Read.
   523  func NewRequest(req *http.Request, withBody bool) (*Request, error) {
   524  	r := &Request{
   525  		Method:      req.Method,
   526  		URL:         req.URL.String(),
   527  		HTTPVersion: req.Proto,
   528  		HeadersSize: -1,
   529  		BodySize:    req.ContentLength,
   530  		QueryString: []QueryString{},
   531  		Headers:     headers(proxyutil.RequestHeader(req).Map()),
   532  		Cookies:     cookies(req.Cookies()),
   533  	}
   534  
   535  	for n, vs := range req.URL.Query() {
   536  		for _, v := range vs {
   537  			r.QueryString = append(r.QueryString, QueryString{
   538  				Name:  n,
   539  				Value: v,
   540  			})
   541  		}
   542  	}
   543  
   544  	pd, err := postData(req, withBody)
   545  	if err != nil {
   546  		return nil, err
   547  	}
   548  	r.PostData = pd
   549  
   550  	return r, nil
   551  }
   552  
   553  // ModifyResponse logs responses.
   554  func (l *Logger) ModifyResponse(res *http.Response) error {
   555  	ctx := martian.NewContext(res.Request)
   556  	if ctx.SkippingLogging() {
   557  		return nil
   558  	}
   559  	id := ctx.ID()
   560  
   561  	return l.RecordResponse(id, res)
   562  }
   563  
   564  // RecordResponse logs an HTTP response, associating it with the previously-logged
   565  // HTTP request with the same ID.
   566  func (l *Logger) RecordResponse(id string, res *http.Response) error {
   567  	hres, err := NewResponse(res, l.bodyLogging(res))
   568  	if err != nil {
   569  		return err
   570  	}
   571  
   572  	l.mu.Lock()
   573  	defer l.mu.Unlock()
   574  
   575  	if e, ok := l.entries[id]; ok {
   576  		e.Response = hres
   577  		e.Time = time.Since(e.StartedDateTime).Nanoseconds() / 1000000
   578  	}
   579  
   580  	return nil
   581  }
   582  
   583  // NewResponse constructs and returns a Response from resp. If withBody is true,
   584  // resp.Body is read to EOF and replaced with a copy in a bytes.Buffer. An error
   585  // is returned (and resp.Body may be in an intermediate state) if an error is
   586  // returned from resp.Body.Read.
   587  func NewResponse(res *http.Response, withBody bool) (*Response, error) {
   588  	r := &Response{
   589  		HTTPVersion: res.Proto,
   590  		Status:      res.StatusCode,
   591  		StatusText:  http.StatusText(res.StatusCode),
   592  		HeadersSize: -1,
   593  		BodySize:    res.ContentLength,
   594  		Headers:     headers(proxyutil.ResponseHeader(res).Map()),
   595  		Cookies:     cookies(res.Cookies()),
   596  	}
   597  
   598  	if res.StatusCode >= 300 && res.StatusCode < 400 {
   599  		r.RedirectURL = res.Header.Get("Location")
   600  	}
   601  
   602  	r.Content = &Content{
   603  		Encoding: "base64",
   604  		MimeType: res.Header.Get("Content-Type"),
   605  	}
   606  
   607  	if withBody {
   608  		mv := messageview.New()
   609  		if err := mv.SnapshotResponse(res); err != nil {
   610  			return nil, err
   611  		}
   612  
   613  		br, err := mv.BodyReader(messageview.Decode())
   614  		if err != nil {
   615  			return nil, err
   616  		}
   617  
   618  		body, err := ioutil.ReadAll(br)
   619  		if err != nil {
   620  			return nil, err
   621  		}
   622  
   623  		r.Content.Text = body
   624  		r.Content.Size = int64(len(body))
   625  	}
   626  	return r, nil
   627  }
   628  
   629  // Export returns the in-memory log.
   630  func (l *Logger) Export() *HAR {
   631  	l.mu.Lock()
   632  	defer l.mu.Unlock()
   633  
   634  	es := make([]*Entry, 0, len(l.entries))
   635  	curr := l.tail
   636  	for curr != nil {
   637  		curr = curr.next
   638  		es = append(es, curr)
   639  		if curr == l.tail {
   640  			break
   641  		}
   642  	}
   643  
   644  	return l.makeHAR(es)
   645  }
   646  
   647  // ExportAndReset returns the in-memory log for completed requests, clearing them.
   648  func (l *Logger) ExportAndReset() *HAR {
   649  	l.mu.Lock()
   650  	defer l.mu.Unlock()
   651  
   652  	es := make([]*Entry, 0, len(l.entries))
   653  	curr := l.tail
   654  	prev := l.tail
   655  	var first *Entry
   656  	for curr != nil {
   657  		curr = curr.next
   658  		if curr.Response != nil {
   659  			es = append(es, curr)
   660  			delete(l.entries, curr.ID)
   661  		} else {
   662  			if first == nil {
   663  				first = curr
   664  			}
   665  			prev.next = curr
   666  			prev = curr
   667  		}
   668  		if curr == l.tail {
   669  			break
   670  		}
   671  	}
   672  	if len(l.entries) == 0 {
   673  		l.tail = nil
   674  	} else {
   675  		l.tail = prev
   676  		l.tail.next = first
   677  	}
   678  
   679  	return l.makeHAR(es)
   680  }
   681  
   682  func (l *Logger) makeHAR(es []*Entry) *HAR {
   683  	return &HAR{
   684  		Log: &Log{
   685  			Version: "1.2",
   686  			Creator: l.creator,
   687  			Entries: es,
   688  		},
   689  	}
   690  }
   691  
   692  // Reset clears the in-memory log of entries.
   693  func (l *Logger) Reset() {
   694  	l.mu.Lock()
   695  	defer l.mu.Unlock()
   696  
   697  	l.entries = make(map[string]*Entry)
   698  	l.tail = nil
   699  }
   700  
   701  func cookies(cs []*http.Cookie) []Cookie {
   702  	hcs := make([]Cookie, 0, len(cs))
   703  
   704  	for _, c := range cs {
   705  		var expires string
   706  		if !c.Expires.IsZero() {
   707  			expires = c.Expires.Format(time.RFC3339)
   708  		}
   709  
   710  		hcs = append(hcs, Cookie{
   711  			Name:        c.Name,
   712  			Value:       c.Value,
   713  			Path:        c.Path,
   714  			Domain:      c.Domain,
   715  			HTTPOnly:    c.HttpOnly,
   716  			Secure:      c.Secure,
   717  			Expires:     c.Expires,
   718  			Expires8601: expires,
   719  		})
   720  	}
   721  
   722  	return hcs
   723  }
   724  
   725  func headers(hs http.Header) []Header {
   726  	hhs := make([]Header, 0, len(hs))
   727  
   728  	for n, vs := range hs {
   729  		for _, v := range vs {
   730  			hhs = append(hhs, Header{
   731  				Name:  n,
   732  				Value: v,
   733  			})
   734  		}
   735  	}
   736  
   737  	return hhs
   738  }
   739  
   740  func postData(req *http.Request, logBody bool) (*PostData, error) {
   741  	// If the request has no body (no Content-Length and Transfer-Encoding isn't
   742  	// chunked), skip the post data.
   743  	if req.ContentLength <= 0 && len(req.TransferEncoding) == 0 {
   744  		return nil, nil
   745  	}
   746  
   747  	ct := req.Header.Get("Content-Type")
   748  	mt, ps, err := mime.ParseMediaType(ct)
   749  	if err != nil {
   750  		log.Errorf("har: cannot parse Content-Type header %q: %v", ct, err)
   751  		mt = ct
   752  	}
   753  
   754  	pd := &PostData{
   755  		MimeType: mt,
   756  		Params:   []Param{},
   757  	}
   758  
   759  	if !logBody {
   760  		return pd, nil
   761  	}
   762  
   763  	mv := messageview.New()
   764  	if err := mv.SnapshotRequest(req); err != nil {
   765  		return nil, err
   766  	}
   767  
   768  	br, err := mv.BodyReader()
   769  	if err != nil {
   770  		return nil, err
   771  	}
   772  
   773  	switch mt {
   774  	case "multipart/form-data":
   775  		mpr := multipart.NewReader(br, ps["boundary"])
   776  
   777  		for {
   778  			p, err := mpr.NextPart()
   779  			if err == io.EOF {
   780  				break
   781  			}
   782  			if err != nil {
   783  				return nil, err
   784  			}
   785  			defer p.Close()
   786  
   787  			body, err := ioutil.ReadAll(p)
   788  			if err != nil {
   789  				return nil, err
   790  			}
   791  
   792  			pd.Params = append(pd.Params, Param{
   793  				Name:        p.FormName(),
   794  				Filename:    p.FileName(),
   795  				ContentType: p.Header.Get("Content-Type"),
   796  				Value:       string(body),
   797  			})
   798  		}
   799  	case "application/x-www-form-urlencoded":
   800  		body, err := ioutil.ReadAll(br)
   801  		if err != nil {
   802  			return nil, err
   803  		}
   804  
   805  		vs, err := url.ParseQuery(string(body))
   806  		if err != nil {
   807  			return nil, err
   808  		}
   809  
   810  		for n, vs := range vs {
   811  			for _, v := range vs {
   812  				pd.Params = append(pd.Params, Param{
   813  					Name:  n,
   814  					Value: v,
   815  				})
   816  			}
   817  		}
   818  	default:
   819  		body, err := ioutil.ReadAll(br)
   820  		if err != nil {
   821  			return nil, err
   822  		}
   823  
   824  		pd.Text = string(body)
   825  	}
   826  
   827  	return pd, nil
   828  }