1 #!/usr/bin/env python 2 3 """ 4 BaseHTTPRequestHandler classes. 5 """ 6 7 import Generic 8 from Helpers.Request import MessageBodyStream 9 from Helpers.Response import ConvertingStream 10 from Helpers.Auth import UserInfo 11 from cgi import parse_qs, FieldStorage 12 import Cookie 13 from StringIO import StringIO 14 15 class Transaction(Generic.Transaction): 16 17 """ 18 BaseHTTPRequestHandler transaction interface. 19 """ 20 21 def __init__(self, trans): 22 23 """ 24 Initialise the transaction using the BaseHTTPRequestHandler instance 25 'trans'. 26 """ 27 28 self.trans = trans 29 30 # Other attributes of interest in instances of this class. 31 32 self.content_type = None 33 self.response_code = 200 34 self.content = StringIO() 35 self.headers_out = {} 36 self.cookies_out = Cookie.SimpleCookie() 37 self.user = None 38 39 # Define the incoming cookies. 40 41 self.cookies_in = Cookie.SimpleCookie(self.get_headers().get("cookie")) 42 43 # Cached information. 44 45 self.storage_body = None 46 47 def commit(self): 48 49 """ 50 A special method, synchronising the transaction with framework-specific 51 objects. 52 """ 53 54 self.trans.send_response(self.response_code) 55 if self.content_type is not None: 56 self.trans.send_header("Content-Type", self.format_content_type(self.content_type)) 57 58 for header, value in self.headers_out.items(): 59 self.trans.send_header(self.format_header_value(header), self.format_header_value(value)) 60 61 # NOTE: May not be using the appropriate method. 62 63 for morsel in self.cookies_out.values(): 64 self.trans.send_header("Set-Cookie", morsel.OutputString()) 65 66 # Add possibly missing content length information. 67 # NOTE: This is really inefficient, but we need to buffer things to 68 # NOTE: permit out of order header setting. 69 70 self.content.seek(0) 71 content = self.content.read() 72 73 if not self.headers_out.has_key("Content-Length"): 74 self.trans.send_header("Content-Length", str(len(content))) 75 76 self.trans.end_headers() 77 self.trans.wfile.write(content) 78 79 # Request-related methods. 80 81 def get_request_stream(self): 82 83 """ 84 Returns the request stream for the transaction. 85 """ 86 87 return MessageBodyStream(self.trans.rfile, self.get_headers()) 88 89 def get_request_method(self): 90 91 """ 92 Returns the request method. 93 """ 94 95 return self.trans.command 96 97 def get_headers(self): 98 99 """ 100 Returns all request headers as a dictionary-like object mapping header 101 names to values. 102 103 NOTE: If duplicate header names are permitted, then this interface will 104 NOTE: need to change. 105 """ 106 107 return self.trans.headers 108 109 def get_header_values(self, key): 110 111 """ 112 Returns a list of all request header values associated with the given 113 'key'. Note that according to RFC 2616, 'key' is treated as a 114 case-insensitive string. 115 """ 116 117 return self.convert_to_list(self.trans.headers.get(key)) 118 119 def get_content_type(self): 120 121 """ 122 Returns the content type specified on the request, along with the 123 charset employed. 124 """ 125 126 return self.parse_content_type(self.trans.headers.get("content-type")) 127 128 def get_content_charsets(self): 129 130 """ 131 Returns the character set preferences. 132 """ 133 134 return self.parse_content_preferences(self.trans.headers.get("accept-charset")) 135 136 def get_content_languages(self): 137 138 """ 139 Returns extracted language information from the transaction. 140 """ 141 142 return self.parse_content_preferences(self.trans.headers.get("accept-language")) 143 144 def get_path(self): 145 146 """ 147 Returns the entire path from the request. 148 """ 149 150 return self.trans.path 151 152 def get_path_without_query(self): 153 154 """ 155 Returns the entire path from the request minus the query string. 156 """ 157 158 # Remove the query string from the end of the path. 159 160 return self.trans.path.split("?")[0] 161 162 def get_path_info(self): 163 164 """ 165 Returns the "path info" (the part of the URL after the resource name 166 handling the current request) from the request. 167 """ 168 169 return self.get_path_without_query() 170 171 def get_query_string(self): 172 173 """ 174 Returns the query string from the path in the request. 175 """ 176 177 t = self.trans.path.split("?") 178 if len(t) == 1: 179 return "" 180 else: 181 182 # NOTE: Overlook erroneous usage of "?" characters in the path. 183 184 return "?".join(t[1:]) 185 186 # Higher level request-related methods. 187 188 def get_fields_from_path(self): 189 190 """ 191 Extracts the form fields from the path specified in the transaction. The 192 underlying framework may refuse to supply fields from the path if 193 handling a POST transaction. 194 195 Returns a dictionary mapping field names to lists of values (even if a 196 single value is associated with any given field name). 197 """ 198 199 return parse_qs(self.get_query_string(), keep_blank_values=1) 200 201 def get_fields_from_body(self, encoding=None): 202 203 """ 204 Extracts the form fields from the message body in the transaction. The 205 optional 'encoding' parameter specifies the character encoding of the 206 message body for cases where no such information is available, but where 207 the default encoding is to be overridden. 208 209 Returns a dictionary mapping field names to lists of values (even if a 210 single value is associated with any given field name). 211 """ 212 213 encoding = self.get_content_type().charset or encoding or "iso-8859-1" 214 215 if self.storage_body is None: 216 self.storage_body = FieldStorage(fp=self.get_request_stream(), headers=self.get_headers(), 217 environ={"REQUEST_METHOD" : self.get_request_method()}, keep_blank_values=1) 218 219 # Avoid strange design issues with FieldStorage by checking the internal 220 # field list directly. 221 222 fields = {} 223 if self.storage_body.list is not None: 224 225 # Traverse the storage, finding each field value. 226 227 for field_name in self.storage_body.keys(): 228 fields[field_name] = [] 229 for field_value in self.storage_body.getlist(field_name): 230 fields[field_name].append(unicode(field_value, encoding)) 231 return fields 232 233 def get_user(self): 234 235 """ 236 Extracts user information from the transaction. 237 238 Returns a username as a string or None if no user is defined. 239 """ 240 241 if self.user is not None: 242 return self.user 243 244 auth_header = self.get_headers().get("authorization") 245 if auth_header: 246 return UserInfo(auth_header).username 247 else: 248 return None 249 250 def get_cookies(self): 251 252 """ 253 Obtains cookie information from the request. 254 255 Returns a dictionary mapping cookie names to cookie objects. 256 """ 257 258 return self.cookies_in 259 260 def get_cookie(self, cookie_name): 261 262 """ 263 Obtains cookie information from the request. 264 265 Returns a cookie object for the given 'cookie_name' or None if no such 266 cookie exists. 267 """ 268 269 return self.cookies_in.get(cookie_name) 270 271 # Response-related methods. 272 273 def get_response_stream(self): 274 275 """ 276 Returns the response stream for the transaction. 277 """ 278 279 # Return a stream which is later emptied into the real stream. 280 # Unicode can upset this operation. Using either the specified charset, 281 # the same charset as that used in the request, or a default encoding. 282 283 encoding = self.get_content_type().charset or "utf-8" 284 if self.content_type: 285 encoding = self.content_type.charset or encoding 286 return ConvertingStream(self.content, encoding) 287 288 def get_response_code(self): 289 290 """ 291 Get the response code associated with the transaction. If no response 292 code is defined, None is returned. 293 """ 294 295 return self.response_code 296 297 def set_response_code(self, response_code): 298 299 """ 300 Set the 'response_code' using a numeric constant defined in the HTTP 301 specification. 302 """ 303 304 self.response_code = response_code 305 306 def set_header_value(self, header, value): 307 308 """ 309 Set the HTTP 'header' with the given 'value'. 310 """ 311 312 # The header is not written out immediately due to the buffering in use. 313 314 self.headers_out[header] = value 315 316 def set_content_type(self, content_type): 317 318 """ 319 Sets the 'content_type' for the response. 320 """ 321 322 # The content type has to be written as a header, before actual content, 323 # but after the response line. This means that some kind of buffering is 324 # required. Hence, we don't write the header out immediately. 325 326 self.content_type = content_type 327 328 # Higher level response-related methods. 329 330 def set_cookie(self, cookie): 331 332 """ 333 Stores the given 'cookie' object in the response. 334 """ 335 336 # NOTE: If multiple cookies of the same name could be specified, this 337 # NOTE: could need changing. 338 339 self.cookies_out[cookie.name] = cookie.value 340 341 def set_cookie_value(self, name, value, path=None, expires=None): 342 343 """ 344 Stores a cookie with the given 'name' and 'value' in the response. 345 346 The optional 'path' is a string which specifies the scope of the cookie, 347 and the optional 'expires' parameter is a value compatible with the 348 time.time function, and indicates the expiry date/time of the cookie. 349 """ 350 351 self.cookies_out[name] = value 352 if path is not None: 353 self.cookies_out[name]["path"] = path 354 if expires is not None: 355 self.cookies_out[name]["expires"] = expires 356 357 def delete_cookie(self, cookie_name): 358 359 """ 360 Adds to the response a request that the cookie with the given 361 'cookie_name' be deleted/discarded by the client. 362 """ 363 364 # Create a special cookie, given that we do not know whether the browser 365 # has been sent the cookie or not. 366 # NOTE: Magic discovered in Webware. 367 368 self.cookies_out[cookie_name] = "" 369 self.cookies_out[cookie_name]["path"] = "/" 370 self.cookies_out[cookie_name]["expires"] = 0 371 self.cookies_out[cookie_name]["max-age"] = 0 372 373 # Application-specific methods. 374 375 def set_user(self, username): 376 377 """ 378 An application-specific method which sets the user information with 379 'username' in the transaction. This affects subsequent calls to 380 'get_user'. 381 """ 382 383 self.user = username 384 385 # vim: tabstop=4 expandtab shiftwidth=4