1 #!/usr/bin/env python 2 3 """ 4 BaseHTTPRequestHandler classes. 5 6 Copyright (C) 2004, 2005, 2006, 2007 Paul Boddie <paul@boddie.org.uk> 7 8 This library is free software; you can redistribute it and/or 9 modify it under the terms of the GNU Lesser General Public 10 License as published by the Free Software Foundation; either 11 version 2.1 of the License, or (at your option) any later version. 12 13 This library is distributed in the hope that it will be useful, 14 but WITHOUT ANY WARRANTY; without even the implied warranty of 15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 Lesser General Public License for more details. 17 18 You should have received a copy of the GNU Lesser General Public 19 License along with this library; if not, write to the Free Software 20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 21 """ 22 23 import WebStack.Generic 24 from WebStack.Helpers.Request import MessageBodyStream, get_body_fields, decode_value, get_storage_items, Cookie 25 from WebStack.Helpers.Response import ConvertingStream 26 from WebStack.Helpers.Auth import UserInfo 27 from WebStack.Helpers.Session import SessionStore 28 from cgi import parse_qs, FieldStorage 29 from Cookie import SimpleCookie 30 from StringIO import StringIO 31 32 class Transaction(WebStack.Generic.Transaction): 33 34 """ 35 BaseHTTPRequestHandler transaction interface. 36 """ 37 38 def __init__(self, trans): 39 40 """ 41 Initialise the transaction using the BaseHTTPRequestHandler instance 42 'trans'. 43 """ 44 45 self.trans = trans 46 47 # Other attributes of interest in instances of this class. 48 49 self.content_type = None 50 self.response_code = 200 51 self.content = StringIO() 52 self.headers_out = {} 53 self.cookies_out = SimpleCookie() 54 55 # Define the incoming cookies. 56 57 self.cookies_in = SimpleCookie(self.get_headers().get("cookie")) 58 59 # Cached information. 60 61 self.storage_body = None 62 63 # Special objects retained throughout the transaction. 64 65 self.session_store = None 66 67 def commit(self): 68 69 """ 70 A special method, synchronising the transaction with framework-specific 71 objects. 72 """ 73 74 # Close the session store. 75 76 if self.session_store is not None: 77 self.session_store.close() 78 79 # Prepare the response. 80 81 self.trans.send_response(self.response_code) 82 if self.content_type is not None: 83 self.trans.send_header("Content-Type", str(self.content_type)) 84 85 for header, value in self.headers_out.items(): 86 self.trans.send_header(self.format_header_value(header), self.format_header_value(value)) 87 88 # NOTE: May not be using the appropriate method. 89 90 for morsel in self.cookies_out.values(): 91 self.trans.send_header("Set-Cookie", morsel.OutputString()) 92 93 # Add possibly missing content length information. 94 # NOTE: This is really inefficient, but we need to buffer things to 95 # NOTE: permit out of order header setting. 96 97 self.content.seek(0) 98 content = self.content.read() 99 100 if not self.headers_out.has_key("Content-Length"): 101 self.trans.send_header("Content-Length", str(len(content))) 102 103 self.trans.end_headers() 104 self.trans.wfile.write(content) 105 106 def rollback(self): 107 108 """ 109 A special method, partially synchronising the transaction with 110 framework-specific objects, but discarding previously emitted content 111 that is to be replaced by an error message. 112 """ 113 114 self.content = StringIO() 115 self.headers_out = {} 116 self.cookies_out = SimpleCookie() 117 118 # Server-related methods. 119 120 def get_server_name(self): 121 122 "Returns the server name." 123 124 # As ultimately found in SocketServer.BaseServer via SocketServer.BaseRequestHandler. 125 # NOTE: We assume that any usage of "" can be taken to mean "localhost". 126 127 return self.trans.server.server_address[0] or "localhost" 128 129 def get_server_port(self): 130 131 "Returns the server port as a string." 132 133 # As ultimately found in SocketServer.BaseServer via SocketServer.BaseRequestHandler. 134 135 return str(self.trans.server.server_address[1]) 136 137 # Request-related methods. 138 139 def get_request_stream(self): 140 141 """ 142 Returns the request stream for the transaction. 143 """ 144 145 return MessageBodyStream(self.trans.rfile, self.get_headers()) 146 147 def get_request_method(self): 148 149 """ 150 Returns the request method. 151 """ 152 153 return self.trans.command 154 155 def get_headers(self): 156 157 """ 158 Returns all request headers as a dictionary-like object mapping header 159 names to values. 160 161 NOTE: If duplicate header names are permitted, then this interface will 162 NOTE: need to change. 163 """ 164 165 return self.trans.headers 166 167 def get_header_values(self, key): 168 169 """ 170 Returns a list of all request header values associated with the given 171 'key'. Note that according to RFC 2616, 'key' is treated as a 172 case-insensitive string. 173 """ 174 175 return self.convert_to_list(self.trans.headers.get(key)) 176 177 def get_content_type(self): 178 179 """ 180 Returns the content type specified on the request, along with the 181 charset employed. 182 """ 183 184 return self.parse_content_type(self.trans.headers.get("content-type")) 185 186 def get_content_charsets(self): 187 188 """ 189 Returns the character set preferences. 190 """ 191 192 return self.parse_content_preferences(self.trans.headers.get("accept-charset")) 193 194 def get_content_languages(self): 195 196 """ 197 Returns extracted language information from the transaction. 198 """ 199 200 return self.parse_content_preferences(self.trans.headers.get("accept-language")) 201 202 def get_path(self, encoding=None): 203 204 """ 205 Returns the entire path from the request as a Unicode object. Any "URL 206 encoded" character values in the part of the path before the query 207 string will be decoded and presented as genuine characters; the query 208 string will remain "URL encoded", however. 209 210 If the optional 'encoding' is set, use that in preference to the default 211 encoding to convert the path into a form not containing "URL encoded" 212 character values. 213 """ 214 215 path = self.get_path_without_query(encoding) 216 qs = self.get_query_string() 217 if qs: 218 return path + "?" + qs 219 else: 220 return path 221 222 def get_path_without_query(self, encoding=None): 223 224 """ 225 Returns the entire path from the request minus the query string as a 226 Unicode object containing genuine characters (as opposed to "URL 227 encoded" character values). 228 229 If the optional 'encoding' is set, use that in preference to the default 230 encoding to convert the path into a form not containing "URL encoded" 231 character values. 232 """ 233 234 # Remove the query string from the end of the path. 235 236 return self.decode_path(self.trans.path.split("?")[0], encoding) 237 238 def get_path_info(self, encoding=None): 239 240 """ 241 Returns the "path info" (the part of the URL after the resource name 242 handling the current request) from the request as a Unicode object 243 containing genuine characters (as opposed to "URL encoded" character 244 values). 245 246 If the optional 'encoding' is set, use that in preference to the default 247 encoding to convert the path into a form not containing "URL encoded" 248 character values. 249 """ 250 251 return self.get_path_without_query(encoding) 252 253 def get_query_string(self): 254 255 """ 256 Returns the query string from the path in the request. 257 """ 258 259 t = self.trans.path.split("?") 260 if len(t) == 1: 261 return "" 262 else: 263 264 # NOTE: Overlook erroneous usage of "?" characters in the path. 265 266 return "?".join(t[1:]) 267 268 # Higher level request-related methods. 269 270 def get_fields_from_path(self, encoding=None): 271 272 """ 273 Extracts fields (or request parameters) from the path specified in the 274 transaction. The underlying framework may refuse to supply fields from 275 the path if handling a POST transaction. The optional 'encoding' 276 parameter specifies the character encoding of the query string for cases 277 where the default encoding is to be overridden. 278 279 Returns a dictionary mapping field names to lists of values (even if a 280 single value is associated with any given field name). 281 """ 282 283 # NOTE: Support at best ISO-8859-1 values. 284 285 encoding = encoding or self.default_charset 286 287 fields = {} 288 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 289 name = decode_value(name, encoding) 290 fields[name] = [] 291 for value in values: 292 value = decode_value(value, encoding) 293 fields[name].append(value) 294 return fields 295 296 def get_fields_from_body(self, encoding=None): 297 298 """ 299 Extracts fields (or request parameters) from the message body in the 300 transaction. The optional 'encoding' parameter specifies the character 301 encoding of the message body for cases where no such information is 302 available, but where the default encoding is to be overridden. 303 304 Returns a dictionary mapping field names to lists of values (even if a 305 single value is associated with any given field name). Each value is 306 either a Unicode object (representing a simple form field, for example) 307 or a WebStack.Helpers.Request.FileContent object (representing a file 308 upload form field). 309 """ 310 311 encoding = encoding or self.get_content_type().charset or self.default_charset 312 313 if self.storage_body is None: 314 self.storage_body = FieldStorage(fp=self.get_request_stream(), headers=self.get_headers(), 315 environ={"REQUEST_METHOD" : self.get_request_method()}, keep_blank_values=1) 316 317 # Avoid strange design issues with FieldStorage by checking the internal 318 # field list directly. 319 320 fields = {} 321 if self.storage_body.list is not None: 322 323 # Traverse the storage, finding each field value. 324 325 fields = get_body_fields(get_storage_items(self.storage_body), encoding) 326 327 return fields 328 329 def get_fields(self, encoding=None): 330 331 """ 332 Extracts fields (or request parameters) from both the path specified in 333 the transaction as well as the message body. The optional 'encoding' 334 parameter specifies the character encoding of the message body for cases 335 where no such information is available, but where the default encoding 336 is to be overridden. 337 338 Returns a dictionary mapping field names to lists of values (even if a 339 single value is associated with any given field name). Each value is 340 either a Unicode object (representing a simple form field, for example) 341 or a WebStack.Helpers.Request.FileContent object (representing a file 342 upload form field). 343 344 Where a given field name is used in both the path and message body to 345 specify values, the values from both sources will be combined into a 346 single list associated with that field name. 347 """ 348 349 # Combine the two sources. 350 351 fields = {} 352 fields.update(self.get_fields_from_path()) 353 for name, values in self.get_fields_from_body(encoding).items(): 354 if not fields.has_key(name): 355 fields[name] = values 356 else: 357 fields[name] += values 358 return fields 359 360 def get_user(self): 361 362 """ 363 Extracts user information from the transaction. 364 365 Returns a username as a string or None if no user is defined. 366 """ 367 368 if self.user is not None: 369 return self.user 370 371 auth_header = self.get_headers().get("authorization") 372 if auth_header: 373 return UserInfo(auth_header).username 374 else: 375 return None 376 377 def get_cookies(self): 378 379 """ 380 Obtains cookie information from the request. 381 382 Returns a dictionary mapping cookie names to cookie objects. 383 """ 384 385 return self.process_cookies(self.cookies_in) 386 387 def get_cookie(self, cookie_name): 388 389 """ 390 Obtains cookie information from the request. 391 392 Returns a cookie object for the given 'cookie_name' or None if no such 393 cookie exists. 394 """ 395 396 cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name)) 397 if cookie is not None: 398 return Cookie(cookie_name, self.decode_cookie_value(cookie.value)) 399 else: 400 return None 401 402 # Response-related methods. 403 404 def get_response_stream(self): 405 406 """ 407 Returns the response stream for the transaction. 408 """ 409 410 # Return a stream which is later emptied into the real stream. 411 # Unicode can upset this operation. Using either the specified charset 412 # or a default encoding. 413 414 encoding = self.get_response_stream_encoding() 415 return ConvertingStream(self.content, encoding) 416 417 def get_response_stream_encoding(self): 418 419 """ 420 Returns the response stream encoding. 421 """ 422 423 if self.content_type: 424 encoding = self.content_type.charset 425 else: 426 encoding = None 427 return encoding or self.default_charset 428 429 def get_response_code(self): 430 431 """ 432 Get the response code associated with the transaction. If no response 433 code is defined, None is returned. 434 """ 435 436 return self.response_code 437 438 def set_response_code(self, response_code): 439 440 """ 441 Set the 'response_code' using a numeric constant defined in the HTTP 442 specification. 443 """ 444 445 self.response_code = response_code 446 447 def set_header_value(self, header, value): 448 449 """ 450 Set the HTTP 'header' with the given 'value'. 451 """ 452 453 # The header is not written out immediately due to the buffering in use. 454 455 self.headers_out[header] = value 456 457 def set_content_type(self, content_type): 458 459 """ 460 Sets the 'content_type' for the response. 461 """ 462 463 # The content type has to be written as a header, before actual content, 464 # but after the response line. This means that some kind of buffering is 465 # required. Hence, we don't write the header out immediately. 466 467 self.content_type = content_type 468 469 # Higher level response-related methods. 470 471 def set_cookie(self, cookie): 472 473 """ 474 Stores the given 'cookie' object in the response. 475 """ 476 477 # NOTE: If multiple cookies of the same name could be specified, this 478 # NOTE: could need changing. 479 480 self.set_cookie_value(cookie.name, cookie.value) 481 482 def set_cookie_value(self, name, value, path=None, expires=None): 483 484 """ 485 Stores a cookie with the given 'name' and 'value' in the response. 486 487 The optional 'path' is a string which specifies the scope of the cookie, 488 and the optional 'expires' parameter is a value compatible with the 489 time.time function, and indicates the expiry date/time of the cookie. 490 """ 491 492 name = self.encode_cookie_value(name) 493 self.cookies_out[name] = self.encode_cookie_value(value) 494 if path is not None: 495 self.cookies_out[name]["path"] = path 496 if expires is not None: 497 self.cookies_out[name]["expires"] = expires 498 499 def delete_cookie(self, cookie_name): 500 501 """ 502 Adds to the response a request that the cookie with the given 503 'cookie_name' be deleted/discarded by the client. 504 """ 505 506 # Create a special cookie, given that we do not know whether the browser 507 # has been sent the cookie or not. 508 # NOTE: Magic discovered in Webware. 509 510 name = self.encode_cookie_value(cookie_name) 511 self.cookies_out[name] = "" 512 self.cookies_out[name]["path"] = "/" 513 self.cookies_out[name]["expires"] = 0 514 self.cookies_out[name]["max-age"] = 0 515 516 # Session-related methods. 517 518 def get_session(self, create=1): 519 520 """ 521 Gets a session corresponding to an identifier supplied in the 522 transaction. 523 524 If no session has yet been established according to information 525 provided in the transaction then the optional 'create' parameter 526 determines whether a new session will be established. 527 528 Where no session has been established and where 'create' is set to 0 529 then None is returned. In all other cases, a session object is created 530 (where appropriate) and returned. 531 """ 532 533 # NOTE: Requires configuration. 534 535 if self.session_store is None: 536 self.session_store = SessionStore(self, "WebStack-sessions") 537 return self.session_store.get_session(create) 538 539 def expire_session(self): 540 541 """ 542 Expires any session established according to information provided in the 543 transaction. 544 """ 545 546 # NOTE: Requires configuration. 547 548 if self.session_store is None: 549 self.session_store = SessionStore(self, "WebStack-sessions") 550 self.session_store.expire_session() 551 552 # vim: tabstop=4 expandtab shiftwidth=4