1 """Base classes for server/gateway implementations""" 2 3 from types import StringType 4 from util import FileWrapper, guess_scheme, is_hop_by_hop 5 from headers import Headers 6 7 import sys, os, time 8 9 __all__ = ['BaseHandler', 'SimpleHandler', 'BaseCGIHandler', 'CGIHandler'] 10 11 try: 12 dict 13 except 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 35 def 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 42 class 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 try: 126 if not self.result_is_file() or not self.sendfile(): 127 for data in self.result: 128 self.write(data) 129 self.finish_content() 130 finally: 131 self.close() 132 133 134 def get_scheme(self): 135 """Return the URL scheme being used""" 136 return guess_scheme(self.environ) 137 138 139 def set_content_length(self): 140 """Compute Content-Length or switch to chunked encoding if possible""" 141 try: 142 blocks = len(self.result) 143 except (TypeError,AttributeError,NotImplementedError): 144 pass 145 else: 146 if blocks==1: 147 self.headers['Content-Length'] = str(self.bytes_sent) 148 return 149 # XXX Try for chunked encoding if origin server and client is 1.1 150 151 152 def cleanup_headers(self): 153 """Make any necessary header changes or defaults 154 155 Subclasses can extend this to add other defaults. 156 """ 157 if 'Content-Length' not in self.headers: 158 self.set_content_length() 159 160 def start_response(self, status, headers,exc_info=None): 161 """'start_response()' callable as specified by PEP 333""" 162 163 if exc_info: 164 try: 165 if self.headers_sent: 166 # Re-raise original exception if headers sent 167 raise exc_info[0], exc_info[1], exc_info[2] 168 finally: 169 exc_info = None # avoid dangling circular ref 170 elif self.headers is not None: 171 raise AssertionError("Headers already set!") 172 173 assert type(status) is StringType,"Status must be a string" 174 assert len(status)>=4,"Status must be at least 4 characters" 175 assert int(status[:3]),"Status message must begin w/3-digit code" 176 assert status[3]==" ", "Status message must have a space after code" 177 if __debug__: 178 for name,val in headers: 179 assert type(name) is StringType,"Header names must be strings" 180 assert type(val) is StringType,"Header values must be strings" 181 assert not is_hop_by_hop(name),"Hop-by-hop headers not allowed" 182 self.status = status 183 self.headers = self.headers_class(headers) 184 return self.write 185 186 187 def send_preamble(self): 188 """Transmit version/status/date/server, via self._write()""" 189 if self.origin_server: 190 if self.client_is_modern(): 191 self._write('HTTP/%s %s\r\n' % (self.http_version,self.status)) 192 if 'Date' not in self.headers: 193 self._write( 194 'Date: %s\r\n' % format_date_time(time.time()) 195 ) 196 if self.server_software and 'Server' not in self.headers: 197 self._write('Server: %s\r\n' % self.server_software) 198 else: 199 self._write('Status: %s\r\n' % self.status) 200 201 def write(self, data): 202 """'write()' callable as specified by PEP 333""" 203 204 assert type(data) is StringType,"write() argument must be string" 205 206 if not self.status: 207 raise AssertionError("write() before start_response()") 208 209 elif not self.headers_sent: 210 # Before the first output, send the stored headers 211 self.bytes_sent = len(data) # make sure we know content-length 212 self.send_headers() 213 else: 214 self.bytes_sent += len(data) 215 216 # XXX check Content-Length and truncate if too many bytes written? 217 self._write(data) 218 self._flush() 219 220 221 def sendfile(self): 222 """Platform-specific file transmission 223 224 Override this method in subclasses to support platform-specific 225 file transmission. It is only called if the application's 226 return iterable ('self.result') is an instance of 227 'self.wsgi_file_wrapper'. 228 229 This method should return a true value if it was able to actually 230 transmit the wrapped file-like object using a platform-specific 231 approach. It should return a false value if normal iteration 232 should be used instead. An exception can be raised to indicate 233 that transmission was attempted, but failed. 234 235 NOTE: this method should call 'self.send_headers()' if 236 'self.headers_sent' is false and it is going to attempt direct 237 transmission of the file. 238 """ 239 return False # No platform-specific transmission by default 240 241 242 def finish_content(self): 243 """Ensure headers and content have both been sent""" 244 if not self.headers_sent: 245 # Only zero Content-Length if not set by the application (so 246 # that HEAD requests can be satisfied properly, see #3839) 247 self.headers.setdefault('Content-Length', "0") 248 self.send_headers() 249 else: 250 pass # XXX check if content-length was too short? 251 252 def close(self): 253 """Close the iterable (if needed) and reset all instance vars 254 255 Subclasses may want to also drop the client connection. 256 """ 257 try: 258 if hasattr(self.result,'close'): 259 self.result.close() 260 finally: 261 self.result = self.headers = self.status = self.environ = None 262 self.bytes_sent = 0; self.headers_sent = False 263 264 265 def send_headers(self): 266 """Transmit headers to the client, via self._write()""" 267 self.cleanup_headers() 268 self.headers_sent = True 269 if not self.origin_server or self.client_is_modern(): 270 self.send_preamble() 271 self._write(str(self.headers)) 272 273 274 def result_is_file(self): 275 """True if 'self.result' is an instance of 'self.wsgi_file_wrapper'""" 276 wrapper = self.wsgi_file_wrapper 277 return wrapper is not None and isinstance(self.result,wrapper) 278 279 280 def client_is_modern(self): 281 """True if client can accept status and headers""" 282 return self.environ['SERVER_PROTOCOL'].upper() != 'HTTP/0.9' 283 284 285 def log_exception(self,exc_info): 286 """Log the 'exc_info' tuple in the server log 287 288 Subclasses may override to retarget the output or change its format. 289 """ 290 try: 291 from traceback import print_exception 292 stderr = self.get_stderr() 293 print_exception( 294 exc_info[0], exc_info[1], exc_info[2], 295 self.traceback_limit, stderr 296 ) 297 stderr.flush() 298 finally: 299 exc_info = None 300 301 def handle_error(self): 302 """Log current error, and send error output to client if possible""" 303 self.log_exception(sys.exc_info()) 304 if not self.headers_sent: 305 self.result = self.error_output(self.environ, self.start_response) 306 self.finish_response() 307 # XXX else: attempt advanced recovery techniques for HTML or text? 308 309 def error_output(self, environ, start_response): 310 """WSGI mini-app to create error output 311 312 By default, this just uses the 'error_status', 'error_headers', 313 and 'error_body' attributes to generate an output page. It can 314 be overridden in a subclass to dynamically generate diagnostics, 315 choose an appropriate message for the user's preferred language, etc. 316 317 Note, however, that it's not recommended from a security perspective to 318 spit out diagnostics to any old user; ideally, you should have to do 319 something special to enable diagnostic output, which is why we don't 320 include any here! 321 """ 322 start_response(self.error_status,self.error_headers[:],sys.exc_info()) 323 return [self.error_body] 324 325 326 # Pure abstract methods; *must* be overridden in subclasses 327 328 def _write(self,data): 329 """Override in subclass to buffer data for send to client 330 331 It's okay if this method actually transmits the data; BaseHandler 332 just separates write and flush operations for greater efficiency 333 when the underlying system actually has such a distinction. 334 """ 335 raise NotImplementedError 336 337 def _flush(self): 338 """Override in subclass to force sending of recent '_write()' calls 339 340 It's okay if this method is a no-op (i.e., if '_write()' actually 341 sends the data. 342 """ 343 raise NotImplementedError 344 345 def get_stdin(self): 346 """Override in subclass to return suitable 'wsgi.input'""" 347 raise NotImplementedError 348 349 def get_stderr(self): 350 """Override in subclass to return suitable 'wsgi.errors'""" 351 raise NotImplementedError 352 353 def add_cgi_vars(self): 354 """Override in subclass to insert CGI variables in 'self.environ'""" 355 raise NotImplementedError 356 357 358 class SimpleHandler(BaseHandler): 359 """Handler that's just initialized with streams, environment, etc. 360 361 This handler subclass is intended for synchronous HTTP/1.0 origin servers, 362 and handles sending the entire response output, given the correct inputs. 363 364 Usage:: 365 366 handler = SimpleHandler( 367 inp,out,err,env, multithread=False, multiprocess=True 368 ) 369 handler.run(app)""" 370 371 def __init__(self,stdin,stdout,stderr,environ, 372 multithread=True, multiprocess=False 373 ): 374 self.stdin = stdin 375 self.stdout = stdout 376 self.stderr = stderr 377 self.base_env = environ 378 self.wsgi_multithread = multithread 379 self.wsgi_multiprocess = multiprocess 380 381 def get_stdin(self): 382 return self.stdin 383 384 def get_stderr(self): 385 return self.stderr 386 387 def add_cgi_vars(self): 388 self.environ.update(self.base_env) 389 390 def _write(self,data): 391 self.stdout.write(data) 392 self._write = self.stdout.write 393 394 def _flush(self): 395 self.stdout.flush() 396 self._flush = self.stdout.flush 397 398 399 class BaseCGIHandler(SimpleHandler): 400 401 """CGI-like systems using input/output/error streams and environ mapping 402 403 Usage:: 404 405 handler = BaseCGIHandler(inp,out,err,env) 406 handler.run(app) 407 408 This handler class is useful for gateway protocols like ReadyExec and 409 FastCGI, that have usable input/output/error streams and an environment 410 mapping. It's also the base class for CGIHandler, which just uses 411 sys.stdin, os.environ, and so on. 412 413 The constructor also takes keyword arguments 'multithread' and 414 'multiprocess' (defaulting to 'True' and 'False' respectively) to control 415 the configuration sent to the application. It sets 'origin_server' to 416 False (to enable CGI-like output), and assumes that 'wsgi.run_once' is 417 False. 418 """ 419 420 origin_server = False 421 422 423 class CGIHandler(BaseCGIHandler): 424 425 """CGI-based invocation via sys.stdin/stdout/stderr and os.environ 426 427 Usage:: 428 429 CGIHandler().run(app) 430 431 The difference between this class and BaseCGIHandler is that it always 432 uses 'wsgi.run_once' of 'True', 'wsgi.multithread' of 'False', and 433 'wsgi.multiprocess' of 'True'. It does not take any initialization 434 parameters, but always uses 'sys.stdin', 'os.environ', and friends. 435 436 If you need to override any of these parameters, use BaseCGIHandler 437 instead. 438 """ 439 440 wsgi_run_once = True 441 # Do not allow os.environ to leak between requests in Google App Engine 442 # and other multi-run CGI use cases. This is not easily testable. 443 # See http://bugs.python.org/issue7250 444 os_environ = {} 445 446 def __init__(self): 447 BaseCGIHandler.__init__( 448 self, sys.stdin, sys.stdout, sys.stderr, dict(os.environ.items()), 449 multithread=False, multiprocess=True 450 ) 451