OLD | NEW |
(Empty) | |
| 1 import base64 |
| 2 import cgi |
| 3 import Cookie |
| 4 import StringIO |
| 5 import tempfile |
| 6 import urlparse |
| 7 |
| 8 import stash |
| 9 from utils import HTTPException |
| 10 |
| 11 missing = object() |
| 12 |
| 13 |
| 14 class Server(object): |
| 15 """Data about the server environment |
| 16 |
| 17 .. attribute:: config |
| 18 |
| 19 Environment configuration information with information about the |
| 20 various servers running, their hostnames and ports. |
| 21 |
| 22 .. attribute:: stash |
| 23 |
| 24 Stash object holding state stored on the server between requests. |
| 25 |
| 26 """ |
| 27 config = None |
| 28 |
| 29 def __init__(self, request): |
| 30 self.stash = stash.Stash(request.url_parts.path) |
| 31 |
| 32 |
| 33 class InputFile(object): |
| 34 max_buffer_size = 1024*1024 |
| 35 |
| 36 def __init__(self, rfile, length): |
| 37 """File-like object used to provide a seekable view of request body data
""" |
| 38 self._file = rfile |
| 39 self.length = length |
| 40 |
| 41 self._file_position = 0 |
| 42 |
| 43 if length > self.max_buffer_size: |
| 44 self._buf = tempfile.TemporaryFile(mode="rw+b") |
| 45 else: |
| 46 self._buf = StringIO.StringIO() |
| 47 |
| 48 @property |
| 49 def _buf_position(self): |
| 50 rv = self._buf.tell() |
| 51 assert rv <= self._file_position |
| 52 return rv |
| 53 |
| 54 def read(self, bytes=-1): |
| 55 assert self._buf_position <= self._file_position |
| 56 |
| 57 if bytes < 0: |
| 58 bytes = self.length - self._buf_position |
| 59 bytes_remaining = min(bytes, self.length - self._buf_position) |
| 60 |
| 61 if bytes_remaining == 0: |
| 62 return "" |
| 63 |
| 64 if self._buf_position != self._file_position: |
| 65 buf_bytes = min(bytes_remaining, self._file_position - self._buf_pos
ition) |
| 66 old_data = self._buf.read(buf_bytes) |
| 67 bytes_remaining -= buf_bytes |
| 68 else: |
| 69 old_data = "" |
| 70 |
| 71 assert self._buf_position == self._file_position, ( |
| 72 "Before reading buffer position (%i) didn't match file position (%i)
" % |
| 73 (self._buf_position, self._file_position)) |
| 74 new_data = self._file.read(bytes_remaining) |
| 75 self._buf.write(new_data) |
| 76 self._file_position += bytes_remaining |
| 77 assert self._buf_position == self._file_position, ( |
| 78 "After reading buffer position (%i) didn't match file position (%i)"
% |
| 79 (self._buf_position, self._file_position)) |
| 80 |
| 81 return old_data + new_data |
| 82 |
| 83 def tell(self): |
| 84 return self._buf_position |
| 85 |
| 86 def seek(self, offset): |
| 87 if offset > self.length or offset < 0: |
| 88 raise ValueError |
| 89 if offset <= self._file_position: |
| 90 self._buf.seek(offset) |
| 91 else: |
| 92 self.read(offset - self._file_position) |
| 93 |
| 94 def readline(self, max_bytes=None): |
| 95 if max_bytes is None: |
| 96 max_bytes = self.length - self._buf_position |
| 97 |
| 98 if self._buf_position < self._file_position: |
| 99 data = self._buf.readline(max_bytes) |
| 100 if data.endswith("\n") or len(data) == max_bytes: |
| 101 return data |
| 102 else: |
| 103 data = "" |
| 104 |
| 105 assert self._buf_position == self._file_position |
| 106 |
| 107 initial_position = self._file_position |
| 108 found = False |
| 109 buf = [] |
| 110 max_bytes -= len(data) |
| 111 while not found: |
| 112 readahead = self.read(min(2, max_bytes)) |
| 113 max_bytes -= len(readahead) |
| 114 for i, c in enumerate(readahead): |
| 115 if c == "\n": |
| 116 buf.append(readahead[:i+1]) |
| 117 found = True |
| 118 break |
| 119 if not found: |
| 120 buf.append(readahead) |
| 121 if not readahead or not max_bytes: |
| 122 break |
| 123 new_data = "".join(buf) |
| 124 data += new_data |
| 125 self.seek(initial_position + len(new_data)) |
| 126 return data |
| 127 |
| 128 def readlines(self): |
| 129 rv = [] |
| 130 while True: |
| 131 data = self.readline() |
| 132 if data: |
| 133 rv.append(data) |
| 134 else: |
| 135 break |
| 136 return rv |
| 137 |
| 138 def next(self): |
| 139 data = self.readline() |
| 140 if data: |
| 141 return data |
| 142 else: |
| 143 raise StopIteration |
| 144 |
| 145 def __iter__(self): |
| 146 return self |
| 147 |
| 148 |
| 149 class Request(object): |
| 150 """Object representing a HTTP request. |
| 151 |
| 152 .. attribute:: doc_root |
| 153 |
| 154 The local directory to use as a base when resolving paths |
| 155 |
| 156 .. attribute:: route_match |
| 157 |
| 158 Regexp match object from matching the request path to the route |
| 159 selected for the request. |
| 160 |
| 161 .. attribute:: protocol_version |
| 162 |
| 163 HTTP version specified in the request. |
| 164 |
| 165 .. attribute:: method |
| 166 |
| 167 HTTP method in the request. |
| 168 |
| 169 .. attribute:: request_path |
| 170 |
| 171 Request path as it appears in the HTTP request. |
| 172 |
| 173 .. attribute:: url |
| 174 |
| 175 Absolute URL for the request. |
| 176 |
| 177 .. attribute:: headers |
| 178 |
| 179 List of request headers. |
| 180 |
| 181 .. attribute:: raw_input |
| 182 |
| 183 File-like object representing the body of the request. |
| 184 |
| 185 .. attribute:: url_parts |
| 186 |
| 187 Parts of the requested URL as obtained by urlparse.urlsplit(path) |
| 188 |
| 189 .. attribute:: request_line |
| 190 |
| 191 Raw request line |
| 192 |
| 193 .. attribute:: headers |
| 194 |
| 195 RequestHeaders object providing a dictionary-like representation of |
| 196 the request headers. |
| 197 |
| 198 .. attribute:: body |
| 199 |
| 200 Request body as a string |
| 201 |
| 202 .. attribute:: GET |
| 203 |
| 204 MultiDict representing the parameters supplied with the request. |
| 205 Note that these may be present on non-GET requests; the name is |
| 206 chosen to be familiar to users of other systems such as PHP. |
| 207 |
| 208 .. attribute:: POST |
| 209 |
| 210 MultiDict representing the request body parameters. Most parameters |
| 211 are present as string values, but file uploads have file-like |
| 212 values. |
| 213 |
| 214 .. attribute:: cookies |
| 215 |
| 216 Cookies object representing cookies sent with the request with a |
| 217 dictionary-like interface. |
| 218 |
| 219 .. attribute:: auth |
| 220 |
| 221 Object with username and password properties representing any |
| 222 credentials supplied using HTTP authentication. |
| 223 |
| 224 .. attribute:: server |
| 225 |
| 226 Server object containing information about the server environment. |
| 227 """ |
| 228 |
| 229 def __init__(self, request_handler): |
| 230 self.doc_root = request_handler.server.router.doc_root |
| 231 self.route_match = None # Set by the router |
| 232 |
| 233 self.protocol_version = request_handler.protocol_version |
| 234 self.method = request_handler.command |
| 235 |
| 236 scheme = request_handler.server.scheme |
| 237 host = request_handler.headers.get("Host") |
| 238 port = request_handler.server.server_address[1] |
| 239 |
| 240 if host is None: |
| 241 host = request_handler.server.server_address[0] |
| 242 else: |
| 243 if ":" in host: |
| 244 host, port = host.split(":", 1) |
| 245 |
| 246 self.request_path = request_handler.path |
| 247 |
| 248 if self.request_path.startswith(scheme + "://"): |
| 249 self.url = request_handler.path |
| 250 else: |
| 251 self.url = "%s://%s:%s%s" % (scheme, |
| 252 host, |
| 253 port, |
| 254 self.request_path) |
| 255 self.url_parts = urlparse.urlsplit(self.url) |
| 256 |
| 257 self._raw_headers = request_handler.headers |
| 258 |
| 259 self.request_line = request_handler.raw_requestline |
| 260 |
| 261 self._headers = None |
| 262 |
| 263 self.raw_input = InputFile(request_handler.rfile, |
| 264 int(self.headers.get("Content-Length", 0))) |
| 265 self._body = None |
| 266 |
| 267 self._GET = None |
| 268 self._POST = None |
| 269 self._cookies = None |
| 270 self._auth = None |
| 271 |
| 272 self.server = Server(self) |
| 273 |
| 274 def __repr__(self): |
| 275 return "<Request %s %s>" % (self.method, self.url) |
| 276 |
| 277 @property |
| 278 def GET(self): |
| 279 if self._GET is None: |
| 280 params = urlparse.parse_qsl(self.url_parts.query, keep_blank_values=
True) |
| 281 self._GET = MultiDict() |
| 282 for key, value in params: |
| 283 self._GET.add(key, value) |
| 284 return self._GET |
| 285 |
| 286 @property |
| 287 def POST(self): |
| 288 if self._POST is None: |
| 289 #Work out the post parameters |
| 290 pos = self.raw_input.tell() |
| 291 self.raw_input.seek(0) |
| 292 fs = cgi.FieldStorage(fp=self.raw_input, |
| 293 environ={"REQUEST_METHOD": self.method}, |
| 294 headers=self.headers, |
| 295 keep_blank_values=True) |
| 296 self._POST = MultiDict.from_field_storage(fs) |
| 297 self.raw_input.seek(pos) |
| 298 return self._POST |
| 299 |
| 300 @property |
| 301 def cookies(self): |
| 302 if self._cookies is None: |
| 303 parser = Cookie.BaseCookie() |
| 304 cookie_headers = self.headers.get("cookie", "") |
| 305 parser.load(cookie_headers) |
| 306 cookies = Cookies() |
| 307 for key, value in parser.iteritems(): |
| 308 cookies[key] = CookieValue(value) |
| 309 self._cookies = cookies |
| 310 return self._cookies |
| 311 |
| 312 @property |
| 313 def headers(self): |
| 314 if self._headers is None: |
| 315 self._headers = RequestHeaders(self._raw_headers) |
| 316 return self._headers |
| 317 |
| 318 @property |
| 319 def body(self): |
| 320 if self._body is None: |
| 321 pos = self.raw_input.tell() |
| 322 self.raw_input.seek(0) |
| 323 self._body = self.raw_input.read() |
| 324 self.raw_input.seek(pos) |
| 325 return self._body |
| 326 |
| 327 @property |
| 328 def auth(self): |
| 329 if self._auth is None: |
| 330 self._auth = Authentication(self.headers) |
| 331 return self._auth |
| 332 |
| 333 |
| 334 class RequestHeaders(dict): |
| 335 """Dictionary-like API for accessing request headers.""" |
| 336 def __init__(self, items): |
| 337 for key, value in zip(items.keys(), items.values()): |
| 338 key = key.lower() |
| 339 if key in self: |
| 340 self[key].append(value) |
| 341 else: |
| 342 dict.__setitem__(self, key, [value]) |
| 343 |
| 344 def __getitem__(self, key): |
| 345 """Get all headers of a certain (case-insensitive) name. If there is |
| 346 more than one, the values are returned comma separated""" |
| 347 values = dict.__getitem__(self, key.lower()) |
| 348 if len(values) == 1: |
| 349 return values[0] |
| 350 else: |
| 351 return ", ".join(values) |
| 352 |
| 353 def __setitem__(self, name, value): |
| 354 raise Exception |
| 355 |
| 356 def get(self, key, default=None): |
| 357 """Get a string representing all headers with a particular value, |
| 358 with multiple headers separated by a comma. If no header is found |
| 359 return a default value |
| 360 |
| 361 :param key: The header name to look up (case-insensitive) |
| 362 :param default: The value to return in the case of no match |
| 363 """ |
| 364 try: |
| 365 return self[key] |
| 366 except KeyError: |
| 367 return default |
| 368 |
| 369 def get_list(self, key, default=missing): |
| 370 """Get all the header values for a particular field name as |
| 371 a list""" |
| 372 try: |
| 373 return dict.__getitem__(self, key.lower()) |
| 374 except KeyError: |
| 375 if default is not missing: |
| 376 return default |
| 377 else: |
| 378 raise |
| 379 |
| 380 def __contains__(self, key): |
| 381 return dict.__contains__(self, key.lower()) |
| 382 |
| 383 def iteritems(self): |
| 384 for item in self: |
| 385 yield item, self[item] |
| 386 |
| 387 def itervalues(self): |
| 388 for item in self: |
| 389 yield self[item] |
| 390 |
| 391 class CookieValue(object): |
| 392 """Representation of cookies. |
| 393 |
| 394 Note that cookies are considered read-only and the string value |
| 395 of the cookie will not change if you update the field values. |
| 396 However this is not enforced. |
| 397 |
| 398 .. attribute:: key |
| 399 |
| 400 The name of the cookie. |
| 401 |
| 402 .. attribute:: value |
| 403 |
| 404 The value of the cookie |
| 405 |
| 406 .. attribute:: expires |
| 407 |
| 408 The expiry date of the cookie |
| 409 |
| 410 .. attribute:: path |
| 411 |
| 412 The path of the cookie |
| 413 |
| 414 .. attribute:: comment |
| 415 |
| 416 The comment of the cookie. |
| 417 |
| 418 .. attribute:: domain |
| 419 |
| 420 The domain with which the cookie is associated |
| 421 |
| 422 .. attribute:: max_age |
| 423 |
| 424 The max-age value of the cookie. |
| 425 |
| 426 .. attribute:: secure |
| 427 |
| 428 Whether the cookie is marked as secure |
| 429 |
| 430 .. attribute:: httponly |
| 431 |
| 432 Whether the cookie is marked as httponly |
| 433 |
| 434 """ |
| 435 def __init__(self, morsel): |
| 436 self.key = morsel.key |
| 437 self.value = morsel.value |
| 438 |
| 439 for attr in ["expires", "path", |
| 440 "comment", "domain", "max-age", |
| 441 "secure", "version", "httponly"]: |
| 442 setattr(self, attr.replace("-", "_"), morsel[attr]) |
| 443 |
| 444 self._str = morsel.OutputString() |
| 445 |
| 446 def __str__(self): |
| 447 return self._str |
| 448 |
| 449 def __repr__(self): |
| 450 return self._str |
| 451 |
| 452 def __eq__(self, other): |
| 453 """Equality comparison for cookies. Compares to other cookies |
| 454 based on value alone and on non-cookies based on the equality |
| 455 of self.value with the other object so that a cookie with value |
| 456 "ham" compares equal to the string "ham" |
| 457 """ |
| 458 if hasattr(other, "value"): |
| 459 return self.value == other.value |
| 460 return self.value == other |
| 461 |
| 462 |
| 463 class MultiDict(dict): |
| 464 """Dictionary type that holds multiple values for each |
| 465 key""" |
| 466 #TODO: this should perhaps also order the keys |
| 467 def __init__(self): |
| 468 pass |
| 469 |
| 470 def __setitem__(self, name, value): |
| 471 dict.__setitem__(self, name, [value]) |
| 472 |
| 473 def add(self, name, value): |
| 474 if name in self: |
| 475 dict.__getitem__(self, name).append(value) |
| 476 else: |
| 477 dict.__setitem__(self, name, [value]) |
| 478 |
| 479 def __getitem__(self, key): |
| 480 """Get the first value with a given key""" |
| 481 #TODO: should this instead be the last value? |
| 482 return self.first(key) |
| 483 |
| 484 def first(self, key, default=missing): |
| 485 """Get the first value with a given key |
| 486 |
| 487 :param key: The key to lookup |
| 488 :param default: The default to return if key is |
| 489 not found (throws if nothing is |
| 490 specified) |
| 491 """ |
| 492 if key in self and dict.__getitem__(self, key): |
| 493 return dict.__getitem__(self, key)[0] |
| 494 elif default is not missing: |
| 495 return default |
| 496 raise KeyError |
| 497 |
| 498 def last(self, key, default=missing): |
| 499 """Get the last value with a given key |
| 500 |
| 501 :param key: The key to lookup |
| 502 :param default: The default to return if key is |
| 503 not found (throws if nothing is |
| 504 specified) |
| 505 """ |
| 506 if key in self and dict.__getitem__(self, key): |
| 507 return dict.__getitem__(self, key)[-1] |
| 508 elif default is not missing: |
| 509 return default |
| 510 raise KeyError |
| 511 |
| 512 def get_list(self, key): |
| 513 """Get all values with a given key as a list |
| 514 |
| 515 :param key: The key to lookup |
| 516 """ |
| 517 return dict.__getitem__(self, key) |
| 518 |
| 519 @classmethod |
| 520 def from_field_storage(cls, fs): |
| 521 self = cls() |
| 522 if fs.list is None: |
| 523 return self |
| 524 for key in fs: |
| 525 values = fs[key] |
| 526 if not isinstance(values, list): |
| 527 values = [values] |
| 528 |
| 529 for value in values: |
| 530 if value.filename: |
| 531 value = value |
| 532 else: |
| 533 value = value.value |
| 534 self.add(key, value) |
| 535 return self |
| 536 |
| 537 |
| 538 class Cookies(MultiDict): |
| 539 """MultiDict specialised for Cookie values""" |
| 540 def __init__(self): |
| 541 pass |
| 542 |
| 543 def __getitem__(self, key): |
| 544 return self.last(key) |
| 545 |
| 546 |
| 547 class Authentication(object): |
| 548 """Object for dealing with HTTP Authentication |
| 549 |
| 550 .. attribute:: username |
| 551 |
| 552 The username supplied in the HTTP Authorization |
| 553 header, or None |
| 554 |
| 555 .. attribute:: password |
| 556 |
| 557 The password supplied in the HTTP Authorization |
| 558 header, or None |
| 559 """ |
| 560 def __init__(self, headers): |
| 561 self.username = None |
| 562 self.password = None |
| 563 |
| 564 auth_schemes = {"Basic": self.decode_basic} |
| 565 |
| 566 if "authorization" in headers: |
| 567 header = headers.get("authorization") |
| 568 auth_type, data = header.split(" ", 1) |
| 569 if auth_type in auth_schemes: |
| 570 self.username, self.password = auth_schemes[auth_type](data) |
| 571 else: |
| 572 raise HTTPException(400, "Unsupported authentication scheme %s"
% auth_type) |
| 573 |
| 574 def decode_basic(self, data): |
| 575 decoded_data = base64.decodestring(data) |
| 576 return decoded_data.split(":", 1) |
OLD | NEW |