diff --git a/har/entrylist.go b/har/entrylist.go new file mode 100644 index 000000000..b9ea73c3a --- /dev/null +++ b/har/entrylist.go @@ -0,0 +1,125 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// 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 har + +import ( + "container/list" + "sync" +) + +// EntryList implements the har.EntryContainer interface for the storage of har.Entry +type EntryList struct { + mu sync.Mutex + items *list.List +} + +func NewEntryList() *EntryList { + return &EntryList{ + items: list.New(), + } +} + +// AddEntry adds an entry to the entry list +func (el *EntryList) AddEntry(entry *Entry) { + el.mu.Lock() + defer el.mu.Unlock() + + el.items.PushBack(entry) +} + +// Entries returns a slice containing all entries +func (el *EntryList) Entries() []*Entry { + el.mu.Lock() + defer el.mu.Unlock() + + es := make([]*Entry, 0, el.items.Len()) + + for e := el.items.Front(); e != nil; e = e.Next() { + es = append(es, e.Value.(*Entry)) + } + + return es +} + +// RemoveMatches takes a matcher function and returns all entries that return true from the function +func (el *EntryList) RemoveCompleted() []*Entry { + el.mu.Lock() + defer el.mu.Unlock() + + es := make([]*Entry, 0, el.items.Len()) + var next *list.Element + + for e := el.items.Front(); e != nil; e = next { + next = e.Next() + + entry := getEntry(e) + if entry.Response != nil { + es = append(es, entry) + el.items.Remove(e) + } + } + + return es +} + +// RemoveEntry removes and entry from the entry list via the entry's id +func (el *EntryList) RemoveEntry(id string) *Entry { + el.mu.Lock() + defer el.mu.Unlock() + + if e, en := el.retrieveElementEntry(id); e != nil { + el.items.Remove(e) + + return en + } + + return nil +} + +// Reset reinitializes the entrylist +func (el *EntryList) Reset() { + el.mu.Lock() + defer el.mu.Unlock() + + el.items.Init() +} + +// RetrieveEntry returns an entry from the entrylist via the entry's id +func (el *EntryList) RetrieveEntry(id string) *Entry { + el.mu.Lock() + defer el.mu.Unlock() + + _, en := el.retrieveElementEntry(id) + + return en +} + +func getEntry(e *list.Element) *Entry { + if e != nil { + return e.Value.(*Entry) + } + + return nil +} + +func (el *EntryList) retrieveElementEntry(id string) (*list.Element, *Entry) { + for e := el.items.Front(); e != nil; e = e.Next() { + if en := getEntry(e); en.ID == id { + return e, en + } + } + + return nil, nil +} diff --git a/har/entrylist_test.go b/har/entrylist_test.go new file mode 100644 index 000000000..ce9eadebd --- /dev/null +++ b/har/entrylist_test.go @@ -0,0 +1,74 @@ +// Copyright 2020 Google Inc. All rights reserved. +// +// 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 har + +import ( + "net/http" + "testing" + + "github.com/google/martian/v3" +) + +func TestEntryList(t *testing.T) { + ids := make([]string, 3) + urls := make([]string, 3) + + logger := NewLogger() + + urls[0] = "http://0.example.com/path" + urls[1] = "http://1.example.com/path" + urls[2] = "http://2.example.com/path" + + for idx, url := range urls { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + t.Fatalf("http.NewRequest(): got %v, want no error", err) + } + + _, remove, err := martian.TestContext(req, nil, nil) + if err != nil { + t.Fatalf("martian.TestContext(): got %v, want no error", err) + } + defer remove() + + if err := logger.ModifyRequest(req); err != nil { + t.Fatalf("ModifyRequest(): got %v, want no error", err) + } + + ids[idx] = logger.Entries.Entries()[idx].ID + } + + for idx, url := range urls { + if got, want := logger.Entries.RetrieveEntry(ids[idx]).Request.URL, url; got != want { + t.Errorf("RetrieveEntry(): got %q, want %q", got, want) + } + } + + if got, want := logger.Entries.RemoveEntry(ids[0]).Request.URL, urls[0]; got != want { + t.Errorf("RemoveEntry: got %q, want %q", got, want) + } + + if got := logger.Entries.RemoveEntry(ids[0]); got != nil { + t.Errorf("RemoveEntry: should not have retrieve an entry") + } + + if got, want := logger.Entries.RetrieveEntry(ids[2]).Request.URL, urls[2]; got != want { + t.Errorf("RemoveEntry got %q, want %q", got, want) + } + + if got := logger.Entries.RetrieveEntry(""); got != nil { + t.Errorf("RetrieveEntry: should not have retrieve an entry") + } +} diff --git a/har/har.go b/har/har.go index 7f38ddac9..d626f021f 100644 --- a/har/har.go +++ b/har/har.go @@ -29,7 +29,6 @@ import ( "net/http" "net/url" "strings" - "sync" "time" "unicode/utf8" @@ -39,6 +38,16 @@ import ( "github.com/google/martian/v3/proxyutil" ) +// EntryContainer is an interface for the storage of the har entries +type EntryContainer interface { + AddEntry(entry *Entry) + Entries() []*Entry + RemoveCompleted() []*Entry + RemoveEntry(id string) *Entry + Reset() + RetrieveEntry(id string) *Entry +} + // Logger maintains request and response log entries. type Logger struct { bodyLogging func(*http.Response) bool @@ -46,9 +55,7 @@ type Logger struct { creator *Creator - mu sync.Mutex - entries map[string]*Entry - tail *Entry + Entries EntryContainer } // HAR is the top level object of a HAR log. @@ -91,7 +98,6 @@ type Entry struct { // Timings describes various phases within request-response round trip. All // times are specified in milliseconds. Timings *Timings `json:"timings"` - next *Entry } // Request holds data about an individual HTTP request. @@ -392,13 +398,19 @@ func NewLogger() *Logger { Name: "martian proxy", Version: "2.0.0", }, - entries: make(map[string]*Entry), + Entries: NewEntryList(), } l.SetOption(BodyLogging(true)) l.SetOption(PostDataLogging(true)) return l } +// SetEntries allows the changing of the entry container to another struct which +// implements the EntryContainer interface +func (l *Logger) SetEntries(ec EntryContainer) { + l.Entries = ec +} + // SetOption sets configurable options on the logger. func (l *Logger) SetOption(opts ...Option) { for _, opt := range opts { @@ -434,19 +446,11 @@ func (l *Logger) RecordRequest(id string, req *http.Request) error { Timings: &Timings{}, } - l.mu.Lock() - defer l.mu.Unlock() - - if _, exists := l.entries[id]; exists { + if l.Entries.RetrieveEntry(id) != nil { return fmt.Errorf("Duplicate request ID: %s", id) } - l.entries[id] = entry - if l.tail == nil { - l.tail = entry - } - entry.next = l.tail.next - l.tail.next = entry - l.tail = entry + + l.Entries.AddEntry(entry) return nil } @@ -504,10 +508,7 @@ func (l *Logger) RecordResponse(id string, res *http.Response) error { return err } - l.mu.Lock() - defer l.mu.Unlock() - - if e, ok := l.entries[id]; ok { + if e := l.Entries.RetrieveEntry(id); e != nil { e.Response = hres e.Time = time.Since(e.StartedDateTime).Nanoseconds() / 1000000 } @@ -563,53 +564,14 @@ func NewResponse(res *http.Response, withBody bool) (*Response, error) { // Export returns the in-memory log. func (l *Logger) Export() *HAR { - l.mu.Lock() - defer l.mu.Unlock() - - es := make([]*Entry, 0, len(l.entries)) - curr := l.tail - for curr != nil { - curr = curr.next - es = append(es, curr) - if curr == l.tail { - break - } - } + es := l.Entries.Entries() return l.makeHAR(es) } // ExportAndReset returns the in-memory log for completed requests, clearing them. func (l *Logger) ExportAndReset() *HAR { - l.mu.Lock() - defer l.mu.Unlock() - - es := make([]*Entry, 0, len(l.entries)) - curr := l.tail - prev := l.tail - var first *Entry - for curr != nil { - curr = curr.next - if curr.Response != nil { - es = append(es, curr) - delete(l.entries, curr.ID) - } else { - if first == nil { - first = curr - } - prev.next = curr - prev = curr - } - if curr == l.tail { - break - } - } - if len(l.entries) == 0 { - l.tail = nil - } else { - l.tail = prev - l.tail.next = first - } + es := l.Entries.RemoveCompleted() return l.makeHAR(es) } @@ -626,11 +588,7 @@ func (l *Logger) makeHAR(es []*Entry) *HAR { // Reset clears the in-memory log of entries. func (l *Logger) Reset() { - l.mu.Lock() - defer l.mu.Unlock() - - l.entries = make(map[string]*Entry) - l.tail = nil + l.Entries.Reset() } func cookies(cs []*http.Cookie) []Cookie {