Skip to content
Open
Show file tree
Hide file tree
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
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ require (
github.com/Microsoft/go-winio v0.6.2
github.com/ProtonMail/go-crypto v1.3.0
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/elazarl/goproxy v1.7.2
github.com/emirpasic/gods v1.18.1
github.com/gliderlabs/ssh v0.3.8
github.com/go-git/gcfg/v2 v2.0.2
Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
Expand Down
154 changes: 122 additions & 32 deletions plumbing/transport/http/proxy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
"net"
"net/http"
"strings"
"sync"
"sync/atomic"
"testing"

"github.com/elazarl/goproxy"
fixtures "github.com/go-git/go-git-fixtures/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -39,9 +39,7 @@ type ProxySuite struct {
func (s *ProxySuite) TestAdvertisedReferencesHTTP() {
var proxiedRequests int32

proxy := goproxy.NewProxyHttpServer()

setupHTTPProxy(proxy, &proxiedRequests)
proxy := newTestProxy(&proxiedRequests)

httpProxyAddr := setupProxyServer(s.T(), proxy, false, true)

Expand Down Expand Up @@ -76,8 +74,7 @@ func (s *ProxySuite) TestAdvertisedReferencesHTTP() {
func (s *ProxySuite) TestAdvertisedReferencesHTTPS() {
var proxiedRequests int32

proxy := goproxy.NewProxyHttpServer()
setupHTTPSProxy(proxy, &proxiedRequests)
proxy := newTestProxy(&proxiedRequests)

httpsProxyAddr := setupProxyServer(s.T(), proxy, true, true)

Expand Down Expand Up @@ -179,37 +176,130 @@ func setupProxyServer(t testing.TB, handler http.Handler, isTLS, schemaAddr bool
return httpProxyAddr
}

func setupHTTPProxy(proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) {
// The request is being forwarded to the local test git server in this handler.
var proxyHandler goproxy.FuncReqHandler = func(req *http.Request, _ *goproxy.ProxyCtx) (*http.Request, *http.Response) {
if strings.Contains(req.Host, "localhost") {
user, pass, _ := parseBasicAuth(req.Header.Get("Proxy-Authorization"))
if user != "user" || pass != "pass" {
return req, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusUnauthorized, "")
}
atomic.AddInt32(proxiedRequests, 1)
return req, nil
}
// Reject if it isn't our request.
return req, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "")
// testProxy is a minimal HTTP/HTTPS proxy for testing.
type testProxy struct {
proxiedRequests *int32
}

func newTestProxy(proxiedRequests *int32) *testProxy {
return &testProxy{proxiedRequests: proxiedRequests}
}

func (p *testProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Check proxy authentication
user, pass, _ := parseBasicAuth(r.Header.Get("Proxy-Authorization"))
if user != "user" || pass != "pass" {
http.Error(w, "Proxy Authentication Required", http.StatusProxyAuthRequired)
return
}

if r.Method == http.MethodConnect {
// HTTPS proxy: handle CONNECT requests
p.handleConnect(w, r)
} else {
// HTTP proxy: forward the request
p.handleHTTP(w, r)
}
proxy.OnRequest().Do(proxyHandler)
}

func setupHTTPSProxy(proxy *goproxy.ProxyHttpServer, proxiedRequests *int32) {
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
if strings.Contains(host, "github.com") {
user, pass, _ := parseBasicAuth(ctx.Req.Header.Get("Proxy-Authorization"))
if user != "user" || pass != "pass" {
return goproxy.RejectConnect, host
}
atomic.AddInt32(proxiedRequests, 1)
return goproxy.OkConnect, host
func (p *testProxy) handleConnect(w http.ResponseWriter, r *http.Request) {
// Only allow connections to github.com for HTTPS tests
if !strings.Contains(r.Host, "github.com") && !strings.Contains(r.Host, "localhost") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

atomic.AddInt32(p.proxiedRequests, 1)

// Establish connection to the target
targetConn, err := net.Dial("tcp", r.Host)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}

// Hijack the connection first
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
targetConn.Close()
return
}

clientConn, _, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
targetConn.Close()
return
}

// Send 200 Connection Established response manually after hijacking
_, err = clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
if err != nil {
targetConn.Close()
clientConn.Close()
return
}

// Tunnel data between client and target.
// Use one goroutine for client->target, current goroutine for target->client.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
_, _ = io.Copy(targetConn, clientConn)
targetConn.Close() // Signal EOF to the other direction
}()

_, _ = io.Copy(clientConn, targetConn)
clientConn.Close() // Signal EOF to the other direction

wg.Wait()
}

func (p *testProxy) handleHTTP(w http.ResponseWriter, r *http.Request) {
// Only allow requests to localhost for HTTP tests
if !strings.Contains(r.Host, "localhost") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}

atomic.AddInt32(p.proxiedRequests, 1)

// Create a new request to the target
outReq := r.Clone(r.Context())
outReq.RequestURI = ""

// Remove hop-by-hop headers
hopHeaders := []string{
"Connection",
"Proxy-Connection", // non-standard but still sent by some proxies
"Keep-Alive",
"Proxy-Authorization",
"TE",
"Trailer",
"Transfer-Encoding",
"Upgrade",
}
for _, h := range hopHeaders {
outReq.Header.Del(h)
}

resp, err := http.DefaultTransport.RoundTrip(outReq)
if err != nil {
http.Error(w, err.Error(), http.StatusBadGateway)
return
}
defer resp.Body.Close()

// Copy response headers
for key, values := range resp.Header {
for _, value := range values {
w.Header().Add(key, value)
}
// Reject if it isn't our request.
return goproxy.RejectConnect, host
}
proxy.OnRequest().HandleConnect(proxyHandler)
w.WriteHeader(resp.StatusCode)
_, _ = io.Copy(w, resp.Body)
}

// adapted from https://github.com/golang/go/blob/2ef70d9d0f98832c8103a7968b195e560a8bb262/src/net/http/request.go#L959
Expand Down
Loading