3 # Copyright 2007 Google Inc.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """A Python request handler.
19 The module must be imported inside that runtime sandbox so that the logging
20 module is the sandboxed version.
33 from google
.appengine
.api
import api_base_pb
34 from google
.appengine
.api
import apiproxy_stub_map
35 from google
.appengine
.api
import appinfo
36 from google
.appengine
.api
.logservice
import log_service_pb
37 from google
.appengine
.api
.logservice
import logservice
38 from google
.appengine
.ext
.remote_api
import remote_api_stub
39 from google
.appengine
.runtime
import background
40 from google
.appengine
.runtime
import request_environment
41 from google
.appengine
.runtime
import runtime
42 from google
.appengine
.runtime
import shutdown
43 from google
.appengine
.tools
.devappserver2
import http_runtime_constants
44 from google
.appengine
.tools
.devappserver2
.python
import request_state
47 # Copied from httplib; done so we don't have to import httplib which breaks
48 # our httplib "forwarder" as the environment variable that controls which
49 # implementation we get is not yet set.
53 101: 'Switching Protocols',
58 203: 'Non-Authoritative Information',
61 206: 'Partial Content',
63 300: 'Multiple Choices',
64 301: 'Moved Permanently',
70 307: 'Temporary Redirect',
74 402: 'Payment Required',
77 405: 'Method Not Allowed',
78 406: 'Not Acceptable',
79 407: 'Proxy Authentication Required',
80 408: 'Request Timeout',
83 411: 'Length Required',
84 412: 'Precondition Failed',
85 413: 'Request Entity Too Large',
86 414: 'Request-URI Too Long',
87 415: 'Unsupported Media Type',
88 416: 'Requested Range Not Satisfiable',
89 417: 'Expectation Failed',
91 500: 'Internal Server Error',
92 501: 'Not Implemented',
94 503: 'Service Unavailable',
95 504: 'Gateway Timeout',
96 505: 'HTTP Version Not Supported',
99 class RequestHandler(object):
100 """A WSGI application that forwards requests to a user-provided app."""
102 _PYTHON_LIB_DIR
= os
.path
.dirname(os
.path
.dirname(google
.__file
__))
104 def __init__(self
, config
):
106 if appinfo
.MODULE_SEPARATOR
not in config
.version_id
:
107 module_id
= appinfo
.DEFAULT_MODULE
108 version_id
= config
.version_id
110 module_id
, version_id
= config
.version_id
.split(appinfo
.MODULE_SEPARATOR
)
112 self
.environ_template
= {
113 'APPLICATION_ID': config
.app_id
,
114 'CURRENT_MODULE_ID': module_id
,
115 'CURRENT_VERSION_ID': version_id
,
116 'DATACENTER': config
.datacenter
.encode('ascii'),
117 'INSTANCE_ID': config
.instance_id
.encode('ascii'),
118 'APPENGINE_RUNTIME': 'python27',
119 'AUTH_DOMAIN': config
.auth_domain
.encode('ascii'),
122 'SERVER_SOFTWARE': http_runtime_constants
.SERVER_SOFTWARE
,
124 'wsgi.multithread': config
.threadsafe
,
126 self
._command
_globals
= {} # Use to evaluate interactive requests.
127 self
.environ_template
.update((env
.key
, env
.value
) for env
in config
.environ
)
129 def __call__(self
, environ
, start_response
):
130 remote_api_stub
.RemoteStub
._SetRequestId
(
131 environ
[http_runtime_constants
.REQUEST_ID_ENVIRON
])
132 request_type
= environ
.pop(http_runtime_constants
.REQUEST_TYPE_HEADER
, None)
133 request_state
.start_request(
134 environ
[http_runtime_constants
.REQUEST_ID_ENVIRON
])
136 if request_type
== 'background':
137 response
= self
.handle_background_request(environ
)
138 elif request_type
== 'shutdown':
139 response
= self
.handle_shutdown_request(environ
)
140 elif request_type
== 'interactive':
141 response
= self
.handle_interactive_request(environ
)
143 response
= self
.handle_normal_request(environ
)
145 request_state
.end_request(
146 environ
[http_runtime_constants
.REQUEST_ID_ENVIRON
])
147 error
= response
.get('error', 0)
148 self
._flush
_logs
(response
.get('logs', []))
150 response_code
= response
.get('response_code', 200)
151 status
= '%d %s' % (response_code
, httplib_responses
.get(
152 response_code
, 'Unknown Status Code'))
153 start_response(status
, response
.get('headers', []))
154 return [response
.get('body', '')]
156 start_response('404 Not Found', [])
159 start_response('500 Internal Server Error',
160 [(http_runtime_constants
.ERROR_CODE_HEADER
, str(error
))])
163 def handle_normal_request(self
, environ
):
164 user_environ
= self
.get_user_environ(environ
)
165 script
= environ
.pop(http_runtime_constants
.SCRIPT_HEADER
)
166 body
= environ
['wsgi.input'].read(int(environ
.get('CONTENT_LENGTH', 0)))
167 url
= 'http://%s:%s%s?%s' % (user_environ
['SERVER_NAME'],
168 user_environ
['SERVER_PORT'],
169 urllib
.quote(environ
['PATH_INFO']),
170 environ
['QUERY_STRING'])
171 return runtime
.HandleRequest(user_environ
, script
, url
, body
,
172 self
.config
.application_root
,
173 self
._PYTHON
_LIB
_DIR
)
175 def handle_background_request(self
, environ
):
176 return background
.Handle(self
.get_user_environ(environ
))
178 def handle_shutdown_request(self
, environ
):
179 response
, exc
= shutdown
.Handle(self
.get_user_environ(environ
))
181 for request
in request_state
.get_request_states():
182 if (request
.request_id
!=
183 environ
[http_runtime_constants
.REQUEST_ID_ENVIRON
]):
184 request
.inject_exception(exc
[1])
187 def handle_interactive_request(self
, environ
):
188 code
= environ
['wsgi.input'].read().replace('\r\n', '\n')
190 user_environ
= self
.get_user_environ(environ
)
191 if 'HTTP_CONTENT_LENGTH' in user_environ
:
192 del user_environ
['HTTP_CONTENT_LENGTH']
193 user_environ
['REQUEST_METHOD'] = 'GET'
194 url
= 'http://%s:%s%s?%s' % (user_environ
['SERVER_NAME'],
195 user_environ
['SERVER_PORT'],
196 urllib
.quote(environ
['PATH_INFO']),
197 environ
['QUERY_STRING'])
199 results_io
= cStringIO
.StringIO()
200 old_sys_stdout
= sys
.stdout
203 error
= logservice
.LogsBuffer()
204 request_environment
.current_request
.Init(error
, user_environ
)
205 url
= urlparse
.urlsplit(url
)
206 environ
.update(runtime
.CgiDictFromParsedUrl(url
))
207 sys
.stdout
= results_io
210 __import__('appengine_config', self
._command
_globals
)
211 except ImportError as e
:
212 if 'appengine_config' not in e
.message
:
214 compiled_code
= compile(code
, '<string>', 'exec')
215 exec(compiled_code
, self
._command
_globals
)
217 traceback
.print_exc(file=results_io
)
220 'response_code': 200,
221 'headers': [('Content-Type', 'text/plain')],
222 'body': results_io
.getvalue(),
223 'logs': error
.parse_logs()}
225 request_environment
.current_request
.Clear()
226 sys
.stdout
= old_sys_stdout
228 def get_user_environ(self
, environ
):
229 """Returns a dict containing the environ to pass to the user's application.
232 environ: A dict containing the request WSGI environ.
235 A dict containing the environ representing an HTTP request.
237 user_environ
= self
.environ_template
.copy()
238 self
.copy_headers(environ
, user_environ
)
239 user_environ
['REQUEST_METHOD'] = environ
.get('REQUEST_METHOD', 'GET')
240 content_type
= environ
.get('CONTENT_TYPE')
242 user_environ
['HTTP_CONTENT_TYPE'] = content_type
243 content_length
= environ
.get('CONTENT_LENGTH')
245 user_environ
['HTTP_CONTENT_LENGTH'] = content_length
248 def copy_headers(self
, source_environ
, dest_environ
):
249 """Copy headers from source_environ to dest_environ.
251 This extracts headers that represent environ values and propagates all
252 other headers which are not used for internal implementation details or
253 headers that are stripped.
256 source_environ: The source environ dict.
257 dest_environ: The environ dict to populate.
259 for env
in http_runtime_constants
.ENVIRONS_TO_PROPAGATE
:
260 value
= source_environ
.get(
261 http_runtime_constants
.INTERNAL_ENVIRON_PREFIX
+ env
, None)
262 if value
is not None:
263 dest_environ
[env
] = value
264 for name
, value
in source_environ
.items():
265 if (name
.startswith('HTTP_') and
266 not name
.startswith(http_runtime_constants
.INTERNAL_ENVIRON_PREFIX
)):
267 dest_environ
[name
] = value
269 def _flush_logs(self
, logs
):
270 """Flushes logs using the LogService API.
273 logs: A list of tuples (timestamp_usec, level, message).
275 logs_group
= log_service_pb
.UserAppLogGroup()
276 for timestamp_usec
, level
, message
in logs
:
277 log_line
= logs_group
.add_log_line()
278 log_line
.set_timestamp_usec(timestamp_usec
)
279 log_line
.set_level(level
)
280 log_line
.set_message(message
)
281 request
= log_service_pb
.FlushRequest()
282 request
.set_logs(logs_group
.Encode())
283 response
= api_base_pb
.VoidProto()
284 apiproxy_stub_map
.MakeSyncCall('logservice', 'Flush', request
, response
)