blob: 8b9504778248284bde812ed12e41c1315e1f3065 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# 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
17from __future__ import absolute_import
18
19# pylint: disable=g-bad-name
20import copy
21import json
22import logging
23from six.moves import urllib
24import zlib
25
26from . import util
27
28_logger = logging.getLogger(__name__)
29
30_METHOD_OVERRIDE = 'X-HTTP-METHOD-OVERRIDE'
31
32
33class 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