github.com/coreos/goproxy@v0.0.0-20190513173959-f8dc2d7ba04e/https.go (about)

     1  package goproxy
     2  
     3  import (
     4  	"bufio"
     5  	"crypto/tls"
     6  	"errors"
     7  	"io"
     8  	"io/ioutil"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"strconv"
    14  	"strings"
    15  	"sync/atomic"
    16  )
    17  
    18  type ConnectActionLiteral int
    19  
    20  const (
    21  	ConnectAccept = iota
    22  	ConnectReject
    23  	ConnectMitm
    24  	ConnectHijack
    25  	ConnectHTTPMitm
    26  	ConnectProxyAuthHijack
    27  )
    28  
    29  var (
    30  	OkConnect       = &ConnectAction{Action: ConnectAccept, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
    31  	MitmConnect     = &ConnectAction{Action: ConnectMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
    32  	HTTPMitmConnect = &ConnectAction{Action: ConnectHTTPMitm, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
    33  	RejectConnect   = &ConnectAction{Action: ConnectReject, TLSConfig: TLSConfigFromCA(&GoproxyCa)}
    34  )
    35  
    36  type ConnectAction struct {
    37  	Action    ConnectActionLiteral
    38  	Hijack    func(req *http.Request, client net.Conn, ctx *ProxyCtx)
    39  	TLSConfig func(host string, ctx *ProxyCtx) (*tls.Config, error)
    40  }
    41  
    42  func stripPort(s string) string {
    43  	ix := strings.IndexRune(s, ':')
    44  	if ix == -1 {
    45  		return s
    46  	}
    47  	return s[:ix]
    48  }
    49  
    50  func (proxy *ProxyHttpServer) dial(network, addr string) (c net.Conn, err error) {
    51  	if proxy.Tr.Dial != nil {
    52  		return proxy.Tr.Dial(network, addr)
    53  	}
    54  	return net.Dial(network, addr)
    55  }
    56  
    57  func (proxy *ProxyHttpServer) connectDial(network, addr string) (c net.Conn, err error) {
    58  	if proxy.ConnectDial == nil {
    59  		return proxy.dial(network, addr)
    60  	}
    61  	return proxy.ConnectDial(network, addr)
    62  }
    63  
    64  func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request) {
    65  	ctx := &ProxyCtx{Req: r, Session: atomic.AddInt64(&proxy.sess, 1), proxy: proxy}
    66  
    67  	hij, ok := w.(http.Hijacker)
    68  	if !ok {
    69  		panic("httpserver does not support hijacking")
    70  	}
    71  
    72  	proxyClient, _, e := hij.Hijack()
    73  	if e != nil {
    74  		panic("Cannot hijack connection " + e.Error())
    75  	}
    76  
    77  	ctx.Logf("Running %d CONNECT handlers", len(proxy.httpsHandlers))
    78  	todo, host := OkConnect, r.URL.Host
    79  	for i, h := range proxy.httpsHandlers {
    80  		newtodo, newhost := h.HandleConnect(host, ctx)
    81  
    82  		// If found a result, break the loop immediately
    83  		if newtodo != nil {
    84  			todo, host = newtodo, newhost
    85  			ctx.Logf("on %dth handler: %v %s", i, todo, host)
    86  			break
    87  		}
    88  	}
    89  	switch todo.Action {
    90  	case ConnectAccept:
    91  		if !hasPort.MatchString(host) {
    92  			host += ":80"
    93  		}
    94  		targetSiteCon, err := proxy.connectDial("tcp", host)
    95  		if err != nil {
    96  			httpError(proxyClient, ctx, err)
    97  			return
    98  		}
    99  		ctx.Logf("Accepting CONNECT to %s", host)
   100  		proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
   101  		go copyAndClose(ctx, targetSiteCon, proxyClient)
   102  		go copyAndClose(ctx, proxyClient, targetSiteCon)
   103  	case ConnectHijack:
   104  		ctx.Logf("Hijacking CONNECT to %s", host)
   105  		proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
   106  		todo.Hijack(r, proxyClient, ctx)
   107  	case ConnectHTTPMitm:
   108  		proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
   109  		ctx.Logf("Assuming CONNECT is plain HTTP tunneling, mitm proxying it")
   110  		targetSiteCon, err := proxy.connectDial("tcp", host)
   111  		if err != nil {
   112  			ctx.Warnf("Error dialing to %s: %s", host, err.Error())
   113  			return
   114  		}
   115  		for {
   116  			client := bufio.NewReader(proxyClient)
   117  			remote := bufio.NewReader(targetSiteCon)
   118  			req, err := http.ReadRequest(client)
   119  			if err != nil && err != io.EOF {
   120  				ctx.Warnf("cannot read request of MITM HTTP client: %+#v", err)
   121  			}
   122  			if err != nil {
   123  				return
   124  			}
   125  			req, resp := proxy.filterRequest(req, ctx)
   126  			if resp == nil {
   127  				if err := req.Write(targetSiteCon); err != nil {
   128  					httpError(proxyClient, ctx, err)
   129  					return
   130  				}
   131  				resp, err = http.ReadResponse(remote, req)
   132  				if err != nil {
   133  					httpError(proxyClient, ctx, err)
   134  					return
   135  				}
   136  			}
   137  			resp = proxy.filterResponse(resp, ctx)
   138  			if err := resp.Write(proxyClient); err != nil {
   139  				httpError(proxyClient, ctx, err)
   140  				return
   141  			}
   142  		}
   143  	case ConnectMitm:
   144  		proxyClient.Write([]byte("HTTP/1.0 200 OK\r\n\r\n"))
   145  		ctx.Logf("Assuming CONNECT is TLS, mitm proxying it")
   146  		// this goes in a separate goroutine, so that the net/http server won't think we're
   147  		// still handling the request even after hijacking the connection. Those HTTP CONNECT
   148  		// request can take forever, and the server will be stuck when "closed".
   149  		// TODO: Allow Server.Close() mechanism to shut down this connection as nicely as possible
   150  		tlsConfig := defaultTLSConfig
   151  		if todo.TLSConfig != nil {
   152  			var err error
   153  			tlsConfig, err = todo.TLSConfig(host, ctx)
   154  			if err != nil {
   155  				httpError(proxyClient, ctx, err)
   156  				return
   157  			}
   158  		}
   159  		go func() {
   160  			//TODO: cache connections to the remote website
   161  			rawClientTls := tls.Server(proxyClient, tlsConfig)
   162  			if err := rawClientTls.Handshake(); err != nil {
   163  				ctx.Warnf("Cannot handshake client %v %v", r.Host, err)
   164  				return
   165  			}
   166  			defer rawClientTls.Close()
   167  			clientTlsReader := bufio.NewReader(rawClientTls)
   168  
   169  			for !isEof(clientTlsReader) {
   170  				req, err := http.ReadRequest(clientTlsReader)
   171  				if err != nil && err != io.EOF {
   172  					return
   173  				}
   174  				if err != nil {
   175  					ctx.Warnf("Cannot read TLS request from mitm'd client %v %v", r.Host, err)
   176  					return
   177  				}
   178  
   179  				req.RemoteAddr = r.RemoteAddr // since we're converting the request, need to carry over the original connecting IP as well
   180  				ctx.Logf("req %v", r.Host)
   181  				req.URL, err = url.Parse("https://" + r.Host + req.URL.String())
   182  
   183  				// Bug fix which goproxy fails to provide request
   184  				// information URL in the context when does HTTPS MITM
   185  				ctx.Req = req
   186  
   187  				req, resp := proxy.filterRequest(req, ctx)
   188  				if resp == nil {
   189  					if isWebSocketRequest(req) {
   190  						ctx.Logf("Request looks like websocket upgrade.")
   191  						proxy.serveWebsocketTLS(ctx, w, req, tlsConfig, rawClientTls)
   192  						return
   193  					}
   194  					if err != nil {
   195  						ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path)
   196  						return
   197  					}
   198  					removeProxyHeaders(ctx, req)
   199  					resp, err = ctx.RoundTrip(req)
   200  					if err != nil {
   201  						ctx.Warnf("Cannot read TLS response from mitm'd server %v", err)
   202  						return
   203  					}
   204  					ctx.Logf("resp %v", resp.Status)
   205  				}
   206  
   207  				resp = proxy.filterResponse(resp, ctx)
   208  				text := resp.Status
   209  				statusCode := strconv.Itoa(resp.StatusCode) + " "
   210  				if strings.HasPrefix(text, statusCode) {
   211  					text = text[len(statusCode):]
   212  				}
   213  				// always use 1.1 to support chunked encoding
   214  				if _, err := io.WriteString(rawClientTls, "HTTP/1.1"+" "+statusCode+text+"\r\n"); err != nil {
   215  					ctx.Warnf("Cannot write TLS response HTTP status from mitm'd client: %v", err)
   216  					return
   217  				}
   218  				// Since we don't know the length of resp, return chunked encoded response
   219  				// TODO: use a more reasonable scheme
   220  				resp.Header.Del("Content-Length")
   221  				resp.Header.Set("Transfer-Encoding", "chunked")
   222  				if err := resp.Header.Write(rawClientTls); err != nil {
   223  					ctx.Warnf("Cannot write TLS response header from mitm'd client: %v", err)
   224  					return
   225  				}
   226  				if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil {
   227  					ctx.Warnf("Cannot write TLS response header end from mitm'd client: %v", err)
   228  					return
   229  				}
   230  				chunked := newChunkedWriter(rawClientTls)
   231  				if _, err := io.Copy(chunked, resp.Body); err != nil {
   232  					ctx.Warnf("Cannot write TLS response body from mitm'd client: %v", err)
   233  					return
   234  				}
   235  				if err := chunked.Close(); err != nil {
   236  					ctx.Warnf("Cannot write TLS chunked EOF from mitm'd client: %v", err)
   237  					return
   238  				}
   239  				if _, err = io.WriteString(rawClientTls, "\r\n"); err != nil {
   240  					ctx.Warnf("Cannot write TLS response chunked trailer from mitm'd client: %v", err)
   241  					return
   242  				}
   243  			}
   244  			ctx.Logf("Exiting on EOF")
   245  		}()
   246  	case ConnectProxyAuthHijack:
   247  		proxyClient.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\n"))
   248  		todo.Hijack(r, proxyClient, ctx)
   249  	case ConnectReject:
   250  		if ctx.Resp != nil {
   251  			if err := ctx.Resp.Write(proxyClient); err != nil {
   252  				ctx.Warnf("Cannot write response that reject http CONNECT: %v", err)
   253  			}
   254  		}
   255  		proxyClient.Close()
   256  	}
   257  }
   258  
   259  func httpError(w io.WriteCloser, ctx *ProxyCtx, err error) {
   260  	if _, err := io.WriteString(w, "HTTP/1.1 502 Bad Gateway\r\n\r\n"); err != nil {
   261  		ctx.Warnf("Error responding to client: %s", err)
   262  	}
   263  	if err := w.Close(); err != nil {
   264  		ctx.Warnf("Error closing client connection: %s", err)
   265  	}
   266  }
   267  
   268  func copyAndClose(ctx *ProxyCtx, w, r net.Conn) {
   269  	connOk := true
   270  	if _, err := io.Copy(w, r); err != nil {
   271  		connOk = false
   272  		ctx.Warnf("Error copying to client: %s", err)
   273  	}
   274  	if err := r.Close(); err != nil && connOk {
   275  		ctx.Warnf("Error closing: %s", err)
   276  	}
   277  }
   278  
   279  func dialerFromEnv(proxy *ProxyHttpServer) func(network, addr string) (net.Conn, error) {
   280  	https_proxy := os.Getenv("HTTPS_PROXY")
   281  	if https_proxy == "" {
   282  		https_proxy = os.Getenv("https_proxy")
   283  	}
   284  	if https_proxy == "" {
   285  		return nil
   286  	}
   287  	return proxy.NewConnectDialToProxy(https_proxy)
   288  }
   289  
   290  func (proxy *ProxyHttpServer) NewConnectDialToProxy(https_proxy string) func(network, addr string) (net.Conn, error) {
   291  	u, err := url.Parse(https_proxy)
   292  	if err != nil {
   293  		return nil
   294  	}
   295  	if u.Scheme == "" || u.Scheme == "http" {
   296  		if strings.IndexRune(u.Host, ':') == -1 {
   297  			u.Host += ":80"
   298  		}
   299  		return func(network, addr string) (net.Conn, error) {
   300  			connectReq := &http.Request{
   301  				Method: "CONNECT",
   302  				URL:    &url.URL{Opaque: addr},
   303  				Host:   addr,
   304  				Header: make(http.Header),
   305  			}
   306  			c, err := proxy.dial(network, u.Host)
   307  			if err != nil {
   308  				return nil, err
   309  			}
   310  			connectReq.Write(c)
   311  			// Read response.
   312  			// Okay to use and discard buffered reader here, because
   313  			// TLS server will not speak until spoken to.
   314  			br := bufio.NewReader(c)
   315  			resp, err := http.ReadResponse(br, connectReq)
   316  			if err != nil {
   317  				c.Close()
   318  				return nil, err
   319  			}
   320  			if resp.StatusCode != 200 {
   321  				resp, _ := ioutil.ReadAll(resp.Body)
   322  				c.Close()
   323  				return nil, errors.New("proxy refused connection" + string(resp))
   324  			}
   325  			return c, nil
   326  		}
   327  	}
   328  	if u.Scheme == "https" || u.Scheme == "wss" {
   329  		if strings.IndexRune(u.Host, ':') == -1 {
   330  			u.Host += ":443"
   331  		}
   332  		return func(network, addr string) (net.Conn, error) {
   333  			c, err := proxy.dial(network, u.Host)
   334  			if err != nil {
   335  				return nil, err
   336  			}
   337  			c = tls.Client(c, proxy.Tr.TLSClientConfig)
   338  			connectReq := &http.Request{
   339  				Method: "CONNECT",
   340  				URL:    &url.URL{Opaque: addr},
   341  				Host:   addr,
   342  				Header: make(http.Header),
   343  			}
   344  			connectReq.Write(c)
   345  			// Read response.
   346  			// Okay to use and discard buffered reader here, because
   347  			// TLS server will not speak until spoken to.
   348  			br := bufio.NewReader(c)
   349  			resp, err := http.ReadResponse(br, connectReq)
   350  			if err != nil {
   351  				c.Close()
   352  				return nil, err
   353  			}
   354  			if resp.StatusCode != 200 {
   355  				body, _ := ioutil.ReadAll(io.LimitReader(resp.Body, 500))
   356  				resp.Body.Close()
   357  				c.Close()
   358  				return nil, errors.New("proxy refused connection" + string(body))
   359  			}
   360  			return c, nil
   361  		}
   362  	}
   363  	return nil
   364  }
   365  
   366  func TLSConfigFromCA(ca *tls.Certificate) func(host string, ctx *ProxyCtx) (*tls.Config, error) {
   367  	return func(host string, ctx *ProxyCtx) (*tls.Config, error) {
   368  		config := *defaultTLSConfig
   369  		ctx.Logf("signing for %s", stripPort(host))
   370  		cert, err := signHost(*ca, []string{stripPort(host)})
   371  		if err != nil {
   372  			ctx.Warnf("Cannot sign host certificate with provided CA: %s", err)
   373  			return nil, err
   374  		}
   375  		config.Certificates = append(config.Certificates, cert)
   376  		return &config, nil
   377  	}
   378  }