From 14d9ca6d54e52319fc1d5364c2dd551da20d9a73 Mon Sep 17 00:00:00 2001 From: Dan Carley Date: Wed, 13 Nov 2013 18:06:11 +0000 Subject: [PATCH] Filter HTTP methods on output Add a new CLI flag to specify which HTTP methods should be replayed by HTTPOutput. If specified, any requests not matching will be dropped. This can be useful for replaying requests against stateful environments. e.g. You may not want to reproduce POST requests against an application if it results in additional calls to the outside world. Some variations I considered: - Filtering on input instead of output. However we don't currently do any request parsing on input, and doing so would likely have an impact on performance. - A request filtering plugin. We might want to revisit this if we add anymore options to HTTPOutput. It could also be used to modify requests in an existing file capture. Although because all plugins expect byte slices we'd potentially have to parse requests more than once. I don't think this warrants a separate test for HTTPOutput yet. But, again, if we add anymore then we should split them out, and DRY up if possible. Because we don't want to overload the scenarios covered by that one test. --- README.md | 10 ++++++++++ output_http.go | 8 +++++++- output_http_test.go | 8 +++++++- plugins.go | 2 +- settings.go | 2 ++ settings_methods.go | 26 ++++++++++++++++++++++++++ settings_methods_test.go | 24 ++++++++++++++++++++++++ test_input.go | 4 ++++ 8 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 settings_methods.go create mode 100644 settings_methods_test.go diff --git a/README.md b/README.md index fd45f20..dd7b819 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,16 @@ gor --input-raw :80 --output-http "http://staging.server" \ --output-http-header "Enable-Feature-X: true" ``` +## Filtering HTTP methods + +Requests not matching a specified whitelist can be filtered out. For example to strip non-nullipotent requests: + +``` +gor --input-raw :80 --output-http "http://staging.server" \ + --output-http-method GET \ + --output-http-method OPTIONS +``` + ### Basic Auth If your development or staging environment is protected by Basic Authentication then those credentials can be injected in during the replay: diff --git a/output_http.go b/output_http.go index 1b6b6b3..f2c765f 100644 --- a/output_http.go +++ b/output_http.go @@ -42,11 +42,12 @@ type HTTPOutput struct { limit int headers HTTPHeaders + methods HTTPMethods elasticSearch *es.ESPlugin } -func NewHTTPOutput(options string, headers HTTPHeaders, elasticSearchAddr string) io.Writer { +func NewHTTPOutput(options string, headers HTTPHeaders, methods HTTPMethods, elasticSearchAddr string) io.Writer { o := new(HTTPOutput) optionsArr := strings.Split(options, "|") @@ -58,6 +59,7 @@ func NewHTTPOutput(options string, headers HTTPHeaders, elasticSearchAddr string o.address = address o.headers = headers + o.methods = methods if elasticSearchAddr != "" { o.elasticSearch = new(es.ESPlugin) @@ -92,6 +94,10 @@ func (o *HTTPOutput) sendRequest(data []byte) { return } + if len(o.methods) > 0 && !o.methods.Contains(request.Method) { + return + } + client := &http.Client{ CheckRedirect: customCheckRedirect, } diff --git a/output_http_test.go b/output_http_test.go index 1fdc43b..da57884 100644 --- a/output_http_test.go +++ b/output_http_test.go @@ -22,13 +22,18 @@ func TestHTTPOutput(t *testing.T) { input := NewTestInput() headers := HTTPHeaders{HTTPHeader{"User-Agent", "Gor"}} - output := NewHTTPOutput("127.0.0.1:50003", headers, "") + methods := HTTPMethods{"GET", "PUT", "POST"} + output := NewHTTPOutput("127.0.0.1:50003", headers, methods, "") startHTTP("127.0.0.1:50003", func(req *http.Request) { if req.Header.Get("User-Agent") != "Gor" { t.Error("Wrong header") } + if req.Method == "OPTIONS" { + t.Error("Wrong method") + } + wg.Done() }) @@ -40,6 +45,7 @@ func TestHTTPOutput(t *testing.T) { for i := 0; i < 100; i++ { wg.Add(2) input.EmitPOST() + input.EmitOPTIONS() input.EmitGET() } diff --git a/plugins.go b/plugins.go index 4e7c117..e96e187 100644 --- a/plugins.go +++ b/plugins.go @@ -41,6 +41,6 @@ func InitPlugins() { } for _, options := range Settings.outputHTTP { - Plugins.Outputs = append(Plugins.Outputs, NewHTTPOutput(options, Settings.outputHTTPHeaders, Settings.outputHTTPElasticSearch)) + Plugins.Outputs = append(Plugins.Outputs, NewHTTPOutput(options, Settings.outputHTTPHeaders, Settings.outputHTTPMethods, Settings.outputHTTPElasticSearch)) } } diff --git a/settings.go b/settings.go index bd86c58..1e89e82 100644 --- a/settings.go +++ b/settings.go @@ -29,6 +29,7 @@ type AppSettings struct { outputHTTP MultiOption outputHTTPHeaders HTTPHeaders + outputHTTPMethods HTTPMethods outputHTTPElasticSearch string } @@ -60,6 +61,7 @@ func init() { flag.Var(&Settings.outputHTTP, "output-http", "Forwards incoming requests to given http address.\n\t# Redirect all incoming requests to staging.com address \n\tgor --input-raw :80 --output-http http://staging.com") flag.Var(&Settings.outputHTTPHeaders, "output-http-header", "Inject additional headers to http reqest:\n\tgor --input-raw :8080 --output-http staging.com --output-http-header 'User-Agent: Gor'") + flag.Var(&Settings.outputHTTPMethods, "output-http-method", "Whitelist of HTTP methods to replay. Anything else will be dropped:\n\tgor --input-raw :8080 --output-http staging.com --output-http-method GET --output-http-method OPTIONS") flag.StringVar(&Settings.outputHTTPElasticSearch, "output-http-elasticsearch", "", "Send request and response stats to ElasticSearch:\n\tgor --input-raw :8080 --output-http staging.com --output-http-elasticsearch 'es_host:api_port/index_name'") } diff --git a/settings_methods.go b/settings_methods.go new file mode 100644 index 0000000..82bb82c --- /dev/null +++ b/settings_methods.go @@ -0,0 +1,26 @@ +package gor + +import ( + "fmt" + "strings" +) + +type HTTPMethods []string + +func (h *HTTPMethods) String() string { + return fmt.Sprint(*h) +} + +func (h *HTTPMethods) Set(value string) error { + *h = append(*h, strings.ToUpper(value)) + return nil +} + +func (h *HTTPMethods) Contains(value string) bool { + for _, method := range *h { + if value == method { + return true + } + } + return false +} diff --git a/settings_methods_test.go b/settings_methods_test.go new file mode 100644 index 0000000..13563af --- /dev/null +++ b/settings_methods_test.go @@ -0,0 +1,24 @@ +package gor + +import ( + "testing" +) + +func TestHTTPMethods(t *testing.T) { + methods := HTTPMethods{} + + methods.Set("lower") + methods.Set("UPPER") + + if !methods.Contains("LOWER") { + t.Error("Does not contain LOWER") + } + + if !methods.Contains("UPPER") { + t.Error("Does not contain UPPER") + } + + if methods.Contains("ABSENT") { + t.Error("Does contain ABSENT") + } +} diff --git a/test_input.go b/test_input.go index d8c6cdb..c86b822 100644 --- a/test_input.go +++ b/test_input.go @@ -26,6 +26,10 @@ func (i *TestInput) EmitPOST() { i.data <- []byte("POST /pub/WWW/ HTTP/1.1\nHost: www.w3.org\r\n\r\na=1&b=2\r\n\r\n") } +func (i *TestInput) EmitOPTIONS() { + i.data <- []byte("OPTIONS / HTTP/1.1\nHost: www.w3.org\r\n\r\n") +} + func (i *TestInput) String() string { return "Test Input" }