Merge branch 'main' into avm99963-monorail
Merged commit 34d8229ae2b51fb1a15bd208e6fe6185c94f6266
GitOrigin-RevId: 7ee0917f93a577e475f8e09526dd144d245593f4
diff --git a/third_party/endpoints/api_request.py b/third_party/endpoints/api_request.py
new file mode 100644
index 0000000..8b95047
--- /dev/null
+++ b/third_party/endpoints/api_request.py
@@ -0,0 +1,193 @@
+# Copyright 2016 Google Inc. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Cloud Endpoints API request-related data and functions."""
+
+from __future__ import absolute_import
+
+# pylint: disable=g-bad-name
+import copy
+import json
+import logging
+from six.moves import urllib
+import zlib
+
+from . import util
+
+_logger = logging.getLogger(__name__)
+
+_METHOD_OVERRIDE = 'X-HTTP-METHOD-OVERRIDE'
+
+
+class ApiRequest(object):
+ """Simple data object representing an API request.
+
+ Parses the request from environment variables into convenient pieces
+ and stores them as members.
+ """
+ def __init__(self, environ, base_paths=None):
+ """Constructor.
+
+ Args:
+ environ: An environ dict for the request as defined in PEP-333.
+
+ Raises:
+ ValueError: If the path for the request is invalid.
+ """
+ self.headers = util.get_headers_from_environ(environ)
+ self.http_method = environ['REQUEST_METHOD']
+ self.url_scheme = environ['wsgi.url_scheme']
+ self.server = environ['SERVER_NAME']
+ self.port = environ['SERVER_PORT']
+ self.path = environ['PATH_INFO']
+ self.request_uri = environ.get('REQUEST_URI')
+ if self.request_uri is not None and len(self.request_uri) < len(self.path):
+ self.request_uri = None
+ self.query = environ.get('QUERY_STRING')
+ self.body = environ['wsgi.input'].read()
+ if self.body and self.headers.get('CONTENT-ENCODING') == 'gzip':
+ # Increasing wbits to 16 + MAX_WBITS is necessary to be able to decode
+ # gzipped content (as opposed to zlib-encoded content).
+ # If there's an error in the decompression, it could be due to another
+ # part of the serving chain that already decompressed it without clearing
+ # the header. If so, just ignore it and continue.
+ try:
+ self.body = zlib.decompress(self.body, 16 + zlib.MAX_WBITS)
+ except zlib.error:
+ pass
+ if _METHOD_OVERRIDE in self.headers:
+ # the query arguments in the body will be handled by ._process_req_body()
+ self.http_method = self.headers[_METHOD_OVERRIDE]
+ del self.headers[_METHOD_OVERRIDE] # wsgiref.headers.Headers doesn't implement .pop()
+ self.source_ip = environ.get('REMOTE_ADDR')
+ self.relative_url = self._reconstruct_relative_url(environ)
+
+ if not base_paths:
+ base_paths = set()
+ elif isinstance(base_paths, list):
+ base_paths = set(base_paths)
+
+ # Find a base_path in the path
+ for base_path in base_paths:
+ if self.path.startswith(base_path):
+ self.path = self.path[len(base_path):]
+ if self.request_uri is not None:
+ self.request_uri = self.request_uri[len(base_path):]
+ self.base_path = base_path
+ break
+ else:
+ raise ValueError('Invalid request path: %s' % self.path)
+
+ if self.query:
+ self.parameters = urllib.parse.parse_qs(self.query, keep_blank_values=True)
+ else:
+ self.parameters = {}
+ self.body_json = self._process_req_body(self.body) if self.body else {}
+ self.request_id = None
+
+ # Check if it's a batch request. We'll only handle single-element batch
+ # requests on the dev server (and we need to handle them because that's
+ # what RPC and JS calls typically show up as). Pull the request out of the
+ # list and record the fact that we're processing a batch.
+ if isinstance(self.body_json, list):
+ if len(self.body_json) != 1:
+ _logger.warning('Batch requests with more than 1 element aren\'t '
+ 'supported in devappserver2. Only the first element '
+ 'will be handled. Found %d elements.',
+ len(self.body_json))
+ else:
+ _logger.info('Converting batch request to single request.')
+ self.body_json = self.body_json[0]
+ self.body = json.dumps(self.body_json)
+ self._is_batch = True
+ else:
+ self._is_batch = False
+
+ def _process_req_body(self, body):
+ """Process the body of the HTTP request.
+
+ If the body is valid JSON, return the JSON as a dict.
+ Else, convert the key=value format to a dict and return that.
+
+ Args:
+ body: The body of the HTTP request.
+ """
+ try:
+ return json.loads(body)
+ except ValueError:
+ return urllib.parse.parse_qs(body, keep_blank_values=True)
+
+ def _reconstruct_relative_url(self, environ):
+ """Reconstruct the relative URL of this request.
+
+ This is based on the URL reconstruction code in Python PEP 333:
+ http://www.python.org/dev/peps/pep-0333/#url-reconstruction. Rebuild the
+ URL from the pieces available in the environment.
+
+ Args:
+ environ: An environ dict for the request as defined in PEP-333
+
+ Returns:
+ The portion of the URL from the request after the server and port.
+ """
+ url = urllib.parse.quote(environ.get('SCRIPT_NAME', ''))
+ url += urllib.parse.quote(environ.get('PATH_INFO', ''))
+ if environ.get('QUERY_STRING'):
+ url += '?' + environ['QUERY_STRING']
+ return url
+
+ def reconstruct_hostname(self, port_override=None):
+ """Reconstruct the hostname of a request.
+
+ This is based on the URL reconstruction code in Python PEP 333:
+ http://www.python.org/dev/peps/pep-0333/#url-reconstruction. Rebuild the
+ hostname from the pieces available in the environment.
+
+ Args:
+ port_override: str, An override for the port on the returned hostname.
+
+ Returns:
+ The hostname portion of the URL from the request, not including the
+ URL scheme.
+ """
+ url = self.server
+ port = port_override or self.port
+ if port and ((self.url_scheme == 'https' and str(port) != '443') or
+ (self.url_scheme != 'https' and str(port) != '80')):
+ url += ':{0}'.format(port)
+
+ return url
+
+ def reconstruct_full_url(self, port_override=None):
+ """Reconstruct the full URL of a request.
+
+ This is based on the URL reconstruction code in Python PEP 333:
+ http://www.python.org/dev/peps/pep-0333/#url-reconstruction. Rebuild the
+ hostname from the pieces available in the environment.
+
+ Args:
+ port_override: str, An override for the port on the returned full URL.
+
+ Returns:
+ The full URL from the request, including the URL scheme.
+ """
+ return '{0}://{1}{2}'.format(self.url_scheme,
+ self.reconstruct_hostname(port_override),
+ self.relative_url)
+
+ def copy(self):
+ return copy.deepcopy(self)
+
+ def is_batch(self):
+ return self._is_batch