123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- 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}
- 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")
- 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) {
- 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),
- }
- 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),
- }
- 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
- }
- }
|