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