From 6ef8c05ead157edb048a19377fd560aed273ddd8 Mon Sep 17 00:00:00 2001 From: Max Leske Date: Thu, 18 Aug 2022 20:27:56 +0200 Subject: [PATCH] feat: set "Host" header from dest_addr override - "Host" header should be overridden with value of `dest_addr` override to ensure that requests will be accepted by targets. - Added test for the above change. - Converted `config.FTWConfig.TestOverride.Input` from `map[string]string` to `test.Input` for improved validation and better parsing Co-authored-by: Dan Kegel --- config/config.go | 11 ++++-- config/config_test.go | 13 ++++--- config/types.go | 13 ++++--- runner/run.go | 44 +++++++++++------------- runner/run_test.go | 79 +++++++++++++++++++++++++++++++++++++------ test/types.go | 25 +++++++------- 6 files changed, 122 insertions(+), 63 deletions(-) diff --git a/config/config.go b/config/config.go index 079c872..6fb1bbf 100644 --- a/config/config.go +++ b/config/config.go @@ -15,7 +15,7 @@ import ( // or uses `.ftw.yaml` as default file func NewConfigFromFile(cfgFile string) error { // kaonf merges by default but we never want to merge in this case - FTWConfig = nil + Reset() // Global koanf instance. Use "." as the key path delimiter. This can be "/" or any character. var k = koanf.New(".") @@ -47,7 +47,7 @@ func NewConfigFromFile(cfgFile string) error { // NewConfigFromEnv reads configuration information from environment variables that start with `FTW_` func NewConfigFromEnv() error { // kaonf merges by default but we never want to merge in this case - FTWConfig = nil + Reset() var err error var k = koanf.New(".") @@ -70,7 +70,7 @@ func NewConfigFromEnv() error { // NewConfigFromString initializes the configuration from a yaml formatted string. Useful for testing. func NewConfigFromString(conf string) error { // kaonf merges by default but we never want to merge in this case - FTWConfig = nil + Reset() var k = koanf.New(".") var err error @@ -87,6 +87,11 @@ func NewConfigFromString(conf string) error { return err } +// Reset configuration to uninitialized state +func Reset() { + FTWConfig = nil +} + func loadDefaults() { // Note: kaonf has a way to set defaults. However, kaonf's merge behavior // will overwrite defaults when the associated field is empty in nested diff --git a/config/config_test.go b/config/config_test.go index 3faf0d4..68fcac4 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -2,6 +2,7 @@ package config import ( "os" + "reflect" "strings" "testing" @@ -53,8 +54,8 @@ func TestNewConfigConfig(t *testing.T) { t.Errorf("Failed! Len must be > 0") } - if len(FTWConfig.TestOverride.Input) == 0 { - t.Errorf("Failed! Input Len must be > 0") + if reflect.ValueOf(FTWConfig.TestOverride.Input).IsZero() { + t.Errorf("Failed! Input must not be empty") } for id, text := range FTWConfig.TestOverride.Ignore { @@ -66,12 +67,10 @@ func TestNewConfigConfig(t *testing.T) { } } - for setting, value := range FTWConfig.TestOverride.Input { - if setting == "dest_addr" && value != "httpbin.org" { - t.Errorf("Looks like we are not overriding destination!") - } + overrides := FTWConfig.TestOverride.Input + if overrides.DestAddr != nil && *overrides.DestAddr != "httpbin.org" { + t.Errorf("Looks like we are not overriding destination!") } - } func TestNewConfigBadConfig(t *testing.T) { diff --git a/config/types.go b/config/types.go index ae42668..f932e41 100644 --- a/config/types.go +++ b/config/types.go @@ -1,5 +1,7 @@ package config +import "github.com/fzipi/go-ftw/test" + // RunMode represents the mode of the test run type RunMode string @@ -24,12 +26,13 @@ type FTWConfiguration struct { } // FTWTestOverride holds four lists: -// Global allows you to override global parameters in tests. An example usage is if you want to change the `dest_addr` of all tests to point to an external IP or host. -// Ignore is for tests you want to ignore. It will still execute the test, but ignore the results. You should add a comment on why you ignore the test -// ForcePass is for tests you want to pass unconditionally. Test will be executed, and pass even when the test fails. You should add a comment on why you force pass the test -// ForceFail is for tests you want to fail unconditionally. Test will be executed, and fail even when the test passes. You should add a comment on why you force fail the test +// +// Input allows you to override input parameters in tests. An example usage is if you want to change the `dest_addr` of all tests to point to an external IP or host. +// Ignore is for tests you want to ignore. It will still execute the test, but ignore the results. You should add a comment on why you ignore the test +// ForcePass is for tests you want to pass unconditionally. Test will be executed, and pass even when the test fails. You should add a comment on why you force pass the test +// ForceFail is for tests you want to fail unconditionally. Test will be executed, and fail even when the test passes. You should add a comment on why you force fail the test type FTWTestOverride struct { - Input map[string]string `koanf:"input"` + Input test.Input `koanf:"input"` Ignore map[string]string `koanf:"ignore"` ForcePass map[string]string `koanf:"forcepass"` ForceFail map[string]string `koanf:"forcefail"` diff --git a/runner/run.go b/runner/run.go index d6e660b..3d36a53 100644 --- a/runner/run.go +++ b/runner/run.go @@ -1,10 +1,8 @@ package runner import ( - "errors" "fmt" "regexp" - "strconv" "time" "github.com/fzipi/go-ftw/check" @@ -103,9 +101,9 @@ func RunStage(runContext *TestRunContext, ftwCheck *check.FTWCheck, testCase tes } // Do not even run test if result is overridden. Just use the override and display the overridden result. - if overriden := overridenTestResult(ftwCheck, testCase.TestTitle); overriden != Failed { - addResultToStats(overriden, testCase.TestTitle, &runContext.Stats) - displayResult(runContext.Output, overriden, time.Duration(0), time.Duration(0)) + if overridden := overriddenTestResult(ftwCheck, testCase.TestTitle); overridden != Failed { + addResultToStats(overridden, testCase.TestTitle, &runContext.Stats) + displayResult(runContext.Output, overridden, time.Duration(0), time.Duration(0)) return } @@ -268,7 +266,7 @@ func displayResult(quiet bool, result TestResult, roundTripTime time.Duration, s } } -func overridenTestResult(c *check.FTWCheck, id string) TestResult { +func overriddenTestResult(c *check.FTWCheck, id string) TestResult { if c.ForcedIgnore(id) { return Ignored } @@ -358,28 +356,24 @@ func printUnlessQuietMode(quiet bool, format string, a ...interface{}) { // applyInputOverride will check if config had global overrides and write that into the test. func applyInputOverride(testRequest *test.Input) error { - var retErr error overrides := config.FTWConfig.TestOverride.Input - for s, v := range overrides { - value := v - switch s { - case "port": - port, err := strconv.Atoi(value) - if err != nil { - retErr = errors.New("ftw/run: error getting overriden port") - } - testRequest.Port = &port - case "dest_addr": - oDestAddr := &value - testRequest.DestAddr = oDestAddr - case "protocol": - oProtocol := &value - testRequest.Protocol = oProtocol - default: - retErr = fmt.Errorf("ftw/run: override of '%s' not implemented yet", s) + if overrides.Port != nil { + testRequest.Port = overrides.Port + } + if overrides.DestAddr != nil { + testRequest.DestAddr = overrides.DestAddr + if testRequest.Headers == nil { + testRequest.Headers = ftwhttp.Header{} + } + if testRequest.Headers.Get("Host") == "" { + testRequest.Headers.Set("Host", *overrides.DestAddr) } } - return retErr + if overrides.Protocol != nil { + testRequest.Protocol = overrides.Protocol + } + + return nil } func notRunningInCloudMode(c *check.FTWCheck) bool { diff --git a/runner/run_test.go b/runner/run_test.go index f3046d4..43eeb12 100644 --- a/runner/run_test.go +++ b/runner/run_test.go @@ -5,13 +5,14 @@ import ( "net/http" "net/http/httptest" "os" - "strconv" "testing" "github.com/fzipi/go-ftw/check" "github.com/fzipi/go-ftw/config" "github.com/fzipi/go-ftw/ftwhttp" "github.com/fzipi/go-ftw/test" + + "github.com/rs/zerolog/log" ) var yamlConfig = ` @@ -326,7 +327,10 @@ func setUpLogFileForTestServer(t *testing.T) (logFilePath string) { t.Error(err) } logFilePath = file.Name() - t.Cleanup(func() { os.Remove(logFilePath) }) + t.Cleanup(func() { + os.Remove(logFilePath) + log.Info().Msgf("Deleting temporary file '%s'", logFilePath) + }) } return logFilePath } @@ -376,18 +380,21 @@ func replaceDestinationInTest(ftwTest *test.FTWTest, d ftwhttp.Destination) { } func replaceDestinationInConfiguration(dest ftwhttp.Destination) { - input := config.FTWConfig.TestOverride.Input - for key, value := range input { - if key == "dest_addr" && value == "TEST_ADDR" { - input[key] = dest.DestAddr - } - if key == "port" && value == "-1" { - input[key] = strconv.FormatInt(int64(dest.Port), 10) - } + replaceableAddress := "TEST_ADDR" + replaceablePort := -1 + + input := &config.FTWConfig.TestOverride.Input + if input.DestAddr != nil && *input.DestAddr == replaceableAddress { + input.DestAddr = &dest.DestAddr + } + if input.Port != nil && *input.Port == replaceablePort { + input.Port = &dest.Port } } func TestRun(t *testing.T) { + t.Cleanup(config.Reset) + err := config.NewConfigFromString(yamlConfig) if err != nil { t.Errorf("Failed!") @@ -440,6 +447,8 @@ func TestRun(t *testing.T) { } func TestOverrideRun(t *testing.T) { + t.Cleanup(config.Reset) + // setup test webserver (not a waf) err := config.NewConfigFromString(yamlConfigOverride) if err != nil { @@ -471,6 +480,8 @@ func TestOverrideRun(t *testing.T) { } func TestBrokenOverrideRun(t *testing.T) { + t.Cleanup(config.Reset) + err := config.NewConfigFromString(yamlBrokenConfigOverride) if err != nil { t.Errorf("Failed!") @@ -502,6 +513,7 @@ func TestBrokenOverrideRun(t *testing.T) { } func TestBrokenPortOverrideRun(t *testing.T) { + t.Cleanup(config.Reset) // TestServer initialized first to retrieve the correct port number dest, logFilePath := newTestServer(t, logText) @@ -536,6 +548,8 @@ func TestBrokenPortOverrideRun(t *testing.T) { } func TestDisabledRun(t *testing.T) { + t.Cleanup(config.Reset) + err := config.NewConfigFromString(yamlConfig) if err != nil { t.Errorf("Failed!") @@ -560,6 +574,8 @@ func TestDisabledRun(t *testing.T) { } func TestLogsRun(t *testing.T) { + t.Cleanup(config.Reset) + // setup test webserver (not a waf) dest, logFilePath := newTestServer(t, logText) @@ -584,6 +600,8 @@ func TestLogsRun(t *testing.T) { } func TestCloudRun(t *testing.T) { + t.Cleanup(config.Reset) + err := config.NewConfigFromString(yamlCloudConfig) if err != nil { t.Errorf("Failed!") @@ -597,7 +615,7 @@ func TestCloudRun(t *testing.T) { t.Run("don't show time and execute all", func(t *testing.T) { for testCaseIndex, testCaseDummy := range ftwTestDummy.Tests { for stageIndex := range testCaseDummy.Stages { - // Read the tests for every stage so we an replace the destination + // Read the tests for every stage so we can replace the destination // in each run. The server needs to be configured for each stage // individually. ftwTest, err := test.GetTestFromYaml([]byte(yamlTestLogs)) @@ -645,6 +663,8 @@ func TestCloudRun(t *testing.T) { } func TestFailedTestsRun(t *testing.T) { + t.Cleanup(config.Reset) + err := config.NewConfigFromString(yamlConfig) dest, logFilePath := newTestServer(t, logText) if err != nil { @@ -665,3 +685,40 @@ func TestFailedTestsRun(t *testing.T) { } }) } + +func TestApplyInputOverrideSetHostFromDestAddr(t *testing.T) { + t.Cleanup(config.Reset) + + originalHost := "original.com" + overrideHost := "override.com" + testInput := test.Input{ + DestAddr: &originalHost, + } + config.FTWConfig = &config.FTWConfiguration{ + TestOverride: config.FTWTestOverride{ + Input: test.Input{ + DestAddr: &overrideHost, + }, + }, + } + + err := applyInputOverride(&testInput) + if err != nil { + t.Error("Failed to apply input overrides", err) + } + + if *testInput.DestAddr != overrideHost { + t.Error("`dest_addr` should have been overridden") + } + if testInput.Headers == nil { + t.Error("Header map must exist after overriding `dest_addr`") + } + + hostHeader := testInput.Headers.Get("Host") + if hostHeader == "" { + t.Error("Host header must be set after overriding `dest_addr`") + } + if hostHeader != overrideHost { + t.Error("Host header must be identical to `dest_addr` after overrding `dest_addr`") + } +} diff --git a/test/types.go b/test/types.go index d7644de..4ff02bd 100644 --- a/test/types.go +++ b/test/types.go @@ -5,18 +5,18 @@ import "github.com/fzipi/go-ftw/ftwhttp" // Input represents the input request in a stage // The fields `Version`, `Method` and `URI` we want to explicitly now when they are set to "" type Input struct { - DestAddr *string `yaml:"dest_addr,omitempty"` - Port *int `yaml:"port,omitempty"` - Protocol *string `yaml:"protocol,omitempty"` - URI *string `yaml:"uri,omitempty"` - Version *string `yaml:"version,omitempty"` - Headers ftwhttp.Header `yaml:"headers,omitempty"` - Method *string `yaml:"method,omitempty"` - Data *string `yaml:"data,omitempty"` - SaveCookie bool `yaml:"save_cookie,omitempty"` - StopMagic bool `yaml:"stop_magic"` - EncodedRequest string `yaml:"encoded_request,omitempty"` - RAWRequest string `yaml:"raw_request,omitempty"` + DestAddr *string `yaml:"dest_addr,omitempty" koanf:"dest_addr,omitempty"` + Port *int `yaml:"port,omitempty" koanf:"port,omitempty"` + Protocol *string `yaml:"protocol,omitempty" koanf:"protocol,omitempty"` + URI *string `yaml:"uri,omitempty" koanf:"uri,omitempty"` + Version *string `yaml:"version,omitempty" koanf:"version,omitempty"` + Headers ftwhttp.Header `yaml:"headers,omitempty" koanf:"headers,omitempty"` + Method *string `yaml:"method,omitempty" koanf:"method,omitempty"` + Data *string `yaml:"data,omitempty" koanf:"data,omitempty"` + SaveCookie bool `yaml:"save_cookie,omitempty" koanf:"save_cookie,omitempty"` + StopMagic bool `yaml:"stop_magic" koanf:"stop_magic,omitempty"` + EncodedRequest string `yaml:"encoded_request,omitempty" koanf:"encoded_request,omitempty"` + RAWRequest string `yaml:"raw_request,omitempty" koanf:"raw_request,omitempty"` } // Output is the response expected from the test @@ -28,6 +28,7 @@ type Output struct { ExpectError bool `yaml:"expect_error,omitempty"` } +// Stage is an individual test stage type Stage struct { Input Input `yaml:"input"` Output Output `yaml:"output"`