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 := proxyutil.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 -days 3650 -addext subjectAltName=DNS:<hostname>,IP:<ip>
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)