1"""Base classes for server/gateway implementations""" 2 3from types import StringType 4from util import FileWrapper, guess_scheme, is_hop_by_hop 5from headers import Headers 6 7import sys, os, time 8 9__all__ = ['BaseHandler', 'SimpleHandler', 'BaseCGIHandler', 'CGIHandler'] 10 11try: 12 dict 13except NameError: 14 def dict(items): 15 d = {} 16 for k,v in items: 17 d[k] = v 18 return d 19 20# Uncomment for 2.2 compatibility. 21#try: 22# True 23# False 24#except NameError: 25# True = not None 26# False = not True 27 28 29# Weekday and month names for HTTP date/time formatting; always English! 30_weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] 31_monthname = [None, # Dummy so we can use 1-based month numbers 32 "Jan", "Feb", "Mar", "Apr", "May", "Jun", 33 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 34 35def format_date_time(timestamp): 36 year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) 37 return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( 38 _weekdayname[wd], day, _monthname[month], year, hh, mm, ss 39 ) 40 41 42class BaseHandler: 43 """Manage the invocation of a WSGI application""" 44 45 # Configuration parameters; can override per-subclass or per-instance 46 wsgi_version = (1,0) 47 wsgi_multithread = True 48 wsgi_multiprocess = True 49 wsgi_run_once = False 50 51 origin_server = True # We are transmitting direct to client 52 http_version = "1.0" # Version that should be used for response 53 server_software = None # String name of server software, if any 54 55 # os_environ is used to supply configuration from the OS environment: 56 # by default it's a copy of 'os.environ' as of import time, but you can 57 # override this in e.g. your __init__ method. 58 os_environ = dict(os.environ.items()) 59 60 # Collaborator classes 61 wsgi_file_wrapper = FileWrapper # set to None to disable 62 headers_class = Headers # must be a Headers-like class 63 64 # Error handling (also per-subclass or per-instance) 65 traceback_limit = None # Print entire traceback to self.get_stderr() 66 error_status = "500 Internal Server Error" 67 error_headers = [('Content-Type','text/plain')] 68 error_body = "A server error occurred. Please contact the administrator." 69 70 # State variables (don't mess with these) 71 status = result = None 72 headers_sent = False 73 headers = None 74 bytes_sent = 0 75 76 def run(self, application): 77 """Invoke the application""" 78 # Note to self: don't move the close()! Asynchronous servers shouldn't 79 # call close() from finish_response(), so if you close() anywhere but 80 # the double-error branch here, you'll break asynchronous servers by 81 # prematurely closing. Async servers must return from 'run()' without 82 # closing if there might still be output to iterate over. 83 try: 84 self.setup_environ() 85 self.result = application(self.environ, self.start_response) 86 self.finish_response() 87 except: 88 try: 89 self.handle_error() 90 except: 91 # If we get an error handling an error, just give up already! 92 self.close() 93 raise # ...and let the actual server figure it out. 94 95 96 def setup_environ(self): 97 """Set up the environment for one request""" 98 99 env = self.environ = self.os_environ.copy() 100 self.add_cgi_vars() 101 102 env['wsgi.input'] = self.get_stdin() 103 env['wsgi.errors'] = self.get_stderr() 104 env['wsgi.version'] = self.wsgi_version 105 env['wsgi.run_once'] = self.wsgi_run_once 106 env['wsgi.url_scheme'] = self.get_scheme() 107 env['wsgi.multithread'] = self.wsgi_multithread 108 env['wsgi.multiprocess'] = self.wsgi_multiprocess 109 110 if self.wsgi_file_wrapper is not None: 111 env['wsgi.file_wrapper'] = self.wsgi_file_wrapper 112 113 if self.origin_server and self.server_software: 114 env.setdefault('SERVER_SOFTWARE',self.server_software) 115 116 117 def finish_response(self): 118 """Send any iterable data, then close self and the iterable 119 120 Subclasses intended for use in asynchronous servers will 121 want to redefine this method, such that it sets up callbacks 122 in the event loop to iterate over the data, and to call 123 'self.close()' once the response is finished. 124 """ 125 if not self.result_is_file() or not self.sendfile(): 126 for data in self.result: 127 self.write(data) 128 self.finish_content() 129 self.close() 130 131 132 def get_scheme(self): 133 """Return the URL scheme being used""" 134 return guess_scheme(self.environ) 135 136 137 def set_content_length(self): 138 """Compute Content-Length or switch to chunked encoding if possible""" 139 try: 140 blocks = len(self.result) 141 except (TypeError,AttributeError,NotImplementedError): 142 pass 143 else: 144 if blocks==1: 145 self.headers['Content-Length'] = str(self.bytes_sent) 146 return 147 # XXX Try for chunked encoding if origin server and client is 1.1 148 149 150 def cleanup_headers(self): 151 """Make any necessary header changes or defaults 152 153 Subclasses can extend this to add other defaults. 154 """ 155 if 'Content-Length' not in self.headers: 156 self.set_content_length() 157 158 def start_response(self, status, headers,exc_info=None): 159 """'start_response()' callable as specified by PEP 333""" 160 161 if exc_info: 162 try: 163 if self.headers_sent: 164 # Re-raise original exception if headers sent 165 raise exc_info[0], exc_info[1], exc_info[2] 166 finally: 167 exc_info = None # avoid dangling circular ref 168 elif self.headers is not None: 169 raise AssertionError("Headers already set!") 170 171 assert type(status) is StringType,"Status must be a string" 172 assert len(status)>=4,"Status must be at least 4 characters" 173 assert int(status[:3]),"Status message must begin w/3-digit code" 174 assert status[3]==" ", "Status message must have a space after code" 175 if __debug__: 176 for name,val in headers: 177 assert type(name) is StringType,"Header names must be strings" 178 assert type(val) is StringType,"Header values must be strings" 179 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed" 180 self.status = status 181 self.headers = self.headers_class(headers) 182 return self.write 183 184 185 def send_preamble(self): 186 """Transmit version/status/date/server, via self._write()""" 187 if self.origin_server: 188 if self.client_is_modern(): 189 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status)) 190 if 'Date' not in self.headers: 191 self._write( 192 'Date: %s\r\n' % format_date_time(time.time()) 193 ) 194 if self.server_software and 'Server' not in self.headers: 195 self._write('Server: %s\r\n' % self.server_software) 196 else: 197 self._write('Status: %s\r\n' % self.status) 198 199 def write(self, data): 200 """'write()' callable as specified by PEP 333""" 201 202 assert type(data) is StringType,"write() argument must be string" 203 204 if not self.status: 205 raise AssertionError("write() before start_response()") 206 207 elif not self.headers_sent: 208 # Before the first output, send the stored headers 209 self.bytes_sent = len(data) # make sure we know content-length 210 self.send_headers() 211 else: 212 self.bytes_sent += len(data) 213 214 # XXX check Content-Length and truncate if too many bytes written? 215 self._write(data) 216 self._flush() 217 218 219 def sendfile(self): 220 """Platform-specific file transmission 221 222 Override this method in subclasses to support platform-specific 223 file transmission. It is only called if the application's 224 return iterable ('self.result') is an instance of 225 'self.wsgi_file_wrapper'. 226 227 This method should return a true value if it was able to actually 228 transmit the wrapped file-like object using a platform-specific 229 approach. It should return a false value if normal iteration 230 should be used instead. An exception can be raised to indicate 231 that transmission was attempted, but failed. 232 233 NOTE: this method should call 'self.send_headers()' if 234 'self.headers_sent' is false and it is going to attempt direct 235 transmission of the file. 236 """ 237 return False # No platform-specific transmission by default 238 239 240 def finish_content(self): 241 """Ensure headers and content have both been sent""" 242 if not self.headers_sent: 243 # Only zero Content-Length if not set by the application (so 244 # that HEAD requests can be satisfied properly, see #3839) 245 self.headers.setdefault('Content-Length', "0") 246 self.send_headers() 247 else: 248 pass # XXX check if content-length was too short? 249 250 def close(self): 251 """Close the iterable (if needed) and reset all instance vars 252 253 Subclasses may want to also drop the client connection. 254 """ 255 try: 256 if hasattr(self.result,'close'): 257 self.result.close() 258 finally: 259 self.result = self.headers = self.status = self.environ = None 260 self.bytes_sent = 0; self.headers_sent = False 261 262 263 def send_headers(self): 264 """Transmit headers to the client, via self._write()""" 265 self.cleanup_headers() 266 self.headers_sent = True 267 if not self.origin_server or self.client_is_modern(): 268 self.send_preamble() 269 self._write(str(self.headers)) 270 271 272 def result_is_file(self): 273 """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'""" 274 wrapper = self.wsgi_file_wrapper 275 return wrapper is not None and isinstance(self.result,wrapper) 276 277 278 def client_is_modern(self): 279 """True if client can accept status and headers""" 280 return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9' 281 282 283 def log_exception(self,exc_info): 284 """Log the 'exc_info' tuple in the server log 285 286 Subclasses may override to retarget the output or change its format. 287 """ 288 try: 289 from traceback import print_exception 290 stderr = self.get_stderr() 291 print_exception( 292 exc_info[0], exc_info[1], exc_info[2], 293 self.traceback_limit, stderr 294 ) 295 stderr.flush() 296 finally: 297 exc_info = None 298 299 def handle_error(self): 300 """Log current error, and send error output to client if possible""" 301 self.log_exception(sys.exc_info()) 302 if not self.headers_sent: 303 self.result = self.error_output(self.environ, self.start_response) 304 self.finish_response() 305 # XXX else: attempt advanced recovery techniques for HTML or text? 306 307 def error_output(self, environ, start_response): 308 """WSGI mini-app to create error output 309 310 By default, this just uses the 'error_status', 'error_headers', 311 and 'error_body' attributes to generate an output page. It can 312 be overridden in a subclass to dynamically generate diagnostics, 313 choose an appropriate message for the user's preferred language, etc. 314 315 Note, however, that it's not recommended from a security perspective to 316 spit out diagnostics to any old user; ideally, you should have to do 317 something special to enable diagnostic output, which is why we don't 318 include any here! 319 """ 320 start_response(self.error_status,self.error_headers[:],sys.exc_info()) 321 return [self.error_body] 322 323 324 # Pure abstract methods; *must* be overridden in subclasses 325 326 def _write(self,data): 327 """Override in subclass to buffer data for send to client 328 329 It's okay if this method actually transmits the data; BaseHandler 330 just separates write and flush operations for greater efficiency 331 when the underlying system actually has such a distinction. 332 """ 333 raise NotImplementedError 334 335 def _flush(self): 336 """Override in subclass to force sending of recent '_write()' calls 337 338 It's okay if this method is a no-op (i.e., if '_write()' actually 339 sends the data. 340 """ 341 raise NotImplementedError 342 343 def get_stdin(self): 344 """Override in subclass to return suitable 'wsgi.input'""" 345 raise NotImplementedError 346 347 def get_stderr(self): 348 """Override in subclass to return suitable 'wsgi.errors'""" 349 raise NotImplementedError 350 351 def add_cgi_vars(self): 352 """Override in subclass to insert CGI variables in 'self.environ'""" 353 raise NotImplementedError 354 355 356class SimpleHandler(BaseHandler): 357 """Handler that's just initialized with streams, environment, etc. 358 359 This handler subclass is intended for synchronous HTTP/1.0 origin servers, 360 and handles sending the entire response output, given the correct inputs. 361 362 Usage:: 363 364 handler = SimpleHandler( 365 inp,out,err,env, multithread=False, multiprocess=True 366 ) 367 handler.run(app)""" 368 369 def __init__(self,stdin,stdout,stderr,environ, 370 multithread=True, multiprocess=False 371 ): 372 self.stdin = stdin 373 self.stdout = stdout 374 self.stderr = stderr 375 self.base_env = environ 376 self.wsgi_multithread = multithread 377 self.wsgi_multiprocess = multiprocess 378 379 def get_stdin(self): 380 return self.stdin 381 382 def get_stderr(self): 383 return self.stderr 384 385 def add_cgi_vars(self): 386 self.environ.update(self.base_env) 387 388 def _write(self,data): 389 self.stdout.write(data) 390 self._write = self.stdout.write 391 392 def _flush(self): 393 self.stdout.flush() 394 self._flush = self.stdout.flush 395 396 397class BaseCGIHandler(SimpleHandler): 398 399 """CGI-like systems using input/output/error streams and environ mapping 400 401 Usage:: 402 403 handler = BaseCGIHandler(inp,out,err,env) 404 handler.run(app) 405 406 This handler class is useful for gateway protocols like ReadyExec and 407 FastCGI, that have usable input/output/error streams and an environment 408 mapping. It's also the base class for CGIHandler, which just uses 409 sys.stdin, os.environ, and so on. 410 411 The constructor also takes keyword arguments 'multithread' and 412 'multiprocess' (defaulting to 'True' and 'False' respectively) to control 413 the configuration sent to the application. It sets 'origin_server' to 414 False (to enable CGI-like output), and assumes that 'wsgi.run_once' is 415 False. 416 """ 417 418 origin_server = False 419 420 421class CGIHandler(BaseCGIHandler): 422 423 """CGI-based invocation via sys.stdin/stdout/stderr and os.environ 424 425 Usage:: 426 427 CGIHandler().run(app) 428 429 The difference between this class and BaseCGIHandler is that it always 430 uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and 431 'wsgi.multiprocess' of 'True'. It does not take any initialization 432 parameters, but always uses 'sys.stdin', 'os.environ', and friends. 433 434 If you need to override any of these parameters, use BaseCGIHandler 435 instead. 436 """ 437 438 wsgi_run_once = True 439 # Do not allow os.environ to leak between requests in Google App Engine 440 # and other multi-run CGI use cases. This is not easily testable. 441 # See http://bugs.python.org/issue7250 442 os_environ = {} 443 444 def __init__(self): 445 BaseCGIHandler.__init__( 446 self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()), 447 multithread=False, multiprocess=True 448 ) 449