Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
1c4a034
chore: add go.sum file and update dependencies
Mathious6 Apr 9, 2025
a4da780
feat: implement request conversion functions for HAR file generation
Mathious6 Apr 9, 2025
a310dae
feat: add unit tests for HTTP request conversion in the converter pac…
Mathious6 Apr 9, 2025
429f56f
fix: update comments for clarity in HAR and Cookie structures
Mathious6 Apr 10, 2025
46f9ace
fix: cookie and header conversion functions and header size computing
Mathious6 Apr 10, 2025
4378af2
refactor: replace string literals with constants for main keys in con…
Mathious6 Apr 10, 2025
b2da8e5
feat: implement FromHTTPResponse function and associated tests for HT…
Mathious6 Apr 10, 2025
41052e4
feat: implement BuildHAR function to create HAR format from HTTP requ…
Mathious6 Apr 10, 2025
1dd631a
feat: add example main.go for HAR file generation and management
Mathious6 Apr 10, 2025
a275c77
refactor: replace net/http with bogdanfinn/fhttp
Mathious6 Apr 12, 2025
47bf115
fix: return empty QueryString[] instead of nil
Mathious6 Apr 12, 2025
778025e
feat: update example main.go to use tls_client
Mathious6 Apr 12, 2025
018968b
chore: add launch configuration for debugging example/main.go
Mathious6 Apr 12, 2025
63fb4ee
fix: replace HeaderOrderKey with http.HeaderOrderKey in common and re…
Mathious6 Apr 12, 2025
03b040a
feat: add getServerIPAddress function to retrieve server IP address i…
Mathious6 Apr 12, 2025
f5641f4
chore: add .DS_Store to .gitignore to prevent tracking of macOS syste…
Mathious6 Apr 12, 2025
22c74a0
feat: add Save method to HAR struct for exporting to JSON file
Mathious6 Apr 12, 2025
b7c9efa
fix: update Save method in HAR struct to use provided filename for ou…
Mathious6 Apr 12, 2025
9513544
feat: implement EntryBuilder and HARHandler for managing HAR entries …
Mathious6 Apr 12, 2025
b89a8d8
fix: update locateRedirectURL function to return a pointer to the red…
Mathious6 Apr 12, 2025
32b27e1
fix: correct header names in request constants and dereference redire…
Mathious6 Apr 12, 2025
0b17911
fix: refactor getServerIPAddress function to use url.Parse for improv…
Mathious6 Apr 12, 2025
5dbad43
feat: add Total method to Timings struct for calculating total reques…
Mathious6 Apr 12, 2025
2b54fb2
fix: initialize Timings in NewEntry and update response handling to c…
Mathious6 Apr 12, 2025
81a3fb6
fix: update HARHandler to use dynamic version from harkit package
Mathious6 Apr 12, 2025
2abccfb
fix: update Total method in Timings struct to ignore negative values …
Mathious6 Apr 12, 2025
e8b7581
fix: update EntryBuilder and HARHandler to conditionally resolve serv…
Mathious6 Apr 12, 2025
e172969
refactor: streamline main function by removing unused imports and sim…
Mathious6 Apr 12, 2025
1a33e4a
fix: update PostData struct to use omitempty for params and text fields
Mathious6 Apr 12, 2025
50eb5c2
refactor: update request handling to use constants and streamline cod…
Mathious6 Apr 12, 2025
211f109
fix: update JSON formatting in Save method to use consistent indentat…
Mathious6 Apr 15, 2025
628b378
fix: update headers handling to include Content-Length in request and…
Mathious6 Apr 15, 2025
c7c9d12
fix: update response handling to use content size instead of Content-…
Mathious6 Apr 15, 2025
2053197
fix: update header size calculation to use single CRLF instead of dou…
Mathious6 Apr 15, 2025
3790505
fix: enhance AddRequest method to merge cookies and update cookie hea…
Mathious6 Apr 15, 2025
3659ae7
fix: refactor request handling to streamline GET and POST methods and…
Mathious6 Apr 15, 2025
80c0fad
fix: update HAR entry handling to use NVPair type and improve cookie …
Mathious6 Apr 15, 2025
c21726b
fix: update comments for clarity and improve variable naming in entry…
Mathious6 Apr 15, 2025
cd21c90
fix: enhance NewEntryWithRequest to clone request and preserve body, …
Mathious6 Apr 15, 2025
3cdb1e7
fix: refactor HAR entry creation to use NewEntry and improve request …
Mathious6 Apr 16, 2025
480d58d
chore: update Dockerfile to use Microsoft container image, streamline…
Mathious6 Jun 16, 2025
53e64c6
chore: remove Dockerfile and Zsh configuration, update devcontainer.j…
Mathious6 Jun 16, 2025
7b4dd88
fix: refactor example main function to improve request handling and a…
Mathious6 Jun 17, 2025
0b3d1cd
chore: update devcontainer setup with initialization and post-create …
Mathious6 Jun 17, 2025
44c0274
feat: add proxy support in main function to conditionally use Charles…
Mathious6 Jun 17, 2025
5aee718
add: update example to replace tls-client with httpkit and adjust rel…
Mathious6 Jul 23, 2025
ebd9d57
refactor: streamline cookie and request handling by introducing helpe…
Mathious6 Jul 24, 2025
d980b91
refactor: simplify HAR entry handling by removing EntryBuilder and in…
Mathious6 Jul 24, 2025
bffaefd
refactor: replace NewHandler with AddEntry in example functions to st…
Mathious6 Jul 24, 2025
f6b1e57
feat: add HARVersion constant and enhance documentation for HARHandle…
Mathious6 Jul 25, 2025
6f1eb75
chore: update version to v1.0.0
Mathious6 Aug 25, 2025
0b896f0
chore: update devcontainer.json to include note for future Go image v…
Mathious6 Aug 25, 2025
7d3d45d
refactor: standardize HTTP version handling by introducing DefaultReq…
Mathious6 Aug 25, 2025
0ea7cfb
feat: enhance HAR time handling by introducing HARTime type and updat…
Mathious6 Aug 25, 2025
1fa0053
refactor: enhance request handling by introducing protocol header man…
Mathious6 Aug 25, 2025
5c3a7ef
refactor: improve response handling by introducing protocol header ma…
Mathious6 Aug 25, 2025
f83c88d
feat: enhance HAR entry logging by including client proxy information…
Mathious6 Aug 25, 2025
1c3eeed
refactor: simplify request handling in AddEntry by removing request c…
Mathious6 Aug 25, 2025
081dea9
refactor: improve request handling by updating header processing and …
Mathious6 Aug 25, 2025
e9c0c96
refactor: update HTTP protocol handling in tests by replacing hardcod…
Mathious6 Aug 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions .devcontainer/.zshrc

