359 lines
12 KiB
Go
359 lines
12 KiB
Go
/*
|
|
Copyright 2021 The Flux authors
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package proxy
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/elazarl/goproxy"
|
|
"github.com/fluxcd/pkg/gittestserver"
|
|
"github.com/go-logr/logr"
|
|
. "github.com/onsi/gomega"
|
|
|
|
"github.com/fluxcd/source-controller/pkg/git"
|
|
"github.com/fluxcd/source-controller/pkg/git/gogit"
|
|
"github.com/fluxcd/source-controller/pkg/git/libgit2"
|
|
"github.com/fluxcd/source-controller/pkg/git/libgit2/managed"
|
|
"github.com/fluxcd/source-controller/pkg/git/strategy"
|
|
)
|
|
|
|
// These tests are run in a different _test.go file because go-git uses the ProxyFromEnvironment function of the net/http package
|
|
// which caches the Proxy settings, hence not including other tests in the same file ensures a clean proxy setup for the tests to run.
|
|
func TestCheckoutStrategyForImplementation_Proxied(t *testing.T) {
|
|
// for libgit2 we are only testing for managed transport,
|
|
// as unmanaged is sunsetting.
|
|
// Unmanaged transport does not support HTTP_PROXY.
|
|
managed.InitManagedTransport(logr.Discard())
|
|
|
|
type cleanupFunc func()
|
|
|
|
type testCase struct {
|
|
name string
|
|
gitImpl git.Implementation
|
|
url string
|
|
branch string
|
|
setupGitProxy func(g *WithT, proxy *goproxy.ProxyHttpServer, proxyGotRequest *bool) (*git.AuthOptions, cleanupFunc)
|
|
shortTimeout bool
|
|
wantUsedProxy bool
|
|
wantError bool
|
|
}
|
|
|
|
g := NewWithT(t)
|
|
|
|
// Get a free port for proxy to use.
|
|
l, err := net.Listen("tcp", ":0")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
proxyAddr := fmt.Sprintf("localhost:%d", l.Addr().(*net.TCPAddr).Port)
|
|
g.Expect(l.Close()).ToNot(HaveOccurred())
|
|
|
|
cases := []testCase{
|
|
{
|
|
name: "gogit_HTTP_PROXY",
|
|
gitImpl: gogit.Implementation,
|
|
url: "http://example.com/bar/test-reponame",
|
|
branch: "main",
|
|
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxyGotRequest *bool) (*git.AuthOptions, cleanupFunc) {
|
|
// Create the git server.
|
|
gitServer, err := gittestserver.NewTempGitServer()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
username := "test-user"
|
|
password := "test-password"
|
|
gitServer.Auth(username, password)
|
|
gitServer.KeyDir(gitServer.Root())
|
|
|
|
g.Expect(gitServer.StartHTTP()).ToNot(HaveOccurred())
|
|
|
|
// Initialize a git repo.
|
|
err = gitServer.InitRepo("../testdata/repo1", "main", "bar/test-reponame")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
u, err := url.Parse(gitServer.HTTPAddress())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// The request is being forwarded to the local test git server in this handler.
|
|
var proxyHandler goproxy.FuncReqHandler = func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
|
userAgent := req.Header.Get("User-Agent")
|
|
if strings.Contains(req.Host, "example.com") && strings.Contains(userAgent, "git") {
|
|
*proxyGotRequest = true
|
|
req.Host = u.Host
|
|
req.URL.Host = req.Host
|
|
return req, nil
|
|
}
|
|
// Reject if it isnt our request.
|
|
return req, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "")
|
|
}
|
|
proxy.OnRequest().Do(proxyHandler)
|
|
|
|
return &git.AuthOptions{
|
|
Transport: git.HTTP,
|
|
Username: username,
|
|
Password: password,
|
|
}, func() {
|
|
os.RemoveAll(gitServer.Root())
|
|
gitServer.StopHTTP()
|
|
}
|
|
},
|
|
shortTimeout: false,
|
|
wantUsedProxy: true,
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "gogit_HTTPS_PROXY",
|
|
gitImpl: gogit.Implementation,
|
|
url: "https://github.com/git-fixtures/basic",
|
|
branch: "master",
|
|
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxyGotRequest *bool) (*git.AuthOptions, cleanupFunc) {
|
|
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
|
// We don't check for user agent as this handler is only going to process CONNECT requests, and because Go's net/http
|
|
// is the one making such a request on behalf of go-git, adding a check for the go net/http user agent (Go-http-client)
|
|
// would only allow false positives from any request originating from Go's net/http.
|
|
if strings.Contains(host, "github.com") {
|
|
*proxyGotRequest = true
|
|
return goproxy.OkConnect, host
|
|
}
|
|
// Reject if it isnt our request.
|
|
return goproxy.RejectConnect, host
|
|
}
|
|
proxy.OnRequest().HandleConnect(proxyHandler)
|
|
|
|
// go-git does not allow to use an HTTPS proxy and a custom root CA at the same time.
|
|
// See https://github.com/fluxcd/source-controller/pull/524#issuecomment-1006673163.
|
|
return nil, func() {}
|
|
},
|
|
shortTimeout: false,
|
|
wantUsedProxy: true,
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "gogit_NO_PROXY",
|
|
gitImpl: gogit.Implementation,
|
|
url: "https://192.0.2.1/bar/test-reponame",
|
|
branch: "main",
|
|
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxyGotRequest *bool) (*git.AuthOptions, cleanupFunc) {
|
|
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
|
// We shouldn't hit the proxy so we just want to check for any interaction, then reject.
|
|
*proxyGotRequest = true
|
|
return goproxy.RejectConnect, host
|
|
}
|
|
proxy.OnRequest().HandleConnect(proxyHandler)
|
|
|
|
return nil, func() {}
|
|
},
|
|
shortTimeout: true,
|
|
wantUsedProxy: false,
|
|
wantError: true,
|
|
},
|
|
{
|
|
name: "libgit2_HTTPS_PROXY",
|
|
gitImpl: libgit2.Implementation,
|
|
url: "https://example.com/bar/test-reponame",
|
|
branch: "main",
|
|
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxyGotRequest *bool) (*git.AuthOptions, cleanupFunc) {
|
|
// Create the git server.
|
|
gitServer, err := gittestserver.NewTempGitServer()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
username := "test-user"
|
|
password := "test-password"
|
|
gitServer.Auth(username, password)
|
|
gitServer.KeyDir(gitServer.Root())
|
|
|
|
// Start the HTTPS server.
|
|
examplePublicKey, err := os.ReadFile("../testdata/certs/server.pem")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
examplePrivateKey, err := os.ReadFile("../testdata/certs/server-key.pem")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
exampleCA, err := os.ReadFile("../testdata/certs/ca.pem")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
err = gitServer.StartHTTPS(examplePublicKey, examplePrivateKey, exampleCA, "example.com")
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Initialize a git repo.
|
|
repoPath := "bar/test-reponame"
|
|
err = gitServer.InitRepo("../testdata/repo1", "main", repoPath)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
u, err := url.Parse(gitServer.HTTPAddress())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// The request is being forwarded to the local test git server in this handler.
|
|
// The certificate used here is valid for both example.com and localhost.
|
|
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
|
defer managed.RemoveTransportOptions("https://example.com/bar/test-reponame")
|
|
// Check if the host matches with the git server address and the user-agent is the expected git client.
|
|
userAgent := ctx.Req.Header.Get("User-Agent")
|
|
if strings.Contains(host, "example.com") && strings.Contains(userAgent, "libgit2") {
|
|
*proxyGotRequest = true
|
|
return goproxy.OkConnect, u.Host
|
|
}
|
|
// Reject if it isn't our request.
|
|
return goproxy.RejectConnect, host
|
|
}
|
|
proxy.OnRequest().HandleConnect(proxyHandler)
|
|
|
|
return &git.AuthOptions{
|
|
Transport: git.HTTPS,
|
|
Username: username,
|
|
Password: password,
|
|
CAFile: exampleCA,
|
|
TransportOptionsURL: "https://proxy-test",
|
|
}, func() {
|
|
os.RemoveAll(gitServer.Root())
|
|
gitServer.StopHTTP()
|
|
}
|
|
},
|
|
shortTimeout: false,
|
|
wantUsedProxy: true,
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "libgit2_HTTP_PROXY",
|
|
gitImpl: libgit2.Implementation,
|
|
url: "http://example.com/bar/test-reponame",
|
|
branch: "main",
|
|
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxyGotRequest *bool) (*git.AuthOptions, cleanupFunc) {
|
|
// Create the git server.
|
|
gitServer, err := gittestserver.NewTempGitServer()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
err = gitServer.StartHTTP()
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// Initialize a git repo.
|
|
repoPath := "bar/test-reponame"
|
|
err = gitServer.InitRepo("../testdata/repo1", "main", repoPath)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
u, err := url.Parse(gitServer.HTTPAddress())
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
// The request is being forwarded to the local test git server in this handler.
|
|
// The certificate used here is valid for both example.com and localhost.
|
|
var proxyHandler goproxy.FuncReqHandler = func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
|
|
userAgent := req.Header.Get("User-Agent")
|
|
if strings.Contains(req.Host, "example.com") && strings.Contains(userAgent, "libgit2") {
|
|
*proxyGotRequest = true
|
|
req.Host = u.Host
|
|
req.URL.Host = req.Host
|
|
return req, nil
|
|
}
|
|
// Reject if it isnt our request.
|
|
return req, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusForbidden, "")
|
|
}
|
|
proxy.OnRequest().Do(proxyHandler)
|
|
|
|
return &git.AuthOptions{
|
|
Transport: git.HTTP,
|
|
TransportOptionsURL: "http://proxy-test",
|
|
}, func() {
|
|
os.RemoveAll(gitServer.Root())
|
|
gitServer.StopHTTP()
|
|
}
|
|
},
|
|
shortTimeout: false,
|
|
wantUsedProxy: true,
|
|
wantError: false,
|
|
},
|
|
{
|
|
name: "libgit2_NO_PROXY",
|
|
gitImpl: libgit2.Implementation,
|
|
url: "https://192.0.2.1/bar/test-reponame",
|
|
branch: "main",
|
|
setupGitProxy: func(g *WithT, proxy *goproxy.ProxyHttpServer, proxyGotRequest *bool) (*git.AuthOptions, cleanupFunc) {
|
|
var proxyHandler goproxy.FuncHttpsHandler = func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) {
|
|
// We shouldn't hit the proxy so we just want to check for any interaction, then reject.
|
|
*proxyGotRequest = true
|
|
return goproxy.RejectConnect, host
|
|
}
|
|
proxy.OnRequest().HandleConnect(proxyHandler)
|
|
|
|
return nil, func() {}
|
|
},
|
|
shortTimeout: false,
|
|
wantUsedProxy: false,
|
|
wantError: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
g := NewWithT(t)
|
|
|
|
// Run a proxy server.
|
|
proxy := goproxy.NewProxyHttpServer()
|
|
proxy.Verbose = true
|
|
|
|
proxyGotRequest := false
|
|
authOpts, cleanup := tt.setupGitProxy(g, proxy, &proxyGotRequest)
|
|
defer cleanup()
|
|
|
|
proxyServer := http.Server{
|
|
Addr: proxyAddr,
|
|
Handler: proxy,
|
|
}
|
|
l, err := net.Listen("tcp", proxyServer.Addr)
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
go proxyServer.Serve(l)
|
|
defer proxyServer.Close()
|
|
|
|
// Set the proxy env vars for both HTTP and HTTPS because go-git caches them.
|
|
os.Setenv("HTTPS_PROXY", fmt.Sprintf("http://%s", proxyAddr))
|
|
defer os.Unsetenv("HTTPS_PROXY")
|
|
|
|
os.Setenv("HTTP_PROXY", fmt.Sprintf("http://%s", proxyAddr))
|
|
defer os.Unsetenv("HTTP_PROXY")
|
|
|
|
os.Setenv("NO_PROXY", "*.0.2.1")
|
|
defer os.Unsetenv("NO_PROXY")
|
|
|
|
// Checkout the repo.
|
|
checkoutStrategy, err := strategy.CheckoutStrategyForImplementation(context.TODO(), tt.gitImpl, git.CheckoutOptions{
|
|
Branch: tt.branch,
|
|
})
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
// for the NO_PROXY test we dont want to wait the 30s for it to timeout/fail, so shorten the timeout
|
|
checkoutCtx := context.TODO()
|
|
if tt.shortTimeout {
|
|
var cancel context.CancelFunc
|
|
checkoutCtx, cancel = context.WithTimeout(context.TODO(), 1*time.Second)
|
|
defer cancel()
|
|
}
|
|
|
|
_, err = checkoutStrategy.Checkout(checkoutCtx, tmpDir, tt.url, authOpts)
|
|
if tt.wantError {
|
|
g.Expect(err).To(HaveOccurred())
|
|
} else {
|
|
g.Expect(err).ToNot(HaveOccurred())
|
|
}
|
|
|
|
g.Expect(proxyGotRequest).To(Equal(tt.wantUsedProxy))
|
|
})
|
|
}
|
|
}
|