mirror of
https://github.com/go-i2p/go-connfilter.git
synced 2025-07-14 06:57:54 -04:00
Start a filtering library
This commit is contained in:
45
filter.go
Normal file
45
filter.go
Normal 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
228
http/httpinspector.go
Normal 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
|
||||
}
|
81
http/httpinspector_test.go
Normal file
81
http/httpinspector_test.go
Normal 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...
|
Reference in New Issue
Block a user