Repository: StackStorm/st2

Issue:
Web Hook Rules check http headers in case sensitive manner
## SUMMARY

The case used for the header name in trigger.headers[<headername>] in a web-hook rule is treated in a case sensitive manner. HTTP headers are case insensitive so the case of the name in the headers should not e relevant.

### STACKSTORM VERSION

3.2.0

##### OS, environment, install method

Seen on one-line install and HA

## Steps to reproduce the problem

See https://github.com/StackStorm/st2/issues/4995 for initial case.
1. Configure webhookrule with trigger.headers['X-GitHub-Event']
2. Send in header via curl of X-GitHub-Event to webhook
3. Rule doesn't match
4. Change rule to be trigger.headers['X-Github-Event'] - rule matches

## Expected Results

As http headers are case insensitive then it should not matter what case is used in the rule. Therefore no matter what case header is or case of rule then they should match.

## Actual Results

Only matched when rule defined as X-Github-Event




Discussion:
@armab Issue raised as discussed. I can take a look at a PR fix for this.
Someone brought up this problem in slack today, so i started looking into it.

The hard part that i see is around backwards compatability. If someone has already written a rule with a criteria:

```yaml
trigger.headers['X-Github-Token']:
        type: "eq"
        pattern : "customvalue"
```

We need to provide this same key to them. The trigger instance itself is just a JSON blob and we used `jsonpath_rw` to render the key's expression into a value.

- https://github.com/StackStorm/st2/blob/master/st2reactor/st2reactor/rules/filter.py#L140
- https://github.com/StackStorm/st2/blob/master/st2reactor/st2reactor/rules/filter.py#L86
- https://github.com/StackStorm/st2/blob/master/st2reactor/st2reactor/rules/filter.py#L31
- https://github.com/StackStorm/st2/blob/master/st2common/st2common/util/payload.py#L34-L39
- https://github.com/StackStorm/st2/blob/master/st2common/st2common/util/payload.py#L17

**Idea 1**
One thought i had on how to fix or workaround this and still provide backwards compatability:

In the webhook trigger instances we could return another parameter that is the exact same `headers` dictionary but with all of the keys set to lowercase, maybe call it something like `headers_lower` and then your code would look like: 
```yaml
trigger.headers_lower['x-github-token']:
        type: "eq"
        pattern : "customvalue"
```

This extra field would need to be inserted here (maybe other places too, but this is the first one i found): 
- https://github.com/StackStorm/st2/blob/master/st2api/st2api/controllers/v1/webhooks.py#L161

**Idea 2**
My other thought would be to use some other expression language (Jinja, etc) to allow some more complex expressions so we can manipulate the header keys. Maybe providing a custom filter `dict_lower_keys` that takes a dictionary as input and returns a dictionary with all of the keys lowercased as output, example:

```yaml
"{{ (trigger.headers | dict_lower_keys)['x-github-token'] }}":
        type: "eq"
        pattern : "customvalue"
```

I think this would take a decent amount of work though in order to detect if the expression is Jinja or not and then render it properly. Might be useful in some other scenarios though?

We would need to make this change here:
- https://github.com/StackStorm/st2/blob/master/st2reactor/st2reactor/rules/filter.py#L140

Resolving patch:
diff --git a/st2api/st2api/controllers/v1/webhooks.py b/st2api/st2api/controllers/v1/webhooks.py
--- a/st2api/st2api/controllers/v1/webhooks.py
+++ b/st2api/st2api/controllers/v1/webhooks.py
@@ -172,6 +172,7 @@ def post(self, hook, webhook_body_api, headers, requester_user):
             payload = {}
 
             payload["headers"] = headers
+            payload["headers_lower"] = {k.lower(): v for k, v in headers.items()}
             payload["body"] = body
 
             # Dispatch trigger instance for each of the trigger found


Test patch:
diff --git a/st2api/tests/unit/controllers/v1/test_webhooks.py b/st2api/tests/unit/controllers/v1/test_webhooks.py
--- a/st2api/tests/unit/controllers/v1/test_webhooks.py
+++ b/st2api/tests/unit/controllers/v1/test_webhooks.py
@@ -388,6 +388,33 @@ def test_authentication_headers_should_be_removed(self, dispatch_mock):
         )
         self.assertNotIn("Cookie", dispatch_mock.call_args[1]["payload"]["headers"])
 
+    @mock.patch.object(
+        TriggerInstancePublisher, "publish_trigger", mock.MagicMock(return_value=True)
+    )
+    @mock.patch.object(
+        WebhooksController, "_is_valid_hook", mock.MagicMock(return_value=True)
+    )
+    @mock.patch.object(
+        HooksHolder,
+        "get_triggers_for_hook",
+        mock.MagicMock(return_value=[DUMMY_TRIGGER_DICT]),
+    )
+    @mock.patch("st2common.transport.reactor.TriggerDispatcher.dispatch")
+    def test_st2_webhook_lower_header(self, dispatch_mock):
+        data = WEBHOOK_1
+        post_resp = self.__do_post(
+            "git", data, headers={"X-Github-Token": "customvalue"}
+        )
+        self.assertEqual(post_resp.status_int, http_client.ACCEPTED)
+        self.assertEqual(
+            dispatch_mock.call_args[1]["payload"]["headers"]["X-Github-Token"],
+            "customvalue",
+        )
+        self.assertEqual(
+            dispatch_mock.call_args[1]["payload"]["headers_lower"]["x-github-token"],
+            "customvalue",
+        )
+
     def __do_post(self, hook, webhook, expect_errors=False, headers=None):
         return self.app.post_json(
             "/v1/webhooks/" + hook,