Skip to content

AdguardTeam/gomitmproxy

Folders and files

NameName
Last commit message
Last commit date
Nov 11, 2019
Nov 8, 2019
Nov 11, 2019
Nov 5, 2019
Nov 6, 2019
Nov 6, 2019
Nov 1, 2019
Nov 11, 2019
Nov 11, 2019
Nov 11, 2019
Nov 8, 2019
Nov 11, 2019
Nov 11, 2019
Nov 11, 2019
Nov 5, 2019
Nov 11, 2019

Repository files navigation

Build Status 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 (!)
    • Unit tests
    • Check & fix TODOs
  • 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)