Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancel MITM request context when its handling finishes #634

Merged
merged 3 commits into from
Jan 21, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 115 additions & 96 deletions https.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package goproxy

import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
Expand Down Expand Up @@ -212,49 +213,62 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request
// Take the original value before filtering the request
closeConn := req.Close

// since we're converting the request, need to carry over the
// original connecting IP as well
req.RemoteAddr = r.RemoteAddr
ctx.Logf("req %v", r.Host)
ctx.Req = req

req, resp := proxy.filterRequest(req, ctx)
if resp == nil {
// Establish a connection with the remote server only if the proxy
// doesn't produce a response
if targetSiteCon == nil {
targetSiteCon, err = proxy.connectDial(ctx, "tcp", host)
if err != nil {
ctx.Warnf("Error dialing to %s: %s", host, err.Error())
return
if requestOk := func(req *http.Request) bool {
// Since we handled the request parsing by our own, we manually
// need to set a cancellable context when we finished the request
// processing (same behaviour of the stdlib)
requestContext, finishRequest := context.WithCancel(req.Context())
req = req.WithContext(requestContext)
defer finishRequest()

// since we're converting the request, need to carry over the
// original connecting IP as well
req.RemoteAddr = r.RemoteAddr
ctx.Logf("req %v", r.Host)
ctx.Req = req

req, resp := proxy.filterRequest(req, ctx)
if resp == nil {
// Establish a connection with the remote server only if the proxy
// doesn't produce a response
if targetSiteCon == nil {
targetSiteCon, err = proxy.connectDial(ctx, "tcp", host)
if err != nil {
ctx.Warnf("Error dialing to %s: %s", host, err.Error())
return false
}
remote = bufio.NewReader(targetSiteCon)
}
remote = bufio.NewReader(targetSiteCon)
}

if err := req.Write(targetSiteCon); err != nil {
httpError(proxyClient, ctx, err)
return
if err := req.Write(targetSiteCon); err != nil {
httpError(proxyClient, ctx, err)
return false
}
resp, err = func() (*http.Response, error) {
defer req.Body.Close()
return http.ReadResponse(remote, req)
}()
if err != nil {
httpError(proxyClient, ctx, err)
return false
}
}
resp, err = func() (*http.Response, error) {
defer req.Body.Close()
return http.ReadResponse(remote, req)
}()
resp = proxy.filterResponse(resp, ctx)
err = resp.Write(proxyClient)
_ = resp.Body.Close()
if err != nil {
httpError(proxyClient, ctx, err)
return
return false
}
}
resp = proxy.filterResponse(resp, ctx)
err = resp.Write(proxyClient)
_ = resp.Body.Close()
if err != nil {
httpError(proxyClient, ctx, err)
return

return true
}(req); !requestOk {
break
}

if closeConn {
ctx.Logf("Non-persistent connection; closing")
return
break
}
}
case ConnectMitm:
Expand Down Expand Up @@ -311,74 +325,79 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request
// Take the original value before filtering the request
closeConn := req.Close

// 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 req.Method == "PRI" {
// Handle HTTP/2 connections.

// NOTE: As of 1.22, golang's http module will not recognize or
// parse the HTTP Body for PRI requests. This leaves the body of
// the http2.ClientPreface ("SM\r\n\r\n") on the wire which we need
// to clear before setting up the connection.
reader := clientTlsReader.Reader()
_, err := reader.Discard(6)
if err != nil {
ctx.Warnf("Failed to process HTTP2 client preface: %v", err)
return
if continueLoop := func(req *http.Request) bool {
// Since we handled the request parsing by our own, we manually
// need to set a cancellable context when we finished the request
// processing (same behaviour of the stdlib)
requestContext, finishRequest := context.WithCancel(req.Context())
req = req.WithContext(requestContext)
defer finishRequest()

// 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 req.Method == "PRI" {
// Handle HTTP/2 connections.

// NOTE: As of 1.22, golang's http module will not recognize or
// parse the HTTP Body for PRI requests. This leaves the body of
// the http2.ClientPreface ("SM\r\n\r\n") on the wire which we need
// to clear before setting up the connection.
reader := clientTlsReader.Reader()
_, err := reader.Discard(6)
if err != nil {
ctx.Warnf("Failed to process HTTP2 client preface: %v", err)
return false
}
if !proxy.AllowHTTP2 {
ctx.Warnf("HTTP2 connection failed: disallowed")
return false
}
tr := H2Transport{reader, rawClientTls, tlsConfig.Clone(), host}
if _, err := tr.RoundTrip(req); err != nil {
ctx.Warnf("HTTP2 connection failed: %v", err)
} else {
ctx.Logf("Exiting on EOF")
}
return false
}
if !proxy.AllowHTTP2 {
ctx.Warnf("HTTP2 connection failed: disallowed")
return
if isWebSocketRequest(req) {
ctx.Logf("Request looks like websocket upgrade.")
if req.URL.Scheme == "http" {
ctx.Logf("Enforced HTTP websocket forwarding over TLS")
proxy.serveWebsocket(ctx, rawClientTls, req)
} else {
proxy.serveWebsocketTLS(ctx, req, tlsConfig, rawClientTls)
}
return false
}
tr := H2Transport{reader, rawClientTls, tlsConfig.Clone(), host}
if _, err := tr.RoundTrip(req); err != nil {
ctx.Warnf("HTTP2 connection failed: %v", err)
} else {
ctx.Logf("Exiting on EOF")
if err != nil {
if req.URL != nil {
ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path)
} else {
ctx.Warnf("Illegal URL %s", "https://"+r.Host)
}
return false
}
return
}
if isWebSocketRequest(req) {
ctx.Logf("Request looks like websocket upgrade.")
if req.URL.Scheme == "http" {
ctx.Logf("Enforced HTTP websocket forwarding over TLS")
proxy.serveWebsocket(ctx, rawClientTls, req)
} else {
proxy.serveWebsocketTLS(ctx, req, tlsConfig, rawClientTls)
if !proxy.KeepHeader {
RemoveProxyHeaders(ctx, req)
}
return
}
if err != nil {
if req.URL != nil {
ctx.Warnf("Illegal URL %s", "https://"+r.Host+req.URL.Path)
} else {
ctx.Warnf("Illegal URL %s", "https://"+r.Host)
resp, err = func() (*http.Response, error) {
// explicitly discard request body to avoid data races in certain RoundTripper implementations
// see https://github.com/golang/go/issues/61596#issuecomment-1652345131
defer req.Body.Close()
return ctx.RoundTrip(req)
}()
if err != nil {
ctx.Warnf("Cannot read TLS response from mitm'd server %v", err)
return false
}
return
ctx.Logf("resp %v", resp.Status)
}
if !proxy.KeepHeader {
RemoveProxyHeaders(ctx, req)
}
resp, err = func() (*http.Response, error) {
// explicitly discard request body to avoid data races in certain RoundTripper implementations
// see https://github.com/golang/go/issues/61596#issuecomment-1652345131
defer req.Body.Close()
return 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)

// Run defer inside a custom function to prevent response body memory leak
if ok := func() bool {
resp = proxy.filterResponse(resp, ctx)
defer resp.Body.Close()

text := resp.Status
Expand Down Expand Up @@ -437,7 +456,7 @@ func (proxy *ProxyHttpServer) handleHttps(w http.ResponseWriter, r *http.Request
}

return true
}(); !ok {
}(req); !continueLoop {
return
}

Expand Down
Loading