Adrià Vilanova MartÃnez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame] | 1 | # Copyright 2016 Google Inc. All Rights Reserved. |
| 2 | # |
| 3 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | # you may not use this file except in compliance with the License. |
| 5 | # You may obtain a copy of the License at |
| 6 | # |
| 7 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | # |
| 9 | # Unless required by applicable law or agreed to in writing, software |
| 10 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | # See the License for the specific language governing permissions and |
| 13 | # limitations under the License. |
| 14 | |
| 15 | """Cloud Endpoints API request-related data and functions.""" |
| 16 | |
| 17 | from __future__ import absolute_import |
| 18 | |
| 19 | # pylint: disable=g-bad-name |
| 20 | import copy |
| 21 | import json |
| 22 | import logging |
| 23 | from six.moves import urllib |
| 24 | import zlib |
| 25 | |
| 26 | from . import util |
| 27 | |
| 28 | _logger = logging.getLogger(__name__) |
| 29 | |
| 30 | _METHOD_OVERRIDE = 'X-HTTP-METHOD-OVERRIDE' |
| 31 | |
| 32 | |
| 33 | class ApiRequest(object): |
| 34 | """Simple data object representing an API request. |
| 35 | |
| 36 | Parses the request from environment variables into convenient pieces |
| 37 | and stores them as members. |
| 38 | """ |
| 39 | def __init__(self, environ, base_paths=None): |
| 40 | """Constructor. |
| 41 | |
| 42 | Args: |
| 43 | environ: An environ dict for the request as defined in PEP-333. |
| 44 | |
| 45 | Raises: |
| 46 | ValueError: If the path for the request is invalid. |
| 47 | """ |
| 48 | self.headers = util.get_headers_from_environ(environ) |
| 49 | self.http_method = environ['REQUEST_METHOD'] |
| 50 | self.url_scheme = environ['wsgi.url_scheme'] |
| 51 | self.server = environ['SERVER_NAME'] |
| 52 | self.port = environ['SERVER_PORT'] |
| 53 | self.path = environ['PATH_INFO'] |
| 54 | self.request_uri = environ.get('REQUEST_URI') |
| 55 | if self.request_uri is not None and len(self.request_uri) < len(self.path): |
| 56 | self.request_uri = None |
| 57 | self.query = environ.get('QUERY_STRING') |
| 58 | self.body = environ['wsgi.input'].read() |
| 59 | if self.body and self.headers.get('CONTENT-ENCODING') == 'gzip': |
| 60 | # Increasing wbits to 16 + MAX_WBITS is necessary to be able to decode |
| 61 | # gzipped content (as opposed to zlib-encoded content). |
| 62 | # If there's an error in the decompression, it could be due to another |
| 63 | # part of the serving chain that already decompressed it without clearing |
| 64 | # the header. If so, just ignore it and continue. |
| 65 | try: |
| 66 | self.body = zlib.decompress(self.body, 16 + zlib.MAX_WBITS) |
| 67 | except zlib.error: |
| 68 | pass |
| 69 | if _METHOD_OVERRIDE in self.headers: |
| 70 | # the query arguments in the body will be handled by ._process_req_body() |
| 71 | self.http_method = self.headers[_METHOD_OVERRIDE] |
| 72 | del self.headers[_METHOD_OVERRIDE] # wsgiref.headers.Headers doesn't implement .pop() |
| 73 | self.source_ip = environ.get('REMOTE_ADDR') |
| 74 | self.relative_url = self._reconstruct_relative_url(environ) |
| 75 | |
| 76 | if not base_paths: |
| 77 | base_paths = set() |
| 78 | elif isinstance(base_paths, list): |
| 79 | base_paths = set(base_paths) |
| 80 | |
| 81 | # Find a base_path in the path |
| 82 | for base_path in base_paths: |
| 83 | if self.path.startswith(base_path): |
| 84 | self.path = self.path[len(base_path):] |
| 85 | if self.request_uri is not None: |
| 86 | self.request_uri = self.request_uri[len(base_path):] |
| 87 | self.base_path = base_path |
| 88 | break |
| 89 | else: |
| 90 | raise ValueError('Invalid request path: %s' % self.path) |
| 91 | |
| 92 | if self.query: |
| 93 | self.parameters = urllib.parse.parse_qs(self.query, keep_blank_values=True) |
| 94 | else: |
| 95 | self.parameters = {} |
| 96 | self.body_json = self._process_req_body(self.body) if self.body else {} |
| 97 | self.request_id = None |
| 98 | |
| 99 | # Check if it's a batch request. We'll only handle single-element batch |
| 100 | # requests on the dev server (and we need to handle them because that's |
| 101 | # what RPC and JS calls typically show up as). Pull the request out of the |
| 102 | # list and record the fact that we're processing a batch. |
| 103 | if isinstance(self.body_json, list): |
| 104 | if len(self.body_json) != 1: |
| 105 | _logger.warning('Batch requests with more than 1 element aren\'t ' |
| 106 | 'supported in devappserver2. Only the first element ' |
| 107 | 'will be handled. Found %d elements.', |
| 108 | len(self.body_json)) |
| 109 | else: |
| 110 | _logger.info('Converting batch request to single request.') |
| 111 | self.body_json = self.body_json[0] |
| 112 | self.body = json.dumps(self.body_json) |
| 113 | self._is_batch = True |
| 114 | else: |
| 115 | self._is_batch = False |
| 116 | |
| 117 | def _process_req_body(self, body): |
| 118 | """Process the body of the HTTP request. |
| 119 | |
| 120 | If the body is valid JSON, return the JSON as a dict. |
| 121 | Else, convert the key=value format to a dict and return that. |
| 122 | |
| 123 | Args: |
| 124 | body: The body of the HTTP request. |
| 125 | """ |
| 126 | try: |
| 127 | return json.loads(body) |
| 128 | except ValueError: |
| 129 | return urllib.parse.parse_qs(body, keep_blank_values=True) |
| 130 | |
| 131 | def _reconstruct_relative_url(self, environ): |
| 132 | """Reconstruct the relative URL of this request. |
| 133 | |
| 134 | This is based on the URL reconstruction code in Python PEP 333: |
| 135 | http://www.python.org/dev/peps/pep-0333/#url-reconstruction. Rebuild the |
| 136 | URL from the pieces available in the environment. |
| 137 | |
| 138 | Args: |
| 139 | environ: An environ dict for the request as defined in PEP-333 |
| 140 | |
| 141 | Returns: |
| 142 | The portion of the URL from the request after the server and port. |
| 143 | """ |
| 144 | url = urllib.parse.quote(environ.get('SCRIPT_NAME', '')) |
| 145 | url += urllib.parse.quote(environ.get('PATH_INFO', '')) |
| 146 | if environ.get('QUERY_STRING'): |
| 147 | url += '?' + environ['QUERY_STRING'] |
| 148 | return url |
| 149 | |
| 150 | def reconstruct_hostname(self, port_override=None): |
| 151 | """Reconstruct the hostname of a request. |
| 152 | |
| 153 | This is based on the URL reconstruction code in Python PEP 333: |
| 154 | http://www.python.org/dev/peps/pep-0333/#url-reconstruction. Rebuild the |
| 155 | hostname from the pieces available in the environment. |
| 156 | |
| 157 | Args: |
| 158 | port_override: str, An override for the port on the returned hostname. |
| 159 | |
| 160 | Returns: |
| 161 | The hostname portion of the URL from the request, not including the |
| 162 | URL scheme. |
| 163 | """ |
| 164 | url = self.server |
| 165 | port = port_override or self.port |
| 166 | if port and ((self.url_scheme == 'https' and str(port) != '443') or |
| 167 | (self.url_scheme != 'https' and str(port) != '80')): |
| 168 | url += ':{0}'.format(port) |
| 169 | |
| 170 | return url |
| 171 | |
| 172 | def reconstruct_full_url(self, port_override=None): |
| 173 | """Reconstruct the full URL of a request. |
| 174 | |
| 175 | This is based on the URL reconstruction code in Python PEP 333: |
| 176 | http://www.python.org/dev/peps/pep-0333/#url-reconstruction. Rebuild the |
| 177 | hostname from the pieces available in the environment. |
| 178 | |
| 179 | Args: |
| 180 | port_override: str, An override for the port on the returned full URL. |
| 181 | |
| 182 | Returns: |
| 183 | The full URL from the request, including the URL scheme. |
| 184 | """ |
| 185 | return '{0}://{1}{2}'.format(self.url_scheme, |
| 186 | self.reconstruct_hostname(port_override), |
| 187 | self.relative_url) |
| 188 | |
| 189 | def copy(self): |
| 190 | return copy.deepcopy(self) |
| 191 | |
| 192 | def is_batch(self): |
| 193 | return self._is_batch |