proxy

package
v0.1.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 17, 2026 License: MIT Imports: 36 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrEmptyRequest    = errors.New("empty request")
	ErrEmptyResponse   = errors.New("empty response")
	ErrInvalidRequest  = errors.New("invalid request line")
	ErrInvalidResponse = errors.New("invalid status line")
)

Functions

func CheckLineEndings

func CheckLineEndings(raw []byte) string

CheckLineEndings detects line ending issues in HTTP headers. Returns a description of the issue, or empty string if OK.

func Compress

func Compress(data []byte, encoding string) ([]byte, error)

Compress compresses data with the specified encoding. Returns (compressed data, error). Unknown encodings return the original data unchanged.

Handles the same normalization as Decompress for consistency.

func Decompress

func Decompress(data []byte, encoding string) ([]byte, bool)

Decompress decompresses data based on Content-Encoding. Returns (decompressed data, wasCompressed). If wasCompressed is true but returned data is nil, decompression failed. Unknown encodings return (original data, false).

Handles: - Case variations: "GZIP", "Gzip" normalized to "gzip" - Whitespace: " gzip " trimmed - x-gzip alias: treated as gzip - deflate: tries raw DEFLATE first, then zlib-wrapped - Multiple encodings (e.g., "gzip, br"): skipped (can't partially decode)

func NormalizeEncoding

func NormalizeEncoding(encoding string) (string, bool)

NormalizeEncoding normalizes a Content-Encoding header value. Returns the normalized encoding and whether it's a single supported encoding. Multiple encodings (e.g., "gzip, br") return ("", false) since we can't partially decode.

func ParseRequestLine

func ParseRequestLine(line []byte) (method, path, query, version string, err error)

ParseRequestLine extracts method, path, query, version from request line. Tolerant: accepts malformed lines if method and path are extractable.

func PathWithoutQuery

func PathWithoutQuery(p string) string

PathWithoutQuery returns the path portion before any query string.

Types

type CertManager

type CertManager struct {
	// contains filtered or unexported fields
}

CertManager handles CA certificate loading/generation and on-demand certificate generation for HTTPS MITM interception.

func (*CertManager) CACert

func (m *CertManager) CACert() *x509.Certificate

CACert returns the CA certificate for clients to trust.

func (*CertManager) Close

func (m *CertManager) Close() error

Close releases resources held by the cert cache.

func (*CertManager) GetCertificate

func (m *CertManager) GetCertificate(hostname string) (*tls.Certificate, error)

GetCertificate returns a certificate for the hostname. Generates and caches if not already cached.

type H2RequestData

type H2RequestData struct {
	// Pseudo-headers
	Method    string `json:"method" msgpack:"m"`    // from :method
	Scheme    string `json:"scheme" msgpack:"s"`    // from :scheme
	Authority string `json:"authority" msgpack:"a"` // from :authority
	Path      string `json:"path" msgpack:"p"`      // from :path

	// Regular headers (not pseudo-headers)
	Headers Headers `json:"headers" msgpack:"h"`

	// Body is the request body
	Body []byte `json:"body,omitempty" msgpack:"b,omitempty"`
}

H2RequestData represents an HTTP/2 request for history storage.

func (*H2RequestData) GetHeader

func (r *H2RequestData) GetHeader(name string) string

GetHeader returns the first header value with the given name (case-insensitive).

func (*H2RequestData) SetHeader

func (r *H2RequestData) SetHeader(name, value string)

SetHeader sets or replaces the first header with the given name (case-insensitive).

type H2ResponseData

type H2ResponseData struct {
	// StatusCode from :status pseudo-header
	StatusCode int `json:"status_code" msgpack:"sc"`

	// Regular headers (not pseudo-headers)
	Headers Headers `json:"headers" msgpack:"h"`

	// Body is the response body
	Body []byte `json:"body,omitempty" msgpack:"b,omitempty"`
}

H2ResponseData represents an HTTP/2 response for history storage.

func (*H2ResponseData) GetHeader

func (r *H2ResponseData) GetHeader(name string) string

GetHeader returns the first header value with the given name (case-insensitive).

func (*H2ResponseData) SetHeader

func (r *H2ResponseData) SetHeader(name, value string)

SetHeader sets or replaces the first header with the given name (case-insensitive).

type Header struct {
	// Name preserves original casing and whitespace anomalies
	// (e.g., "Content-Type", "content-type", or "Header " with trailing space)
	Name string `json:"name" msgpack:"n"`

	// Value is the header value with leading/trailing whitespace trimmed
	Value string `json:"value" msgpack:"v"`

	// RawLine contains the original wire bytes for this header (excluding line ending).
	// Used by SerializeRaw() to preserve exact wire format including obs-fold.
	// nil when header was programmatically created or Wire format not tracked.
	RawLine []byte `json:"raw_line,omitempty" msgpack:"rl,omitempty"`
}

Header represents a single HTTP header preserving original formatting.

type Headers

type Headers []Header

Headers is a slice of Header with helper methods for case-insensitive access. JSON serializes as an array, same as []Header.

func (*Headers) Get

func (h *Headers) Get(name string) string

Get returns the first header value with the given name (case-insensitive). Returns empty string if not found.

func (*Headers) Remove

func (h *Headers) Remove(name string)

Remove removes all headers with the given name (case-insensitive).

func (*Headers) Set

func (h *Headers) Set(name, value string)

Set sets or replaces the first header with the given name (case-insensitive). If not found, appends a new header. Clears RawLine since the programmatic value no longer matches the original wire bytes.

type HistoryEntry

type HistoryEntry struct {
	// Offset is the monotonic history index
	Offset uint32 `json:"offset" msgpack:"o"`

	// Protocol identifies the HTTP version: "http/1.1", "h2", or "websocket"
	Protocol string `json:"protocol" msgpack:"pr"`

	// HTTP/1.1 request/response (nil for HTTP/2)
	Request  *RawHTTP1Request  `json:"request,omitempty" msgpack:"rq,omitempty"`
	Response *RawHTTP1Response `json:"response,omitempty" msgpack:"rs,omitempty"`

	// HTTP/2 request/response (nil for HTTP/1.1)
	H2Request  *H2RequestData  `json:"h2_request,omitempty" msgpack:"h2q,omitempty"`
	H2Response *H2ResponseData `json:"h2_response,omitempty" msgpack:"h2r,omitempty"`
	H2StreamID uint32          `json:"h2_stream_id,omitempty" msgpack:"h2s,omitempty"` // for debugging/correlation

	// WSFrames contains WebSocket frames for Protocol="websocket" entries.
	// The handshake is stored in Request/Response; frames are appended here.
	WSFrames []WSFrame `json:"ws_frames,omitempty" msgpack:"ws,omitempty"`

	// Timing metadata
	Timestamp time.Time     `json:"timestamp" msgpack:"ts"`
	Duration  time.Duration `json:"duration" msgpack:"d"`
}

HistoryEntry represents a stored request/response pair. Embeds the parsed types directly for memory efficiency. The SerializeRaw() methods on Request/Response reconstruct wire bytes on demand.

func (*HistoryEntry) FormatRequest

func (e *HistoryEntry) FormatRequest(buf *bytes.Buffer) []byte

FormatRequest returns the request in wire-compatible format. For HTTP/1.1, uses SerializeRaw to preserve anomalies like bare-LF. For HTTP/2, builds a similar text format from pseudo-headers and headers.

func (*HistoryEntry) FormatResponse

func (e *HistoryEntry) FormatResponse(buf *bytes.Buffer) []byte

FormatResponse returns the response in wire-compatible format. For HTTP/1.1, uses SerializeRaw to preserve anomalies like bare-LF. For HTTP/2, builds a similar text format from pseudo-headers and headers.

func (*HistoryEntry) GetHost

func (e *HistoryEntry) GetHost() string

GetHost returns the host for any protocol.

func (*HistoryEntry) GetMethod

func (e *HistoryEntry) GetMethod() string

GetMethod returns the request method for any protocol.

func (*HistoryEntry) GetPath

func (e *HistoryEntry) GetPath() string

GetPath returns the request path for any protocol.

func (*HistoryEntry) GetRequestHeader

func (e *HistoryEntry) GetRequestHeader(name string) string

GetRequestHeader returns a request header value (case-insensitive).

func (*HistoryEntry) GetResponseHeader

func (e *HistoryEntry) GetResponseHeader(name string) string

GetResponseHeader returns a response header value (case-insensitive).

func (*HistoryEntry) GetStatusCode

func (e *HistoryEntry) GetStatusCode() int

GetStatusCode returns the response status code for any protocol.

type HistoryMeta

type HistoryMeta struct {
	Offset      uint32        `msgpack:"o"`
	Protocol    string        `msgpack:"pr"`
	Method      string        `msgpack:"m"`
	Host        string        `msgpack:"h"`
	Path        string        `msgpack:"p"` // includes query string
	Status      int           `msgpack:"s"`
	ContentType string        `msgpack:"ct"`
	RespLen     int           `msgpack:"rl"`
	H2StreamID  uint32        `msgpack:"h2,omitempty"`
	Timestamp   time.Time     `msgpack:"ts"`
	Duration    time.Duration `msgpack:"d"`
}

HistoryMeta holds lightweight metadata extracted at store time. Used by summary/list paths to avoid deserializing full request/response bodies.

type HistoryStore

type HistoryStore struct {
	// contains filtered or unexported fields
}

HistoryStore provides typed access to proxy history backed by store.Storage.

func (*HistoryStore) Close

func (h *HistoryStore) Close()

Close closes the underlying storage.

func (*HistoryStore) Count

func (h *HistoryStore) Count() int

Count returns total number of entries.

func (*HistoryStore) Get

func (h *HistoryStore) Get(offset uint32) (*HistoryEntry, bool)

Get retrieves an entry by offset.

func (*HistoryStore) GetMeta

func (h *HistoryStore) GetMeta(offset uint32) (*HistoryMeta, bool)

GetMeta retrieves lightweight metadata for an entry by offset.

func (*HistoryStore) List

func (h *HistoryStore) List(count int, startOffset uint32) []*HistoryEntry

List returns entries starting from startOffset, up to count. Returns entries in offset order.

func (*HistoryStore) ListMeta

func (h *HistoryStore) ListMeta(count int, startOffset uint32) []HistoryMeta

ListMeta returns metadata for entries starting from startOffset, up to count. Only deserializes lightweight metadata, skipping full request/response bodies.

func (*HistoryStore) Store

func (h *HistoryStore) Store(entry *HistoryEntry) uint32

Store adds an entry and assigns the next offset. Returns the assigned offset.

func (*HistoryStore) Update

func (h *HistoryStore) Update(entry *HistoryEntry)

Update persists changes to an existing entry. The entry must have been previously stored (Offset must be valid).

type JSONModifier

type JSONModifier func(body []byte, setJSON map[string]any, removeJSON []string) ([]byte, error)

JSONModifier modifies JSON body with set/remove operations. Provided by service layer to avoid circular imports.

type Modifications

type Modifications struct {
	Method        string            // Override HTTP method
	SetHeaders    map[string]string // Add or replace headers
	RemoveHeaders []string          // Remove headers by name
	Body          []byte            // Replace entire body (mutually exclusive with JSON mods)
	SetJSON       map[string]any    // Modify JSON fields
	RemoveJSON    []string          // Remove JSON fields
	SetParams     map[string]string // Set query parameters
	RemoveParams  []string          // Remove query parameters
}

Modifications specifies changes to apply to a request.

type ProxyServer

type ProxyServer struct {
	// contains filtered or unexported fields
}

ProxyServer is an HTTP proxy server that captures request/response pairs.

func NewProxyServer

func NewProxyServer(port int, configDir string, maxBodyBytes int, historyStorage store.Storage, timeouts TimeoutConfig) (*ProxyServer, error)

NewProxyServer creates a new proxy server with HTTPS MITM support. configDir is the directory for CA certificates (e.g., ~/.sectool). maxBodyBytes limits request and response body sizes stored in history. historyStorage is the storage backend for proxy history entries.

func (*ProxyServer) Addr

func (s *ProxyServer) Addr() string

Addr returns the proxy listener address (e.g., "127.0.0.1:12345").

func (*ProxyServer) CertManager

func (s *ProxyServer) CertManager() *CertManager

CertManager returns the certificate manager for external access.

func (*ProxyServer) History

func (s *ProxyServer) History() *HistoryStore

History returns the history store for external access.

func (*ProxyServer) Serve

func (s *ProxyServer) Serve() error

Serve starts accepting connections. Blocks until shutdown.

func (*ProxyServer) SetRuleApplier

func (s *ProxyServer) SetRuleApplier(applier RuleApplier)

SetRuleApplier sets the rule applier for all handlers. Call after construction but before Serve().

func (*ProxyServer) Shutdown

func (s *ProxyServer) Shutdown(ctx context.Context) error

Shutdown gracefully stops the server.

func (*ProxyServer) WaitReady

func (s *ProxyServer) WaitReady(ctx context.Context) error

WaitReady blocks until Serve() has entered its accept loop.

type RawHTTP1Request

type RawHTTP1Request struct {
	// Request line components
	Method  string `json:"method" msgpack:"m"`                    // "GET", "POST", etc.
	Path    string `json:"path" msgpack:"p"`                      // path without query string, e.g., "/path"
	Query   string `json:"query,omitempty" msgpack:"q,omitempty"` // query string without leading ?, e.g., "foo=bar"
	Version string `json:"version" msgpack:"v"`                   // "HTTP/1.1" or "HTTP/1.0"

	// Headers preserves order and original name casing/whitespace
	Headers Headers `json:"headers" msgpack:"h"`

	// Body is the request body (decoded if chunked, raw otherwise)
	// For chunked encoding, this contains the reassembled body without chunk framing
	Body []byte `json:"body,omitempty" msgpack:"b,omitempty"`

	// Trailers for chunked encoding (raw bytes, rare but must preserve)
	// TODO - FUTURE - Parse trailers into []Header if trailer rules are needed
	Trailers []byte `json:"trailers,omitempty" msgpack:"t,omitempty"`

	// Protocol metadata for replay fidelity
	Protocol string `json:"protocol" msgpack:"pr"` // "http/1.1" - stored for history/replay

	// Wire contains metadata about the original wire encoding.
	// Used by SerializeRaw() to preserve exact wire format.
	Wire *WireFormat `json:"wire,omitempty" msgpack:"w,omitempty"`
}

RawHTTP1Request represents a parsed HTTP/1.1 request with wire-level fidelity. The SerializeRaw() method reconstructs wire bytes from the stored components.

func (*RawHTTP1Request) GetHeader

func (r *RawHTTP1Request) GetHeader(name string) string

GetHeader returns the first header value with the given name (case-insensitive).

func (*RawHTTP1Request) RemoveHeader

func (r *RawHTTP1Request) RemoveHeader(name string)

RemoveHeader removes all headers with the given name (case-insensitive).

func (*RawHTTP1Request) SerializeRaw

func (r *RawHTTP1Request) SerializeRaw(buf *bytes.Buffer, preserveChunked bool) []byte

SerializeRaw reconstructs wire bytes preserving original formatting when available. If preserveChunked is true and Wire.WasChunked is true, emits chunked encoding. Uses Header.RawLine when available to preserve exact original bytes including obs-fold. Uses bare LF line endings when Wire.UsedBareLF is true. Falls back to standard formatting when Wire is nil or RawLine is not available.

func (*RawHTTP1Request) SetHeader

func (r *RawHTTP1Request) SetHeader(name, value string)

SetHeader sets or replaces the first header with the given name (case-insensitive).

type RawHTTP1Response

type RawHTTP1Response struct {
	// Status line components
	Version    string `json:"version" msgpack:"v"`                          // "HTTP/1.1" or "HTTP/1.0"
	StatusCode int    `json:"status_code" msgpack:"sc"`                     // 200, 404, etc.
	StatusText string `json:"status_text,omitempty" msgpack:"st,omitempty"` // "OK", "Not Found", etc.

	// Headers preserves order and original name casing
	Headers Headers `json:"headers" msgpack:"h"`

	// Body is the response body (decoded if chunked, raw otherwise)
	Body []byte `json:"body,omitempty" msgpack:"b,omitempty"`

	// Trailers for chunked encoding (raw bytes)
	// TODO - FUTURE - Parse trailers into []Header if trailer rules are needed
	Trailers []byte `json:"trailers,omitempty" msgpack:"t,omitempty"`

	// Wire contains metadata about the original wire encoding.
	// Used by SerializeRaw() to preserve exact wire format.
	Wire *WireFormat `json:"wire,omitempty" msgpack:"w,omitempty"`
}

RawHTTP1Response represents a parsed HTTP/1.1 response with wire-level fidelity.

func (*RawHTTP1Response) GetHeader

func (r *RawHTTP1Response) GetHeader(name string) string

GetHeader returns the first header value with the given name (case-insensitive).

func (*RawHTTP1Response) RemoveHeader

func (r *RawHTTP1Response) RemoveHeader(name string)

RemoveHeader removes all headers with the given name (case-insensitive).

func (*RawHTTP1Response) SerializeHeaders

func (r *RawHTTP1Response) SerializeHeaders(buf *bytes.Buffer) []byte

SerializeHeaders reconstructs the status line and headers only (no body). Useful for SendRequestResult where headers and body are returned separately.

func (*RawHTTP1Response) SerializeRaw

func (r *RawHTTP1Response) SerializeRaw(buf *bytes.Buffer, preserveChunked bool) []byte

SerializeRaw reconstructs wire bytes preserving original formatting when available. If preserveChunked is true and Wire.WasChunked is true, emits chunked encoding. Uses Header.RawLine when available to preserve exact original bytes including obs-fold. Uses bare LF line endings when Wire.UsedBareLF is true. Falls back to standard formatting when Wire is nil or RawLine is not available.

func (*RawHTTP1Response) SetHeader

func (r *RawHTTP1Response) SetHeader(name, value string)

SetHeader sets or replaces the first header with the given name (case-insensitive).

type RuleApplier

type RuleApplier interface {
	// ApplyRequestRules applies request header and body rules.
	// Returns the modified request (may be same instance if no changes).
	ApplyRequestRules(req *RawHTTP1Request) *RawHTTP1Request

	// ApplyResponseRules applies response header and body rules.
	// Handles decompression/recompression for body rules.
	ApplyResponseRules(resp *RawHTTP1Response) *RawHTTP1Response

	// ApplyRequestBodyOnlyRules applies only body rules to a request body.
	// Used by HTTP/2 where headers are sent separately before body.
	// Requires headers for Content-Encoding detection (compression-aware).
	// Does not apply header rules.
	// Returns error if recompression fails (caller should reset stream).
	ApplyRequestBodyOnlyRules(body []byte, headers Headers) ([]byte, error)

	// ApplyResponseBodyOnlyRules applies only body rules to a response body.
	// Used by HTTP/2 where headers are sent separately before body.
	// Requires headers for Content-Encoding detection (compression-aware).
	// Does not apply header rules.
	ApplyResponseBodyOnlyRules(body []byte, headers Headers) []byte

	// ApplyWSRules applies WebSocket rules to frame payload.
	// direction is "ws:to-server" or "ws:to-client".
	ApplyWSRules(payload []byte, direction string) []byte

	// HasBodyRules returns true if there are body rules for request or response.
	// Used by HTTP/2 handler to decide whether to buffer full bodies.
	// isRequest=true checks for request_body rules, false checks for response_body rules.
	HasBodyRules(isRequest bool) bool
}

RuleApplier applies match/replace rules to requests and responses. Implemented by the service layer (NativeProxyBackend). Rules are applied in the order they were added (list order).

type SendOptions

type SendOptions struct {
	RawRequest    []byte         // Raw HTTP request bytes
	Target        Target         // Where to send
	Modifications *Modifications // Optional changes
	Force         bool           // Bypass validation

	// Protocol specifies the original request's protocol.
	// Values: "http/1.1", "h2", or "" (defaults to http/1.1)
	// When "h2", the sender will negotiate HTTP/2 with the server.
	Protocol string
}

SendOptions configures request sending.

type SendResult

type SendResult struct {
	Response *RawHTTP1Response
	Duration time.Duration
}

type Sender

type Sender struct {
	// JSONModifier is called to apply JSON modifications to request body.
	// If nil, SetJSON/RemoveJSON modifications are ignored.
	JSONModifier JSONModifier

	// Timeouts holds configurable timeout values for dial, read, and write.
	// Zero values mean no timeout.
	Timeouts TimeoutConfig
}

Sender sends HTTP requests with wire-level fidelity.

func (*Sender) Send

func (s *Sender) Send(ctx context.Context, opts SendOptions) (*SendResult, error)

Send sends a request and returns the response.

func (*Sender) SendWithRedirects

func (s *Sender) SendWithRedirects(ctx context.Context, opts SendOptions) (*SendResult, error)

SendWithRedirects sends a request and follows redirects.

type Target

type Target struct {
	Hostname  string
	Port      int
	UsesHTTPS bool
}

Target specifies where to send a request.

type TimeoutConfig

type TimeoutConfig struct {
	DialTimeout  time.Duration
	ReadTimeout  time.Duration
	WriteTimeout time.Duration
}

TimeoutConfig holds configurable timeout values for proxy operations.

type WSFrame

type WSFrame struct {
	// Direction is "to-server" or "to-client"
	Direction string `json:"direction" msgpack:"dr"`

	// Opcode is the WebSocket opcode (1=text, 2=binary, 8=close, 9=ping, 10=pong)
	Opcode byte `json:"opcode" msgpack:"op"`

	// Payload is the frame payload (unmasked)
	Payload []byte `json:"payload,omitempty" msgpack:"pl,omitempty"`

	// Timestamp when the frame was captured
	Timestamp time.Time `json:"timestamp" msgpack:"ts"`
}

WSFrame represents a single WebSocket frame stored in history.

type WireFormat

type WireFormat struct {
	// WasChunked indicates the body was received with chunked transfer encoding.
	// When true, SerializeRaw can optionally re-emit chunked encoding.
	WasChunked bool `json:"was_chunked,omitempty" msgpack:"wc,omitempty"`

	// UsedBareLF indicates the message used bare LF (\n) instead of CRLF (\r\n).
	// When true, SerializeRaw uses bare LF for line endings.
	UsedBareLF bool `json:"used_bare_lf,omitempty" msgpack:"lf,omitempty"`
}

WireFormat stores metadata about the original wire encoding.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL