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"
"net/http"
"net/http/httptest"
2022-01-26 12:08:38 +05:30
"os"
2021-03-05 16:19:46 +05:30
"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"
2022-08-13 15:12:31 +05:30
apipkg "gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
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"
2022-08-13 15:12:31 +05:30
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upstream/roundtripper"
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
}
2022-01-26 12:08:38 +05:30
func TestMain ( m * testing . M ) {
// Secret should be configured before any Geo API poll happens to prevent
// race conditions where the first API call happens without a secret path
testhelper . ConfigureSecret ( )
os . Exit ( m . Run ( ) )
}
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 ) ,
}
2022-10-11 01:57:18 +05:30
} , nil )
2021-03-05 16:19:46 +05:30
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 )
}
2022-08-13 15:12:31 +05:30
func TestPollGeoProxyApiStopsWhenExplicitlyDisabled ( t * testing . T ) {
up := upstream {
enableGeoProxyFeature : false ,
geoProxyPollSleep : func ( time . Duration ) { } ,
geoPollerDone : make ( chan struct { } ) ,
}
go up . pollGeoProxyAPI ( )
select {
case <- up . geoPollerDone :
// happy
case <- time . After ( 10 * time . Second ) :
t . Fatal ( "timeout" )
}
}
func TestPollGeoProxyApiStopsWhenGeoNotEnabled ( t * testing . T ) {
remoteServer , rsDeferredClose := startRemoteServer ( "Geo primary" )
defer rsDeferredClose ( )
geoProxyEndpointResponseBody := ` { "geo_enabled":false} `
railsServer , deferredClose := startRailsServer ( "Local Rails server" , & geoProxyEndpointResponseBody )
defer deferredClose ( )
cfg := newUpstreamConfig ( railsServer . URL )
roundTripper := roundtripper . NewBackendRoundTripper ( cfg . Backend , "" , 1 * time . Minute , true )
remoteServerUrl := helper . URLMustParse ( remoteServer . URL )
up := upstream {
Config : * cfg ,
RoundTripper : roundTripper ,
APIClient : apipkg . NewAPI ( remoteServerUrl , "" , roundTripper ) ,
enableGeoProxyFeature : true ,
geoProxyPollSleep : func ( time . Duration ) { } ,
geoPollerDone : make ( chan struct { } ) ,
}
go up . pollGeoProxyAPI ( )
select {
case <- up . geoPollerDone :
// happy
case <- time . After ( 10 * time . Second ) :
t . Fatal ( "timeout" )
}
}
2021-10-27 15:23:28 +05:30
// 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 ( )
2022-08-13 15:12:31 +05:30
geoProxyEndpointResponseBody := fmt . Sprintf ( ` { "geo_enabled":true,"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 ) {
2022-08-13 15:12:31 +05:30
geoProxyEndpointResponseBody := ` { "geo_enabled":false} `
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 ) {
2022-08-13 15:12:31 +05:30
geoProxyEndpointResponseBody := ` { "geo_enabled":false} `
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 ( )
2022-08-13 15:12:31 +05:30
geoProxyEndpointEnabledResponseBody := fmt . Sprintf ( ` { "geo_enabled":true,"geo_proxy_url":"%v"} ` , remoteServer . URL )
geoProxyEndpointDisabledResponseBody := ` { "geo_enabled":true} `
2021-11-18 22:05:49 +05:30
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 )
}
2022-06-21 17:19:12 +05:30
func TestGeoProxyUpdatesExtraDataWhenChanged ( t * testing . T ) {
var expectedGeoProxyExtraData string
2022-03-02 08:16:31 +05:30
remoteServer := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
require . Equal ( t , "1" , r . Header . Get ( "Gitlab-Workhorse-Geo-Proxy" ) , "custom proxy header" )
2022-06-21 17:19:12 +05:30
require . Equal ( t , expectedGeoProxyExtraData , r . Header . Get ( "Gitlab-Workhorse-Geo-Proxy-Extra-Data" ) , "custom extra data header" )
2022-03-02 08:16:31 +05:30
w . WriteHeader ( http . StatusOK )
} ) )
defer remoteServer . Close ( )
2022-08-13 15:12:31 +05:30
geoProxyEndpointExtraData1 := fmt . Sprintf ( ` { "geo_enabled":true,"geo_proxy_url":"%v","geo_proxy_extra_data":"data1"} ` , remoteServer . URL )
geoProxyEndpointExtraData2 := fmt . Sprintf ( ` { "geo_enabled":true,"geo_proxy_url":"%v","geo_proxy_extra_data":"data2"} ` , remoteServer . URL )
geoProxyEndpointExtraData3 := fmt . Sprintf ( ` { "geo_enabled":true,"geo_proxy_url":"%v"} ` , remoteServer . URL )
2022-06-21 17:19:12 +05:30
geoProxyEndpointResponseBody := geoProxyEndpointExtraData1
expectedGeoProxyExtraData = "data1"
2022-03-02 08:16:31 +05:30
railsServer , deferredClose := startRailsServer ( "Local Rails server" , & geoProxyEndpointResponseBody )
defer deferredClose ( )
2022-06-21 17:19:12 +05:30
ws , wsDeferredClose , waitForNextApiPoll := startWorkhorseServer ( railsServer . URL , true )
2022-03-02 08:16:31 +05:30
defer wsDeferredClose ( )
http . Get ( ws . URL )
2022-06-21 17:19:12 +05:30
// Verify that the expected header changes after next updated poll.
geoProxyEndpointResponseBody = geoProxyEndpointExtraData2
expectedGeoProxyExtraData = "data2"
waitForNextApiPoll ( )
http . Get ( ws . URL )
// Validate that non-existing extra data results in empty header
geoProxyEndpointResponseBody = geoProxyEndpointExtraData3
expectedGeoProxyExtraData = ""
waitForNextApiPoll ( )
http . Get ( ws . URL )
}
func TestGeoProxySetsCustomHeader ( t * testing . T ) {
testCases := [ ] struct {
desc string
json string
extraData string
} {
2022-08-13 15:12:31 +05:30
{ "no extra data" , ` { "geo_enabled":true,"geo_proxy_url":"%v"} ` , "" } ,
{ "with extra data" , ` { "geo_enabled":true,"geo_proxy_url":"%v","geo_proxy_extra_data":"extra-geo-data"} ` , "extra-geo-data" } ,
2022-06-21 17:19:12 +05:30
}
for _ , tc := range testCases {
t . Run ( tc . desc , func ( t * testing . T ) {
remoteServer := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
require . Equal ( t , "1" , r . Header . Get ( "Gitlab-Workhorse-Geo-Proxy" ) , "custom proxy header" )
require . Equal ( t , tc . extraData , r . Header . Get ( "Gitlab-Workhorse-Geo-Proxy-Extra-Data" ) , "custom proxy extra data header" )
w . WriteHeader ( http . StatusOK )
} ) )
defer remoteServer . Close ( )
geoProxyEndpointResponseBody := fmt . Sprintf ( tc . json , remoteServer . URL )
railsServer , deferredClose := startRailsServer ( "Local Rails server" , & geoProxyEndpointResponseBody )
defer deferredClose ( )
ws , wsDeferredClose , _ := startWorkhorseServer ( railsServer . URL , true )
defer wsDeferredClose ( )
http . Get ( ws . URL )
} )
}
2022-03-02 08:16:31 +05:30
}
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 ( )
2022-07-23 23:45:48 +05:30
body , err := io . ReadAll ( resp . Body )
2021-03-05 16:19:46 +05:30
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 ( )
2022-08-13 15:12:31 +05:30
geoProxyEndpointResponseBody := fmt . Sprintf ( ` { "geo_enabled":true,"geo_proxy_url":"%v"} ` , remoteServer . URL )
2021-12-11 22:18:48 +05:30
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 )
2022-10-11 01:57:18 +05:30
upstreamHandler := newUpstream ( * cfg , logrus . StandardLogger ( ) , myConfigureRoutes , nil )
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
}