gomitmproxy

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Mar 22, 2021 License: GPL-3.0 Imports: 18 Imported by: 12

README

Code Coverage Go Report Card GolangCI Go Doc

gomitmproxy

This is a customizable HTTP proxy with TLS interception support. It was created as a part of AdGuard Home. However, it can be used for different purposes so we decided to make it a separate project.

Features

  • HTTP proxy
  • HTTP over TLS (HTTPS) proxy
  • Proxy authorization
  • TLS termination

How to use gomitmproxy

Simple HTTP proxy
package main

import (
	"log"
	"net"
	"os"
	"os/signal"
	"syscall"

	"github.com/AdguardTeam/gomitmproxy"
)

func main() {
	proxy := gomitmproxy.NewProxy(gomitmproxy.Config{
		ListenAddr: &net.TCPAddr{
			IP:   net.IPv4(0, 0, 0, 0),
			Port: 8080,
		},
	})
	err := proxy.Start()
	if err != nil {
		log.Fatal(err)
	}

	signalChannel := make(chan os.Signal, 1)
	signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM)
	<-signalChannel

	// Clean up
	proxy.Close()
}
Modifying requests and responses

You can modify requests and responses using OnRequest and OnResponse handlers.

The example below will block requests to example.net and add a short comment to the end of every HTML response.

proxy := gomitmproxy.NewProxy(gomitmproxy.Config{
    ListenAddr: &net.TCPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 8080,
    },
    OnRequest: func(session *gomitmproxy.Session) (request *http.Request, response *http.Response) {
        req := session.Request()

        log.Printf("onRequest: %s %s", req.Method, req.URL.String())

        if req.URL.Host == "example.net" {
            body := strings.NewReader("<html><body><h1>Replaced response</h1></body></html>")
            res := gomitmproxy.NewResponse(http.StatusOK, body, req)
            res.Header.Set("Content-Type", "text/html")

            // Use session props to pass the information about request being blocked
            session.SetProp("blocked", true)
            return nil, res
        }

        return nil, nil
    },
    OnResponse: func(session *gomitmproxy.Session) *http.Response {
        log.Printf("onResponse: %s", session.Request().URL.String())

        if _, ok := session.GetProp("blocked"); ok {
            log.Printf("onResponse: was blocked")
        }

        res := session.Response()
        req := session.Request()
    
        if strings.Index(res.Header.Get("Content-Type"), "text/html") != 0 {
            // Do nothing with non-HTML responses
            return nil
        }
    
        b, err := proxyutil.ReadDecompressedBody(res)
        // Close the original body
        _ = res.Body.Close()
        if err != nil {
            return proxyutil.NewErrorResponse(req, err)
        }
    
        // Use latin1 before modifying the body
        // Using this 1-byte encoding will let us preserve all original characters
        // regardless of what exactly is the encoding
        body, err := proxyutil.DecodeLatin1(bytes.NewReader(b))
        if err != nil {
            return proxyutil.NewErrorResponse(session.Request(), err)
        }
    
        // Modifying the original body
        modifiedBody, err := proxyutil.EncodeLatin1(body + "<!-- EDITED -->")
        if err != nil {
            return proxyutil.NewErrorResponse(session.Request(), err)
        }
    
        res.Body = ioutil.NopCloser(bytes.NewReader(modifiedBody))
        res.Header.Del("Content-Encoding")
        res.ContentLength = int64(len(modifiedBody))
        return res
    },
})
Proxy authorization

If you want to protect your proxy with Basic authentication, set Username and Password fields in the proxy configuration.

proxy := gomitmproxy.NewProxy(gomitmproxy.Config{
    ListenAddr: &net.TCPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 8080,
    },
    Username: "user",
    Password: "pass",
})
HTTP over TLS (HTTPS) proxy

If you want to protect yourself from eavesdropping on your traffic to proxy, you can configure it to work over a TLS tunnel. This is really simple to do, just set a *tls.Config instance in your proxy configuration.

tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{*proxyCert},
}
proxy := gomitmproxy.NewProxy(gomitmproxy.Config{
    ListenAddr: addr,
    TLSConfig:  tlsConfig,
})
TLS interception

If you want to do TLS termination, you first need to prepare a self-signed certificate that will be used as a certificates authority. Use the following openssl commands to do this.

openssl genrsa -out demo.key 2048
openssl req -new -x509 -key demo.key -out demo.crt

