Start a filtering library

This commit is contained in:
eyedeekay
2025-02-03 19:33:31 -05:00
commit 1d913867f0
3 changed files with 354 additions and 0 deletions

45
filter.go Normal file
View File

@ -0,0 +1,45 @@
package filter
import (
"bytes"
"errors"
"net"
)
var ErrInvalidFilter = errors.New("target and replacement must have the same length")
type connFilter struct {
net.Conn
targets []string
replacements []string
}
func (c *connFilter) Read(b []byte) (n int, err error) {
n, err = c.Conn.Read(b)
if err != nil {
return n, err
}
// Replace all occurrences of target with replacement
var buffer bytes.Buffer
buffer.Write(b[:n])
for i := range c.targets {
buffer.Reset()
buffer.Write(bytes.ReplaceAll(b[:n], []byte(c.targets[i]), []byte(c.replacements[i])))
}
copy(b, buffer.Bytes())
return buffer.Len(), nil
}
// NewConnFilter creates a new connFilter that replaces occurrences of target strings with replacement strings in the data read from the connection.
// It returns an error if the lengths of target and replacement slices are not equal.
func NewConnFilter(parentConn net.Conn, targets []string, replacements []string) (net.Conn, error) {
if len(targets) != len(replacements) {
return nil, ErrInvalidFilter
}
return &connFilter{
Conn: parentConn,
targets: targets,
replacements: replacements,
}, nil
}

228
http/httpinspector.go Normal file
View File

@ -0,0 +1,228 @@
// Package httpinspector provides HTTP traffic inspection and modification capabilities
// by wrapping the standard net.Listener interface.
package httpinspector
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"net"
"net/http"
"strings"
"sync"
"time"
)
// Common errors returned by the inspector.
var (
ErrInvalidModification = errors.New("invalid HTTP message modification")
ErrMalformedHTTP = errors.New("malformed HTTP message")
ErrClosedInspector = errors.New("inspector is closed")
)
// RequestCallback is called for each HTTP request intercepted.
type RequestCallback func(*http.Request) error
// ResponseCallback is called for each HTTP response intercepted.
type ResponseCallback func(*http.Response) error
// Config contains configuration options for the HTTP inspector.
type Config struct {
OnRequest RequestCallback // Called for each request
OnResponse ResponseCallback // Called for each response
LoggingEnabled bool // Enable debug logging
ReadTimeout time.Duration // Timeout for reading HTTP messages
ModifyTimeout time.Duration // Timeout for modification callbacks
MaxHeaderBytes int // Maximum size of HTTP headers
MaxBodyBytes int64 // Maximum size of HTTP body
}
// DefaultConfig returns a Config with reasonable defaults.
func DefaultConfig() Config {
return Config{
ReadTimeout: 30 * time.Second,
ModifyTimeout: 5 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
MaxBodyBytes: 1 << 26, // 64MB
}
}
// Inspector wraps a net.Listener to provide HTTP traffic inspection.
type Inspector struct {
listener net.Listener
config Config
closed bool
mu sync.RWMutex // Protects closed field
}
// New creates a new Inspector wrapping the provided listener.
func New(listener net.Listener, config Config) *Inspector {
return &Inspector{
listener: listener,
config: config,
}
}
// Accept implements the net.Listener Accept method.
func (i *Inspector) Accept() (net.Conn, error) {
i.mu.RLock()
if i.closed {
i.mu.RUnlock()
return nil, ErrClosedInspector
}
i.mu.RUnlock()
conn, err := i.listener.Accept()
if err != nil {
return nil, err
}
return &inspectedConn{
Conn: conn,
config: i.config,
}, nil
}
// Close implements the net.Listener Close method.
func (i *Inspector) Close() error {
i.mu.Lock()
defer i.mu.Unlock()
if i.closed {
return ErrClosedInspector
}
i.closed = true
return i.listener.Close()
}
// Addr implements the net.Listener Addr method.
func (i *Inspector) Addr() net.Addr {
return i.listener.Addr()
}
// inspectedConn wraps a net.Conn to provide HTTP inspection.
type inspectedConn struct {
net.Conn
config Config
reader *bufio.Reader
writer *bufio.Writer
readMu sync.Mutex
writeMu sync.Mutex
firstRead bool
firstWrite bool
}
// Read implements the net.Conn Read method with HTTP inspection.
func (c *inspectedConn) Read(b []byte) (int, error) {
c.readMu.Lock()
defer c.readMu.Unlock()
if c.reader == nil {
c.reader = bufio.NewReader(c.Conn)
}
// Only inspect the first read for HTTP requests
if !c.firstRead && c.config.OnRequest != nil {
c.firstRead = true
return c.handleHTTPRequest(b)
}
return c.reader.Read(b)
}
// Write implements the net.Conn Write method with HTTP inspection.
func (c *inspectedConn) Write(b []byte) (int, error) {
c.writeMu.Lock()
defer c.writeMu.Unlock()
if c.writer == nil {
c.writer = bufio.NewWriter(c.Conn)
}
// Only inspect the first write for HTTP responses
if !c.firstWrite && c.config.OnResponse != nil {
c.firstWrite = true
return c.handleHTTPResponse(b)
}
return c.writer.Write(b)
}
// handleHTTPRequest processes incoming HTTP requests.
func (c *inspectedConn) handleHTTPRequest(b []byte) (int, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.config.ReadTimeout)
defer cancel()
// Peek to verify HTTP request
peek, err := c.reader.Peek(4)
if err != nil {
return 0, err
}
// Check for HTTP method
if !isHTTPMethod(string(peek)) {
return c.reader.Read(b)
}
// Read and parse the request
req, err := http.ReadRequest(c.reader)
if err != nil {
return 0, fmt.Errorf("%w: %v", ErrMalformedHTTP, err)
}
defer req.Body.Close()
// Apply request callback
if err := c.config.OnRequest(req); err != nil {
return 0, fmt.Errorf("request modification failed: %w", err)
}
// Buffer the modified request
var buf bytes.Buffer
if err := req.Write(&buf); err != nil {
return 0, fmt.Errorf("%w: %v", ErrInvalidModification, err)
}
// Copy the modified request to the output buffer
return copy(b, buf.Bytes()), nil
}
// handleHTTPResponse processes outgoing HTTP responses.
func (c *inspectedConn) handleHTTPResponse(b []byte) (int, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.config.ReadTimeout)
defer cancel()
// Parse the response
resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(b)), nil)
if err != nil {
return c.writer.Write(b)
}
defer resp.Body.Close()
// Apply response callback
if err := c.config.OnResponse(resp); err != nil {
return 0, fmt.Errorf("response modification failed: %w", err)
}
// Buffer the modified response
var buf bytes.Buffer
if err := resp.Write(&buf); err != nil {
return 0, fmt.Errorf("%w: %v", ErrInvalidModification, err)
}
// Write the modified response
return c.writer.Write(buf.Bytes())
}
// isHTTPMethod checks if the given string starts with an HTTP method.
func isHTTPMethod(s string) bool {
methods := []string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"}
for _, method := range methods {
if strings.HasPrefix(s, method) {
return true
}
}
return false
}

