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