Now you can use it to initialize MITMConfig:

tlsCert, err := tls.LoadX509KeyPair("demo.crt", "demo.key")
if err != nil {
    log.Fatal(err)
}
privateKey := tlsCert.PrivateKey.(*rsa.PrivateKey)

x509c, err := x509.ParseCertificate(tlsCert.Certificate[0])
if err != nil {
    log.Fatal(err)
}

mitmConfig, err := mitm.NewConfig(x509c, privateKey, nil)
if err != nil {
    log.Fatal(err)
}

mitmConfig.SetValidity(time.Hour * 24 * 7) // generate certs valid for 7 days
mitmConfig.SetOrganization("gomitmproxy")  // cert organization

Please note that you can set MITMExceptions to a list of hostnames, which will be excluded from TLS interception.

proxy := gomitmproxy.NewProxy(gomitmproxy.Config{
    ListenAddr: &net.TCPAddr{
        IP:   net.IPv4(0, 0, 0, 0),
        Port: 3333,
    },
    MITMConfig:     mitmConfig,
    MITMExceptions: []string{"example.com"},
})

If you configure the APIHost, you'll be able to download the CA certificate from http://[APIHost]/cert.crt when the proxy is configured.

// Navigate to http://gomitmproxy/cert.crt to download the CA certificate
proxy.APIHost = "gomitmproxy"
Custom certs storage

By default, gomitmproxy uses an in-memory map-based storage for the certificates, generated while doing TLS interception. It is often necessary to use a different kind of certificates storage. If this is your case, you can supply your own implementation of the CertsStorage interface.

// CustomCertsStorage - an example of a custom cert storage
type CustomCertsStorage struct {
	certsCache map[string]*tls.Certificate // cache with the generated certificates
}

// Get gets the certificate from the storage
func (c *CustomCertsStorage) Get(key string) (*tls.Certificate, bool) {
	v, ok := c.certsCache[key]
	return v, ok
}

// Set saves the certificate to the storage
func (c *CustomCertsStorage) Set(key string, cert *tls.Certificate) {
	c.certsCache[key] = cert
}

Then pass it to the NewConfig function.

mitmConfig, err := mitm.NewConfig(x509c, privateKey, &CustomCertsStorage{
    certsCache: map[string]*tls.Certificate{}},
)

Notable alternatives

  • martian - an awesome debugging proxy with TLS interception support.
  • goproxy - also supports TLS interception and requests.

TODO

  • Basic HTTP proxy without MITM
  • Proxy
    • Expose APIs for the library users
    • How-to doc
    • Travis configuration
    • Proxy-Authorization
    • WebSockets support (see this)
    • certsCache -- allow custom implementations
    • Support HTTP CONNECT over TLS
    • Test plain HTTP requests inside HTTP CONNECT
    • Test memory leaks
    • Editing response body in a callback
    • Handle unknown content-encoding values
    • Handle CONNECT to APIHost properly (without trying to actually connect anywhere)
    • Allow hijacking connections (!)
    • Multiple listeners
    • Unit tests
    • Check & fix TODOs
    • Allow specifying net.Dialer
    • Specify timeouts for http.Transport
  • MITM
    • Basic MITM
    • MITM exceptions
    • Handle invalid server certificates properly (not just reset connections)
    • Pass the most important tests on badssl.com/dashboard
    • Handle certificate authentication
    • Allow configuring minimum supported TLS version
    • OCSP check (see example)
    • (?) HPKP (see example)
    • (?) CT logs (see example)
    • (?) CRLSets (see example)

Documentation

Overview

