Skip to content

Commit

Permalink
ext/har: add HAR logger extension
Browse files Browse the repository at this point in the history
Port HAR logging from abourget/goproxy to ext package.
Closes elazarl#609
  • Loading branch information
CameronBadman committed Dec 31, 2024
1 parent 408830d commit 97bba32
Show file tree
Hide file tree
Showing 5 changed files with 592 additions and 3 deletions.
4 changes: 3 additions & 1 deletion ext/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ module github.com/elazarl/goproxy/ext

go 1.20

replace github.com/elazarl/goproxy => ../

require (
github.com/elazarl/goproxy v0.0.0-20241217120900-7711dfa3811c
github.com/elazarl/goproxy v0.0.0
golang.org/x/net v0.33.0
golang.org/x/text v0.21.0
)
2 changes: 0 additions & 2 deletions ext/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
github.com/elazarl/goproxy v0.0.0-20241217120900-7711dfa3811c h1:yWAGp1CjD1mQGLUsADqPn5s1n2AkGAX33XLDUgoXzyo=
github.com/elazarl/goproxy v0.0.0-20241217120900-7711dfa3811c/go.mod h1:P73liMk9TZCyF9fXG/RyMeSizmATvpvy3ZS61/1eXn4=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
Expand Down
110 changes: 110 additions & 0 deletions ext/har/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package har


import (
"encoding/json"
"net/http"
"os"
"sync"
"time"

"github.com/elazarl/goproxy"
)

// Logger implements a HAR logging extension for goproxy
type Logger struct {
mu sync.Mutex
har *Har
captureContent bool
}

// NewLogger creates a new HAR logger instance
func NewLogger() *Logger {
return &Logger{
har: New(),
}
}

// OnRequest handles incoming HTTP requests
func (l *Logger) OnRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
// Store the start time in context for later use
if ctx != nil {
ctx.UserData = time.Now()
}
return req, nil
}

// OnResponse handles HTTP responses
func (l *Logger) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
if resp == nil || ctx == nil || ctx.Req == nil || ctx.UserData == nil {
return resp
}

startTime, ok := ctx.UserData.(time.Time)
if !ok {
return resp
}

// Create HAR entry
entry := Entry{
StartedDateTime: startTime,
Time: time.Since(startTime).Milliseconds(),
Request: ParseRequest(ctx.Req, l.captureContent),
Response: ParseResponse(resp, l.captureContent),
Cache: Cache{},
Timings: Timings{
Send: 0,
Wait: time.Since(startTime).Milliseconds(),
Receive: 0,
},
}

// Add server IP
entry.FillIPAddress(ctx.Req)

// Add to HAR log thread-safely
l.mu.Lock()
l.har.AppendEntry(entry)
l.mu.Unlock()

return resp
}

// SetCaptureContent enables or disables request/response body capture
func (l *Logger) SetCaptureContent(capture bool) {
l.mu.Lock()
defer l.mu.Unlock()
l.captureContent = capture
}

// SaveToFile writes the current HAR log to a file
func (l *Logger) SaveToFile(filename string) error {
l.mu.Lock()
defer l.mu.Unlock()

file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()

encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(l.har)
}

// Clear resets the HAR log
func (l *Logger) Clear() {
l.mu.Lock()
defer l.mu.Unlock()
l.har = New()
}

// GetEntries returns a copy of the current HAR entries
func (l *Logger) GetEntries() []Entry {
l.mu.Lock()
defer l.mu.Unlock()
entries := make([]Entry, len(l.har.Log.Entries))
copy(entries, l.har.Log.Entries)
return entries
}
114 changes: 114 additions & 0 deletions ext/har/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@

package har_test

import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"

"github.com/elazarl/goproxy"
"github.com/elazarl/goproxy/ext/har"
)

type ConstantHandler string

func (h ConstantHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, string(h))
}

func oneShotProxy(proxy *goproxy.ProxyHttpServer) (client *http.Client, s *httptest.Server) {
s = httptest.NewServer(proxy)

proxyUrl, _ := url.Parse(s.URL)
tr := &http.Transport{Proxy: http.ProxyURL(proxyUrl)}
client = &http.Client{Transport: tr}
return
}

func TestHarLogger(t *testing.T) {
// Create a response we expect
expected := "hello world"
background := httptest.NewServer(ConstantHandler(expected))
defer background.Close()

// Set up the proxy with HAR logger
proxy := goproxy.NewProxyHttpServer()
logger := har.NewLogger()
logger.SetCaptureContent(true)

proxy.OnRequest().DoFunc(logger.OnRequest)
proxy.OnResponse().DoFunc(logger.OnResponse)

client, proxyserver := oneShotProxy(proxy)
defer proxyserver.Close()

// Make a request
resp, err := client.Get(background.URL)
if err != nil {
t.Fatal(err)
}

// Read the response
msg, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()

if string(msg) != expected {
t.Errorf("Expected '%s', actual '%s'", expected, string(msg))
}

// Test POST request with content
postData := "test=value"
req, err := http.NewRequest("POST", background.URL, bytes.NewBufferString(postData))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

resp, err = client.Do(req)
if err != nil {
t.Fatal(err)
}
resp.Body.Close()

// Save HAR file and verify content
tmpfile := "test.har"
err = logger.SaveToFile(tmpfile)
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile)

// Read and verify HAR content
harData, err := os.ReadFile(tmpfile)
if err != nil {
t.Fatal(err)
}

var harLog har.Har
if err := json.Unmarshal(harData, &harLog); err != nil {
t.Fatal(err)
}

// Verify we captured both requests
if len(harLog.Log.Entries) != 2 {
t.Errorf("Expected 2 entries in HAR log, got %d", len(harLog.Log.Entries))
}

// Verify GET request
if harLog.Log.Entries[0].Request.Method != "GET" {
t.Errorf("Expected GET request, got %s", harLog.Log.Entries[0].Request.Method)
}

// Verify POST request
if harLog.Log.Entries[1].Request.Method != "POST" {
t.Errorf("Expected POST request, got %s", harLog.Log.Entries[1].Request.Method)
}
}
Loading

0 comments on commit 97bba32

Please sign in to comment.