├── LICENSE ├── Procfile ├── README.md ├── cert.go ├── example_test.go ├── index.html ├── mitm.go ├── mitm_test.go └── try.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Keith Rarick 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python -m SimpleHTTPServer $PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mitm - mitm is a SSL-capable man-in-the-middle proxy for use with golang net/http. 2 | 3 | It is heavily inspired by the mitmproxy project (https://mitmproxy.org/). 4 | 5 | **Install** 6 | 7 | go get github.com/kr/mitm 8 | 9 | **Docs** 10 | 11 | http://godoc.org/github.com/kr/mitm 12 | 13 | **Contributors** 14 | 15 | * Keith Rarick (@kr) 16 | * Blake Mizerany (@bmizerany) 17 | * Sponsored in part by Rainforest QA (http://rainforestqa.com) 18 | -------------------------------------------------------------------------------- /cert.go: -------------------------------------------------------------------------------- 1 | package mitm 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "crypto/x509/pkix" 10 | "encoding/pem" 11 | "errors" 12 | "fmt" 13 | "math/big" 14 | "time" 15 | ) 16 | 17 | const ( 18 | caMaxAge = 5 * 365 * 24 * time.Hour 19 | leafMaxAge = 24 * time.Hour 20 | caUsage = x509.KeyUsageDigitalSignature | 21 | x509.KeyUsageContentCommitment | 22 | x509.KeyUsageKeyEncipherment | 23 | x509.KeyUsageDataEncipherment | 24 | x509.KeyUsageKeyAgreement | 25 | x509.KeyUsageCertSign | 26 | x509.KeyUsageCRLSign 27 | leafUsage = caUsage 28 | ) 29 | 30 | func genCert(ca *tls.Certificate, names []string) (*tls.Certificate, error) { 31 | now := time.Now().Add(-1 * time.Hour).UTC() 32 | if !ca.Leaf.IsCA { 33 | return nil, errors.New("CA cert is not a CA") 34 | } 35 | serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) 36 | serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to generate serial number: %s", err) 39 | } 40 | tmpl := &x509.Certificate{ 41 | SerialNumber: serialNumber, 42 | Subject: pkix.Name{CommonName: names[0]}, 43 | NotBefore: now, 44 | NotAfter: now.Add(leafMaxAge), 45 | KeyUsage: leafUsage, 46 | BasicConstraintsValid: true, 47 | DNSNames: names, 48 | SignatureAlgorithm: x509.ECDSAWithSHA512, 49 | } 50 | key, err := genKeyPair() 51 | if err != nil { 52 | return nil, err 53 | } 54 | x, err := x509.CreateCertificate(rand.Reader, tmpl, ca.Leaf, key.Public(), ca.PrivateKey) 55 | if err != nil { 56 | return nil, err 57 | } 58 | cert := new(tls.Certificate) 59 | cert.Certificate = append(cert.Certificate, x) 60 | cert.PrivateKey = key 61 | cert.Leaf, _ = x509.ParseCertificate(x) 62 | return cert, nil 63 | } 64 | 65 | func genKeyPair() (*ecdsa.PrivateKey, error) { 66 | return ecdsa.GenerateKey(elliptic.P521(), rand.Reader) 67 | } 68 | 69 | func GenCA(name string) (certPEM, keyPEM []byte, err error) { 70 | now := time.Now().UTC() 71 | tmpl := &x509.Certificate{ 72 | SerialNumber: big.NewInt(1), 73 | Subject: pkix.Name{CommonName: name}, 74 | NotBefore: now, 75 | NotAfter: now.Add(caMaxAge), 76 | KeyUsage: caUsage, 77 | BasicConstraintsValid: true, 78 | IsCA: true, 79 | MaxPathLen: 2, 80 | SignatureAlgorithm: x509.ECDSAWithSHA512, 81 | } 82 | key, err := genKeyPair() 83 | if err != nil { 84 | return 85 | } 86 | certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) 87 | if err != nil { 88 | return 89 | } 90 | keyDER, err := x509.MarshalECPrivateKey(key) 91 | if err != nil { 92 | return 93 | } 94 | certPEM = pem.EncodeToMemory(&pem.Block{ 95 | Type: "CERTIFICATE", 96 | Bytes: certDER, 97 | }) 98 | keyPEM = pem.EncodeToMemory(&pem.Block{ 99 | Type: "ECDSA PRIVATE KEY", 100 | Bytes: keyDER, 101 | }) 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package mitm 2 | 3 | import ( 4 | "crypto/tls" 5 | "log" 6 | "net/http" 7 | "testing" 8 | ) 9 | 10 | type codeRecorder struct { 11 | http.ResponseWriter 12 | 13 | code int 14 | } 15 | 16 | func (w *codeRecorder) WriteHeader(code int) { 17 | w.ResponseWriter.WriteHeader(code) 18 | w.code = code 19 | } 20 | 21 | func ExampleProxy(t *testing.T) { 22 | ca, err := loadCA() 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | 27 | p := &Proxy{ 28 | CA: &ca, 29 | Wrap: func(upstream http.Handler) http.Handler { 30 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 31 | cr := &codeRecorder{ResponseWriter: w} 32 | log.Println("Got Content-Type:", r.Header.Get("Content-Type")) 33 | upstream.ServeHTTP(cr, r) 34 | log.Println("Got Status:", cr.code) 35 | }) 36 | }, 37 | } 38 | listenAndServe(p) 39 | } 40 | 41 | func loadCA() (cert tls.Certificate, err error) { panic("example only") } 42 | func listenAndServe(_ http.Handler) { panic("example only") } 43 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | ok 2 | -------------------------------------------------------------------------------- /mitm.go: -------------------------------------------------------------------------------- 1 | package mitm 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "log" 7 | "net" 8 | "net/http" 9 | "net/http/httputil" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // Proxy is a forward proxy that substitutes its own certificate 15 | // for incoming TLS connections in place of the upstream server's 16 | // certificate. 17 | type Proxy struct { 18 | // Wrap specifies a function for optionally wrapping upstream for 19 | // inspecting the decrypted HTTP request and response. 20 | Wrap func(upstream http.Handler) http.Handler 21 | 22 | // CA specifies the root CA for generating leaf certs for each incoming 23 | // TLS request. 24 | CA *tls.Certificate 25 | 26 | // TLSServerConfig specifies the tls.Config to use when generating leaf 27 | // cert using CA. 28 | TLSServerConfig *tls.Config 29 | 30 | // TLSClientConfig specifies the tls.Config to use when establishing 31 | // an upstream connection for proxying. 32 | TLSClientConfig *tls.Config 33 | 34 | // FlushInterval specifies the flush interval 35 | // to flush to the client while copying the 36 | // response body. 37 | // If zero, no periodic flushing is done. 38 | FlushInterval time.Duration 39 | } 40 | 41 | func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { 42 | if r.Method == "CONNECT" { 43 | p.serveConnect(w, r) 44 | return 45 | } 46 | rp := &httputil.ReverseProxy{ 47 | Director: httpDirector, 48 | FlushInterval: p.FlushInterval, 49 | } 50 | p.Wrap(rp).ServeHTTP(w, r) 51 | } 52 | 53 | func (p *Proxy) serveConnect(w http.ResponseWriter, r *http.Request) { 54 | var ( 55 | err error 56 | sconn *tls.Conn 57 | name = dnsName(r.Host) 58 | ) 59 | 60 | if name == "" { 61 | log.Println("cannot determine cert name for " + r.Host) 62 | http.Error(w, "no upstream", 503) 63 | return 64 | } 65 | 66 | provisionalCert, err := p.cert(name) 67 | if err != nil { 68 | log.Println("cert", err) 69 | http.Error(w, "no upstream", 503) 70 | return 71 | } 72 | 73 | sConfig := new(tls.Config) 74 | if p.TLSServerConfig != nil { 75 | *sConfig = *p.TLSServerConfig 76 | } 77 | sConfig.Certificates = []tls.Certificate{*provisionalCert} 78 | sConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { 79 | cConfig := new(tls.Config) 80 | if p.TLSClientConfig != nil { 81 | *cConfig = *p.TLSClientConfig 82 | } 83 | cConfig.ServerName = hello.ServerName 84 | sconn, err = tls.Dial("tcp", r.Host, cConfig) 85 | if err != nil { 86 | log.Println("dial", r.Host, err) 87 | return nil, err 88 | } 89 | return p.cert(hello.ServerName) 90 | } 91 | 92 | cconn, err := handshake(w, sConfig) 93 | if err != nil { 94 | log.Println("handshake", r.Host, err) 95 | return 96 | } 97 | defer cconn.Close() 98 | if sconn == nil { 99 | log.Println("could not determine cert name for " + r.Host) 100 | return 101 | } 102 | defer sconn.Close() 103 | 104 | od := &oneShotDialer{c: sconn} 105 | rp := &httputil.ReverseProxy{ 106 | Director: httpsDirector, 107 | Transport: &http.Transport{DialTLS: od.Dial}, 108 | FlushInterval: p.FlushInterval, 109 | } 110 | 111 | ch := make(chan int) 112 | wc := &onCloseConn{cconn, func() { ch <- 0 }} 113 | http.Serve(&oneShotListener{wc}, p.Wrap(rp)) 114 | <-ch 115 | } 116 | 117 | func (p *Proxy) cert(names ...string) (*tls.Certificate, error) { 118 | return genCert(p.CA, names) 119 | } 120 | 121 | var okHeader = []byte("HTTP/1.1 200 OK\r\n\r\n") 122 | 123 | // handshake hijacks w's underlying net.Conn, responds to the CONNECT request 124 | // and manually performs the TLS handshake. It returns the net.Conn or and 125 | // error if any. 126 | func handshake(w http.ResponseWriter, config *tls.Config) (net.Conn, error) { 127 | raw, _, err := w.(http.Hijacker).Hijack() 128 | if err != nil { 129 | http.Error(w, "no upstream", 503) 130 | return nil, err 131 | } 132 | if _, err = raw.Write(okHeader); err != nil { 133 | raw.Close() 134 | return nil, err 135 | } 136 | conn := tls.Server(raw, config) 137 | err = conn.Handshake() 138 | if err != nil { 139 | conn.Close() 140 | raw.Close() 141 | return nil, err 142 | } 143 | return conn, nil 144 | } 145 | 146 | func httpDirector(r *http.Request) { 147 | r.URL.Host = r.Host 148 | r.URL.Scheme = "http" 149 | } 150 | 151 | func httpsDirector(r *http.Request) { 152 | r.URL.Host = r.Host 153 | r.URL.Scheme = "https" 154 | } 155 | 156 | // dnsName returns the DNS name in addr, if any. 157 | func dnsName(addr string) string { 158 | host, _, err := net.SplitHostPort(addr) 159 | if err != nil { 160 | return "" 161 | } 162 | return host 163 | } 164 | 165 | // namesOnCert returns the dns names 166 | // in the peer's presented cert. 167 | func namesOnCert(conn *tls.Conn) []string { 168 | // TODO(kr): handle IP addr SANs. 169 | c := conn.ConnectionState().PeerCertificates[0] 170 | if len(c.DNSNames) > 0 { 171 | // If Subject Alt Name is given, 172 | // we ignore the common name. 173 | // This matches behavior of crypto/x509. 174 | return c.DNSNames 175 | } 176 | return []string{c.Subject.CommonName} 177 | } 178 | 179 | // A oneShotDialer implements net.Dialer whos Dial only returns a 180 | // net.Conn as specified by c followed by an error for each subsequent Dial. 181 | type oneShotDialer struct { 182 | c net.Conn 183 | mu sync.Mutex 184 | } 185 | 186 | func (d *oneShotDialer) Dial(network, addr string) (net.Conn, error) { 187 | d.mu.Lock() 188 | defer d.mu.Unlock() 189 | if d.c == nil { 190 | return nil, errors.New("closed") 191 | } 192 | c := d.c 193 | d.c = nil 194 | return c, nil 195 | } 196 | 197 | // A oneShotListener implements net.Listener whos Accept only returns a 198 | // net.Conn as specified by c followed by an error for each subsequent Accept. 199 | type oneShotListener struct { 200 | c net.Conn 201 | } 202 | 203 | func (l *oneShotListener) Accept() (net.Conn, error) { 204 | if l.c == nil { 205 | return nil, errors.New("closed") 206 | } 207 | c := l.c 208 | l.c = nil 209 | return c, nil 210 | } 211 | 212 | func (l *oneShotListener) Close() error { 213 | return nil 214 | } 215 | 216 | func (l *oneShotListener) Addr() net.Addr { 217 | return l.c.LocalAddr() 218 | } 219 | 220 | // A onCloseConn implements net.Conn and calls its f on Close. 221 | type onCloseConn struct { 222 | net.Conn 223 | f func() 224 | } 225 | 226 | func (c *onCloseConn) Close() error { 227 | if c.f != nil { 228 | c.f() 229 | c.f = nil 230 | } 231 | return c.Conn.Close() 232 | } 233 | -------------------------------------------------------------------------------- /mitm_test.go: -------------------------------------------------------------------------------- 1 | package mitm 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "flag" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "net/http/httptest" 11 | "net/url" 12 | "os" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | var hostname, _ = os.Hostname() 18 | 19 | var ( 20 | nettest = flag.Bool("nettest", false, "run tests over network") 21 | ) 22 | 23 | func init() { 24 | flag.Parse() 25 | } 26 | 27 | func genCA() (cert tls.Certificate, err error) { 28 | certPEM, keyPEM, err := GenCA(hostname) 29 | if err != nil { 30 | return tls.Certificate{}, err 31 | } 32 | cert, err = tls.X509KeyPair(certPEM, keyPEM) 33 | if err != nil { 34 | return tls.Certificate{}, err 35 | } 36 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 37 | return cert, err 38 | } 39 | 40 | func testProxy(t *testing.T, ca *tls.Certificate, setupReq func(req *http.Request), wrap func(http.Handler) http.Handler, downstream http.HandlerFunc, checkResp func(*http.Response)) { 41 | ds := httptest.NewTLSServer(downstream) 42 | defer ds.Close() 43 | 44 | p := &Proxy{ 45 | CA: ca, 46 | TLSClientConfig: &tls.Config{ 47 | InsecureSkipVerify: true, 48 | }, 49 | TLSServerConfig: &tls.Config{ 50 | MinVersion: tls.VersionTLS12, 51 | }, 52 | Wrap: wrap, 53 | } 54 | 55 | l, err := net.Listen("tcp", "localhost:0") 56 | if err != nil { 57 | t.Fatal("Listen:", err) 58 | } 59 | defer l.Close() 60 | 61 | go func() { 62 | if err := http.Serve(l, p); err != nil { 63 | if !strings.Contains(err.Error(), "use of closed network") { 64 | t.Fatal("Serve:", err) 65 | } 66 | } 67 | }() 68 | 69 | t.Logf("requesting %q", ds.URL) 70 | req, err := http.NewRequest("GET", ds.URL, nil) 71 | if err != nil { 72 | t.Fatal("NewRequest:", err) 73 | } 74 | setupReq(req) 75 | 76 | c := &http.Client{ 77 | Transport: &http.Transport{ 78 | Proxy: func(r *http.Request) (*url.URL, error) { 79 | u := *r.URL 80 | u.Scheme = "https" 81 | u.Host = l.Addr().String() 82 | return &u, nil 83 | }, 84 | TLSClientConfig: &tls.Config{ 85 | InsecureSkipVerify: true, 86 | }, 87 | }, 88 | } 89 | 90 | resp, err := c.Do(req) 91 | if err != nil { 92 | t.Fatal("Do:", err) 93 | } 94 | checkResp(resp) 95 | } 96 | 97 | func Test(t *testing.T) { 98 | const xHops = "X-Hops" 99 | 100 | ca, err := genCA() 101 | if err != nil { 102 | t.Fatal("loadCA:", err) 103 | } 104 | 105 | testProxy(t, &ca, func(req *http.Request) { 106 | req.Header.Set(xHops, "a") 107 | }, func(upstream http.Handler) http.Handler { 108 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 109 | hops := r.Header.Get("X-Hops") + "b" 110 | r.Header.Set("X-Hops", hops) 111 | upstream.ServeHTTP(w, r) 112 | }) 113 | }, func(w http.ResponseWriter, r *http.Request) { 114 | hops := r.Header.Get(xHops) + "c" 115 | w.Header().Set(xHops, hops) 116 | }, func(resp *http.Response) { 117 | const w = "abc" 118 | if g := resp.Header.Get(xHops); g != w { 119 | t.Errorf("want %s to be %s, got %s", xHops, w, g) 120 | } 121 | }) 122 | } 123 | 124 | func TestNet(t *testing.T) { 125 | if !*nettest { 126 | t.Skip() 127 | } 128 | 129 | ca, err := genCA() 130 | if err != nil { 131 | t.Fatal("loadCA:", err) 132 | } 133 | 134 | var wrapped bool 135 | testProxy(t, &ca, func(req *http.Request) { 136 | nreq, _ := http.NewRequest("GET", "https://mitmtest.herokuapp.com/", nil) 137 | *req = *nreq 138 | }, func(upstream http.Handler) http.Handler { 139 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 140 | wrapped = true 141 | upstream.ServeHTTP(w, r) 142 | }) 143 | }, func(w http.ResponseWriter, r *http.Request) { 144 | t.Fatal("this shouldn't be hit") 145 | }, func(resp *http.Response) { 146 | if !wrapped { 147 | t.Errorf("expected wrap") 148 | } 149 | got, err := ioutil.ReadAll(resp.Body) 150 | if err != nil { 151 | t.Fatal("ReadAll:", err) 152 | } 153 | if code := resp.StatusCode; code != 200 { 154 | t.Errorf("want code 200, got %d", code) 155 | } 156 | if g := string(got); g != "ok\n" { 157 | t.Errorf("want ok, got %q", g) 158 | } 159 | }) 160 | } 161 | -------------------------------------------------------------------------------- /try.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "os" 13 | "path" 14 | "strings" 15 | 16 | "github.com/kr/mitm" 17 | ) 18 | 19 | var ( 20 | hostname, _ = os.Hostname() 21 | 22 | dir = path.Join(os.Getenv("HOME"), ".mitm") 23 | keyFile = path.Join(dir, "ca-key.pem") 24 | certFile = path.Join(dir, "ca-cert.pem") 25 | ) 26 | 27 | func main() { 28 | ca, err := loadCA() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | p := &mitm.Proxy{ 33 | CA: &ca, 34 | TLSServerConfig: &tls.Config{ 35 | MinVersion: tls.VersionTLS12, 36 | //CipherSuites: []uint16{tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA}, 37 | }, 38 | Wrap: cloudToButt, 39 | } 40 | log.Fatal(http.ListenAndServe(":8080", p)) 41 | } 42 | 43 | func loadCA() (cert tls.Certificate, err error) { 44 | // TODO(kr): check file permissions 45 | cert, err = tls.LoadX509KeyPair(certFile, keyFile) 46 | if os.IsNotExist(err) { 47 | cert, err = genCA() 48 | } 49 | if err == nil { 50 | cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0]) 51 | } 52 | return 53 | } 54 | 55 | func genCA() (cert tls.Certificate, err error) { 56 | err = os.MkdirAll(dir, 0700) 57 | if err != nil { 58 | return 59 | } 60 | certPEM, keyPEM, err := mitm.GenCA(hostname) 61 | if err != nil { 62 | return 63 | } 64 | cert, _ = tls.X509KeyPair(certPEM, keyPEM) 65 | err = ioutil.WriteFile(certFile, certPEM, 0400) 66 | if err == nil { 67 | err = ioutil.WriteFile(keyFile, keyPEM, 0400) 68 | } 69 | return cert, err 70 | } 71 | 72 | type cloudToButtResponse struct { 73 | http.ResponseWriter 74 | 75 | sub bool 76 | wroteHeader bool 77 | } 78 | 79 | func (w *cloudToButtResponse) WriteHeader(code int) { 80 | if w.wroteHeader { 81 | return 82 | } 83 | w.wroteHeader = true 84 | ctype := w.Header().Get("Content-Type") 85 | if strings.HasPrefix(ctype, "text/html") { 86 | w.sub = true 87 | } 88 | w.ResponseWriter.WriteHeader(code) 89 | } 90 | 91 | var ( 92 | cloud = []byte("the cloud") 93 | butt = []byte("my butt") 94 | ) 95 | 96 | func (w *cloudToButtResponse) Write(p []byte) (int, error) { 97 | if !w.wroteHeader { 98 | w.WriteHeader(200) 99 | } 100 | if w.sub { 101 | p = bytes.Replace(p, cloud, butt, -1) 102 | } 103 | return w.ResponseWriter.Write(p) 104 | } 105 | 106 | func cloudToButt(upstream http.Handler) http.Handler { 107 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 108 | r.Header.Set("Accept-Encoding", "") 109 | upstream.ServeHTTP(&cloudToButtResponse{ResponseWriter: w}, r) 110 | }) 111 | } 112 | --------------------------------------------------------------------------------