We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Edge Router doesn't handle S3 Presigned URL POSTs properly
# Bug Report

# Detailed description
This is a similar issue to #2329, specific to [S3 Presigned URLs](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-presigned-urls.html). Using the edge router port (`:4566`), it is possible to use `generate_presigned_post`, but attempting to use the resultant URL to upload a file to localstack's S3 fails with a 404.

## Expected behavior
Localstack's edge router port should accept POST requests with an S3 Presigned URL (generated from that same Localstack instance) in the same way that the old S3 port (`:4572`) does.

## Actual behavior
While the old S3 port (`:4572`) handles this fine, the edge router responds with a 404 and a message similar to the following:

```
2020-05-24T15:36:54:INFO:localstack.services.edge: Unable to find forwarding rule for host "localhost:4566", path "/local-job-documents", target header "", auth header ""
```

(borrowed from @philippmalkov's comment on #2329 )

```
aws_1  | 2020-05-27T16:24:38:INFO:localstack.services.edge: Unable to find forwarding rule for host "aws:4566", path "/test-bucket", target header "", auth header ""
```

(our observed case with the below setup)

# Steps to reproduce

## Command used to start LocalStack
```
# Used in docker-compose.yml as:
  aws:
    image: localstack/localstack-light
    environment:
      - SERVICES=dynamodb,s3
      - HOSTNAME_EXTERNAL=aws
      - DEBUG=1
      - DATA_DIR=/tmp/localstack/data
    expose:
      - 4566
      - 4572
    volumes:
      - ./.localstack:/tmp/localstack
```

## Client code (AWS SDK code snippet, or sequence of "awslocal" commands)
Client code is a Django app using `boto3` to generate a Presigned URL and then upload a file to it using `requests`, effectively identical to the examples in [the boto3 docs](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-presigned-urls.html)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `localstack/services/edge.py`
Content:
```
1 import re
2 import os
3 import sys
4 import json
5 import logging
6 from requests.models import Response
7 from localstack import config
8 from localstack.constants import HEADER_LOCALSTACK_TARGET, HEADER_LOCALSTACK_EDGE_URL, LOCALSTACK_ROOT_FOLDER
9 from localstack.utils.common import run, is_root, TMP_THREADS, to_bytes
10 from localstack.utils.common import safe_requests as requests
11 from localstack.services.generic_proxy import ProxyListener, GenericProxy
12 
13 LOG = logging.getLogger(__name__)
14 
15 # Header to indicate that the process should kill itself. This is required because if
16 # this process is started as root, then we cannot kill it from a non-root process
17 HEADER_KILL_SIGNAL = 'x-localstack-kill'
18 
19 
20 class ProxyListenerEdge(ProxyListener):
21 
22     def forward_request(self, method, path, data, headers):
23         if method == 'OPTIONS':
24             return 200
25 
26         # kill the process if we receive this header
27         headers.get(HEADER_KILL_SIGNAL) and os._exit(0)
28 
29         target = headers.get('x-amz-target', '')
30         auth_header = headers.get('authorization', '')
31         host = headers.get('host', '')
32         headers[HEADER_LOCALSTACK_EDGE_URL] = 'https://%s' % host
33 
34         # extract API details
35         api, port, path, host = get_api_from_headers(headers, path)
36 
37         if port and int(port) < 0:
38             return 404
39 
40         if not port:
41             port = get_port_from_custom_rules(method, path, data, headers) or port
42 
43         if not port:
44             if api in ['', None, '_unknown_']:
45                 LOG.info(('Unable to find forwarding rule for host "%s", path "%s", '
46                     'target header "%s", auth header "%s"') % (host, path, target, auth_header))
47             else:
48                 LOG.info(('Unable to determine forwarding port for API "%s" - please '
49                     'make sure this API is enabled via the SERVICES configuration') % api)
50             response = Response()
51             response.status_code = 404
52             response._content = '{"status": "running"}'
53             return response
54 
55         use_ssl = config.USE_SSL
56 
57         connect_host = '%s:%s' % (config.HOSTNAME, port)
58         url = 'http%s://%s%s' % ('s' if use_ssl else '', connect_host, path)
59         headers['Host'] = host
60         function = getattr(requests, method.lower())
61         if isinstance(data, dict):
62             data = json.dumps(data)
63 
64         response = function(url, data=data, headers=headers, verify=False)
65         return response
66 
67 
68 def get_api_from_headers(headers, path=None):
69     """ Determine API and backend port based on Authorization headers. """
70 
71     target = headers.get('x-amz-target', '')
72     host = headers.get('host', '')
73     auth_header = headers.get('authorization', '')
74     ls_target = headers.get(HEADER_LOCALSTACK_TARGET, '')
75     path = path or '/'
76 
77     # initialize result
78     result = '_unknown_', 0
79 
80     # https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
81     try:
82         credential_scope = auth_header.split(',')[0].split()[1]
83         _, _, _, service, _ = credential_scope.split('/')
84         result = service, get_service_port_for_account(service, headers)
85     except Exception:
86         pass
87 
88     result_before = result
89 
90     # Fallback rules and route customizations applied below
91 
92     if host.endswith('cloudfront.net'):
93         path = path or '/'
94         result = 'cloudfront', config.PORT_CLOUDFRONT
95     elif target.startswith('AWSCognitoIdentityProviderService') or 'cognito-idp.' in host:
96         result = 'cognito-idp', config.PORT_COGNITO_IDP
97     elif target.startswith('AWSCognitoIdentityService') or 'cognito-identity.' in host:
98         result = 'cognito-identity', config.PORT_COGNITO_IDENTITY
99     elif result[0] == 's3' or re.match(r'.*s3(\-website)?\.([^\.]+\.)?amazonaws.com', host):
100         host = re.sub(r's3-website\..*\.amazonaws', 's3.amazonaws', host)
101         result = 's3', config.PORT_S3
102     elif result[0] == 'states' in auth_header or host.startswith('states.'):
103         result = 'stepfunctions', config.PORT_STEPFUNCTIONS
104     elif '.execute-api.' in host:
105         result = 'apigateway', config.PORT_APIGATEWAY
106     elif target.startswith('DynamoDBStreams') or host.startswith('streams.dynamodb.'):
107         result = 'dynamodbstreams', config.PORT_DYNAMODBSTREAMS
108     elif ls_target == 'web' or path == '/graph':
109         result = 'web', config.PORT_WEB_UI
110 
111     return result[0], result_before[1] or result[1], path, host
112 
113 
114 def get_port_from_custom_rules(method, path, data, headers):
115     """ Determine backend port based on custom rules. """
116 
117     # detect S3 presigned URLs
118     if 'AWSAccessKeyId=' in path or 'Signature=' in path:
119         return config.PORT_S3
120 
121     # TODO: move S3 public URLs to a separate port/endpoint, OR check ACLs here first
122     stripped = path.strip('/')
123     data_bytes = to_bytes(data or '')
124     if method == 'GET' and '/' in stripped:
125         # assume that this is an S3 GET request with URL path `/<bucket>/<key ...>`
126         return config.PORT_S3
127     if stripped and '/' not in stripped:
128         if method == 'PUT':
129             # assume that this is an S3 PUT bucket request with URL path `/<bucket>`
130             return config.PORT_S3
131         if method == 'POST' and to_bytes('key=') in data_bytes:
132             # assume that this is an S3 POST request with form parameters in the body
133             return config.PORT_S3
134 
135     if path == '/' and to_bytes('QueueName=') in data_bytes:
136         return config.PORT_SQS
137 
138 
139 def get_service_port_for_account(service, headers):
140     # assume we're only using a single account, hence return the static port mapping from config.py
141     return config.service_port(service)
142 
143 
144 def do_start_edge(port, use_ssl, asynchronous=False):
145     try:
146         # start local DNS server, if present
147         from localstack_ext.services import dns_server
148         dns_server.start_servers()
149     except Exception:
150         pass
151 
152     # get port and start Edge
153     print('Starting edge router (http%s port %s)...' % ('s' if use_ssl else '', port))
154     # use use=True here because our proxy allows both, HTTP and HTTPS traffic
155     proxy = GenericProxy(port, ssl=True, update_listener=ProxyListenerEdge())
156     proxy.start()
157     if not asynchronous:
158         proxy.join()
159     return proxy
160 
161 
162 def can_use_sudo():
163     try:
164         run('echo | sudo -S echo', print_error=False)
165         return True
166     except Exception:
167         return False
168 
169 
170 def ensure_can_use_sudo():
171     if not is_root() and not can_use_sudo():
172         print('Please enter your sudo password (required to configure local network):')
173         run('sudo echo', stdin=True)
174 
175 
176 def start_edge(port=None, use_ssl=True, asynchronous=False):
177     if not port:
178         port = config.EDGE_PORT
179     if config.EDGE_PORT_HTTP:
180         do_start_edge(config.EDGE_PORT_HTTP, use_ssl=False, asynchronous=True)
181     if port > 1024 or is_root():
182         return do_start_edge(port, use_ssl, asynchronous=asynchronous)
183 
184     # process requires priviledged port but we're not root -> try running as sudo
185 
186     class Terminator(object):
187 
188         def stop(self, quiet=True):
189             try:
190                 url = 'http%s://localhost:%s' % ('s' if use_ssl else '', port)
191                 requests.verify_ssl = False
192                 requests.post(url, headers={HEADER_KILL_SIGNAL: 'kill'})
193             except Exception:
194                 pass
195 
196     # make sure we can run sudo commands
197     ensure_can_use_sudo()
198 
199     # register a signal handler to terminate the sudo process later on
200     TMP_THREADS.append(Terminator())
201 
202     # start the process as sudo
203     sudo_cmd = 'sudo '
204     python_cmd = sys.executable
205     cmd = '%sPYTHONPATH=.:%s %s %s %s' % (sudo_cmd, LOCALSTACK_ROOT_FOLDER, python_cmd, __file__, port)
206     process = run(cmd, asynchronous=asynchronous)
207     return process
208 
209 
210 if __name__ == '__main__':
211     logging.basicConfig()
212     start_edge(int(sys.argv[1]))
213
```
--- 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/localstack/services/edge.py b/localstack/services/edge.py
--- a/localstack/services/edge.py
+++ b/localstack/services/edge.py
@@ -111,6 +111,16 @@
     return result[0], result_before[1] or result[1], path, host
 
 
+def is_s3_form_data(data_bytes):
+    if(to_bytes('key=') in data_bytes):
+        return True
+
+    if(to_bytes('Content-Disposition: form-data') in data_bytes and to_bytes('name="key"') in data_bytes):
+        return True
+
+    return False
+
+
 def get_port_from_custom_rules(method, path, data, headers):
     """ Determine backend port based on custom rules. """
 
@@ -128,8 +138,8 @@
         if method == 'PUT':
             # assume that this is an S3 PUT bucket request with URL path `/<bucket>`
             return config.PORT_S3
-        if method == 'POST' and to_bytes('key=') in data_bytes:
-            # assume that this is an S3 POST request with form parameters in the body
+        if method == 'POST' and is_s3_form_data(data_bytes):
+            # assume that this is an S3 POST request with form parameters or multipart form in the body
             return config.PORT_S3
 
     if path == '/' and to_bytes('QueueName=') in data_bytes:

{"golden_diff": "diff --git a/localstack/services/edge.py b/localstack/services/edge.py\n--- a/localstack/services/edge.py\n+++ b/localstack/services/edge.py\n@@ -111,6 +111,16 @@\n     return result[0], result_before[1] or result[1], path, host\n \n \n+def is_s3_form_data(data_bytes):\n+    if(to_bytes('key=') in data_bytes):\n+        return True\n+\n+    if(to_bytes('Content-Disposition: form-data') in data_bytes and to_bytes('name=\"key\"') in data_bytes):\n+        return True\n+\n+    return False\n+\n+\n def get_port_from_custom_rules(method, path, data, headers):\n     \"\"\" Determine backend port based on custom rules. \"\"\"\n \n@@ -128,8 +138,8 @@\n         if method == 'PUT':\n             # assume that this is an S3 PUT bucket request with URL path `/<bucket>`\n             return config.PORT_S3\n-        if method == 'POST' and to_bytes('key=') in data_bytes:\n-            # assume that this is an S3 POST request with form parameters in the body\n+        if method == 'POST' and is_s3_form_data(data_bytes):\n+            # assume that this is an S3 POST request with form parameters or multipart form in the body\n             return config.PORT_S3\n \n     if path == '/' and to_bytes('QueueName=') in data_bytes:\n", "issue": "Edge Router doesn't handle S3 Presigned URL POSTs properly\n# Bug Report\r\n\r\n# Detailed description\r\nThis is a similar issue to #2329, specific to [S3 Presigned URLs](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-presigned-urls.html). Using the edge router port (`:4566`), it is possible to use `generate_presigned_post`, but attempting to use the resultant URL to upload a file to localstack's S3 fails with a 404.\r\n\r\n## Expected behavior\r\nLocalstack's edge router port should accept POST requests with an S3 Presigned URL (generated from that same Localstack instance) in the same way that the old S3 port (`:4572`) does.\r\n\r\n## Actual behavior\r\nWhile the old S3 port (`:4572`) handles this fine, the edge router responds with a 404 and a message similar to the following:\r\n\r\n```\r\n2020-05-24T15:36:54:INFO:localstack.services.edge: Unable to find forwarding rule for host \"localhost:4566\", path \"/local-job-documents\", target header \"\", auth header \"\"\r\n```\r\n\r\n(borrowed from @philippmalkov's comment on #2329 )\r\n\r\n```\r\naws_1  | 2020-05-27T16:24:38:INFO:localstack.services.edge: Unable to find forwarding rule for host \"aws:4566\", path \"/test-bucket\", target header \"\", auth header \"\"\r\n```\r\n\r\n(our observed case with the below setup)\r\n\r\n# Steps to reproduce\r\n\r\n## Command used to start LocalStack\r\n```\r\n# Used in docker-compose.yml as:\r\n  aws:\r\n    image: localstack/localstack-light\r\n    environment:\r\n      - SERVICES=dynamodb,s3\r\n      - HOSTNAME_EXTERNAL=aws\r\n      - DEBUG=1\r\n      - DATA_DIR=/tmp/localstack/data\r\n    expose:\r\n      - 4566\r\n      - 4572\r\n    volumes:\r\n      - ./.localstack:/tmp/localstack\r\n```\r\n\r\n## Client code (AWS SDK code snippet, or sequence of \"awslocal\" commands)\r\nClient code is a Django app using `boto3` to generate a Presigned URL and then upload a file to it using `requests`, effectively identical to the examples in [the boto3 docs](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/s3-presigned-urls.html)\n", "before_files": [{"content": "import re\nimport os\nimport sys\nimport json\nimport logging\nfrom requests.models import Response\nfrom localstack import config\nfrom localstack.constants import HEADER_LOCALSTACK_TARGET, HEADER_LOCALSTACK_EDGE_URL, LOCALSTACK_ROOT_FOLDER\nfrom localstack.utils.common import run, is_root, TMP_THREADS, to_bytes\nfrom localstack.utils.common import safe_requests as requests\nfrom localstack.services.generic_proxy import ProxyListener, GenericProxy\n\nLOG = logging.getLogger(__name__)\n\n# Header to indicate that the process should kill itself. This is required because if\n# this process is started as root, then we cannot kill it from a non-root process\nHEADER_KILL_SIGNAL = 'x-localstack-kill'\n\n\nclass ProxyListenerEdge(ProxyListener):\n\n    def forward_request(self, method, path, data, headers):\n        if method == 'OPTIONS':\n            return 200\n\n        # kill the process if we receive this header\n        headers.get(HEADER_KILL_SIGNAL) and os._exit(0)\n\n        target = headers.get('x-amz-target', '')\n        auth_header = headers.get('authorization', '')\n        host = headers.get('host', '')\n        headers[HEADER_LOCALSTACK_EDGE_URL] = 'https://%s' % host\n\n        # extract API details\n        api, port, path, host = get_api_from_headers(headers, path)\n\n        if port and int(port) < 0:\n            return 404\n\n        if not port:\n            port = get_port_from_custom_rules(method, path, data, headers) or port\n\n        if not port:\n            if api in ['', None, '_unknown_']:\n                LOG.info(('Unable to find forwarding rule for host \"%s\", path \"%s\", '\n                    'target header \"%s\", auth header \"%s\"') % (host, path, target, auth_header))\n            else:\n                LOG.info(('Unable to determine forwarding port for API \"%s\" - please '\n                    'make sure this API is enabled via the SERVICES configuration') % api)\n            response = Response()\n            response.status_code = 404\n            response._content = '{\"status\": \"running\"}'\n            return response\n\n        use_ssl = config.USE_SSL\n\n        connect_host = '%s:%s' % (config.HOSTNAME, port)\n        url = 'http%s://%s%s' % ('s' if use_ssl else '', connect_host, path)\n        headers['Host'] = host\n        function = getattr(requests, method.lower())\n        if isinstance(data, dict):\n            data = json.dumps(data)\n\n        response = function(url, data=data, headers=headers, verify=False)\n        return response\n\n\ndef get_api_from_headers(headers, path=None):\n    \"\"\" Determine API and backend port based on Authorization headers. \"\"\"\n\n    target = headers.get('x-amz-target', '')\n    host = headers.get('host', '')\n    auth_header = headers.get('authorization', '')\n    ls_target = headers.get(HEADER_LOCALSTACK_TARGET, '')\n    path = path or '/'\n\n    # initialize result\n    result = '_unknown_', 0\n\n    # https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html\n    try:\n        credential_scope = auth_header.split(',')[0].split()[1]\n        _, _, _, service, _ = credential_scope.split('/')\n        result = service, get_service_port_for_account(service, headers)\n    except Exception:\n        pass\n\n    result_before = result\n\n    # Fallback rules and route customizations applied below\n\n    if host.endswith('cloudfront.net'):\n        path = path or '/'\n        result = 'cloudfront', config.PORT_CLOUDFRONT\n    elif target.startswith('AWSCognitoIdentityProviderService') or 'cognito-idp.' in host:\n        result = 'cognito-idp', config.PORT_COGNITO_IDP\n    elif target.startswith('AWSCognitoIdentityService') or 'cognito-identity.' in host:\n        result = 'cognito-identity', config.PORT_COGNITO_IDENTITY\n    elif result[0] == 's3' or re.match(r'.*s3(\\-website)?\\.([^\\.]+\\.)?amazonaws.com', host):\n        host = re.sub(r's3-website\\..*\\.amazonaws', 's3.amazonaws', host)\n        result = 's3', config.PORT_S3\n    elif result[0] == 'states' in auth_header or host.startswith('states.'):\n        result = 'stepfunctions', config.PORT_STEPFUNCTIONS\n    elif '.execute-api.' in host:\n        result = 'apigateway', config.PORT_APIGATEWAY\n    elif target.startswith('DynamoDBStreams') or host.startswith('streams.dynamodb.'):\n        result = 'dynamodbstreams', config.PORT_DYNAMODBSTREAMS\n    elif ls_target == 'web' or path == '/graph':\n        result = 'web', config.PORT_WEB_UI\n\n    return result[0], result_before[1] or result[1], path, host\n\n\ndef get_port_from_custom_rules(method, path, data, headers):\n    \"\"\" Determine backend port based on custom rules. \"\"\"\n\n    # detect S3 presigned URLs\n    if 'AWSAccessKeyId=' in path or 'Signature=' in path:\n        return config.PORT_S3\n\n    # TODO: move S3 public URLs to a separate port/endpoint, OR check ACLs here first\n    stripped = path.strip('/')\n    data_bytes = to_bytes(data or '')\n    if method == 'GET' and '/' in stripped:\n        # assume that this is an S3 GET request with URL path `/<bucket>/<key ...>`\n        return config.PORT_S3\n    if stripped and '/' not in stripped:\n        if method == 'PUT':\n            # assume that this is an S3 PUT bucket request with URL path `/<bucket>`\n            return config.PORT_S3\n        if method == 'POST' and to_bytes('key=') in data_bytes:\n            # assume that this is an S3 POST request with form parameters in the body\n            return config.PORT_S3\n\n    if path == '/' and to_bytes('QueueName=') in data_bytes:\n        return config.PORT_SQS\n\n\ndef get_service_port_for_account(service, headers):\n    # assume we're only using a single account, hence return the static port mapping from config.py\n    return config.service_port(service)\n\n\ndef do_start_edge(port, use_ssl, asynchronous=False):\n    try:\n        # start local DNS server, if present\n        from localstack_ext.services import dns_server\n        dns_server.start_servers()\n    except Exception:\n        pass\n\n    # get port and start Edge\n    print('Starting edge router (http%s port %s)...' % ('s' if use_ssl else '', port))\n    # use use=True here because our proxy allows both, HTTP and HTTPS traffic\n    proxy = GenericProxy(port, ssl=True, update_listener=ProxyListenerEdge())\n    proxy.start()\n    if not asynchronous:\n        proxy.join()\n    return proxy\n\n\ndef can_use_sudo():\n    try:\n        run('echo | sudo -S echo', print_error=False)\n        return True\n    except Exception:\n        return False\n\n\ndef ensure_can_use_sudo():\n    if not is_root() and not can_use_sudo():\n        print('Please enter your sudo password (required to configure local network):')\n        run('sudo echo', stdin=True)\n\n\ndef start_edge(port=None, use_ssl=True, asynchronous=False):\n    if not port:\n        port = config.EDGE_PORT\n    if config.EDGE_PORT_HTTP:\n        do_start_edge(config.EDGE_PORT_HTTP, use_ssl=False, asynchronous=True)\n    if port > 1024 or is_root():\n        return do_start_edge(port, use_ssl, asynchronous=asynchronous)\n\n    # process requires priviledged port but we're not root -> try running as sudo\n\n    class Terminator(object):\n\n        def stop(self, quiet=True):\n            try:\n                url = 'http%s://localhost:%s' % ('s' if use_ssl else '', port)\n                requests.verify_ssl = False\n                requests.post(url, headers={HEADER_KILL_SIGNAL: 'kill'})\n            except Exception:\n                pass\n\n    # make sure we can run sudo commands\n    ensure_can_use_sudo()\n\n    # register a signal handler to terminate the sudo process later on\n    TMP_THREADS.append(Terminator())\n\n    # start the process as sudo\n    sudo_cmd = 'sudo '\n    python_cmd = sys.executable\n    cmd = '%sPYTHONPATH=.:%s %s %s %s' % (sudo_cmd, LOCALSTACK_ROOT_FOLDER, python_cmd, __file__, port)\n    process = run(cmd, asynchronous=asynchronous)\n    return process\n\n\nif __name__ == '__main__':\n    logging.basicConfig()\n    start_edge(int(sys.argv[1]))\n", "path": "localstack/services/edge.py"}], "after_files": [{"content": "import re\nimport os\nimport sys\nimport json\nimport logging\nfrom requests.models import Response\nfrom localstack import config\nfrom localstack.constants import HEADER_LOCALSTACK_TARGET, HEADER_LOCALSTACK_EDGE_URL, LOCALSTACK_ROOT_FOLDER\nfrom localstack.utils.common import run, is_root, TMP_THREADS, to_bytes\nfrom localstack.utils.common import safe_requests as requests\nfrom localstack.services.generic_proxy import ProxyListener, GenericProxy\n\nLOG = logging.getLogger(__name__)\n\n# Header to indicate that the process should kill itself. This is required because if\n# this process is started as root, then we cannot kill it from a non-root process\nHEADER_KILL_SIGNAL = 'x-localstack-kill'\n\n\nclass ProxyListenerEdge(ProxyListener):\n\n    def forward_request(self, method, path, data, headers):\n        if method == 'OPTIONS':\n            return 200\n\n        # kill the process if we receive this header\n        headers.get(HEADER_KILL_SIGNAL) and os._exit(0)\n\n        target = headers.get('x-amz-target', '')\n        auth_header = headers.get('authorization', '')\n        host = headers.get('host', '')\n        headers[HEADER_LOCALSTACK_EDGE_URL] = 'https://%s' % host\n\n        # extract API details\n        api, port, path, host = get_api_from_headers(headers, path)\n\n        if port and int(port) < 0:\n            return 404\n\n        if not port:\n            port = get_port_from_custom_rules(method, path, data, headers) or port\n\n        if not port:\n            if api in ['', None, '_unknown_']:\n                LOG.info(('Unable to find forwarding rule for host \"%s\", path \"%s\", '\n                    'target header \"%s\", auth header \"%s\"') % (host, path, target, auth_header))\n            else:\n                LOG.info(('Unable to determine forwarding port for API \"%s\" - please '\n                    'make sure this API is enabled via the SERVICES configuration') % api)\n            response = Response()\n            response.status_code = 404\n            response._content = '{\"status\": \"running\"}'\n            return response\n\n        use_ssl = config.USE_SSL\n\n        connect_host = '%s:%s' % (config.HOSTNAME, port)\n        url = 'http%s://%s%s' % ('s' if use_ssl else '', connect_host, path)\n        headers['Host'] = host\n        function = getattr(requests, method.lower())\n        if isinstance(data, dict):\n            data = json.dumps(data)\n\n        response = function(url, data=data, headers=headers, verify=False)\n        return response\n\n\ndef get_api_from_headers(headers, path=None):\n    \"\"\" Determine API and backend port based on Authorization headers. \"\"\"\n\n    target = headers.get('x-amz-target', '')\n    host = headers.get('host', '')\n    auth_header = headers.get('authorization', '')\n    ls_target = headers.get(HEADER_LOCALSTACK_TARGET, '')\n    path = path or '/'\n\n    # initialize result\n    result = '_unknown_', 0\n\n    # https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html\n    try:\n        credential_scope = auth_header.split(',')[0].split()[1]\n        _, _, _, service, _ = credential_scope.split('/')\n        result = service, get_service_port_for_account(service, headers)\n    except Exception:\n        pass\n\n    result_before = result\n\n    # Fallback rules and route customizations applied below\n\n    if host.endswith('cloudfront.net'):\n        path = path or '/'\n        result = 'cloudfront', config.PORT_CLOUDFRONT\n    elif target.startswith('AWSCognitoIdentityProviderService') or 'cognito-idp.' in host:\n        result = 'cognito-idp', config.PORT_COGNITO_IDP\n    elif target.startswith('AWSCognitoIdentityService') or 'cognito-identity.' in host:\n        result = 'cognito-identity', config.PORT_COGNITO_IDENTITY\n    elif result[0] == 's3' or re.match(r'.*s3(\\-website)?\\.([^\\.]+\\.)?amazonaws.com', host):\n        host = re.sub(r's3-website\\..*\\.amazonaws', 's3.amazonaws', host)\n        result = 's3', config.PORT_S3\n    elif result[0] == 'states' in auth_header or host.startswith('states.'):\n        result = 'stepfunctions', config.PORT_STEPFUNCTIONS\n    elif '.execute-api.' in host:\n        result = 'apigateway', config.PORT_APIGATEWAY\n    elif target.startswith('DynamoDBStreams') or host.startswith('streams.dynamodb.'):\n        result = 'dynamodbstreams', config.PORT_DYNAMODBSTREAMS\n    elif ls_target == 'web' or path == '/graph':\n        result = 'web', config.PORT_WEB_UI\n\n    return result[0], result_before[1] or result[1], path, host\n\n\ndef is_s3_form_data(data_bytes):\n    if(to_bytes('key=') in data_bytes):\n        return True\n\n    if(to_bytes('Content-Disposition: form-data') in data_bytes and to_bytes('name=\"key\"') in data_bytes):\n        return True\n\n    return False\n\n\ndef get_port_from_custom_rules(method, path, data, headers):\n    \"\"\" Determine backend port based on custom rules. \"\"\"\n\n    # detect S3 presigned URLs\n    if 'AWSAccessKeyId=' in path or 'Signature=' in path:\n        return config.PORT_S3\n\n    # TODO: move S3 public URLs to a separate port/endpoint, OR check ACLs here first\n    stripped = path.strip('/')\n    data_bytes = to_bytes(data or '')\n    if method == 'GET' and '/' in stripped:\n        # assume that this is an S3 GET request with URL path `/<bucket>/<key ...>`\n        return config.PORT_S3\n    if stripped and '/' not in stripped:\n        if method == 'PUT':\n            # assume that this is an S3 PUT bucket request with URL path `/<bucket>`\n            return config.PORT_S3\n        if method == 'POST' and is_s3_form_data(data_bytes):\n            # assume that this is an S3 POST request with form parameters or multipart form in the body\n            return config.PORT_S3\n\n    if path == '/' and to_bytes('QueueName=') in data_bytes:\n        return config.PORT_SQS\n\n\ndef get_service_port_for_account(service, headers):\n    # assume we're only using a single account, hence return the static port mapping from config.py\n    return config.service_port(service)\n\n\ndef do_start_edge(port, use_ssl, asynchronous=False):\n    try:\n        # start local DNS server, if present\n        from localstack_ext.services import dns_server\n        dns_server.start_servers()\n    except Exception:\n        pass\n\n    # get port and start Edge\n    print('Starting edge router (http%s port %s)...' % ('s' if use_ssl else '', port))\n    # use use=True here because our proxy allows both, HTTP and HTTPS traffic\n    proxy = GenericProxy(port, ssl=True, update_listener=ProxyListenerEdge())\n    proxy.start()\n    if not asynchronous:\n        proxy.join()\n    return proxy\n\n\ndef can_use_sudo():\n    try:\n        run('echo | sudo -S echo', print_error=False)\n        return True\n    except Exception:\n        return False\n\n\ndef ensure_can_use_sudo():\n    if not is_root() and not can_use_sudo():\n        print('Please enter your sudo password (required to configure local network):')\n        run('sudo echo', stdin=True)\n\n\ndef start_edge(port=None, use_ssl=True, asynchronous=False):\n    if not port:\n        port = config.EDGE_PORT\n    if config.EDGE_PORT_HTTP:\n        do_start_edge(config.EDGE_PORT_HTTP, use_ssl=False, asynchronous=True)\n    if port > 1024 or is_root():\n        return do_start_edge(port, use_ssl, asynchronous=asynchronous)\n\n    # process requires priviledged port but we're not root -> try running as sudo\n\n    class Terminator(object):\n\n        def stop(self, quiet=True):\n            try:\n                url = 'http%s://localhost:%s' % ('s' if use_ssl else '', port)\n                requests.verify_ssl = False\n                requests.post(url, headers={HEADER_KILL_SIGNAL: 'kill'})\n            except Exception:\n                pass\n\n    # make sure we can run sudo commands\n    ensure_can_use_sudo()\n\n    # register a signal handler to terminate the sudo process later on\n    TMP_THREADS.append(Terminator())\n\n    # start the process as sudo\n    sudo_cmd = 'sudo '\n    python_cmd = sys.executable\n    cmd = '%sPYTHONPATH=.:%s %s %s %s' % (sudo_cmd, LOCALSTACK_ROOT_FOLDER, python_cmd, __file__, port)\n    process = run(cmd, asynchronous=asynchronous)\n    return process\n\n\nif __name__ == '__main__':\n    logging.basicConfig()\n    start_edge(int(sys.argv[1]))\n", "path": "localstack/services/edge.py"}]}