1 #!/usr/bin/env python 2 3 """ 4 BaseHTTPRequestHandler classes. 5 6 Copyright (C) 2004, 2005, 2006 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 # Server-related methods. 107 108 def get_server_name(self): 109 110 "Returns the server name." 111 112 # As ultimately found in SocketServer.BaseServer via SocketServer.BaseRequestHandler. 113 # NOTE: We assume that any usage of "" can be taken to mean "localhost". 114 115 return self.trans.server.server_address[0] or "localhost" 116 117 def get_server_port(self): 118 119 "Returns the server port as a string." 120 121 # As ultimately found in SocketServer.BaseServer via SocketServer.BaseRequestHandler. 122 123 return str(self.trans.server.server_address[1]) 124 125 # Request-related methods. 126 127 def get_request_stream(self): 128 129 """ 130 Returns the request stream for the transaction. 131 """ 132 133 return MessageBodyStream(self.trans.rfile, self.get_headers()) 134 135 def get_request_method(self): 136 137 """ 138 Returns the request method. 139 """ 140 141 return self.trans.command 142 143 def get_headers(self): 144 145 """ 146 Returns all request headers as a dictionary-like object mapping header 147 names to values. 148 149 NOTE: If duplicate header names are permitted, then this interface will 150 NOTE: need to change. 151 """ 152 153 return self.trans.headers 154 155 def get_header_values(self, key): 156 157 """ 158 Returns a list of all request header values associated with the given 159 'key'. Note that according to RFC 2616, 'key' is treated as a 160 case-insensitive string. 161 """ 162 163 return self.convert_to_list(self.trans.headers.get(key)) 164 165 def get_content_type(self): 166 167 """ 168 Returns the content type specified on the request, along with the 169 charset employed. 170 """ 171 172 return self.parse_content_type(self.trans.headers.get("content-type")) 173 174 def get_content_charsets(self): 175 176 """ 177 Returns the character set preferences. 178 """ 179 180 return self.parse_content_preferences(self.trans.headers.get("accept-charset")) 181 182 def get_content_languages(self): 183 184 """ 185 Returns extracted language information from the transaction. 186 """ 187 188 return self.parse_content_preferences(self.trans.headers.get("accept-language")) 189 190 def get_path(self, encoding=None): 191 192 """ 193 Returns the entire path from the request as a Unicode object. Any "URL 194 encoded" character values in the part of the path before the query 195 string will be decoded and presented as genuine characters; the query 196 string will remain "URL encoded", however. 197 198 If the optional 'encoding' is set, use that in preference to the default 199 encoding to convert the path into a form not containing "URL encoded" 200 character values. 201 """ 202 203 path = self.get_path_without_query(encoding) 204 qs = self.get_query_string() 205 if qs: 206 return path + "?" + qs 207 else: 208 return path 209 210 def get_path_without_query(self, encoding=None): 211 212 """ 213 Returns the entire path from the request minus the query string as a 214 Unicode object containing genuine characters (as opposed to "URL 215 encoded" character values). 216 217 If the optional 'encoding' is set, use that in preference to the default 218 encoding to convert the path into a form not containing "URL encoded" 219 character values. 220 """ 221 222 # Remove the query string from the end of the path. 223 224 return self.decode_path(self.trans.path.split("?")[0], encoding) 225 226 def get_path_info(self, encoding=None): 227 228 """ 229 Returns the "path info" (the part of the URL after the resource name 230 handling the current request) from the request as a Unicode object 231 containing genuine characters (as opposed to "URL encoded" character 232 values). 233 234 If the optional 'encoding' is set, use that in preference to the default 235 encoding to convert the path into a form not containing "URL encoded" 236 character values. 237 """ 238 239 return self.get_path_without_query(encoding) 240 241 def get_query_string(self): 242 243 """ 244 Returns the query string from the path in the request. 245 """ 246 247 t = self.trans.path.split("?") 248 if len(t) == 1: 249 return "" 250 else: 251 252 # NOTE: Overlook erroneous usage of "?" characters in the path. 253 254 return "?".join(t[1:]) 255 256 # Higher level request-related methods. 257 258 def get_fields_from_path(self, encoding=None): 259 260 """ 261 Extracts fields (or request parameters) from the path specified in the 262 transaction. The underlying framework may refuse to supply fields from 263 the path if handling a POST transaction. The optional 'encoding' 264 parameter specifies the character encoding of the query string for cases 265 where the default encoding is to be overridden. 266 267 Returns a dictionary mapping field names to lists of values (even if a 268 single value is associated with any given field name). 269 """ 270 271 # NOTE: Support at best ISO-8859-1 values. 272 273 fields = {} 274 for name, values in parse_qs(self.get_query_string(), keep_blank_values=1).items(): 275 name = decode_value(name, encoding) 276 fields[name] = [] 277 for value in values: 278 value = decode_value(value, encoding) 279 fields[name].append(value) 280 return fields 281 282 def get_fields_from_body(self, encoding=None): 283 284 """ 285 Extracts fields (or request parameters) from the message body in the 286 transaction. The optional 'encoding' parameter specifies the character 287 encoding of the message body for cases where no such information is 288 available, but where the default encoding is to be overridden. 289 290 Returns a dictionary mapping field names to lists of values (even if a 291 single value is associated with any given field name). Each value is 292 either a Unicode object (representing a simple form field, for example) 293 or a WebStack.Helpers.Request.FileContent object (representing a file 294 upload form field). 295 """ 296 297 encoding = encoding or self.get_content_type().charset or self.default_charset 298 299 if self.storage_body is None: 300 self.storage_body = FieldStorage(fp=self.get_request_stream(), headers=self.get_headers(), 301 environ={"REQUEST_METHOD" : self.get_request_method()}, keep_blank_values=1) 302 303 # Avoid strange design issues with FieldStorage by checking the internal 304 # field list directly. 305 306 fields = {} 307 if self.storage_body.list is not None: 308 309 # Traverse the storage, finding each field value. 310 311 fields = get_body_fields(get_storage_items(self.storage_body), encoding) 312 313 return fields 314 315 def get_fields(self, encoding=None): 316 317 """ 318 Extracts fields (or request parameters) from both the path specified in 319 the transaction as well as the message body. The optional 'encoding' 320 parameter specifies the character encoding of the message body for cases 321 where no such information is available, but where the default encoding 322 is to be overridden. 323 324 Returns a dictionary mapping field names to lists of values (even if a 325 single value is associated with any given field name). Each value is 326 either a Unicode object (representing a simple form field, for example) 327 or a WebStack.Helpers.Request.FileContent object (representing a file 328 upload form field). 329 330 Where a given field name is used in both the path and message body to 331 specify values, the values from both sources will be combined into a 332 single list associated with that field name. 333 """ 334 335 # Combine the two sources. 336 337 fields = {} 338 fields.update(self.get_fields_from_path()) 339 for name, values in self.get_fields_from_body(encoding).items(): 340 if not fields.has_key(name): 341 fields[name] = values 342 else: 343 fields[name] += values 344 return fields 345 346 def get_user(self): 347 348 """ 349 Extracts user information from the transaction. 350 351 Returns a username as a string or None if no user is defined. 352 """ 353 354 if self.user is not None: 355 return self.user 356 357 auth_header = self.get_headers().get("authorization") 358 if auth_header: 359 return UserInfo(auth_header).username 360 else: 361 return None 362 363 def get_cookies(self): 364 365 """ 366 Obtains cookie information from the request. 367 368 Returns a dictionary mapping cookie names to cookie objects. 369 """ 370 371 return self.process_cookies(self.cookies_in) 372 373 def get_cookie(self, cookie_name): 374 375 """ 376 Obtains cookie information from the request. 377 378 Returns a cookie object for the given 'cookie_name' or None if no such 379 cookie exists. 380 """ 381 382 cookie = self.cookies_in.get(self.encode_cookie_value(cookie_name)) 383 if cookie is not None: 384 return Cookie(cookie_name, self.decode_cookie_value(cookie.value)) 385 else: 386 return None 387 388 # Response-related methods. 389 390 def get_response_stream(self): 391 392 """ 393 Returns the response stream for the transaction. 394 """ 395 396 # Return a stream which is later emptied into the real stream. 397 # Unicode can upset this operation. Using either the specified charset 398 # or a default encoding. 399 400 encoding = self.get_response_stream_encoding() 401 return ConvertingStream(self.content, encoding) 402 403 def get_response_stream_encoding(self): 404 405 """ 406 Returns the response stream encoding. 407 """ 408 409 if self.content_type: 410 encoding = self.content_type.charset 411 else: 412 encoding = None 413 return encoding or self.default_charset 414 415 def get_response_code(self): 416 417 """ 418 Get the response code associated with the transaction. If no response 419 code is defined, None is returned. 420 """ 421 422 return self.response_code 423 424 def set_response_code(self, response_code): 425 426 """ 427 Set the 'response_code' using a numeric constant defined in the HTTP 428 specification. 429 """ 430 431 self.response_code = response_code 432 433 def set_header_value(self, header, value): 434 435 """ 436 Set the HTTP 'header' with the given 'value'. 437 """ 438 439 # The header is not written out immediately due to the buffering in use. 440 441 self.headers_out[header] = value 442 443 def set_content_type(self, content_type): 444 445 """ 446 Sets the 'content_type' for the response. 447 """ 448 449 # The content type has to be written as a header, before actual content, 450 # but after the response line. This means that some kind of buffering is 451 # required. Hence, we don't write the header out immediately. 452 453 self.content_type = content_type 454 455 # Higher level response-related methods. 456 457 def set_cookie(self, cookie): 458 459 """ 460 Stores the given 'cookie' object in the response. 461 """ 462 463 # NOTE: If multiple cookies of the same name could be specified, this 464 # NOTE: could need changing. 465 466 self.set_cookie_value(cookie.name, cookie.value) 467 468 def set_cookie_value(self, name, value, path=None, expires=None): 469 470 """ 471 Stores a cookie with the given 'name' and 'value' in the response. 472 473 The optional 'path' is a string which specifies the scope of the cookie, 474 and the optional 'expires' parameter is a value compatible with the 475 time.time function, and indicates the expiry date/time of the cookie. 476 """ 477 478 name = self.encode_cookie_value(name) 479 self.cookies_out[name] = self.encode_cookie_value(value) 480 if path is not None: 481 self.cookies_out[name]["path"] = path 482 if expires is not None: 483 self.cookies_out[name]["expires"] = expires 484 485 def delete_cookie(self, cookie_name): 486 487 """ 488 Adds to the response a request that the cookie with the given 489 'cookie_name' be deleted/discarded by the client. 490 """ 491 492 # Create a special cookie, given that we do not know whether the browser 493 # has been sent the cookie or not. 494 # NOTE: Magic discovered in Webware. 495 496 name = self.encode_cookie_value(cookie_name) 497 self.cookies_out[name] = "" 498 self.cookies_out[name]["path"] = "/" 499 self.cookies_out[name]["expires"] = 0 500 self.cookies_out[name]["max-age"] = 0 501 502 # Session-related methods. 503 504 def get_session(self, create=1): 505 506 """ 507 Gets a session corresponding to an identifier supplied in the 508 transaction. 509 510 If no session has yet been established according to information 511 provided in the transaction then the optional 'create' parameter 512 determines whether a new session will be established. 513 514 Where no session has been established and where 'create' is set to 0 515 then None is returned. In all other cases, a session object is created 516 (where appropriate) and returned. 517 """ 518 519 # NOTE: Requires configuration. 520 521 if self.session_store is None: 522 self.session_store = SessionStore(self, "WebStack-sessions") 523 return self.session_store.get_session(create) 524 525 def expire_session(self): 526 527 """ 528 Expires any session established according to information provided in the 529 transaction. 530 """ 531 532 # NOTE: Requires configuration. 533 534 if self.session_store is None: 535 self.session_store = SessionStore(self, "WebStack-sessions") 536 self.session_store.expire_session() 537 538 # vim: tabstop=4 expandtab shiftwidth=4