We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN 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



--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `st2api/st2api/controllers/v1/webhooks.py`
Content:
```
1 # Copyright 2020 The StackStorm Authors.
2 # Copyright 2019 Extreme Networks, Inc.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 import six
17 import uuid
18 from six.moves.urllib import parse as urlparse  # pylint: disable=import-error
19 from six.moves import http_client
20 
21 from st2common import log as logging
22 from st2common.constants.auth import (
23     HEADER_API_KEY_ATTRIBUTE_NAME,
24     HEADER_ATTRIBUTE_NAME,
25 )
26 from st2common.constants.triggers import WEBHOOK_TRIGGER_TYPES
27 from st2common.models.api.trace import TraceContext
28 from st2common.models.api.trigger import TriggerAPI
29 from st2common.models.db.webhook import WebhookDB
30 import st2common.services.triggers as trigger_service
31 from st2common.rbac.types import PermissionType
32 from st2common.rbac.backends import get_rbac_backend
33 from st2common.services.triggerwatcher import TriggerWatcher
34 from st2common.services.trigger_dispatcher import TriggerDispatcherService
35 from st2common.router import abort
36 from st2common.router import Response
37 from st2common.util.jsonify import get_json_type_for_python_value
38 
39 LOG = logging.getLogger(__name__)
40 
41 TRACE_TAG_HEADER = "St2-Trace-Tag"
42 
43 
44 class HooksHolder(object):
45     """
46     Maintains a hook to TriggerDB mapping.
47     """
48 
49     def __init__(self):
50         self._triggers_by_hook = {}
51 
52     def __contains__(self, key):
53         return key in self._triggers_by_hook
54 
55     def add_hook(self, hook, trigger):
56         if hook not in self._triggers_by_hook:
57             self._triggers_by_hook[hook] = []
58         self._triggers_by_hook[hook].append(trigger)
59 
60     def remove_hook(self, hook, trigger):
61         if hook not in self._triggers_by_hook:
62             return False
63         remove_index = -1
64         for idx, item in enumerate(self._triggers_by_hook[hook]):
65             if item["id"] == trigger["id"]:
66                 remove_index = idx
67                 break
68         if remove_index < 0:
69             return False
70         self._triggers_by_hook[hook].pop(remove_index)
71         if not self._triggers_by_hook[hook]:
72             del self._triggers_by_hook[hook]
73         return True
74 
75     def get_triggers_for_hook(self, hook):
76         return self._triggers_by_hook.get(hook, [])
77 
78     def get_all(self):
79         triggers = []
80         for values in six.itervalues(self._triggers_by_hook):
81             triggers.extend(values)
82         return triggers
83 
84 
85 class WebhooksController(object):
86     def __init__(self, *args, **kwargs):
87         self._hooks = HooksHolder()
88         self._base_url = "/webhooks/"
89         self._trigger_types = list(WEBHOOK_TRIGGER_TYPES.keys())
90 
91         self._trigger_dispatcher_service = TriggerDispatcherService(LOG)
92         queue_suffix = self.__class__.__name__
93         self._trigger_watcher = TriggerWatcher(
94             create_handler=self._handle_create_trigger,
95             update_handler=self._handle_update_trigger,
96             delete_handler=self._handle_delete_trigger,
97             trigger_types=self._trigger_types,
98             queue_suffix=queue_suffix,
99             exclusive=True,
100         )
101         self._trigger_watcher.start()
102         self._register_webhook_trigger_types()
103 
104     def get_all(self):
105         # Return only the hooks known by this controller.
106         return self._hooks.get_all()
107 
108     def get_one(self, url, requester_user):
109         triggers = self._hooks.get_triggers_for_hook(url)
110 
111         if not triggers:
112             abort(http_client.NOT_FOUND)
113             return
114 
115         permission_type = PermissionType.WEBHOOK_VIEW
116         rbac_utils = get_rbac_backend().get_utils_class()
117         rbac_utils.assert_user_has_resource_db_permission(
118             user_db=requester_user,
119             resource_db=WebhookDB(name=url),
120             permission_type=permission_type,
121         )
122 
123         # For demonstration purpose return 1st
124         return triggers[0]
125 
126     def post(self, hook, webhook_body_api, headers, requester_user):
127         body = webhook_body_api.data
128 
129         permission_type = PermissionType.WEBHOOK_SEND
130         rbac_utils = get_rbac_backend().get_utils_class()
131         rbac_utils.assert_user_has_resource_db_permission(
132             user_db=requester_user,
133             resource_db=WebhookDB(name=hook),
134             permission_type=permission_type,
135         )
136 
137         headers = self._get_headers_as_dict(headers)
138         headers = self._filter_authentication_headers(headers)
139 
140         # If webhook contains a trace-tag use that else create create a unique trace-tag.
141         trace_context = self._create_trace_context(
142             trace_tag=headers.pop(TRACE_TAG_HEADER, None), hook=hook
143         )
144 
145         if hook == "st2" or hook == "st2/":
146             # When using st2 or system webhook, body needs to always be a dict
147             if not isinstance(body, dict):
148                 type_string = get_json_type_for_python_value(body)
149                 msg = "Webhook body needs to be an object, got: %s" % (type_string)
150                 raise ValueError(msg)
151 
152             trigger = body.get("trigger", None)
153             payload = body.get("payload", None)
154 
155             if not trigger:
156                 msg = "Trigger not specified."
157                 return abort(http_client.BAD_REQUEST, msg)
158 
159             self._trigger_dispatcher_service.dispatch_with_context(
160                 trigger=trigger,
161                 payload=payload,
162                 trace_context=trace_context,
163                 throw_on_validation_error=True,
164             )
165         else:
166             if not self._is_valid_hook(hook):
167                 self._log_request("Invalid hook.", headers, body)
168                 msg = "Webhook %s not registered with st2" % hook
169                 return abort(http_client.NOT_FOUND, msg)
170 
171             triggers = self._hooks.get_triggers_for_hook(hook)
172             payload = {}
173 
174             payload["headers"] = headers
175             payload["body"] = body
176 
177             # Dispatch trigger instance for each of the trigger found
178             for trigger_dict in triggers:
179                 # TODO: Instead of dispatching the whole dict we should just
180                 # dispatch TriggerDB.ref or similar
181                 self._trigger_dispatcher_service.dispatch_with_context(
182                     trigger=trigger_dict,
183                     payload=payload,
184                     trace_context=trace_context,
185                     throw_on_validation_error=True,
186                 )
187 
188         # NOTE: For url encoded request bodies, values will be bytes instead of unicode and this
189         # doesn't work with orjson so we first need to "cast" all the values from bytes to unicode
190 
191         return Response(json=body, status=http_client.ACCEPTED)
192 
193     def _is_valid_hook(self, hook):
194         # TODO: Validate hook payload with payload_schema.
195         return hook in self._hooks
196 
197     def _register_webhook_trigger_types(self):
198         for trigger_type in WEBHOOK_TRIGGER_TYPES.values():
199             trigger_service.create_trigger_type_db(trigger_type)
200 
201     def _create_trace_context(self, trace_tag, hook):
202         # if no trace_tag then create a unique one
203         if not trace_tag:
204             trace_tag = "webhook-%s-%s" % (hook, uuid.uuid4().hex)
205         return TraceContext(trace_tag=trace_tag)
206 
207     def add_trigger(self, trigger):
208         # NOTE: trigger is a dictionary
209         # Note: Permission checking for creating and deleting a webhook is done during rule
210         # creation
211         url = self._get_normalized_url(trigger)
212         LOG.info("Listening to endpoint: %s", urlparse.urljoin(self._base_url, url))
213         self._hooks.add_hook(url, trigger)
214 
215     def update_trigger(self, trigger):
216         pass
217 
218     def remove_trigger(self, trigger):
219         # Note: Permission checking for creating and deleting a webhook is done during rule
220         # creation
221         url = self._get_normalized_url(trigger)
222 
223         removed = self._hooks.remove_hook(url, trigger)
224         if removed:
225             LOG.info(
226                 "Stop listening to endpoint: %s", urlparse.urljoin(self._base_url, url)
227             )
228 
229     def _get_normalized_url(self, trigger):
230         """
231         remove the trailing and leading / so that the hook url and those coming
232         from trigger parameters end up being the same.
233         """
234         return trigger["parameters"]["url"].strip("/")
235 
236     def _get_headers_as_dict(self, headers):
237         headers_dict = {}
238         for key, value in headers.items():
239             headers_dict[key] = value
240         return headers_dict
241 
242     def _filter_authentication_headers(self, headers):
243         auth_headers = [HEADER_API_KEY_ATTRIBUTE_NAME, HEADER_ATTRIBUTE_NAME, "Cookie"]
244         return {key: value for key, value in headers.items() if key not in auth_headers}
245 
246     def _log_request(self, msg, headers, body, log_method=LOG.debug):
247         headers = self._get_headers_as_dict(headers)
248         body = str(body)
249         log_method("%s\n\trequest.header: %s.\n\trequest.body: %s.", msg, headers, body)
250 
251     ##############################################
252     # Event handler methods for the trigger events
253     ##############################################
254 
255     def _handle_create_trigger(self, trigger):
256         LOG.debug('Calling "add_trigger" method (trigger.type=%s)' % (trigger.type))
257         trigger = self._sanitize_trigger(trigger=trigger)
258         self.add_trigger(trigger=trigger)
259 
260     def _handle_update_trigger(self, trigger):
261         LOG.debug('Calling "update_trigger" method (trigger.type=%s)' % (trigger.type))
262         trigger = self._sanitize_trigger(trigger=trigger)
263         self.update_trigger(trigger=trigger)
264 
265     def _handle_delete_trigger(self, trigger):
266         LOG.debug('Calling "remove_trigger" method (trigger.type=%s)' % (trigger.type))
267         trigger = self._sanitize_trigger(trigger=trigger)
268         self.remove_trigger(trigger=trigger)
269 
270     def _sanitize_trigger(self, trigger):
271         sanitized = TriggerAPI.from_model(trigger).to_dict()
272         return sanitized
273 
274 
275 webhooks_controller = WebhooksController()
276
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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 @@
             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

{"golden_diff": "diff --git a/st2api/st2api/controllers/v1/webhooks.py b/st2api/st2api/controllers/v1/webhooks.py\n--- a/st2api/st2api/controllers/v1/webhooks.py\n+++ b/st2api/st2api/controllers/v1/webhooks.py\n@@ -172,6 +172,7 @@\n             payload = {}\n \n             payload[\"headers\"] = headers\n+            payload[\"headers_lower\"] = {k.lower(): v for k, v in headers.items()}\n             payload[\"body\"] = body\n \n             # Dispatch trigger instance for each of the trigger found\n", "issue": "Web Hook Rules check http headers in case sensitive manner\n## SUMMARY\r\n\r\nThe 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.\r\n\r\n### STACKSTORM VERSION\r\n\r\n3.2.0\r\n\r\n##### OS, environment, install method\r\n\r\nSeen on one-line install and HA\r\n\r\n## Steps to reproduce the problem\r\n\r\nSee https://github.com/StackStorm/st2/issues/4995 for initial case.\r\n1. Configure webhookrule with trigger.headers['X-GitHub-Event']\r\n2. Send in header via curl of X-GitHub-Event to webhook\r\n3. Rule doesn't match\r\n4. Change rule to be trigger.headers['X-Github-Event'] - rule matches\r\n\r\n## Expected Results\r\n\r\nAs 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.\r\n\r\n## Actual Results\r\n\r\nOnly matched when rule defined as X-Github-Event\r\n\r\n\n", "before_files": [{"content": "# Copyright 2020 The StackStorm Authors.\n# Copyright 2019 Extreme Networks, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport six\nimport uuid\nfrom six.moves.urllib import parse as urlparse  # pylint: disable=import-error\nfrom six.moves import http_client\n\nfrom st2common import log as logging\nfrom st2common.constants.auth import (\n    HEADER_API_KEY_ATTRIBUTE_NAME,\n    HEADER_ATTRIBUTE_NAME,\n)\nfrom st2common.constants.triggers import WEBHOOK_TRIGGER_TYPES\nfrom st2common.models.api.trace import TraceContext\nfrom st2common.models.api.trigger import TriggerAPI\nfrom st2common.models.db.webhook import WebhookDB\nimport st2common.services.triggers as trigger_service\nfrom st2common.rbac.types import PermissionType\nfrom st2common.rbac.backends import get_rbac_backend\nfrom st2common.services.triggerwatcher import TriggerWatcher\nfrom st2common.services.trigger_dispatcher import TriggerDispatcherService\nfrom st2common.router import abort\nfrom st2common.router import Response\nfrom st2common.util.jsonify import get_json_type_for_python_value\n\nLOG = logging.getLogger(__name__)\n\nTRACE_TAG_HEADER = \"St2-Trace-Tag\"\n\n\nclass HooksHolder(object):\n    \"\"\"\n    Maintains a hook to TriggerDB mapping.\n    \"\"\"\n\n    def __init__(self):\n        self._triggers_by_hook = {}\n\n    def __contains__(self, key):\n        return key in self._triggers_by_hook\n\n    def add_hook(self, hook, trigger):\n        if hook not in self._triggers_by_hook:\n            self._triggers_by_hook[hook] = []\n        self._triggers_by_hook[hook].append(trigger)\n\n    def remove_hook(self, hook, trigger):\n        if hook not in self._triggers_by_hook:\n            return False\n        remove_index = -1\n        for idx, item in enumerate(self._triggers_by_hook[hook]):\n            if item[\"id\"] == trigger[\"id\"]:\n                remove_index = idx\n                break\n        if remove_index < 0:\n            return False\n        self._triggers_by_hook[hook].pop(remove_index)\n        if not self._triggers_by_hook[hook]:\n            del self._triggers_by_hook[hook]\n        return True\n\n    def get_triggers_for_hook(self, hook):\n        return self._triggers_by_hook.get(hook, [])\n\n    def get_all(self):\n        triggers = []\n        for values in six.itervalues(self._triggers_by_hook):\n            triggers.extend(values)\n        return triggers\n\n\nclass WebhooksController(object):\n    def __init__(self, *args, **kwargs):\n        self._hooks = HooksHolder()\n        self._base_url = \"/webhooks/\"\n        self._trigger_types = list(WEBHOOK_TRIGGER_TYPES.keys())\n\n        self._trigger_dispatcher_service = TriggerDispatcherService(LOG)\n        queue_suffix = self.__class__.__name__\n        self._trigger_watcher = TriggerWatcher(\n            create_handler=self._handle_create_trigger,\n            update_handler=self._handle_update_trigger,\n            delete_handler=self._handle_delete_trigger,\n            trigger_types=self._trigger_types,\n            queue_suffix=queue_suffix,\n            exclusive=True,\n        )\n        self._trigger_watcher.start()\n        self._register_webhook_trigger_types()\n\n    def get_all(self):\n        # Return only the hooks known by this controller.\n        return self._hooks.get_all()\n\n    def get_one(self, url, requester_user):\n        triggers = self._hooks.get_triggers_for_hook(url)\n\n        if not triggers:\n            abort(http_client.NOT_FOUND)\n            return\n\n        permission_type = PermissionType.WEBHOOK_VIEW\n        rbac_utils = get_rbac_backend().get_utils_class()\n        rbac_utils.assert_user_has_resource_db_permission(\n            user_db=requester_user,\n            resource_db=WebhookDB(name=url),\n            permission_type=permission_type,\n        )\n\n        # For demonstration purpose return 1st\n        return triggers[0]\n\n    def post(self, hook, webhook_body_api, headers, requester_user):\n        body = webhook_body_api.data\n\n        permission_type = PermissionType.WEBHOOK_SEND\n        rbac_utils = get_rbac_backend().get_utils_class()\n        rbac_utils.assert_user_has_resource_db_permission(\n            user_db=requester_user,\n            resource_db=WebhookDB(name=hook),\n            permission_type=permission_type,\n        )\n\n        headers = self._get_headers_as_dict(headers)\n        headers = self._filter_authentication_headers(headers)\n\n        # If webhook contains a trace-tag use that else create create a unique trace-tag.\n        trace_context = self._create_trace_context(\n            trace_tag=headers.pop(TRACE_TAG_HEADER, None), hook=hook\n        )\n\n        if hook == \"st2\" or hook == \"st2/\":\n            # When using st2 or system webhook, body needs to always be a dict\n            if not isinstance(body, dict):\n                type_string = get_json_type_for_python_value(body)\n                msg = \"Webhook body needs to be an object, got: %s\" % (type_string)\n                raise ValueError(msg)\n\n            trigger = body.get(\"trigger\", None)\n            payload = body.get(\"payload\", None)\n\n            if not trigger:\n                msg = \"Trigger not specified.\"\n                return abort(http_client.BAD_REQUEST, msg)\n\n            self._trigger_dispatcher_service.dispatch_with_context(\n                trigger=trigger,\n                payload=payload,\n                trace_context=trace_context,\n                throw_on_validation_error=True,\n            )\n        else:\n            if not self._is_valid_hook(hook):\n                self._log_request(\"Invalid hook.\", headers, body)\n                msg = \"Webhook %s not registered with st2\" % hook\n                return abort(http_client.NOT_FOUND, msg)\n\n            triggers = self._hooks.get_triggers_for_hook(hook)\n            payload = {}\n\n            payload[\"headers\"] = headers\n            payload[\"body\"] = body\n\n            # Dispatch trigger instance for each of the trigger found\n            for trigger_dict in triggers:\n                # TODO: Instead of dispatching the whole dict we should just\n                # dispatch TriggerDB.ref or similar\n                self._trigger_dispatcher_service.dispatch_with_context(\n                    trigger=trigger_dict,\n                    payload=payload,\n                    trace_context=trace_context,\n                    throw_on_validation_error=True,\n                )\n\n        # NOTE: For url encoded request bodies, values will be bytes instead of unicode and this\n        # doesn't work with orjson so we first need to \"cast\" all the values from bytes to unicode\n\n        return Response(json=body, status=http_client.ACCEPTED)\n\n    def _is_valid_hook(self, hook):\n        # TODO: Validate hook payload with payload_schema.\n        return hook in self._hooks\n\n    def _register_webhook_trigger_types(self):\n        for trigger_type in WEBHOOK_TRIGGER_TYPES.values():\n            trigger_service.create_trigger_type_db(trigger_type)\n\n    def _create_trace_context(self, trace_tag, hook):\n        # if no trace_tag then create a unique one\n        if not trace_tag:\n            trace_tag = \"webhook-%s-%s\" % (hook, uuid.uuid4().hex)\n        return TraceContext(trace_tag=trace_tag)\n\n    def add_trigger(self, trigger):\n        # NOTE: trigger is a dictionary\n        # Note: Permission checking for creating and deleting a webhook is done during rule\n        # creation\n        url = self._get_normalized_url(trigger)\n        LOG.info(\"Listening to endpoint: %s\", urlparse.urljoin(self._base_url, url))\n        self._hooks.add_hook(url, trigger)\n\n    def update_trigger(self, trigger):\n        pass\n\n    def remove_trigger(self, trigger):\n        # Note: Permission checking for creating and deleting a webhook is done during rule\n        # creation\n        url = self._get_normalized_url(trigger)\n\n        removed = self._hooks.remove_hook(url, trigger)\n        if removed:\n            LOG.info(\n                \"Stop listening to endpoint: %s\", urlparse.urljoin(self._base_url, url)\n            )\n\n    def _get_normalized_url(self, trigger):\n        \"\"\"\n        remove the trailing and leading / so that the hook url and those coming\n        from trigger parameters end up being the same.\n        \"\"\"\n        return trigger[\"parameters\"][\"url\"].strip(\"/\")\n\n    def _get_headers_as_dict(self, headers):\n        headers_dict = {}\n        for key, value in headers.items():\n            headers_dict[key] = value\n        return headers_dict\n\n    def _filter_authentication_headers(self, headers):\n        auth_headers = [HEADER_API_KEY_ATTRIBUTE_NAME, HEADER_ATTRIBUTE_NAME, \"Cookie\"]\n        return {key: value for key, value in headers.items() if key not in auth_headers}\n\n    def _log_request(self, msg, headers, body, log_method=LOG.debug):\n        headers = self._get_headers_as_dict(headers)\n        body = str(body)\n        log_method(\"%s\\n\\trequest.header: %s.\\n\\trequest.body: %s.\", msg, headers, body)\n\n    ##############################################\n    # Event handler methods for the trigger events\n    ##############################################\n\n    def _handle_create_trigger(self, trigger):\n        LOG.debug('Calling \"add_trigger\" method (trigger.type=%s)' % (trigger.type))\n        trigger = self._sanitize_trigger(trigger=trigger)\n        self.add_trigger(trigger=trigger)\n\n    def _handle_update_trigger(self, trigger):\n        LOG.debug('Calling \"update_trigger\" method (trigger.type=%s)' % (trigger.type))\n        trigger = self._sanitize_trigger(trigger=trigger)\n        self.update_trigger(trigger=trigger)\n\n    def _handle_delete_trigger(self, trigger):\n        LOG.debug('Calling \"remove_trigger\" method (trigger.type=%s)' % (trigger.type))\n        trigger = self._sanitize_trigger(trigger=trigger)\n        self.remove_trigger(trigger=trigger)\n\n    def _sanitize_trigger(self, trigger):\n        sanitized = TriggerAPI.from_model(trigger).to_dict()\n        return sanitized\n\n\nwebhooks_controller = WebhooksController()\n", "path": "st2api/st2api/controllers/v1/webhooks.py"}], "after_files": [{"content": "# Copyright 2020 The StackStorm Authors.\n# Copyright 2019 Extreme Networks, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport six\nimport uuid\nfrom six.moves.urllib import parse as urlparse  # pylint: disable=import-error\nfrom six.moves import http_client\n\nfrom st2common import log as logging\nfrom st2common.constants.auth import (\n    HEADER_API_KEY_ATTRIBUTE_NAME,\n    HEADER_ATTRIBUTE_NAME,\n)\nfrom st2common.constants.triggers import WEBHOOK_TRIGGER_TYPES\nfrom st2common.models.api.trace import TraceContext\nfrom st2common.models.api.trigger import TriggerAPI\nfrom st2common.models.db.webhook import WebhookDB\nimport st2common.services.triggers as trigger_service\nfrom st2common.rbac.types import PermissionType\nfrom st2common.rbac.backends import get_rbac_backend\nfrom st2common.services.triggerwatcher import TriggerWatcher\nfrom st2common.services.trigger_dispatcher import TriggerDispatcherService\nfrom st2common.router import abort\nfrom st2common.router import Response\nfrom st2common.util.jsonify import get_json_type_for_python_value\n\nLOG = logging.getLogger(__name__)\n\nTRACE_TAG_HEADER = \"St2-Trace-Tag\"\n\n\nclass HooksHolder(object):\n    \"\"\"\n    Maintains a hook to TriggerDB mapping.\n    \"\"\"\n\n    def __init__(self):\n        self._triggers_by_hook = {}\n\n    def __contains__(self, key):\n        return key in self._triggers_by_hook\n\n    def add_hook(self, hook, trigger):\n        if hook not in self._triggers_by_hook:\n            self._triggers_by_hook[hook] = []\n        self._triggers_by_hook[hook].append(trigger)\n\n    def remove_hook(self, hook, trigger):\n        if hook not in self._triggers_by_hook:\n            return False\n        remove_index = -1\n        for idx, item in enumerate(self._triggers_by_hook[hook]):\n            if item[\"id\"] == trigger[\"id\"]:\n                remove_index = idx\n                break\n        if remove_index < 0:\n            return False\n        self._triggers_by_hook[hook].pop(remove_index)\n        if not self._triggers_by_hook[hook]:\n            del self._triggers_by_hook[hook]\n        return True\n\n    def get_triggers_for_hook(self, hook):\n        return self._triggers_by_hook.get(hook, [])\n\n    def get_all(self):\n        triggers = []\n        for values in six.itervalues(self._triggers_by_hook):\n            triggers.extend(values)\n        return triggers\n\n\nclass WebhooksController(object):\n    def __init__(self, *args, **kwargs):\n        self._hooks = HooksHolder()\n        self._base_url = \"/webhooks/\"\n        self._trigger_types = list(WEBHOOK_TRIGGER_TYPES.keys())\n\n        self._trigger_dispatcher_service = TriggerDispatcherService(LOG)\n        queue_suffix = self.__class__.__name__\n        self._trigger_watcher = TriggerWatcher(\n            create_handler=self._handle_create_trigger,\n            update_handler=self._handle_update_trigger,\n            delete_handler=self._handle_delete_trigger,\n            trigger_types=self._trigger_types,\n            queue_suffix=queue_suffix,\n            exclusive=True,\n        )\n        self._trigger_watcher.start()\n        self._register_webhook_trigger_types()\n\n    def get_all(self):\n        # Return only the hooks known by this controller.\n        return self._hooks.get_all()\n\n    def get_one(self, url, requester_user):\n        triggers = self._hooks.get_triggers_for_hook(url)\n\n        if not triggers:\n            abort(http_client.NOT_FOUND)\n            return\n\n        permission_type = PermissionType.WEBHOOK_VIEW\n        rbac_utils = get_rbac_backend().get_utils_class()\n        rbac_utils.assert_user_has_resource_db_permission(\n            user_db=requester_user,\n            resource_db=WebhookDB(name=url),\n            permission_type=permission_type,\n        )\n\n        # For demonstration purpose return 1st\n        return triggers[0]\n\n    def post(self, hook, webhook_body_api, headers, requester_user):\n        body = webhook_body_api.data\n\n        permission_type = PermissionType.WEBHOOK_SEND\n        rbac_utils = get_rbac_backend().get_utils_class()\n        rbac_utils.assert_user_has_resource_db_permission(\n            user_db=requester_user,\n            resource_db=WebhookDB(name=hook),\n            permission_type=permission_type,\n        )\n\n        headers = self._get_headers_as_dict(headers)\n        headers = self._filter_authentication_headers(headers)\n\n        # If webhook contains a trace-tag use that else create create a unique trace-tag.\n        trace_context = self._create_trace_context(\n            trace_tag=headers.pop(TRACE_TAG_HEADER, None), hook=hook\n        )\n\n        if hook == \"st2\" or hook == \"st2/\":\n            # When using st2 or system webhook, body needs to always be a dict\n            if not isinstance(body, dict):\n                type_string = get_json_type_for_python_value(body)\n                msg = \"Webhook body needs to be an object, got: %s\" % (type_string)\n                raise ValueError(msg)\n\n            trigger = body.get(\"trigger\", None)\n            payload = body.get(\"payload\", None)\n\n            if not trigger:\n                msg = \"Trigger not specified.\"\n                return abort(http_client.BAD_REQUEST, msg)\n\n            self._trigger_dispatcher_service.dispatch_with_context(\n                trigger=trigger,\n                payload=payload,\n                trace_context=trace_context,\n                throw_on_validation_error=True,\n            )\n        else:\n            if not self._is_valid_hook(hook):\n                self._log_request(\"Invalid hook.\", headers, body)\n                msg = \"Webhook %s not registered with st2\" % hook\n                return abort(http_client.NOT_FOUND, msg)\n\n            triggers = self._hooks.get_triggers_for_hook(hook)\n            payload = {}\n\n            payload[\"headers\"] = headers\n            payload[\"headers_lower\"] = {k.lower(): v for k, v in headers.items()}\n            payload[\"body\"] = body\n\n            # Dispatch trigger instance for each of the trigger found\n            for trigger_dict in triggers:\n                # TODO: Instead of dispatching the whole dict we should just\n                # dispatch TriggerDB.ref or similar\n                self._trigger_dispatcher_service.dispatch_with_context(\n                    trigger=trigger_dict,\n                    payload=payload,\n                    trace_context=trace_context,\n                    throw_on_validation_error=True,\n                )\n\n        # NOTE: For url encoded request bodies, values will be bytes instead of unicode and this\n        # doesn't work with orjson so we first need to \"cast\" all the values from bytes to unicode\n\n        return Response(json=body, status=http_client.ACCEPTED)\n\n    def _is_valid_hook(self, hook):\n        # TODO: Validate hook payload with payload_schema.\n        return hook in self._hooks\n\n    def _register_webhook_trigger_types(self):\n        for trigger_type in WEBHOOK_TRIGGER_TYPES.values():\n            trigger_service.create_trigger_type_db(trigger_type)\n\n    def _create_trace_context(self, trace_tag, hook):\n        # if no trace_tag then create a unique one\n        if not trace_tag:\n            trace_tag = \"webhook-%s-%s\" % (hook, uuid.uuid4().hex)\n        return TraceContext(trace_tag=trace_tag)\n\n    def add_trigger(self, trigger):\n        # NOTE: trigger is a dictionary\n        # Note: Permission checking for creating and deleting a webhook is done during rule\n        # creation\n        url = self._get_normalized_url(trigger)\n        LOG.info(\"Listening to endpoint: %s\", urlparse.urljoin(self._base_url, url))\n        self._hooks.add_hook(url, trigger)\n\n    def update_trigger(self, trigger):\n        pass\n\n    def remove_trigger(self, trigger):\n        # Note: Permission checking for creating and deleting a webhook is done during rule\n        # creation\n        url = self._get_normalized_url(trigger)\n\n        removed = self._hooks.remove_hook(url, trigger)\n        if removed:\n            LOG.info(\n                \"Stop listening to endpoint: %s\", urlparse.urljoin(self._base_url, url)\n            )\n\n    def _get_normalized_url(self, trigger):\n        \"\"\"\n        remove the trailing and leading / so that the hook url and those coming\n        from trigger parameters end up being the same.\n        \"\"\"\n        return trigger[\"parameters\"][\"url\"].strip(\"/\")\n\n    def _get_headers_as_dict(self, headers):\n        headers_dict = {}\n        for key, value in headers.items():\n            headers_dict[key] = value\n        return headers_dict\n\n    def _filter_authentication_headers(self, headers):\n        auth_headers = [HEADER_API_KEY_ATTRIBUTE_NAME, HEADER_ATTRIBUTE_NAME, \"Cookie\"]\n        return {key: value for key, value in headers.items() if key not in auth_headers}\n\n    def _log_request(self, msg, headers, body, log_method=LOG.debug):\n        headers = self._get_headers_as_dict(headers)\n        body = str(body)\n        log_method(\"%s\\n\\trequest.header: %s.\\n\\trequest.body: %s.\", msg, headers, body)\n\n    ##############################################\n    # Event handler methods for the trigger events\n    ##############################################\n\n    def _handle_create_trigger(self, trigger):\n        LOG.debug('Calling \"add_trigger\" method (trigger.type=%s)' % (trigger.type))\n        trigger = self._sanitize_trigger(trigger=trigger)\n        self.add_trigger(trigger=trigger)\n\n    def _handle_update_trigger(self, trigger):\n        LOG.debug('Calling \"update_trigger\" method (trigger.type=%s)' % (trigger.type))\n        trigger = self._sanitize_trigger(trigger=trigger)\n        self.update_trigger(trigger=trigger)\n\n    def _handle_delete_trigger(self, trigger):\n        LOG.debug('Calling \"remove_trigger\" method (trigger.type=%s)' % (trigger.type))\n        trigger = self._sanitize_trigger(trigger=trigger)\n        self.remove_trigger(trigger=trigger)\n\n    def _sanitize_trigger(self, trigger):\n        sanitized = TriggerAPI.from_model(trigger).to_dict()\n        return sanitized\n\n\nwebhooks_controller = WebhooksController()\n", "path": "st2api/st2api/controllers/v1/webhooks.py"}]}