2021-03-05 16:19:46 +05:30
package upstream
import (
2021-10-27 15:23:28 +05:30
"fmt"
2021-03-05 16:19:46 +05:30
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
2021-11-18 22:05:49 +05:30
"time"
2021-03-05 16:19:46 +05:30
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
2021-10-27 15:23:28 +05:30
"gitlab.com/gitlab-org/gitlab/workhorse/internal/config"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
2021-03-05 16:19:46 +05:30
)
2021-10-27 15:23:28 +05:30
const (
geoProxyEndpoint = "/api/v4/geo/proxy"
testDocumentRoot = "testdata/public"
)
type testCase struct {
desc string
path string
expectedResponse string
}
2021-03-05 16:19:46 +05:30
func TestRouting ( t * testing . T ) {
handle := func ( u * upstream , regex string ) routeEntry {
handler := http . HandlerFunc ( func ( w http . ResponseWriter , _ * http . Request ) {
io . WriteString ( w , regex )
} )
return u . route ( "" , regex , handler )
}
const (
foobar = ` \A/foobar\z `
quxbaz = ` \A/quxbaz\z `
main = ""
)
u := newUpstream ( config . Config { } , logrus . StandardLogger ( ) , func ( u * upstream ) {
u . Routes = [ ] routeEntry {
handle ( u , foobar ) ,
handle ( u , quxbaz ) ,
handle ( u , main ) ,
}
} )
ts := httptest . NewServer ( u )
defer ts . Close ( )
2021-10-27 15:23:28 +05:30
testCases := [ ] testCase {
2021-03-05 16:19:46 +05:30
{ "main route works" , "/" , main } ,
{ "foobar route works" , "/foobar" , foobar } ,
{ "quxbaz route works" , "/quxbaz" , quxbaz } ,
{ "path traversal works, ends up in quxbaz" , "/foobar/../quxbaz" , quxbaz } ,
{ "escaped path traversal does not match any route" , "/foobar%2f%2e%2e%2fquxbaz" , main } ,
{ "double escaped path traversal does not match any route" , "/foobar%252f%252e%252e%252fquxbaz" , main } ,
}
2021-10-27 15:23:28 +05:30
runTestCases ( t , ts , testCases )
}
// This test can be removed when the environment variable `GEO_SECONDARY_PROXY` is removed
func TestGeoProxyFeatureDisabledOnGeoSecondarySite ( t * testing . T ) {
// We could just not set up the primary, but then we'd have to assert
// that the internal API call isn't made. This is easier.
remoteServer , rsDeferredClose := startRemoteServer ( "Geo primary" )
defer rsDeferredClose ( )
geoProxyEndpointResponseBody := fmt . Sprintf ( ` { "geo_proxy_url":"%v"} ` , remoteServer . URL )
2021-11-18 22:05:49 +05:30
railsServer , deferredClose := startRailsServer ( "Local Rails server" , & geoProxyEndpointResponseBody )
2021-10-27 15:23:28 +05:30
defer deferredClose ( )
2021-11-18 22:05:49 +05:30
ws , wsDeferredClose , _ := startWorkhorseServer ( railsServer . URL , false )
2021-10-27 15:23:28 +05:30
defer wsDeferredClose ( )
testCases := [ ] testCase {
{ "jobs request is served locally" , "/api/v4/jobs/request" , "Local Rails server received request to path /api/v4/jobs/request" } ,
{ "health check is served locally" , "/-/health" , "Local Rails server received request to path /-/health" } ,
{ "unknown route is served locally" , "/anything" , "Local Rails server received request to path /anything" } ,
}
runTestCases ( t , ws , testCases )
}
func TestGeoProxyFeatureEnabledOnGeoSecondarySite ( t * testing . T ) {
testCases := [ ] testCase {
2021-11-18 22:05:49 +05:30
{ "push from secondary is forwarded" , "/-/push_from_secondary/foo/bar.git/info/refs" , "Geo primary received request to path /-/push_from_secondary/foo/bar.git/info/refs" } ,
{ "LFS files are served locally" , "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" , "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" } ,
2021-10-27 15:23:28 +05:30
{ "jobs request is forwarded" , "/api/v4/jobs/request" , "Geo primary received request to path /api/v4/jobs/request" } ,
{ "health check is served locally" , "/-/health" , "Local Rails server received request to path /-/health" } ,
{ "unknown route is forwarded" , "/anything" , "Geo primary received request to path /anything" } ,
}
2021-12-11 22:18:48 +05:30
runTestCasesWithGeoProxyEnabled ( t , testCases )
2021-10-27 15:23:28 +05:30
}
// This test can be removed when the environment variable `GEO_SECONDARY_PROXY` is removed
func TestGeoProxyFeatureDisabledOnNonGeoSecondarySite ( t * testing . T ) {
geoProxyEndpointResponseBody := "{}"
2021-11-18 22:05:49 +05:30
railsServer , deferredClose := startRailsServer ( "Local Rails server" , & geoProxyEndpointResponseBody )
2021-10-27 15:23:28 +05:30
defer deferredClose ( )
2021-11-18 22:05:49 +05:30
ws , wsDeferredClose , _ := startWorkhorseServer ( railsServer . URL , false )
2021-10-27 15:23:28 +05:30
defer wsDeferredClose ( )
testCases := [ ] testCase {
2021-11-18 22:05:49 +05:30
{ "LFS files are served locally" , "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" , "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" } ,
2021-10-27 15:23:28 +05:30
{ "jobs request is served locally" , "/api/v4/jobs/request" , "Local Rails server received request to path /api/v4/jobs/request" } ,
{ "health check is served locally" , "/-/health" , "Local Rails server received request to path /-/health" } ,
{ "unknown route is served locally" , "/anything" , "Local Rails server received request to path /anything" } ,
}
runTestCases ( t , ws , testCases )
}
func TestGeoProxyFeatureEnabledOnNonGeoSecondarySite ( t * testing . T ) {
geoProxyEndpointResponseBody := "{}"
2021-11-18 22:05:49 +05:30
railsServer , deferredClose := startRailsServer ( "Local Rails server" , & geoProxyEndpointResponseBody )
2021-10-27 15:23:28 +05:30
defer deferredClose ( )
2021-11-18 22:05:49 +05:30
ws , wsDeferredClose , _ := startWorkhorseServer ( railsServer . URL , true )
2021-10-27 15:23:28 +05:30
defer wsDeferredClose ( )
testCases := [ ] testCase {
2021-11-18 22:05:49 +05:30
{ "LFS files are served locally" , "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" , "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" } ,
2021-10-27 15:23:28 +05:30
{ "jobs request is served locally" , "/api/v4/jobs/request" , "Local Rails server received request to path /api/v4/jobs/request" } ,
{ "health check is served locally" , "/-/health" , "Local Rails server received request to path /-/health" } ,
{ "unknown route is served locally" , "/anything" , "Local Rails server received request to path /anything" } ,
}
runTestCases ( t , ws , testCases )
}
func TestGeoProxyFeatureEnabledButWithAPIError ( t * testing . T ) {
geoProxyEndpointResponseBody := "Invalid response"
2021-11-18 22:05:49 +05:30
railsServer , deferredClose := startRailsServer ( "Local Rails server" , & geoProxyEndpointResponseBody )
2021-10-27 15:23:28 +05:30
defer deferredClose ( )
2021-11-18 22:05:49 +05:30
ws , wsDeferredClose , _ := startWorkhorseServer ( railsServer . URL , true )
2021-10-27 15:23:28 +05:30
defer wsDeferredClose ( )
testCases := [ ] testCase {
2021-11-18 22:05:49 +05:30
{ "LFS files are served locally" , "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" , "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" } ,
2021-10-27 15:23:28 +05:30
{ "jobs request is served locally" , "/api/v4/jobs/request" , "Local Rails server received request to path /api/v4/jobs/request" } ,
{ "health check is served locally" , "/-/health" , "Local Rails server received request to path /-/health" } ,
{ "unknown route is served locally" , "/anything" , "Local Rails server received request to path /anything" } ,
}
runTestCases ( t , ws , testCases )
}
2021-11-18 22:05:49 +05:30
func TestGeoProxyFeatureEnablingAndDisabling ( t * testing . T ) {
remoteServer , rsDeferredClose := startRemoteServer ( "Geo primary" )
defer rsDeferredClose ( )
geoProxyEndpointEnabledResponseBody := fmt . Sprintf ( ` { "geo_proxy_url":"%v"} ` , remoteServer . URL )
geoProxyEndpointDisabledResponseBody := "{}"
geoProxyEndpointResponseBody := geoProxyEndpointEnabledResponseBody
railsServer , deferredClose := startRailsServer ( "Local Rails server" , & geoProxyEndpointResponseBody )
defer deferredClose ( )
ws , wsDeferredClose , waitForNextApiPoll := startWorkhorseServer ( railsServer . URL , true )
defer wsDeferredClose ( )
testCasesLocal := [ ] testCase {
{ "LFS files are served locally" , "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" , "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" } ,
{ "jobs request is served locally" , "/api/v4/jobs/request" , "Local Rails server received request to path /api/v4/jobs/request" } ,
{ "health check is served locally" , "/-/health" , "Local Rails server received request to path /-/health" } ,
{ "unknown route is served locally" , "/anything" , "Local Rails server received request to path /anything" } ,
}
testCasesProxied := [ ] testCase {
{ "push from secondary is forwarded" , "/-/push_from_secondary/foo/bar.git/info/refs" , "Geo primary received request to path /-/push_from_secondary/foo/bar.git/info/refs" } ,
{ "LFS files are served locally" , "/group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" , "Local Rails server received request to path /group/project.git/gitlab-lfs/objects/37446575700829a11278ad3a550f244f45d5ae4fe1552778fa4f041f9eaeecf6" } ,
{ "jobs request is forwarded" , "/api/v4/jobs/request" , "Geo primary received request to path /api/v4/jobs/request" } ,
{ "health check is served locally" , "/-/health" , "Local Rails server received request to path /-/health" } ,
{ "unknown route is forwarded" , "/anything" , "Geo primary received request to path /anything" } ,
}
// Enabled initially, run tests
runTestCases ( t , ws , testCasesProxied )
// Disable proxying and run tests. It's safe to write to
// geoProxyEndpointResponseBody because the polling goroutine is blocked.
geoProxyEndpointResponseBody = geoProxyEndpointDisabledResponseBody
waitForNextApiPoll ( )
runTestCases ( t , ws , testCasesLocal )
// Re-enable proxying and run tests
geoProxyEndpointResponseBody = geoProxyEndpointEnabledResponseBody
waitForNextApiPoll ( )
runTestCases ( t , ws , testCasesProxied )
}
2021-10-27 15:23:28 +05:30
func runTestCases ( t * testing . T , ws * httptest . Server , testCases [ ] testCase ) {
t . Helper ( )
2021-03-05 16:19:46 +05:30
for _ , tc := range testCases {
t . Run ( tc . desc , func ( t * testing . T ) {
2021-10-27 15:23:28 +05:30
resp , err := http . Get ( ws . URL + tc . path )
2021-03-05 16:19:46 +05:30
require . NoError ( t , err )
defer resp . Body . Close ( )
body , err := ioutil . ReadAll ( resp . Body )
require . NoError ( t , err )
require . Equal ( t , 200 , resp . StatusCode , "response code" )
2021-10-27 15:23:28 +05:30
require . Equal ( t , tc . expectedResponse , string ( body ) )
2021-03-05 16:19:46 +05:30
} )
}
}
2021-10-27 15:23:28 +05:30
2021-12-11 22:18:48 +05:30
func runTestCasesWithGeoProxyEnabled ( t * testing . T , testCases [ ] testCase ) {
remoteServer , rsDeferredClose := startRemoteServer ( "Geo primary" )
defer rsDeferredClose ( )
geoProxyEndpointResponseBody := fmt . Sprintf ( ` { "geo_proxy_url":"%v"} ` , remoteServer . URL )
railsServer , deferredClose := startRailsServer ( "Local Rails server" , & geoProxyEndpointResponseBody )
defer deferredClose ( )
ws , wsDeferredClose , _ := startWorkhorseServer ( railsServer . URL , true )
defer wsDeferredClose ( )
runTestCases ( t , ws , testCases )
}
2021-10-27 15:23:28 +05:30
func newUpstreamConfig ( authBackend string ) * config . Config {
return & config . Config {
Version : "123" ,
DocumentRoot : testDocumentRoot ,
Backend : helper . URLMustParse ( authBackend ) ,
ImageResizerConfig : config . DefaultImageResizerConfig ,
}
}
func startRemoteServer ( serverName string ) ( * httptest . Server , func ( ) ) {
ts := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
body := serverName + " received request to path " + r . URL . Path
w . WriteHeader ( 200 )
fmt . Fprint ( w , body )
} ) )
return ts , ts . Close
}
2021-11-18 22:05:49 +05:30
func startRailsServer ( railsServerName string , geoProxyEndpointResponseBody * string ) ( * httptest . Server , func ( ) ) {
2021-10-27 15:23:28 +05:30
ts := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
var body string
if r . URL . Path == geoProxyEndpoint {
w . Header ( ) . Set ( "Content-Type" , "application/vnd.gitlab-workhorse+json" )
2021-11-18 22:05:49 +05:30
body = * geoProxyEndpointResponseBody
2021-10-27 15:23:28 +05:30
} else {
body = railsServerName + " received request to path " + r . URL . Path
}
w . WriteHeader ( 200 )
fmt . Fprint ( w , body )
} ) )
return ts , ts . Close
}
2021-11-18 22:05:49 +05:30
func startWorkhorseServer ( railsServerURL string , enableGeoProxyFeature bool ) ( * httptest . Server , func ( ) , func ( ) ) {
geoProxySleepC := make ( chan struct { } )
geoProxySleep := func ( time . Duration ) {
geoProxySleepC <- struct { } { }
<- geoProxySleepC
}
2021-10-27 15:23:28 +05:30
myConfigureRoutes := func ( u * upstream ) {
// Enable environment variable "feature flag"
u . enableGeoProxyFeature = enableGeoProxyFeature
2021-11-18 22:05:49 +05:30
// Replace the time.Sleep function with geoProxySleep
u . geoProxyPollSleep = geoProxySleep
2021-10-27 15:23:28 +05:30
// call original
configureRoutes ( u )
}
cfg := newUpstreamConfig ( railsServerURL )
upstreamHandler := newUpstream ( * cfg , logrus . StandardLogger ( ) , myConfigureRoutes )
2021-12-11 22:18:48 +05:30
// Secret should be configured before the first Geo API poll happens on server start
// to prevent race conditions where the first API call happens without a secret path
2021-10-27 15:23:28 +05:30
testhelper . ConfigureSecret ( )
2021-12-11 22:18:48 +05:30
ws := httptest . NewServer ( upstreamHandler )
2021-11-18 22:05:49 +05:30
waitForNextApiPoll := func ( ) { }
2021-10-27 15:23:28 +05:30
if enableGeoProxyFeature {
2021-11-18 22:05:49 +05:30
// Wait for geoProxySleep to be entered for the first time
<- geoProxySleepC
waitForNextApiPoll = func ( ) {
// Cause geoProxySleep to return
geoProxySleepC <- struct { } { }
// Wait for geoProxySleep to be entered again
<- geoProxySleepC
}
2021-10-27 15:23:28 +05:30
}
2021-11-18 22:05:49 +05:30
return ws , ws . Close , waitForNextApiPoll
2021-10-27 15:23:28 +05:30
}