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