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 A framework-specific method which returns the request stream for 85 the transaction. 86 """ 87 88 return MessageBodyStream(self.trans.rfile, self.get_headers()) 89 90 def get_request_method(self): 91 92 """ 93 A framework-specific method which gets the request method. 94 """ 95 96 return self.trans.command 97 98 def get_headers(self): 99 100 """ 101 A framework-specific method which returns all request headers as a 102 dictionary-like object mapping header names to values. 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 A framework-specific method which returns a list of all request header 113 values associated with the given 'key'. Note that according to RFC 2616, 114 'key' is treated as a 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 A framework-specific method which gets the content type specified on the 123 request, along with the 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 A framework-specific method which extracts language information from 140 the transaction. 141 """ 142 143 return self.parse_content_preferences(self.trans.headers.get("accept-language")) 144 145 def get_path(self): 146 147 """ 148 A framework-specific method which gets the entire path from the request. 149 """ 150 151 return self.trans.path 152 153 def get_path_without_query(self): 154 155 """ 156 A framework-specific method which gets the entire path from the request 157 minus the query string. 158 """ 159 160 # Remove the query string from the end of the path. 161 162 return self.trans.path.split("?")[0] 163 164 def get_path_info(self): 165 166 """ 167 A framework-specific method which gets the "path info" (the part of the 168 URL after the resource name handling the current request) from the 169 request. 170 """ 171 172 return self.get_path_without_query() 173 174 def get_query_string(self): 175 176 """ 177 A framework-specific method which gets the query string from the path in 178 the request. 179 """ 180 181 t = self.trans.path.split("?") 182 if len(t) == 1: 183 return "" 184 else: 185 186 # NOTE: Overlook erroneous usage of "?" characters in the path. 187 188 return "?".join(t[1:]) 189 190 # Higher level request-related methods. 191 192 def get_fields_from_path(self): 193 194 """ 195 A framework-specific method which extracts the form fields from the 196 path specified in the transaction. The underlying framework may refuse 197 to supply fields from the path if handling a POST transaction. 198 199 Returns a dictionary mapping field names to lists of values (even if a 200 single value is associated with any given field name). 201 """ 202 203 return parse_qs(self.get_query_string(), keep_blank_values=1) 204 205 def get_fields_from_body(self, encoding=None): 206 207 """ 208 A framework-specific method which extracts the form fields from the 209 message body in the transaction. The optional 'encoding' parameter 210 specifies the character encoding of the message body for cases where no 211 such information is available, but where the default encoding is to be 212 overridden. 213 214 Returns a dictionary mapping field names to lists of values (even if a 215 single value is associated with any given field name). 216 """ 217 218 encoding = self.get_content_type().charset or encoding or "iso-8859-1" 219 220 if self.storage_body is None: 221 self.storage_body = FieldStorage(fp=self.get_request_stream(), headers=self.get_headers(), 222 environ={"REQUEST_METHOD" : self.get_request_method()}, keep_blank_values=1) 223 224 # Avoid strange design issues with FieldStorage by checking the internal 225 # field list directly. 226 227 fields = {} 228 if self.storage_body.list is not None: 229 230 # Traverse the storage, finding each field value. 231 232 for field_name in self.storage_body.keys(): 233 fields[field_name] = [] 234 for field_value in self.storage_body.getlist(field_name): 235 fields[field_name].append(unicode(field_value, encoding)) 236 return fields 237 238 def get_user(self): 239 240 """ 241 A framework-specific method which extracts user information from the 242 transaction. 243 244 Returns a username as a string or None if no user is defined. 245 """ 246 247 if self.user is not None: 248 return self.user 249 250 auth_header = self.get_headers().get("authorization") 251 if auth_header: 252 return UserInfo(auth_header).username 253 else: 254 return None 255 256 def get_cookies(self): 257 258 """ 259 A framework-specific method which obtains cookie information from the 260 request. 261 262 Returns a dictionary mapping cookie names to cookie objects. 263 """ 264 265 return self.cookies_in 266 267 def get_cookie(self, cookie_name): 268 269 """ 270 A framework-specific method which obtains cookie information from the 271 request. 272 273 Returns a cookie object for the given 'cookie_name' or None if no such 274 cookie exists. 275 """ 276 277 return self.cookies_in.get(cookie_name) 278 279 # Response-related methods. 280 281 def get_response_stream(self): 282 283 """ 284 A framework-specific method which returns the response stream for 285 the transaction. 286 """ 287 288 # Return a stream which is later emptied into the real stream. 289 # Unicode can upset this operation. Using either the specified charset, 290 # the same charset as that used in the request, or a default encoding. 291 292 encoding = self.get_content_type().charset or "utf-8" 293 if self.content_type: 294 encoding = self.content_type.charset or encoding 295 return ConvertingStream(self.content, encoding) 296 297 def get_response_code(self): 298 299 """ 300 Get the response code associated with the transaction. If no response 301 code is defined, None is returned. 302 """ 303 304 return self.response_code 305 306 def set_response_code(self, response_code): 307 308 """ 309 Set the 'response_code' using a numeric constant defined in the HTTP 310 specification. 311 """ 312 313 self.response_code = response_code 314 315 def set_header_value(self, header, value): 316 317 """ 318 Set the HTTP 'header' with the given 'value'. 319 """ 320 321 # The header is not written out immediately due to the buffering in use. 322 323 self.headers_out[header] = value 324 325 def set_content_type(self, content_type): 326 327 """ 328 A framework-specific method which sets the 'content_type' for the 329 response. 330 """ 331 332 # The content type has to be written as a header, before actual content, 333 # but after the response line. This means that some kind of buffering is 334 # required. Hence, we don't write the header out immediately. 335 336 self.content_type = content_type 337 338 def set_cookie(self, cookie): 339 340 """ 341 A framework-specific method which stores the given 'cookie' object in 342 the response. 343 """ 344 345 # NOTE: If multiple cookies of the same name could be specified, this 346 # NOTE: could need changing. 347 348 self.cookies_out[cookie.name] = cookie.value 349 350 def set_cookie_value(self, name, value, path=None, expires=None): 351 352 """ 353 A framework-specific method which stores a cookie with the given 'name' 354 and 'value' in the response. 355 356 The optional 'path' is a string which specifies the scope of the cookie, 357 and the optional 'expires' parameter is a value compatible with the 358 time.time function, and indicates the expiry date/time of the cookie. 359 """ 360 361 self.cookies_out[name] = value 362 if path is not None: 363 self.cookies_out[name]["path"] = path 364 if expires is not None: 365 self.cookies_out[name]["expires"] = expires 366 367 def delete_cookie(self, cookie_name): 368 369 """ 370 A framework-specific method which adds to the response a request that 371 the cookie with the given 'cookie_name' be deleted/discarded by the 372 client. 373 """ 374 375 # Create a special cookie, given that we do not know whether the browser 376 # has been sent the cookie or not. 377 # NOTE: Magic discovered in Webware. 378 379 self.cookies_out[cookie_name] = "" 380 self.cookies_out[cookie_name]["path"] = "/" 381 self.cookies_out[cookie_name]["expires"] = 0 382 self.cookies_out[cookie_name]["max-age"] = 0 383 384 # Application-specific methods. 385 386 def set_user(self, username): 387 388 """ 389 An application-specific method which sets the user information with 390 'username' in the transaction. This affects subsequent calls to 391 'get_user'. 392 """ 393 394 self.user = username 395 396 # vim: tabstop=4 expandtab shiftwidth=4