Package gomitmproxy implements a configurable mitm proxy wring purely in go.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	ListenAddr *net.TCPAddr // Address to listen to

	// TLSConfig is a config to use for the HTTP over TLS proxy
	// If not set, gomitmproxy will work as a simple plain HTTP proxy
	TLSConfig *tls.Config

	// Username for Proxy-Authorization
	Username string
	// Password for Proxy-Authorization
	Password string

	MITMConfig     *mitm.Config // If not nil, MITM is enabled for the proxy
	MITMExceptions []string     // A list of hostnames for which MITM will be disabled

	// APIHost is a name of the gomitmproxy API
	// If it is set to "", there will be no API
	// Here are the methods exposed:
	// 1. apihost/cert.crt -- serves the authority cert (if MITMConfig is configured)
	APIHost string

	// OnConnect is called when the proxy tries to open a net.Conn.
	// It allows you to hijack the remote connection and replace it with your own.
	//
	// 1. When the proxy handles the HTTP CONNECT.
	//    IMPORTANT: In this case we don't actually use the remote connections.
	//    It is only used to check if the remote endpoint is available
	// 2. When the proxy bypasses data from the client to the remote endpoint.
	//    For instance, it could happen when there's a WebSocket connection.
	OnConnect func(session *Session, proto string, addr string) net.Conn

	// OnRequest is called when the request has been just received,
	// but has not been sent to the remote server.
	//
	// At this stage, it is possible to do the following things:
	// 1. Modify or even replace the request
	// 2. Supply an HTTP response to be written to the client
	//
	// Return nil instead of *http.Request or *http.Response to keep
	// the original request / response
	//
	// Note that even if you supply your own HTTP response here,
	// the OnResponse handler will be called anyway!
	OnRequest func(session *Session) (*http.Request, *http.Response)

	// OnResponse is called when the response has been just received,
	// but has not been sent to the local client.
	//
	// At this stage you can either keep the original response,
	// or you can replace it with a new one.
	OnResponse func(session *Session) *http.Response

	// OnError is called if there's an issue with retrieving
	// the response from the remote server.
	OnError func(session *Session, err error)
}

Config is the configuration of the Proxy

type Context

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

Context contains all the necessary information about the connection that is currently processing by the proxy

func (*Context) GetProp

func (c *Context) GetProp(key string) (interface{}, bool)

GetProp gets context property (previously saved using SetProp)

func (*Context) ID

func (c *Context) ID() string

ID -- context unique ID

func (*Context) IsMITM

func (c *Context) IsMITM() bool

IsMITM returns true if this context is for a MITM'ed connection

func (*Context) SetDeadline

func (c *Context) SetDeadline(t time.Time) error

SetDeadline sets the read and write deadlines associated with the connection. See net.Conn.SetDeadline for more details.

The difference is that our contexts can be nested, so we search for the topmost parent context recursively and call SetDeadline for its connection only as this is the real underlying network connection.

func (*Context) SetProp

func (c *Context) SetProp(key string, val interface{})

SetProp sets the context property

type Proxy

type Proxy struct {
	Config // Proxy configuration
	// contains filtered or unexported fields
}

Proxy is a structure with the proxy server configuration and current state

func NewProxy

func NewProxy(config Config) *Proxy

NewProxy creates a new instance of the Proxy

func (*Proxy) Addr

func (p *Proxy) Addr() net.Addr

Addr returns the address this proxy listens to

func (*Proxy) Close

func (p *Proxy) Close()

Close sets the proxy to the closing state so it stops receiving new connections, finishes processing any inflight requests, and closes existing connections without reading anymore requests from them.

func (*Proxy) Closing

func (p *Proxy) Closing() bool

Closing returns true if the proxy is in the closing state

func (*Proxy) Serve added in v0.2.0

func (p *Proxy) Serve(l net.Listener)

Serve starts reading and processing requests from the specified listener. Please note, that it will close the listener in the end.

func (*Proxy) Start

func (p *Proxy) Start() error

Start starts the proxy server in a separate goroutine

type Session

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

Session contains all the necessary information about the request-response pair that is currently being processed

func (*Session) Ctx

func (s *Session) Ctx() *Context

Ctx returns this session's context

func (*Session) GetProp

func (s *Session) GetProp(key string) (interface{}, bool)

GetProp gets session property (previously saved using SetProp)

func (*Session) ID

func (s *Session) ID() string

ID -- session unique ID

func (*Session) RemoteAddr

func (s *Session) RemoteAddr() string

RemoteAddr returns this session's remote address

func (*Session) Request

func (s *Session) Request() *http.Request

Request returns the HTTP request of this session

func (*Session) Response

func (s *Session) Response() *http.Response

Response returns the HTTP response of this session

func (*Session) SetProp

func (s *Session) SetProp(key string, val interface{})

SetProp sets the session property

Directories

Path Synopsis
examples
Package mitm implements methods for working with certificates and TLS configurations that are used for mitming connections.
Package mitm implements methods for working with certificates and TLS configurations that are used for mitming connections.
Package proxyutil contains different utility methods that will be helpful to gomitmproxy users
Package proxyutil contains different utility methods that will be helpful to gomitmproxy users

Jump to

Keyboard shortcuts

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