We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
API returns 400 for some requests due to "bad" Content-type header
It seems that _some_ API endpoints will return a HTTP 400 when presented with a header such as:

```
Content-type: application/json; charset=utf-8
```

but works fine with:

```
Content-type: application/json
```

I ran into this with a POST to the URL `/webhooks/st2`. I'm currently running version `1.4dev-52`


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `st2common/st2common/util/http.py`
Content:
```
1 # Licensed to the StackStorm, Inc ('StackStorm') under one or more
2 # contributor license agreements.  See the NOTICE file distributed with
3 # this work for additional information regarding copyright ownership.
4 # The ASF licenses this file to You under the Apache License, Version 2.0
5 # (the "License"); you may not use this file except in compliance with
6 # the License.  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 
18 http_client = six.moves.http_client
19 
20 HTTP_SUCCESS = [http_client.OK, http_client.CREATED, http_client.ACCEPTED,
21                 http_client.NON_AUTHORITATIVE_INFORMATION, http_client.NO_CONTENT,
22                 http_client.RESET_CONTENT, http_client.PARTIAL_CONTENT,
23                 http_client.MULTI_STATUS, http_client.IM_USED,
24                 ]
25
```

Path: `st2api/st2api/controllers/v1/webhooks.py`
Content:
```
1 # Licensed to the StackStorm, Inc ('StackStorm') under one or more
2 # contributor license agreements.  See the NOTICE file distributed with
3 # this work for additional information regarding copyright ownership.
4 # The ASF licenses this file to You under the Apache License, Version 2.0
5 # (the "License"); you may not use this file except in compliance with
6 # the License.  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 try:
17     import simplejson as json
18 except ImportError:
19     import json
20 
21 import six
22 import pecan
23 import uuid
24 from pecan import abort
25 from pecan.rest import RestController
26 from six.moves.urllib import parse as urlparse
27 urljoin = urlparse.urljoin
28 
29 from st2common import log as logging
30 from st2common.constants.triggers import WEBHOOK_TRIGGER_TYPES
31 from st2common.models.api.base import jsexpose
32 from st2common.models.api.trace import TraceContext
33 import st2common.services.triggers as trigger_service
34 from st2common.services.triggerwatcher import TriggerWatcher
35 from st2common.transport.reactor import TriggerDispatcher
36 from st2common.rbac.types import PermissionType
37 from st2common.rbac.decorators import request_user_has_webhook_permission
38 
39 http_client = six.moves.http_client
40 
41 LOG = logging.getLogger(__name__)
42 
43 TRACE_TAG_HEADER = 'St2-Trace-Tag'
44 
45 
46 class WebhooksController(RestController):
47     def __init__(self, *args, **kwargs):
48         super(WebhooksController, self).__init__(*args, **kwargs)
49         self._hooks = {}
50         self._base_url = '/webhooks/'
51         self._trigger_types = WEBHOOK_TRIGGER_TYPES.keys()
52 
53         self._trigger_dispatcher = TriggerDispatcher(LOG)
54         queue_suffix = self.__class__.__name__
55         self._trigger_watcher = TriggerWatcher(create_handler=self._handle_create_trigger,
56                                                update_handler=self._handle_update_trigger,
57                                                delete_handler=self._handle_delete_trigger,
58                                                trigger_types=self._trigger_types,
59                                                queue_suffix=queue_suffix,
60                                                exclusive=True)
61         self._trigger_watcher.start()
62         self._register_webhook_trigger_types()
63 
64     @jsexpose()
65     def get_all(self):
66         # Return only the hooks known by this controller.
67         return [trigger for trigger in six.itervalues(self._hooks)]
68 
69     @jsexpose()
70     def get_one(self, name):
71         hook = self._hooks.get(name, None)
72 
73         if not hook:
74             abort(http_client.NOT_FOUND)
75             return
76 
77         return hook
78 
79     @request_user_has_webhook_permission(permission_type=PermissionType.WEBHOOK_SEND)
80     @jsexpose(arg_types=[str], status_code=http_client.ACCEPTED)
81     def post(self, *args, **kwargs):
82         hook = '/'.join(args)  # TODO: There must be a better way to do this.
83 
84         # Note: For backward compatibility reasons we default to application/json if content
85         # type is not explicitly provided
86         content_type = pecan.request.headers.get('Content-Type', 'application/json')
87         body = pecan.request.body
88 
89         try:
90             body = self._parse_request_body(content_type=content_type, body=body)
91         except Exception as e:
92             self._log_request('Failed to parse request body: %s.' % (str(e)), pecan.request)
93             msg = 'Failed to parse request body "%s": %s' % (body, str(e))
94             return pecan.abort(http_client.BAD_REQUEST, msg)
95 
96         headers = self._get_headers_as_dict(pecan.request.headers)
97         # If webhook contains a trace-tag use that else create create a unique trace-tag.
98         trace_context = self._create_trace_context(trace_tag=headers.pop(TRACE_TAG_HEADER, None),
99                                                    hook=hook)
100 
101         if hook == 'st2' or hook == 'st2/':
102             return self._handle_st2_webhook(body, trace_context=trace_context)
103 
104         if not self._is_valid_hook(hook):
105             self._log_request('Invalid hook.', pecan.request)
106             msg = 'Webhook %s not registered with st2' % hook
107             return pecan.abort(http_client.NOT_FOUND, msg)
108 
109         trigger = self._get_trigger_for_hook(hook)
110         payload = {}
111 
112         payload['headers'] = headers
113         payload['body'] = body
114         self._trigger_dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)
115 
116         return body
117 
118     def _parse_request_body(self, content_type, body):
119         if content_type == 'application/json':
120             self._log_request('Parsing request body as JSON', request=pecan.request)
121             body = json.loads(body)
122         elif content_type in ['application/x-www-form-urlencoded', 'multipart/form-data']:
123             self._log_request('Parsing request body as form encoded data', request=pecan.request)
124             body = urlparse.parse_qs(body)
125         else:
126             raise ValueError('Unsupported Content-Type: "%s"' % (content_type))
127 
128         return body
129 
130     def _handle_st2_webhook(self, body, trace_context):
131         trigger = body.get('trigger', None)
132         payload = body.get('payload', None)
133         if not trigger:
134             msg = 'Trigger not specified.'
135             return pecan.abort(http_client.BAD_REQUEST, msg)
136         self._trigger_dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)
137 
138         return body
139 
140     def _is_valid_hook(self, hook):
141         # TODO: Validate hook payload with payload_schema.
142         return hook in self._hooks
143 
144     def _get_trigger_for_hook(self, hook):
145         return self._hooks[hook]
146 
147     def _register_webhook_trigger_types(self):
148         for trigger_type in WEBHOOK_TRIGGER_TYPES.values():
149             trigger_service.create_trigger_type_db(trigger_type)
150 
151     def _create_trace_context(self, trace_tag, hook):
152         # if no trace_tag then create a unique one
153         if not trace_tag:
154             trace_tag = 'webhook-%s-%s' % (hook, uuid.uuid4().hex)
155         return TraceContext(trace_tag=trace_tag)
156 
157     def add_trigger(self, trigger):
158         # Note: Permission checking for creating and deleting a webhook is done during rule
159         # creation
160         url = trigger['parameters']['url']
161         LOG.info('Listening to endpoint: %s', urljoin(self._base_url, url))
162         self._hooks[url] = trigger
163 
164     def update_trigger(self, trigger):
165         pass
166 
167     def remove_trigger(self, trigger):
168         # Note: Permission checking for creating and deleting a webhook is done during rule
169         # creation
170         url = trigger['parameters']['url']
171 
172         if url in self._hooks:
173             LOG.info('Stop listening to endpoint: %s', urljoin(self._base_url, url))
174             del self._hooks[url]
175 
176     def _get_headers_as_dict(self, headers):
177         headers_dict = {}
178         for key, value in headers.items():
179             headers_dict[key] = value
180         return headers_dict
181 
182     def _log_request(self, msg, request, log_method=LOG.debug):
183         headers = self._get_headers_as_dict(request.headers)
184         body = str(request.body)
185         log_method('%s\n\trequest.header: %s.\n\trequest.body: %s.', msg, headers, body)
186 
187     ##############################################
188     # Event handler methods for the trigger events
189     ##############################################
190 
191     def _handle_create_trigger(self, trigger):
192         LOG.debug('Calling "add_trigger" method (trigger.type=%s)' % (trigger.type))
193         trigger = self._sanitize_trigger(trigger=trigger)
194         self.add_trigger(trigger=trigger)
195 
196     def _handle_update_trigger(self, trigger):
197         LOG.debug('Calling "update_trigger" method (trigger.type=%s)' % (trigger.type))
198         trigger = self._sanitize_trigger(trigger=trigger)
199         self.update_trigger(trigger=trigger)
200 
201     def _handle_delete_trigger(self, trigger):
202         LOG.debug('Calling "remove_trigger" method (trigger.type=%s)' % (trigger.type))
203         trigger = self._sanitize_trigger(trigger=trigger)
204         self.remove_trigger(trigger=trigger)
205 
206     def _sanitize_trigger(self, trigger):
207         sanitized = trigger._data
208         if 'id' in sanitized:
209             # Friendly objectid rather than the MongoEngine representation.
210             sanitized['id'] = str(sanitized['id'])
211         return sanitized
212
```
--- 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
@@ -33,6 +33,7 @@
 import st2common.services.triggers as trigger_service
 from st2common.services.triggerwatcher import TriggerWatcher
 from st2common.transport.reactor import TriggerDispatcher
