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