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