+from st2common.util.http import parse_content_type_header
 from st2common.rbac.types import PermissionType
 from st2common.rbac.decorators import request_user_has_webhook_permission
 
@@ -84,6 +85,7 @@
         # Note: For backward compatibility reasons we default to application/json if content
         # type is not explicitly provided
         content_type = pecan.request.headers.get('Content-Type', 'application/json')
+        content_type = parse_content_type_header(content_type=content_type)[0]
         body = pecan.request.body
 
         try:
diff --git a/st2common/st2common/util/http.py b/st2common/st2common/util/http.py
--- a/st2common/st2common/util/http.py
+++ b/st2common/st2common/util/http.py
@@ -17,8 +17,43 @@
 
 http_client = six.moves.http_client
 
+__all__ = [
+    'HTTP_SUCCESS',
+    'parse_content_type_header'
+]
+
 HTTP_SUCCESS = [http_client.OK, http_client.CREATED, http_client.ACCEPTED,
                 http_client.NON_AUTHORITATIVE_INFORMATION, http_client.NO_CONTENT,
                 http_client.RESET_CONTENT, http_client.PARTIAL_CONTENT,
                 http_client.MULTI_STATUS, http_client.IM_USED,
                 ]
+
+
+def parse_content_type_header(content_type):
+    """
+    Parse and normalize request content type and return a tuple with the content type and the
+    options.
+
+    :rype: ``tuple``
+    """
+    if ';' in content_type:
+        split = content_type.split(';')
+        media = split[0]
+        options = {}
+
+        for pair in split[1:]:
+            split_pair = pair.split('=', 1)
+
+            if len(split_pair) != 2:
+                continue
+
+            key = split_pair[0].strip()
+            value = split_pair[1].strip()
+
+            options[key] = value
+    else:
+        media = content_type
+        options = {}
+
+    result = (media, options)
+    return result

