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