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 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 import Cookie 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 = Cookie.SimpleCookie() 38 self.user = None 39 40 # Define the incoming cookies. 41 42 self.cookies_in = Cookie.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.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 return self.cookies_in.get(cookie_name) 319 320 # Response-related methods. 321 322 def get_response_stream(self): 323 324 """ 325 Returns the response stream for the transaction. 326 """ 327 328 # Return a stream which is later emptied into the real stream. 329 # Unicode can upset this operation. Using either the specified charset 330 # or a default encoding. 331 332 encoding = self.get_response_stream_encoding() 333 return ConvertingStream(self.content, encoding) 334 335 def get_response_stream_encoding(self): 336 337 """ 338 Returns the response stream encoding. 339 """ 340 341 if self.content_type: 342 encoding = self.content_type.charset 343 else: 344 encoding = None 345 return encoding or self.default_charset 346 347 def get_response_code(self): 348 349 """ 350 Get the response code associated with the transaction. If no response 351 code is defined, None is returned. 352 """ 353 354 return self.response_code 355 356 def set_response_code(self, response_code): 357 358 """ 359 Set the 'response_code' using a numeric constant defined in the HTTP 360 specification. 361 """ 362 363 self.response_code = response_code 364 365 def set_header_value(self, header, value): 366 367 """ 368 Set the HTTP 'header' with the given 'value'. 369 """ 370 371 # The header is not written out immediately due to the buffering in use. 372 373 self.headers_out[header] = value 374 375 def set_content_type(self, content_type): 376 377 """ 378 Sets the 'content_type' for the response. 379 """ 380 381 # The content type has to be written as a header, before actual content, 382 # but after the response line. This means that some kind of buffering is 383 # required. Hence, we don't write the header out immediately. 384 385 self.content_type = content_type 386 387 # Higher level response-related methods. 388 389 def set_cookie(self, cookie): 390 391 """ 392 Stores the given 'cookie' object in the response. 393 """ 394 395 # NOTE: If multiple cookies of the same name could be specified, this 396 # NOTE: could need changing. 397 398 self.cookies_out[cookie.name] = cookie.value 399 400 def set_cookie_value(self, name, value, path=None, expires=None): 401 402 """ 403 Stores a cookie with the given 'name' and 'value' in the response. 404 405 The optional 'path' is a string which specifies the scope of the cookie, 406 and the optional 'expires' parameter is a value compatible with the 407 time.time function, and indicates the expiry date/time of the cookie. 408 """ 409 410 self.cookies_out[name] = value 411 if path is not None: 412 self.cookies_out[name]["path"] = path 413 if expires is not None: 414 self.cookies_out[name]["expires"] = expires 415 416 def delete_cookie(self, cookie_name): 417 418 """ 419 Adds to the response a request that the cookie with the given 420 'cookie_name' be deleted/discarded by the client. 421 """ 422 423 # Create a special cookie, given that we do not know whether the browser 424 # has been sent the cookie or not. 425 # NOTE: Magic discovered in Webware. 426 427 self.cookies_out[cookie_name] = "" 428 self.cookies_out[cookie_name]["path"] = "/" 429 self.cookies_out[cookie_name]["expires"] = 0 430 self.cookies_out[cookie_name]["max-age"] = 0 431 432 # Session-related methods. 433 434 def get_session(self, create=1): 435 436 """ 437 Gets a session corresponding to an identifier supplied in the 438 transaction. 439 440 If no session has yet been established according to information 441 provided in the transaction then the optional 'create' parameter 442 determines whether a new session will be established. 443 444 Where no session has been established and where 'create' is set to 0 445 then None is returned. In all other cases, a session object is created 446 (where appropriate) and returned. 447 """ 448 449 # NOTE: Requires configuration. 450 451 if self.session_store is None: 452 self.session_store = SessionStore(self, "WebStack-sessions") 453 return self.session_store.get_session(create) 454 455 def expire_session(self): 456 457 """ 458 Expires any session established according to information provided in the 459 transaction. 460 """ 461 462 # NOTE: Requires configuration. 463 464 if self.session_store is None: 465 self.session_store = SessionStore(self, "WebStack-sessions") 466 self.session_store.expire_session() 467 468 # Application-specific methods. 469 470 def set_user(self, username): 471 472 """ 473 An application-specific method which sets the user information with 474 'username' in the transaction. This affects subsequent calls to 475 'get_user'. 476 """ 477 478 self.user = username 479 480 # vim: tabstop=4 expandtab shiftwidth=4