1 #!/usr/bin/env python 2 3 """ 4 BaseHTTPRequestHandler classes. 5 """ 6 7 import Generic 8 from Helpers.Request import MessageBodyStream, get_body_fields, get_storage_items, Cookie 9 from Helpers.Response import ConvertingStream 10 from Helpers.Auth import UserInfo 11 from Helpers.Session import SessionStore 12 from cgi import parse_qs, FieldStorage 13 from Cookie import SimpleCookie 14 from StringIO import StringIO 15 16 class Transaction(Generic.Transaction): 17 18 """ 19 BaseHTTPRequestHandler transaction interface. 20 """ 21 22 def __init__(self, trans): 23 24 """ 25 Initialise the transaction using the BaseHTTPRequestHandler instance 26 'trans'. 27 """ 28 29 self.trans = trans 30 31 # Other attributes of interest in instances of this class. 32 33 self.content_type = None 34 self.response_code = 200 35 self.content = StringIO() 36 self.headers_out = {} 37 self.cookies_out = SimpleCookie() 38 self.user = None 39 self.path_info = None 40 41 # Define the incoming cookies. 42 43 self.cookies_in = SimpleCookie(self.get_headers().get("cookie")) 44 45 # Cached information. 46 47 self.storage_body = None 48 49 # Special objects retained throughout the transaction. 50 51 self.session_store = None 52 53 def commit(self): 54 55 """ 56 A special method, synchronising the transaction with framework-specific 57 objects. 58 """ 59 60 # Close the session store. 61 62 if self.session_store is not None: 63 self.session_store.close() 64 65 # Prepare the response. 66 67 self.trans.send_response(self.response_code) 68 if self.content_type is not None: 69 self.trans.send_header("Content-Type", str(self.content_type)) 70 71 for header, value in self.headers_out.items(): 72 self.trans.send_header(self.format_header_value(header), self.format_header_value(value)) 73 74 # NOTE: May not be using the appropriate method. 75 76 for morsel in self.cookies_out.values(): 77 self.trans.send_header("Set-Cookie", morsel.OutputString()) 78 79 # Add possibly missing content length information. 80 # NOTE: This is really inefficient, but we need to buffer things to 81 # NOTE: permit out of order header setting. 82 83 self.content.seek(0) 84 content = self.content.read() 85 86 if not self.headers_out.has_key("Content-Length"): 87 self.trans.send_header("Content-Length", str(len(content))) 88 89 self.trans.end_headers() 90 self.trans.wfile.write(content) 91 92 # Server-related methods. 93 94 def get_server_name(self): 95 96 "Returns the server name." 97 98 # As ultimately found in SocketServer.BaseServer via SocketServer.BaseRequestHandler. 99 # NOTE: We assume that any usage of "" can be taken to mean "localhost". 100 101 return self.trans.server.server_address[0] or "localhost" 102 103 def get_server_port(self): 104 105 "Returns the server port as a string." 106 107 # As ultimately found in SocketServer.BaseServer via SocketServer.BaseRequestHandler. 108 109 return str(self.trans.server.server_address[1]) 110 111 # Request-related methods. 112 113 def get_request_stream(self): 114 115 """ 116 Returns the request stream for the transaction. 117 """ 118 119 return MessageBodyStream(self.trans.rfile, self.get_headers()) 120 121 def get_request_method(self): 122 123 """ 124 Returns the request method. 125 """ 126 127 return self.trans.command 128 129 def get_headers(self): 130 131 """ 132 Returns all request headers as a dictionary-like object mapping header 133 names to values. 134 135 NOTE: If duplicate header names are permitted, then this interface will 136 NOTE: need to change. 137 """ 138 139 return self.trans.headers 140 141 def get_header_values(self, key): 142 143 """ 144 Returns a list of all request header values associated with the given 145 'key'. Note that according to RFC 2616, 'key' is treated as a 146 case-insensitive string. 147 """ 148 149 return self.convert_to_list(self.trans.headers.get(key)) 150 151 def get_content_type(self): 152 153 """ 154 Returns the content type specified on the request, along with the 155 charset employed. 156 """ 157 158 return self.parse_content_type(self.trans.headers.get("content-type")) 159 160 def get_content_charsets(self): 161 162 """ 163 Returns the character set preferences. 164 """ 165 166 return self.parse_content_preferences(self.trans.headers.get("accept-charset")) 167 168 def get_content_languages(self): 169 170 """ 171 Returns extracted language information from the transaction. 172 """ 173 174 return self.parse_content_preferences(self.trans.headers.get("accept-language")) 175 176 def get_path(self): 177 178 """ 179 Returns the entire path from the request. 180 """ 181 182 return self.trans.path 183 184 def get_path_without_query(self): 185 186 """ 187 Returns the entire path from the request minus the query string. 188 """ 189 190 # Remove the query string from the end of the path. 191 192 return self.trans.path.split("?")[0] 193 194 def get_path_info(self): 195 196 """ 197 Returns the "path info" (the part of the URL after the resource name 198 handling the current request) from the request. 199 """ 200 201 return self.get_path_without_query() 202 203 def get_query_string(self): 204 205 """ 206 Returns the query string from the path in the request. 207 """ 208 209 t = self.trans.path.split("?") 210 if len(t) == 1: 211 return "" 212 else: 213 214 # NOTE: Overlook erroneous usage of "?" characters in the path. 215 216 return "?".join(t[1:]) 217 218 # Higher level request-related methods. 219 220 def get_fields_from_path(self): 221 222 """ 223 Extracts fields (or request parameters) from the path specified in the 224 transaction. The underlying framework may refuse to supply fields from 225 the path if handling a POST transaction. 226 227 Returns a dictionary mapping field names to lists of values (even if a 228 single value is associated with any given field name). 229 """ 230 231 # NOTE: Support at best ISO-8859-1 values. 232 233 fields = {} 234 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 235 fields[name] = [] 236 for value in values: 237 fields[name].append(unicode(value, "iso-8859-1")) 238 return fields 239 240 def get_fields_from_body(self, encoding=None): 241 242 """ 243 Extracts fields (or request parameters) from the message body in the 244 transaction. The optional 'encoding' parameter specifies the character 245 encoding of the message body for cases where no such information is 246 available, but where the default encoding is to be overridden. 247 248 Returns a dictionary mapping field names to lists of values (even if a 249 single value is associated with any given field name). Each value is 250 either a Unicode object (representing a simple form field, for example) 251 or a plain string (representing a file upload form field, for example). 252 """ 253 254 encoding = encoding or self.get_content_type().charset or self.default_charset 255 256 if self.storage_body is None: 257 self.storage_body = FieldStorage(fp=self.get_request_stream(), headers=self.get_headers(), 258 environ={"REQUEST_METHOD" : self.get_request_method()}, keep_blank_values=1) 259 260 # Avoid strange design issues with FieldStorage by checking the internal 261 # field list directly. 262 263 fields = {} 264 if self.storage_body.list is not None: 265 266 # Traverse the storage, finding each field value. 267 268 fields = get_body_fields(get_storage_items(self.storage_body), encoding) 269 270 return fields 271 272 def get_fields(self, encoding=None): 273 274 """ 275 Extracts fields (or request parameters) from both the path specified in 276 the transaction as well as the message body. The optional 'encoding' 277 parameter specifies the character encoding of the message body for cases 278 where no such information is available, but where the default encoding 279 is to be overridden. 280 281 Returns a dictionary mapping field names to lists of values (even if a 282 single value is associated with any given field name). Each value is 283 either a Unicode object (representing a simple form field, for example) 284 or a plain string (representing a file upload form field, for example). 285 286 Where a given field name is used in both the path and message body to 287 specify values, the values from both sources will be combined into a 288 single list associated with that field name. 289 """ 290 291 # Combine the two sources. 292 293 fields = {} 294 fields.update(self.get_fields_from_path()) 295 for name, values in self.get_fields_from_body(encoding).items(): 296 if not fields.has_key(name): 297 fields[name] = values 298 else: 299 fields[name] += values 300 return fields 301 302 def get_user(self): 303 304 """ 305 Extracts user information from the transaction. 306 307 Returns a username as a string or None if no user is defined. 308 """ 309 310 if self.user is not None: 311 return self.user 312 313 auth_header = self.get_headers().get("authorization") 314 if auth_header: 315 return UserInfo(auth_header).username 316 else: 317 return None 318 319 def get_cookies(self): 320 321 """ 322 Obtains cookie information from the request. 323 324 Returns a dictionary mapping cookie names to cookie objects. 325 """ 326 327 return self.process_cookies(self.cookies_in) 328 329 def get_cookie(self, cookie_name): 330 331 """ 332 Obtains cookie information from the request. 333 334 Returns a cookie object for the given 'cookie_name' or None if no such 335 cookie exists. 336 """ 337 338 cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name)) 339 if cookie is not None: 340 return Cookie(cookie_name, self.decode_cookie_value(cookie.value)) 341 else: 342 return None 343 344 # Response-related methods. 345 346 def get_response_stream(self): 347 348 """ 349 Returns the response stream for the transaction. 350 """ 351 352 # Return a stream which is later emptied into the real stream. 353 # Unicode can upset this operation. Using either the specified charset 354 # or a default encoding. 355 356 encoding = self.get_response_stream_encoding() 357 return ConvertingStream(self.content, encoding) 358 359 def get_response_stream_encoding(self): 360 361 """ 362 Returns the response stream encoding. 363 """ 364 365 if self.content_type: 366 encoding = self.content_type.charset 367 else: 368 encoding = None 369 return encoding or self.default_charset 370 371 def get_response_code(self): 372 373 """ 374 Get the response code associated with the transaction. If no response 375 code is defined, None is returned. 376 """ 377 378 return self.response_code 379 380 def set_response_code(self, response_code): 381 382 """ 383 Set the 'response_code' using a numeric constant defined in the HTTP 384 specification. 385 """ 386 387 self.response_code = response_code 388 389 def set_header_value(self, header, value): 390 391 """ 392 Set the HTTP 'header' with the given 'value'. 393 """ 394 395 # The header is not written out immediately due to the buffering in use. 396 397 self.headers_out[header] = value 398 399 def set_content_type(self, content_type): 400 401 """ 402 Sets the 'content_type' for the response. 403 """ 404 405 # The content type has to be written as a header, before actual content, 406 # but after the response line. This means that some kind of buffering is 407 # required. Hence, we don't write the header out immediately. 408 409 self.content_type = content_type 410 411 # Higher level response-related methods. 412 413 def set_cookie(self, cookie): 414 415 """ 416 Stores the given 'cookie' object in the response. 417 """ 418 419 # NOTE: If multiple cookies of the same name could be specified, this 420 # NOTE: could need changing. 421 422 self.set_cookie_value(cookie.name, cookie.value) 423 424 def set_cookie_value(self, name, value, path=None, expires=None): 425 426 """ 427 Stores a cookie with the given 'name' and 'value' in the response. 428 429 The optional 'path' is a string which specifies the scope of the cookie, 430 and the optional 'expires' parameter is a value compatible with the 431 time.time function, and indicates the expiry date/time of the cookie. 432 """ 433 434 name = self.encode_cookie_value(name) 435 self.cookies_out[name] = self.encode_cookie_value(value) 436 if path is not None: 437 self.cookies_out[name]["path"] = path 438 if expires is not None: 439 self.cookies_out[name]["expires"] = expires 440 441 def delete_cookie(self, cookie_name): 442 443 """ 444 Adds to the response a request that the cookie with the given 445 'cookie_name' be deleted/discarded by the client. 446 """ 447 448 # Create a special cookie, given that we do not know whether the browser 449 # has been sent the cookie or not. 450 # NOTE: Magic discovered in Webware. 451 452 name = self.encode_cookie_value(cookie_name) 453 self.cookies_out[name] = "" 454 self.cookies_out[name]["path"] = "/" 455 self.cookies_out[name]["expires"] = 0 456 self.cookies_out[name]["max-age"] = 0 457 458 # Session-related methods. 459 460 def get_session(self, create=1): 461 462 """ 463 Gets a session corresponding to an identifier supplied in the 464 transaction. 465 466 If no session has yet been established according to information 467 provided in the transaction then the optional 'create' parameter 468 determines whether a new session will be established. 469 470 Where no session has been established and where 'create' is set to 0 471 then None is returned. In all other cases, a session object is created 472 (where appropriate) and returned. 473 """ 474 475 # NOTE: Requires configuration. 476 477 if self.session_store is None: 478 self.session_store = SessionStore(self, "WebStack-sessions") 479 return self.session_store.get_session(create) 480 481 def expire_session(self): 482 483 """ 484 Expires any session established according to information provided in the 485 transaction. 486 """ 487 488 # NOTE: Requires configuration. 489 490 if self.session_store is None: 491 self.session_store = SessionStore(self, "WebStack-sessions") 492 self.session_store.expire_session() 493 494 # vim: tabstop=4 expandtab shiftwidth=4