{"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@@ -33,6 +33,7 @@\n import st2common.services.triggers as trigger_service\n from st2common.services.triggerwatcher import TriggerWatcher\n from st2common.transport.reactor import TriggerDispatcher\n+from st2common.util.http import parse_content_type_header\n from st2common.rbac.types import PermissionType\n from st2common.rbac.decorators import request_user_has_webhook_permission\n \n@@ -84,6 +85,7 @@\n         # Note: For backward compatibility reasons we default to application/json if content\n         # type is not explicitly provided\n         content_type = pecan.request.headers.get('Content-Type', 'application/json')\n+        content_type = parse_content_type_header(content_type=content_type)[0]\n         body = pecan.request.body\n \n         try:\ndiff --git a/st2common/st2common/util/http.py b/st2common/st2common/util/http.py\n--- a/st2common/st2common/util/http.py\n+++ b/st2common/st2common/util/http.py\n@@ -17,8 +17,43 @@\n \n http_client = six.moves.http_client\n \n+__all__ = [\n+    'HTTP_SUCCESS',\n+    'parse_content_type_header'\n+]\n+\n HTTP_SUCCESS = [http_client.OK, http_client.CREATED, http_client.ACCEPTED,\n                 http_client.NON_AUTHORITATIVE_INFORMATION, http_client.NO_CONTENT,\n                 http_client.RESET_CONTENT, http_client.PARTIAL_CONTENT,\n                 http_client.MULTI_STATUS, http_client.IM_USED,\n                 ]\n+\n+\n+def parse_content_type_header(content_type):\n+    \"\"\"\n+    Parse and normalize request content type and return a tuple with the content type and the\n+    options.\n+\n+    :rype: ``tuple``\n+    \"\"\"\n+    if ';' in content_type:\n+        split = content_type.split(';')\n+        media = split[0]\n+        options = {}\n+\n+        for pair in split[1:]:\n+            split_pair = pair.split('=', 1)\n+\n+            if len(split_pair) != 2:\n+                continue\n+\n+            key = split_pair[0].strip()\n+            value = split_pair[1].strip()\n+\n+            options[key] = value\n+    else:\n+        media = content_type\n+        options = {}\n+\n+    result = (media, options)\n+    return result\n", "issue": "API returns 400 for some requests due to \"bad\" Content-type header\nIt seems that _some_ API endpoints will return a HTTP 400 when presented with a header such as:\n\n```\nContent-type: application/json; charset=utf-8\n```\n\nbut works fine with:\n\n```\nContent-type: application/json\n```\n\nI ran into this with a POST to the URL `/webhooks/st2`. I'm currently running version `1.4dev-52`\n\n", "before_files": [{"content": "# Licensed to the StackStorm, Inc ('StackStorm') under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  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\n\nhttp_client = six.moves.http_client\n\nHTTP_SUCCESS = [http_client.OK, http_client.CREATED, http_client.ACCEPTED,\n                http_client.NON_AUTHORITATIVE_INFORMATION, http_client.NO_CONTENT,\n                http_client.RESET_CONTENT, http_client.PARTIAL_CONTENT,\n                http_client.MULTI_STATUS, http_client.IM_USED,\n                ]\n", "path": "st2common/st2common/util/http.py"}, {"content": "# Licensed to the StackStorm, Inc ('StackStorm') under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  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\ntry:\n    import simplejson as json\nexcept ImportError:\n    import json\n\nimport six\nimport pecan\nimport uuid\nfrom pecan import abort\nfrom pecan.rest import RestController\nfrom six.moves.urllib import parse as urlparse\nurljoin = urlparse.urljoin\n\nfrom st2common import log as logging\nfrom st2common.constants.triggers import WEBHOOK_TRIGGER_TYPES\nfrom st2common.models.api.base import jsexpose\nfrom st2common.models.api.trace import TraceContext\nimport st2common.services.triggers as trigger_service\nfrom st2common.services.triggerwatcher import TriggerWatcher\nfrom st2common.transport.reactor import TriggerDispatcher\nfrom st2common.rbac.types import PermissionType\nfrom st2common.rbac.decorators import request_user_has_webhook_permission\n\nhttp_client = six.moves.http_client\n\nLOG = logging.getLogger(__name__)\n\nTRACE_TAG_HEADER = 'St2-Trace-Tag'\n\n\nclass WebhooksController(RestController):\n    def __init__(self, *args, **kwargs):\n        super(WebhooksController, self).__init__(*args, **kwargs)\n        self._hooks = {}\n        self._base_url = '/webhooks/'\n        self._trigger_types = WEBHOOK_TRIGGER_TYPES.keys()\n\n        self._trigger_dispatcher = TriggerDispatcher(LOG)\n        queue_suffix = self.__class__.__name__\n        self._trigger_watcher = TriggerWatcher(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        self._trigger_watcher.start()\n        self._register_webhook_trigger_types()\n\n    @jsexpose()\n    def get_all(self):\n        # Return only the hooks known by this controller.\n        return [trigger for trigger in six.itervalues(self._hooks)]\n\n    @jsexpose()\n    def get_one(self, name):\n        hook = self._hooks.get(name, None)\n\n        if not hook:\n            abort(http_client.NOT_FOUND)\n            return\n\n        return hook\n\n    @request_user_has_webhook_permission(permission_type=PermissionType.WEBHOOK_SEND)\n    @jsexpose(arg_types=[str], status_code=http_client.ACCEPTED)\n    def post(self, *args, **kwargs):\n        hook = '/'.join(args)  # TODO: There must be a better way to do this.\n\n        # Note: For backward compatibility reasons we default to application/json if content\n        # type is not explicitly provided\n        content_type = pecan.request.headers.get('Content-Type', 'application/json')\n        body = pecan.request.body\n\n        try:\n            body = self._parse_request_body(content_type=content_type, body=body)\n        except Exception as e:\n            self._log_request('Failed to parse request body: %s.' % (str(e)), pecan.request)\n            msg = 'Failed to parse request body \"%s\": %s' % (body, str(e))\n            return pecan.abort(http_client.BAD_REQUEST, msg)\n\n        headers = self._get_headers_as_dict(pecan.request.headers)\n        # If webhook contains a trace-tag use that else create create a unique trace-tag.\n        trace_context = self._create_trace_context(trace_tag=headers.pop(TRACE_TAG_HEADER, None),\n                                                   hook=hook)\n\n        if hook == 'st2' or hook == 'st2/':\n            return self._handle_st2_webhook(body, trace_context=trace_context)\n\n        if not self._is_valid_hook(hook):\n            self._log_request('Invalid hook.', pecan.request)\n            msg = 'Webhook %s not registered with st2' % hook\n            return pecan.abort(http_client.NOT_FOUND, msg)\n\n        trigger = self._get_trigger_for_hook(hook)\n        payload = {}\n\n        payload['headers'] = headers\n        payload['body'] = body\n        self._trigger_dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)\n\n        return body\n\n    def _parse_request_body(self, content_type, body):\n        if content_type == 'application/json':\n            self._log_request('Parsing request body as JSON', request=pecan.request)\n            body = json.loads(body)\n        elif content_type in ['application/x-www-form-urlencoded', 'multipart/form-data']:\n            self._log_request('Parsing request body as form encoded data', request=pecan.request)\n            body = urlparse.parse_qs(body)\n        else:\n            raise ValueError('Unsupported Content-Type: \"%s\"' % (content_type))\n\n        return body\n\n    def _handle_st2_webhook(self, body, trace_context):\n        trigger = body.get('trigger', None)\n        payload = body.get('payload', None)\n        if not trigger:\n            msg = 'Trigger not specified.'\n            return pecan.abort(http_client.BAD_REQUEST, msg)\n        self._trigger_dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)\n\n        return body\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 _get_trigger_for_hook(self, hook):\n        return self._hooks[hook]\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: Permission checking for creating and deleting a webhook is done during rule\n        # creation\n        url = trigger['parameters']['url']\n        LOG.info('Listening to endpoint: %s', urljoin(self._base_url, url))\n        self._hooks[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 = trigger['parameters']['url']\n\n        if url in self._hooks:\n            LOG.info('Stop listening to endpoint: %s', urljoin(self._base_url, url))\n            del self._hooks[url]\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 _log_request(self, msg, request, log_method=LOG.debug):\n        headers = self._get_headers_as_dict(request.headers)\n        body = str(request.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 = trigger._data\n        if 'id' in sanitized:\n            # Friendly objectid rather than the MongoEngine representation.\n            sanitized['id'] = str(sanitized['id'])\n        return sanitized\n", "path": "st2api/st2api/controllers/v1/webhooks.py"}], "after_files": [{"content": "# Licensed to the StackStorm, Inc ('StackStorm') under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  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\n\nhttp_client = six.moves.http_client\n\n__all__ = [\n    'HTTP_SUCCESS',\n    'parse_content_type_header'\n]\n\nHTTP_SUCCESS = [http_client.OK, http_client.CREATED, http_client.ACCEPTED,\n                http_client.NON_AUTHORITATIVE_INFORMATION, http_client.NO_CONTENT,\n                http_client.RESET_CONTENT, http_client.PARTIAL_CONTENT,\n                http_client.MULTI_STATUS, http_client.IM_USED,\n                ]\n\n\ndef parse_content_type_header(content_type):\n    \"\"\"\n    Parse and normalize request content type and return a tuple with the content type and the\n    options.\n\n    :rype: ``tuple``\n    \"\"\"\n    if ';' in content_type:\n        split = content_type.split(';')\n        media = split[0]\n        options = {}\n\n        for pair in split[1:]:\n            split_pair = pair.split('=', 1)\n\n            if len(split_pair) != 2:\n                continue\n\n            key = split_pair[0].strip()\n            value = split_pair[1].strip()\n\n            options[key] = value\n    else:\n        media = content_type\n        options = {}\n\n    result = (media, options)\n    return result\n", "path": "st2common/st2common/util/http.py"}, {"content": "# Licensed to the StackStorm, Inc ('StackStorm') under one or more\n# contributor license agreements.  See the NOTICE file distributed with\n# this work for additional information regarding copyright ownership.\n# The ASF licenses this file to You under the Apache License, Version 2.0\n# (the \"License\"); you may not use this file except in compliance with\n# the License.  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\ntry:\n    import simplejson as json\nexcept ImportError:\n    import json\n\nimport six\nimport pecan\nimport uuid\nfrom pecan import abort\nfrom pecan.rest import RestController\nfrom six.moves.urllib import parse as urlparse\nurljoin = urlparse.urljoin\n\nfrom st2common import log as logging\nfrom st2common.constants.triggers import WEBHOOK_TRIGGER_TYPES\nfrom st2common.models.api.base import jsexpose\nfrom st2common.models.api.trace import TraceContext\nimport st2common.services.triggers as trigger_service\nfrom st2common.services.triggerwatcher import TriggerWatcher\nfrom st2common.transport.reactor import TriggerDispatcher\nfrom st2common.util.http import parse_content_type_header\nfrom st2common.rbac.types import PermissionType\nfrom st2common.rbac.decorators import request_user_has_webhook_permission\n\nhttp_client = six.moves.http_client\n\nLOG = logging.getLogger(__name__)\n\nTRACE_TAG_HEADER = 'St2-Trace-Tag'\n\n\nclass WebhooksController(RestController):\n    def __init__(self, *args, **kwargs):\n        super(WebhooksController, self).__init__(*args, **kwargs)\n        self._hooks = {}\n        self._base_url = '/webhooks/'\n        self._trigger_types = WEBHOOK_TRIGGER_TYPES.keys()\n\n        self._trigger_dispatcher = TriggerDispatcher(LOG)\n        queue_suffix = self.__class__.__name__\n        self._trigger_watcher = TriggerWatcher(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        self._trigger_watcher.start()\n        self._register_webhook_trigger_types()\n\n    @jsexpose()\n    def get_all(self):\n        # Return only the hooks known by this controller.\n        return [trigger for trigger in six.itervalues(self._hooks)]\n\n    @jsexpose()\n    def get_one(self, name):\n        hook = self._hooks.get(name, None)\n\n        if not hook:\n            abort(http_client.NOT_FOUND)\n            return\n\n        return hook\n\n    @request_user_has_webhook_permission(permission_type=PermissionType.WEBHOOK_SEND)\n    @jsexpose(arg_types=[str], status_code=http_client.ACCEPTED)\n    def post(self, *args, **kwargs):\n        hook = '/'.join(args)  # TODO: There must be a better way to do this.\n\n        # Note: For backward compatibility reasons we default to application/json if content\n        # type is not explicitly provided\n        content_type = pecan.request.headers.get('Content-Type', 'application/json')\n        content_type = parse_content_type_header(content_type=content_type)[0]\n        body = pecan.request.body\n\n        try:\n            body = self._parse_request_body(content_type=content_type, body=body)\n        except Exception as e:\n            self._log_request('Failed to parse request body: %s.' % (str(e)), pecan.request)\n            msg = 'Failed to parse request body \"%s\": %s' % (body, str(e))\n            return pecan.abort(http_client.BAD_REQUEST, msg)\n\n        headers = self._get_headers_as_dict(pecan.request.headers)\n        # If webhook contains a trace-tag use that else create create a unique trace-tag.\n        trace_context = self._create_trace_context(trace_tag=headers.pop(TRACE_TAG_HEADER, None),\n                                                   hook=hook)\n\n        if hook == 'st2' or hook == 'st2/':\n            return self._handle_st2_webhook(body, trace_context=trace_context)\n\n        if not self._is_valid_hook(hook):\n            self._log_request('Invalid hook.', pecan.request)\n            msg = 'Webhook %s not registered with st2' % hook\n            return pecan.abort(http_client.NOT_FOUND, msg)\n\n        trigger = self._get_trigger_for_hook(hook)\n        payload = {}\n\n        payload['headers'] = headers\n        payload['body'] = body\n        self._trigger_dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)\n\n        return body\n\n    def _parse_request_body(self, content_type, body):\n        if content_type == 'application/json':\n            self._log_request('Parsing request body as JSON', request=pecan.request)\n            body = json.loads(body)\n        elif content_type in ['application/x-www-form-urlencoded', 'multipart/form-data']:\n            self._log_request('Parsing request body as form encoded data', request=pecan.request)\n            body = urlparse.parse_qs(body)\n        else:\n            raise ValueError('Unsupported Content-Type: \"%s\"' % (content_type))\n\n        return body\n\n    def _handle_st2_webhook(self, body, trace_context):\n        trigger = body.get('trigger', None)\n        payload = body.get('payload', None)\n        if not trigger:\n            msg = 'Trigger not specified.'\n            return pecan.abort(http_client.BAD_REQUEST, msg)\n        self._trigger_dispatcher.dispatch(trigger, payload=payload, trace_context=trace_context)\n\n        return body\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 _get_trigger_for_hook(self, hook):\n        return self._hooks[hook]\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: Permission checking for creating and deleting a webhook is done during rule\n        # creation\n        url = trigger['parameters']['url']\n        LOG.info('Listening to endpoint: %s', urljoin(self._base_url, url))\n        self._hooks[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 = trigger['parameters']['url']\n\n        if url in self._hooks:\n            LOG.info('Stop listening to endpoint: %s', urljoin(self._base_url, url))\n            del self._hooks[url]\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 _log_request(self, msg, request, log_method=LOG.debug):\n        headers = self._get_headers_as_dict(request.headers)\n        body = str(request.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 = trigger._data\n        if 'id' in sanitized:\n            # Friendly objectid rather than the MongoEngine representation.\n            sanitized['id'] = str(sanitized['id'])\n        return sanitized\n", "path": "st2api/st2api/controllers/v1/webhooks.py"}]}