This file was deleted.

21 changes: 0 additions & 21 deletions .devcontainer/Dockerfile

This file was deleted.

14 changes: 11 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
{
"name": "Go",
"dockerFile": "Dockerfile",
"image": "mcr.microsoft.com/devcontainers/go:1.24", // Switch to 1.25 when it's released
"remoteUser": "vscode",
"shutdownAction": "stopContainer",
"postCreateCommand": "go mod download",
"initializeCommand": "./.devcontainer/initialize.sh",
"postCreateCommand": "./.devcontainer/post-create.sh",
"customizations": {
"vscode": {
"settings": {
Expand All @@ -17,5 +18,12 @@
},
"mounts": [
"source=go-modules,target=/go,type=volume" // Keep go modules in a volume
]
],
"features": {
"ghcr.io/devcontainers-extra/features/zsh-plugins:0": {
"plugins": "git golang zsh-autosuggestions zsh-syntax-highlighting zsh-you-should-use",
"omzPlugins": "https://github.com/zsh-users/zsh-autosuggestions https://github.com/zsh-users/zsh-syntax-highlighting https://github.com/MichaelAquilina/zsh-you-should-use",
"username": "vscode"
}
}
}
13 changes: 13 additions & 0 deletions .devcontainer/initialize.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

if [ "$(uname)" == "Darwin" ]; then
CHARLES_APP=$(mdfind "kMDItemCFBundleIdentifier == 'com.xk72.Charles'" | head -n 1)

if [ -d "$CHARLES_APP" ]; then
rm -rf .certs
mkdir -p .certs
"$CHARLES_APP/Contents/MacOS/Charles" ssl export .certs/charles-ssl.pem
else
echo "Charles is not installed, skipping certificate export."
fi
fi
6 changes: 6 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

go mod download

sudo cp .certs/charles-ssl.pem /usr/local/share/ca-certificates/charles-ssl-proxying-certificate.crt
sudo update-ca-certificates
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# DEVCONTAINER
.certs

# DEBUG
example-harkit
*.har

# OS
.DS_Store
15 changes: 15 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug example/main.go",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/example/main.go"
}
]
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
"files.autoSave": "onFocusChange",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"files.exclude": {
"**/.certs": true
},
// GOLANG SETTINGS:
"go.toolsManagement.autoUpdate": true,
"go.useLanguageServer": true,
Expand Down
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,34 @@
# HAR file management library

