package goproxy import ( "bufio" "crypto/tls" "errors" "io" "io/ioutil" "net" "net/http" "net/url" "os" "regexp" "strconv" "strings" "sync" "sync/atomic" ) type ConnectActionLiteral int const ( ConnectAccept = iota ConnectReject ConnectMitm ConnectHijack ConnectHTTPMitm ConnectProxyAuthHijack ) var ( OkConnect = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)} MitmConnect = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} HTTPMitmConnect = &ConnectAction{Action: ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)} RejectConnect = &ConnectAction{Action: ConnectReject, TLSConfig: TLSConfigFromCA(&GoproxyCa)} httpsRegexp = regexp.MustCompile(`^https:\/\/`) ) type ConnectAction struct { Action ConnectActionLiteral Hijack func(req *http.Request, client net.Conn, ctx *ProxyCtx) TLSConfig func(host string, ctx *ProxyCtx) (*tls.Config, error) } func stripPort(s string) string { ix := strings.IndexRune(s, ':') if ix == -1 { return s } return s[:ix] } func (proxy *ProxyHttpServer) dial(network, addr string) (c net.Conn, err error) { if proxy.Tr.Dial != nil { return proxy.Tr.Dial(network, addr) } return net.Dial(network, addr) } func (proxy *ProxyHttpServer) connectDial(network, addr string) (c net.Conn, err error) { if proxy.ConnectDial == nil { return proxy.dial(network, addr) } return proxy.ConnectDial(network, addr) } func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request) { ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy} hij, ok := w.(http.Hijacker) if !ok { panic("httpserver does not support hijacking") } proxyClient, _, e := hij.Hijack() if e != nil { panic("Cannot hijack connection " + e.Error()) } ctx.Logf("Running %d CONNECT handlers", len(proxy.httpsHandlers)) todo, host := OkConnect, r.URL.Host for i, h := range proxy.httpsHandlers { newtodo, newhost := h.HandleConnect(host, ctx) // If found a result, break the loop immediately if newtodo != nil { todo, host = newtodo, newhost ctx.Logf("on %dth handler: %v %s", i, todo, host) break } } switch todo.Action { case ConnectAccept: if !hasPort.MatchString(host) { host += ":80" } targetSiteCon, err := proxy.connectDial("tcp", host) if err != nil { httpError(proxyClient, ctx, err) return } ctx.Logf("Accepting CONNECT to %s", host) proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) targetTCP, targetOK := targetSiteCon.(*net.TCPConn) proxyClientTCP, clientOK := proxyClient.(*net.TCPConn) if targetOK && clientOK { go copyAndClose(ctx, targetTCP, proxyClientTCP) go copyAndClose(ctx, proxyClientTCP, targetTCP) } else { go func() { var wg sync.WaitGroup wg.Add(2) go copyOrWarn(ctx, targetSiteCon, proxyClient, &wg) go copyOrWarn(ctx, proxyClient, targetSiteCon, &wg) wg.Wait() proxyClient.Close() targetSiteCon.Close() }() } case ConnectHijack: ctx.Logf("Hijacking CONNECT to %s", host) proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) todo.Hijack(r, proxyClient, ctx) case ConnectHTTPMitm: proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) ctx.Logf("Assuming CONNECT is plain HTTP tunneling, mitm proxying it") targetSiteCon, err := proxy.connectDial("tcp", host) if err != nil { ctx.Warnf("Error dialing to %s: %s", host, err.Error()) return } for { client := bufio.NewReader(proxyClient) remote := bufio.NewReader(targetSiteCon) req, err := http.ReadRequest(client) if err != nil && err != io.EOF { ctx.Warnf("cannot read request of MITM HTTP client: %+#v", err) } if err != nil { return } req, resp := proxy.filterRequest(req, ctx) if resp == nil { if err := req.Write(targetSiteCon); err != nil { httpError(proxyClient, ctx, err) return } resp, err = http.ReadResponse(remote, req) if err != nil { httpError(proxyClient, ctx, err) return } defer resp.Body.Close() } resp = proxy.filterResponse(resp, ctx) if err := resp.Write(proxyClient); err != nil { httpError(proxyClient, ctx, err) return } } case ConnectMitm: proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n")) ctx.Logf("Assuming CONNECT is TLS, mitm proxying it") // this goes in a separate goroutine, so that the net/http server won't think we're // still handling the request even after hijacking the connection. Those HTTP CONNECT // request can take forever, and the server will be stuck when "closed". // TODO: Allow Server.Close() mechanism to shut down this connection as nicely as possible tlsConfig := defaultTLSConfig if todo.TLSConfig != nil { var err error tlsConfig, err = todo.TLSConfig(host, ctx) if err != nil { httpError(proxyClient, ctx, err) return } } go func() { //TODO: cache connections to the remote website rawClientTls := tls.Server(proxyClient, tlsConfig) if err := rawClientTls.Handshake(); err != nil { ctx.Warnf("Cannot handshake client %v %v", r.Host, err) return } defer rawClientTls.Close() clientTlsReader := bufio.NewReader(rawClientTls) for !isEof(clientTlsReader) { req, err := http.ReadRequest(clientTlsReader) var ctx = &ProxyCtx{Req: req, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy, UserData: ctx.UserData} if err != nil && err != io.EOF { return } if err != nil { ctx.Warnf("Cannot read TLS request from mitm'd client %v %v", r.Host, err) return } req.RemoteAddr = r.RemoteAddr // since we're converting the request, need to carry over the original connecting IP as well ctx.Logf("req %v", r.Host) if !httpsRegexp.MatchString(req.URL.String()) { req.URL, err = url.Parse("https://" + r.Host + req.URL.String()) } // Bug fix which goproxy fails to provide request // information URL in the context when does HTTPS MITM ctx.Req = req req, resp := proxy.filterRequest(req, ctx) if resp == nil { if err != nil { ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path) return } removeProxyHeaders(ctx, req) resp, err = ctx.RoundTrip(req) if err != nil { ctx.Warnf("Cannot read TLS response from mitm'd server %v", err) return } ctx.Logf("resp %v", resp.Status) } resp = proxy.filterResponse(resp, ctx) defer resp.Body.Close() text := resp.Status statusCode := strconv.Itoa(resp.StatusCode) + " " if strings.HasPrefix(text, statusCode) { text = text[len(statusCode):] } // always use 1.1 to support chunked encoding if _, err := io.WriteString(rawClientTls, "HTTP/1.1"+" "+statusCode+text+"\r\n"); err != nil { ctx.Warnf("Cannot write TLS response HTTP status from mitm'd client: %v", err) return } // Since we don't know the length of resp, return chunked encoded response // TODO: use a more reasonable scheme resp.Header.Del("Content-Length") resp.Header.Set("Transfer-Encoding", "chunked") // Force connection close otherwise chrome will keep CONNECT tunnel open forever resp.Header.Set("Connection", "close") if err := resp.Header.Write(rawClientTls); err != nil { ctx.Warnf("Cannot write TLS response header from mitm'd client: %v", err) return } if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { ctx.Warnf("Cannot write TLS response header end from mitm'd client: %v", err) return } chunked := newChunkedWriter(rawClientTls) if _, err := io.Copy(chunked, resp.Body); err != nil { ctx.Warnf("Cannot write TLS response body from mitm'd client: %v", err) return } if err := chunked.Close(); err != nil { ctx.Warnf("Cannot write TLS chunked EOF from mitm'd client: %v", err) return } if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil { ctx.Warnf("Cannot write TLS response chunked trailer from mitm'd client: %v", err) return } } ctx.Logf("Exiting on EOF") }() case ConnectProxyAuthHijack: proxyClient.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n")) todo.Hijack(r, proxyClient, ctx) case ConnectReject: if ctx.Resp != nil { if err := ctx.Resp.Write(proxyClient); err != nil { ctx.Warnf("Cannot write response that reject http CONNECT: %v", err) } } proxyClient.Close() } } func httpError(w io.WriteCloser, ctx *ProxyCtx, err error) { if _, err := io.WriteString(w, "HTTP/1.1 502 Bad Gateway\r\n\r\n"); err != nil { ctx.Warnf("Error responding to client: %s", err) } if err := w.Close(); err != nil { ctx.Warnf("Error closing client connection: %s", err) } } func copyOrWarn(ctx *ProxyCtx, dst io.Writer, src io.Reader, wg *sync.WaitGroup) { if _, err := io.Copy(dst, src); err != nil { ctx.Warnf("Error copying to client: %s", err) } wg.Done() } func copyAndClose(ctx *ProxyCtx, dst, src *net.TCPConn) { if _, err := io.Copy(dst, src); err != nil { ctx.Warnf("Error copying to client: %s", err) } dst.CloseWrite() src.CloseRead() } func dialerFromEnv(proxy *ProxyHttpServer) func(network, addr string) (net.Conn, error) { https_proxy := os.Getenv("HTTPS_PROXY") if https_proxy == "" { https_proxy = os.Getenv("https_proxy") } if https_proxy == "" { return nil } return proxy.NewConnectDialToProxy(https_proxy) } func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(network, addr string) (net.Conn, error) { return proxy.NewConnectDialToProxyWithHandler(https_proxy, nil) } func (proxy *ProxyHttpServer) NewConnectDialToProxyWithHandler(https_proxy string, connectReqHandler func(req *http.Request)) func(network, addr string) (net.Conn, error) { u, err := url.Parse(https_proxy) if err != nil { return nil } if u.Scheme == "" || u.Scheme == "http" { if strings.IndexRune(u.Host, ':') == -1 { u.Host += ":80" } return func(network, addr string) (net.Conn, error) { connectReq := &http.Request{ Method: "CONNECT", URL: &url.URL{Opaque: addr}, Host: addr, Header: make(http.Header), } if connectReqHandler != nil { connectReqHandler(connectReq) } c, err := proxy.dial(network, u.Host) if err != nil { return nil, err } connectReq.Write(c) // Read response. // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. br := bufio.NewReader(c) resp, err := http.ReadResponse(br, connectReq) if err != nil { c.Close() return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { resp, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } c.Close() return nil, errors.New("proxy refused connection" + string(resp)) } return c, nil } } if u.Scheme == "https" { if strings.IndexRune(u.Host, ':') == -1 { u.Host += ":443" } return func(network, addr string) (net.Conn, error) { c, err := proxy.dial(network, u.Host) if err != nil { return nil, err } c = tls.Client(c, proxy.Tr.TLSClientConfig) connectReq := &http.Request{ Method: "CONNECT", URL: &url.URL{Opaque: addr}, Host: addr, Header: make(http.Header), } if connectReqHandler != nil { connectReqHandler(connectReq) } connectReq.Write(c) // Read response. // Okay to use and discard buffered reader here, because // TLS server will not speak until spoken to. br := bufio.NewReader(c) resp, err := http.ReadResponse(br, connectReq) if err != nil { c.Close() return nil, err } defer resp.Body.Close() if resp.StatusCode != 200 { body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 500)) if err != nil { return nil, err } c.Close() return nil, errors.New("proxy refused connection" + string(body)) } return c, nil } } return nil } func TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *ProxyCtx) (*tls.Config, error) { return func(host string, ctx *ProxyCtx) (*tls.Config, error) { config := *defaultTLSConfig ctx.Logf("signing for %s", stripPort(host)) cert, err := signHost(*ca, []string{stripPort(host)}) if err != nil { ctx.Warnf("Cannot sign host certificate with provided CA: %s", err) return nil, err } config.Certificates = append(config.Certificates, cert) return &config, nil } }