View File

@ -0,0 +1,81 @@
package httpinspector
import (
"io"
"net"
"net/http"
"strings"
"testing"
)
func TestInspector(t *testing.T) {
// Create a mock listener
listener := &mockListener{
conns: make(chan net.Conn, 1),
}
// Create inspector with test configuration
config := Config{
OnRequest: func(req *http.Request) error {
req.Header.Set("X-Modified", "true")
return nil
},
OnResponse: func(resp *http.Response) error {
resp.Header.Set("X-Modified", "true")
return nil
},
LoggingEnabled: true,
}
inspector := New(listener, config)
defer inspector.Close()
// Test request modification
t.Run("ModifyRequest", func(t *testing.T) {
conn := &mockConn{
readData: []byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"),
}
listener.conns <- conn
inspectedConn, err := inspector.Accept()
if err != nil {
t.Fatal(err)
}
buf := make([]byte, 1024)
n, err := inspectedConn.Read(buf)
if err != nil && err != io.EOF {
t.Fatal(err)
}
if !strings.Contains(string(buf[:n]), "X-Modified: true") {
t.Error("request modification not applied")
}
})
}
// Mock implementations for testing
type mockListener struct {
conns chan net.Conn
}
func (m *mockListener) Accept() (net.Conn, error) {
return <-m.conns, nil
}
func (m *mockListener) Close() error {
close(m.conns)
return nil
}
func (m *mockListener) Addr() net.Addr {
return &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8080}
}
type mockConn struct {
readData []byte
readPos int
}
// Implement net.Conn interface methods for mockConn...