OLD | NEW |
(Empty) | |
| 1 from collections import OrderedDict |
| 2 from datetime import datetime, timedelta |
| 3 import Cookie |
| 4 import json |
| 5 import types |
| 6 import uuid |
| 7 import socket |
| 8 |
| 9 from constants import response_codes |
| 10 from logger import get_logger |
| 11 |
| 12 missing = object() |
| 13 |
| 14 class Response(object): |
| 15 """Object representing the response to a HTTP request |
| 16 |
| 17 :param handler: RequestHandler being used for this response |
| 18 :param request: Request that this is the response for |
| 19 |
| 20 .. attribute:: request |
| 21 |
| 22 Request associated with this Response. |
| 23 |
| 24 .. attribute:: encoding |
| 25 |
| 26 The encoding to use when converting unicode to strings for output. |
| 27 |
| 28 .. attribute:: add_required_headers |
| 29 |
| 30 Boolean indicating whether mandatory headers should be added to the |
| 31 response. |
| 32 |
| 33 .. attribute:: send_body_for_head_request |
| 34 |
| 35 Boolean, default False, indicating whether the body content should be |
| 36 sent when the request method is HEAD. |
| 37 |
| 38 .. attribute:: explicit_flush |
| 39 |
| 40 Boolean indicating whether output should be flushed automatically or only |
| 41 when requested. |
| 42 |
| 43 .. attribute:: writer |
| 44 |
| 45 The ResponseWriter for this response |
| 46 |
| 47 .. attribute:: status |
| 48 |
| 49 Status tuple (code, message). Can be set to an integer, in which case the |
| 50 message part is filled in automatically, or a tuple. |
| 51 |
| 52 .. attribute:: headers |
| 53 |
| 54 List of HTTP headers to send with the response. Each item in the list is
a |
| 55 tuple of (name, value). |
| 56 |
| 57 .. attribute:: content |
| 58 |
| 59 The body of the response. This can either be a string or a iterable of re
sponse |
| 60 parts. If it is an iterable, any item may be a string or a function of ze
ro |
| 61 parameters which, when called, returns a string.""" |
| 62 |
| 63 def __init__(self, handler, request): |
| 64 self.request = request |
| 65 self.encoding = "utf8" |
| 66 |
| 67 self.add_required_headers = True |
| 68 self.send_body_for_head_request = False |
| 69 self.explicit_flush = False |
| 70 self.close_connection = False |
| 71 |
| 72 self.writer = ResponseWriter(handler, self) |
| 73 |
| 74 self._status = (200, None) |
| 75 self.headers = ResponseHeaders() |
| 76 self.content = [] |
| 77 |
| 78 self.logger = get_logger() |
| 79 |
| 80 @property |
| 81 def status(self): |
| 82 return self._status |
| 83 |
| 84 @status.setter |
| 85 def status(self, value): |
| 86 if hasattr(value, "__len__"): |
| 87 if len(value) != 2: |
| 88 raise ValueError |
| 89 else: |
| 90 self._status = (int(value[0]), str(value[1])) |
| 91 else: |
| 92 self._status = (int(value), None) |
| 93 |
| 94 def set_cookie(self, name, value, path="/", domain=None, max_age=None, |
| 95 expires=None, secure=False, httponly=False, comment=None): |
| 96 """Set a cookie to be sent with a Set-Cookie header in the |
| 97 response |
| 98 |
| 99 :param name: String name of the cookie |
| 100 :param value: String value of the cookie |
| 101 :param max_age: datetime.timedelta int representing the time (in seconds
) |
| 102 until the cookie expires |
| 103 :param path: String path to which the cookie applies |
| 104 :param domain: String domain to which the cookie applies |
| 105 :param secure: Boolean indicating whether the cookie is marked as secure |
| 106 :param httponly: Boolean indicating whether the cookie is marked as |
| 107 HTTP Only |
| 108 :param comment: String comment |
| 109 :param expires: datetime.datetime or datetime.timedelta indicating a |
| 110 time or interval from now when the cookie expires |
| 111 |
| 112 """ |
| 113 days = dict((i+1, name) for i, name in enumerate(["jan", "feb", "mar", |
| 114 "apr", "may", "jun", |
| 115 "jul", "aug", "sep", |
| 116 "oct", "nov", "dec"])) |
| 117 if value is None: |
| 118 value = '' |
| 119 max_age = 0 |
| 120 expires = timedelta(days=-1) |
| 121 |
| 122 if isinstance(expires, timedelta): |
| 123 expires = datetime.utcnow() + expires |
| 124 |
| 125 if expires is not None: |
| 126 expires_str = expires.strftime("%d %%s %Y %H:%M:%S GMT") |
| 127 expires_str = expires_str % days[expires.month] |
| 128 expires = expires_str |
| 129 |
| 130 if max_age is not None: |
| 131 if hasattr(max_age, "total_seconds"): |
| 132 max_age = int(max_age.total_seconds()) |
| 133 max_age = "%.0d" % max_age |
| 134 |
| 135 m = Cookie.Morsel() |
| 136 |
| 137 def maybe_set(key, value): |
| 138 if value is not None and value is not False: |
| 139 m[key] = value |
| 140 |
| 141 m.set(name, value, value) |
| 142 maybe_set("path", path) |
| 143 maybe_set("domain", domain) |
| 144 maybe_set("comment", comment) |
| 145 maybe_set("expires", expires) |
| 146 maybe_set("max-age", max_age) |
| 147 maybe_set("secure", secure) |
| 148 maybe_set("httponly", httponly) |
| 149 |
| 150 self.headers.append("Set-Cookie", m.OutputString()) |
| 151 |
| 152 def unset_cookie(self, name): |
| 153 """Remove a cookie from those that are being sent with the response""" |
| 154 cookies = self.headers.get("Set-Cookie") |
| 155 parser = Cookie.BaseCookie() |
| 156 for cookie in cookies: |
| 157 parser.load(cookie) |
| 158 |
| 159 if name in parser.keys(): |
| 160 del self.headers["Set-Cookie"] |
| 161 for m in parser.values(): |
| 162 if m.key != name: |
| 163 self.headers.append(("Set-Cookie", m.OutputString())) |
| 164 |
| 165 def delete_cookie(self, name, path="/", domain=None): |
| 166 """Delete a cookie on the client by setting it to the empty string |
| 167 and to expire in the past""" |
| 168 self.set_cookie(name, None, path=path, domain=domain, max_age=0, |
| 169 expires=timedelta(days=-1)) |
| 170 |
| 171 def iter_content(self): |
| 172 """Iterator returning chunks of response body content. |
| 173 |
| 174 If any part of the content is a function, this will be called |
| 175 and the resulting value (if any) returned.""" |
| 176 if type(self.content) in types.StringTypes: |
| 177 yield self.content |
| 178 else: |
| 179 for item in self.content: |
| 180 if hasattr(item, "__call__"): |
| 181 value = item() |
| 182 else: |
| 183 value = item |
| 184 if value: |
| 185 yield value |
| 186 |
| 187 def write_status_headers(self): |
| 188 """Write out the status line and headers for the response""" |
| 189 self.writer.write_status(*self.status) |
| 190 for item in self.headers: |
| 191 self.writer.write_header(*item) |
| 192 self.writer.end_headers() |
| 193 |
| 194 def write_content(self): |
| 195 """Write out the response content""" |
| 196 if self.request.method != "HEAD" or self.send_body_for_head_request: |
| 197 for item in self.iter_content(): |
| 198 self.writer.write_content(item) |
| 199 |
| 200 def write(self): |
| 201 """Write the whole response""" |
| 202 self.write_status_headers() |
| 203 self.write_content() |
| 204 |
| 205 def set_error(self, code, message=""): |
| 206 """Set the response status headers and body to indicate an |
| 207 error""" |
| 208 err = {"code": code, |
| 209 "message": message} |
| 210 data = json.dumps({"error": err}) |
| 211 self.status = code |
| 212 self.headers = [("Content-Type", "text/json"), |
| 213 ("Content-Length", len(data))] |
| 214 self.content = data |
| 215 if code == 500: |
| 216 self.logger.error(message) |
| 217 |
| 218 |
| 219 class MultipartContent(object): |
| 220 def __init__(self, boundary=None, default_content_type=None): |
| 221 self.items = [] |
| 222 if boundary is None: |
| 223 boundary = str(uuid.uuid4()) |
| 224 self.boundary = boundary |
| 225 self.default_content_type = default_content_type |
| 226 |
| 227 def __call__(self): |
| 228 boundary = "--" + self.boundary |
| 229 rv = ["", boundary] |
| 230 for item in self.items: |
| 231 rv.append(str(item)) |
| 232 rv.append(boundary) |
| 233 rv[-1] += "--" |
| 234 return "\r\n".join(rv) |
| 235 |
| 236 def append_part(self, data, content_type=None, headers=None): |
| 237 if content_type is None: |
| 238 content_type = self.default_content_type |
| 239 self.items.append(MultipartPart(data, content_type, headers)) |
| 240 |
| 241 def __iter__(self): |
| 242 #This is hackish; when writing the response we need an iterable |
| 243 #or a string. For a multipart/byterange response we want an |
| 244 #iterable that contains a single callable; the MultipartContent |
| 245 #object itself |
| 246 yield self |
| 247 |
| 248 |
| 249 class MultipartPart(object): |
| 250 def __init__(self, data, content_type=None, headers=None): |
| 251 self.headers = ResponseHeaders() |
| 252 |
| 253 if content_type is not None: |
| 254 self.headers.set("Content-Type", content_type) |
| 255 |
| 256 if headers is not None: |
| 257 for name, value in headers: |
| 258 if name.lower() == "content-type": |
| 259 func = self.headers.set |
| 260 else: |
| 261 func = self.headers.append |
| 262 func(name, value) |
| 263 |
| 264 self.data = data |
| 265 |
| 266 def __str__(self): |
| 267 rv = [] |
| 268 for item in self.headers: |
| 269 rv.append("%s: %s" % item) |
| 270 rv.append("") |
| 271 rv.append(self.data) |
| 272 return "\r\n".join(rv) |
| 273 |
| 274 |
| 275 class ResponseHeaders(object): |
| 276 """Dictionary-like object holding the headers for the response""" |
| 277 def __init__(self): |
| 278 self.data = OrderedDict() |
| 279 |
| 280 def set(self, key, value): |
| 281 """Set a header to a specific value, overwriting any previous header |
| 282 with the same name |
| 283 |
| 284 :param key: Name of the header to set |
| 285 :param value: Value to set the header to |
| 286 """ |
| 287 self.data[key.lower()] = (key, [value]) |
| 288 |
| 289 def append(self, key, value): |
| 290 """Add a new header with a given name, not overwriting any existing |
| 291 headers with the same name |
| 292 |
| 293 :param key: Name of the header to add |
| 294 :param value: Value to set for the header |
| 295 """ |
| 296 if key.lower() in self.data: |
| 297 self.data[key.lower()][1].append(value) |
| 298 else: |
| 299 self.set(key, value) |
| 300 |
| 301 def get(self, key, default=missing): |
| 302 """Get the set values for a particular header.""" |
| 303 try: |
| 304 return self[key] |
| 305 except KeyError: |
| 306 if default is missing: |
| 307 return [] |
| 308 return default |
| 309 |
| 310 def __getitem__(self, key): |
| 311 """Get a list of values for a particular header |
| 312 |
| 313 """ |
| 314 return self.data[key.lower()][1] |
| 315 |
| 316 def __delitem__(self, key): |
| 317 del self.data[key.lower()] |
| 318 |
| 319 def __contains__(self, key): |
| 320 return key.lower() in self.data |
| 321 |
| 322 def __setitem__(self, key, value): |
| 323 self.set(key, value) |
| 324 |
| 325 def __iter__(self): |
| 326 for key, values in self.data.itervalues(): |
| 327 for value in values: |
| 328 yield key, value |
| 329 |
| 330 def items(self): |
| 331 return list(self) |
| 332 |
| 333 def update(self, items_iter): |
| 334 for name, value in items_iter: |
| 335 self.set(name, value) |
| 336 |
| 337 def __repr__(self): |
| 338 return repr(self.data) |
| 339 |
| 340 |
| 341 class ResponseWriter(object): |
| 342 """Object providing an API to write out a HTTP response. |
| 343 |
| 344 :param handler: The RequestHandler being used. |
| 345 :param response: The Response associated with this writer. |
| 346 |
| 347 After each part of the response is written, the output is |
| 348 flushed unless response.explicit_flush is False, in which case |
| 349 the user must call .flush() explicitly.""" |
| 350 def __init__(self, handler, response): |
| 351 self._wfile = handler.wfile |
| 352 self._response = response |
| 353 self._handler = handler |
| 354 self._headers_seen = set() |
| 355 self._headers_complete = False |
| 356 self.content_written = False |
| 357 self.request = response.request |
| 358 |
| 359 def write_status(self, code, message=None): |
| 360 """Write out the status line of a response. |
| 361 |
| 362 :param code: The integer status code of the response. |
| 363 :param message: The message of the response. Defaults to the message com
monly used |
| 364 with the status code.""" |
| 365 if message is None: |
| 366 if code in response_codes: |
| 367 message = response_codes[code][0] |
| 368 else: |
| 369 message = '' |
| 370 self.write("%s %d %s\r\n" % |
| 371 (self._response.request.protocol_version, code, message)) |
| 372 |
| 373 def write_header(self, name, value): |
| 374 """Write out a single header for the response. |
| 375 |
| 376 :param name: Name of the header field |
| 377 :param value: Value of the header field |
| 378 """ |
| 379 self._headers_seen.add(name.lower()) |
| 380 self.write("%s: %s\r\n" % (name, value)) |
| 381 if not self._response.explicit_flush: |
| 382 self.flush() |
| 383 |
| 384 def write_default_headers(self): |
| 385 for name, f in [("Server", self._handler.version_string), |
| 386 ("Date", self._handler.date_time_string)]: |
| 387 if name.lower() not in self._headers_seen: |
| 388 self.write_header(name, f()) |
| 389 |
| 390 if (type(self._response.content) in (str, unicode) and |
| 391 "content-length" not in self._headers_seen): |
| 392 #Would be nice to avoid double-encoding here |
| 393 self.write_header("Content-Length", len(self.encode(self._response.c
ontent))) |
| 394 |
| 395 def end_headers(self): |
| 396 """Finish writing headers and write the separator. |
| 397 |
| 398 Unless add_required_headers on the response is False, |
| 399 this will also add HTTP-mandated headers that have not yet been supplied |
| 400 to the response headers""" |
| 401 |
| 402 if self._response.add_required_headers: |
| 403 self.write_default_headers() |
| 404 |
| 405 self.write("\r\n") |
| 406 if "content-length" not in self._headers_seen: |
| 407 self._response.close_connection = True |
| 408 if not self._response.explicit_flush: |
| 409 self.flush() |
| 410 self._headers_complete = True |
| 411 |
| 412 def write_content(self, data): |
| 413 """Write the body of the response.""" |
| 414 self.write(self.encode(data)) |
| 415 if not self._response.explicit_flush: |
| 416 self.flush() |
| 417 |
| 418 def write(self, data): |
| 419 """Write directly to the response, converting unicode to bytes |
| 420 according to response.encoding. Does not flush.""" |
| 421 self.content_written = True |
| 422 try: |
| 423 self._wfile.write(self.encode(data)) |
| 424 except socket.error: |
| 425 # This can happen if the socket got closed by the remote end |
| 426 pass |
| 427 |
| 428 def encode(self, data): |
| 429 """Convert unicode to bytes according to response.encoding.""" |
| 430 if isinstance(data, str): |
| 431 return data |
| 432 elif isinstance(data, unicode): |
| 433 return data.encode(self._response.encoding) |
| 434 else: |
| 435 raise ValueError |
| 436 |
| 437 def flush(self): |
| 438 """Flush the output.""" |
| 439 try: |
| 440 self._wfile.flush() |
| 441 except socket.error: |
| 442 # This can happen if the socket got closed by the remote end |
| 443 pass |
OLD | NEW |