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 }