A Golang library for parsing and managing HAR (HTTP Archive) files. Provides easy-to-use structs and functions for loading, inspecting, and manipulating HAR files, making HTTP traffic analysis and debugging simpler.
A Golang library for parsing and managing HAR (HTTP Archive) files. Provides easy-to-use structs and functions for loading, inspecting, and manipulating HAR files, making HTTP traffic analysis and debugging simpler. This library is designed to be used with the following libraries:

* [`bogdanfin/tls-client`](https://github.com/bogdanfinn/tls-client)
* The standard **`net/http`** library
* Other **custom request/response structures**

## Purpose

* Provide a **complete and persistent history of requests and responses**.
* Facilitate **monitoring, tracking, and debugging** of request systems.
* Offer a **1:1 equivalent of HAR exports** produced by tools like **Charles Proxy** or **Proxyman** during SSL proxying.

## Functional Requirements / Specifications

* **Multi-source compatibility:** Capture and generate HAR files from **tls-client**, **net/http**, or any **custom structs**.
* **Maximum fidelity to the HAR standard:** Match as closely as possible the format and content produced by **Charles Proxy** (as a reference for export quality).
* **Strict header order preservation:** Maintain the exact order of headers as defined by the TLS layer (which Go does not guarantee by default—requires specific handling).
* **Simplified API:**
* Create a **HAR session** via a dedicated function.
* Add a **request** to the HAR.
* Add the **corresponding response** via a complementary function.
* Explicit handling of **requests without responses** (timeouts, cancellations, network errors).
* **Additional fields beyond the HAR standard:**
* **IP address** used during the TLS connection.
* **Session ID** for session monitoring and tracking.

## Bonus / Potential Extensions

* **Monitoring integration:** Native export compatible with **Prometheus / Grafana**, or extract metrics directly from HAR files for real-time visualization.
* **Advanced historization:** Automatic HAR file storage in an **S3 bucket**, with associated **metadata/tags**:
* Final request status: **success**, **failure**, **timeout**, **HTTP 5xx**, etc.
* **Category / service / user tagging** for easier identification.
79 changes: 79 additions & 0 deletions converter/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package converter

import (
"fmt"
"strings"
"time"

"github.com/Mathious6/harkit/harfile"
http "github.com/bogdanfinn/fhttp"
)

const (
DefaultRequestHTTPVersion = "HTTP/2.0"
ContentLengthKey = "Content-Length"
ContentTypeKey = "Content-Type"
CookieKey = "Cookie"
SetCookieKey = "Set-Cookie"
LocationKey = "Location"
)

func convertCookies(cookies []*http.Cookie) []*harfile.Cookie {
harCookies := make([]*harfile.Cookie, len(cookies))
for index, cookie := range cookies {
harCookies[index] = &harfile.Cookie{
Name: cookie.Name,
Value: cookie.Value,
Path: cookie.Path,
Domain: cookie.Domain,
Expires: formatExpires(cookie.Expires),
HTTPOnly: cookie.HttpOnly,
Secure: cookie.Secure,
}
}
return harCookies
}

func formatExpires(expires time.Time) string {
if expires.IsZero() {
return ""
}
return expires.Format(time.RFC3339Nano)
}

func convertHeaders(header http.Header, contentLength int64) []*harfile.NVPair {
// By default, client adds Content-Length header later on, so we need to add it here.
// We clone the header to avoid modifying the original one to avoid side effects.
clonedHeader := header.Clone()
if contentLength > 0 && clonedHeader.Get(ContentLengthKey) == "" {
clonedHeader.Set(ContentLengthKey, fmt.Sprintf("%d", contentLength))
}

harHeaders := make([]*harfile.NVPair, 0, len(clonedHeader))
seen := make(map[string]bool)

// Used to sort headers in HAR file if needed (e.g. https://github.com/bogdanfinn/tls-client)
order := clonedHeader.Values(http.HeaderOrderKey)
for _, name := range order {
canonical := http.CanonicalHeaderKey(name)
values := clonedHeader.Values(name)

if len(values) > 0 {
for _, value := range values {
harHeaders = append(harHeaders, &harfile.NVPair{Name: canonical, Value: value})
}
seen[canonical] = true
}
}

for name, values := range clonedHeader {
if seen[name] || strings.EqualFold(name, http.HeaderOrderKey) {
continue
}
for _, value := range values {
harHeaders = append(harHeaders, &harfile.NVPair{Name: name, Value: value})
}
}

return harHeaders
}
153 changes: 153 additions & 0 deletions converter/request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package converter

import (
"errors"
"io"
"net/url"
"strings"

"github.com/Mathious6/harkit/harfile"
http "github.com/bogdanfinn/fhttp"
)

const (
applicationXWWWFormURLEncoded = "application/x-www-form-urlencoded"
multipartFormData = "multipart/form-data"
maxMultipartFormDataSize = 32 << 20 // 32 MB limit

methodKey = ":method"
authorityKey = ":authority"
schemeKey = ":scheme"
pathKey = ":path"

hostKey = "Host"
)

func FromHTTPRequest(req *http.Request) (*harfile.Request, error) {
if req == nil {
return nil, errors.New("request cannot be nil")
}

reqProto := DefaultRequestHTTPVersion // WARNING: req.Proto is not always accurate so we force it.

protocolHeader := handleRequestProtocolHeader(reqProto, req.Method, *req.URL)
headers := convertHeaders(req.Header, req.ContentLength)

postData, err := extractRequestPostData(req)
if err != nil {
return nil, err
}

return &harfile.Request{
Method: req.Method,
URL: req.URL.String(),
HTTPVersion: reqProto,
Cookies: convertCookies(req.Cookies()),
Headers: append(protocolHeader, headers...),
QueryString: convertRequestQueryParams(req.URL),
PostData: postData,
HeadersSize: -1,
BodySize: req.ContentLength,
}, nil
}

func handleRequestProtocolHeader(proto string, method string, url url.URL) []*harfile.NVPair {
if proto == "HTTP/2.0" {
return []*harfile.NVPair{
{Name: methodKey, Value: method},
{Name: authorityKey, Value: url.Host},
{Name: schemeKey, Value: url.Scheme},
{Name: pathKey, Value: url.RequestURI()},
}
} else {
return []*harfile.NVPair{
{Name: hostKey, Value: url.Host},
}
}
}

func convertRequestQueryParams(u *url.URL) []*harfile.NVPair {
result := make([]*harfile.NVPair, 0)

for key, values := range u.Query() {
for _, value := range values {
result = append(result, &harfile.NVPair{Name: key, Value: value})
}
}

return result
}

func extractRequestPostData(req *http.Request) (*harfile.PostData, error) {
if req.Body == nil || req.ContentLength == 0 {
return nil, nil
}

body, err := req.GetBody()
if err != nil {
return nil, err
}
defer body.Close()

bodyText, err := io.ReadAll(body)
if err != nil {
return nil, err
}

mimeType := req.Header.Get(ContentTypeKey)
postData := &harfile.PostData{MimeType: mimeType}

if strings.HasPrefix(mimeType, applicationXWWWFormURLEncoded) {
pairs := strings.SplitSeq(string(bodyText), "&")

for pair := range pairs {
nv := strings.SplitN(pair, "=", 2)
if len(nv) == 2 {
name, value := nv[0], nv[1]
postData.Params = append(postData.Params, &harfile.Param{Name: name, Value: value})
}
}

return postData, nil
}

if strings.HasPrefix(mimeType, multipartFormData) {
err := req.ParseMultipartForm(maxMultipartFormDataSize)
if err != nil {
return nil, err
}

for name, values := range req.MultipartForm.Value {
for _, value := range values {
postData.Params = append(postData.Params, &harfile.Param{Name: name, Value: value})
}
}

for name, files := range req.MultipartForm.File {
for _, fileHeader := range files {
file, err := fileHeader.Open()
if err != nil {
return nil, err
}
defer file.Close()

content, err := io.ReadAll(file)
if err != nil {
return nil, err
}

postData.Params = append(postData.Params, &harfile.Param{
Name: name,
FileName: fileHeader.Filename,
ContentType: fileHeader.Header.Get(ContentTypeKey),
Value: string(content),
})
}
}

return postData, nil
}

postData.Text = string(bodyText)
return postData, nil
}
Loading