1 from base64 import b64encode 2 from datetime import ( 3 datetime, 4 timedelta, 5 ) 6 from hashlib import md5 7 import re 8 import struct 9 import zlib 10 try: 11 import simplejson as json 12 except ImportError: 13 import json 14 15 from webob.byterange import ContentRange 16 17 from webob.cachecontrol import ( 18 CacheControl, 19 serialize_cache_control, 20 ) 21 22 from webob.compat import ( 23 PY3, 24 bytes_, 25 native_, 26 text_type, 27 url_quote, 28 urlparse, 29 ) 30 31 from webob.cookies import ( 32 Cookie, 33 make_cookie, 34 ) 35 36 from webob.datetime_utils import ( 37 parse_date_delta, 38 serialize_date_delta, 39 timedelta_to_seconds, 40 ) 41 42 from webob.descriptors import ( 43 CHARSET_RE, 44 SCHEME_RE, 45 converter, 46 date_header, 47 header_getter, 48 list_header, 49 parse_auth, 50 parse_content_range, 51 parse_etag_response, 52 parse_int, 53 parse_int_safe, 54 serialize_auth, 55 serialize_content_range, 56 serialize_etag_response, 57 serialize_int, 58 ) 59 60 from webob.headers import ResponseHeaders 61 from webob.request import BaseRequest 62 from webob.util import status_reasons, status_generic_reasons 63 64 __all__ = ['Response'] 65 66 _PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I) 67 _OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I) 68 69 _gzip_header = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff' 70 71 class Response(object): 72 """ 73 Represents a WSGI response 74 """ 75 76 default_content_type = 'text/html' 77 default_charset = 'UTF-8' # TODO: deprecate 78 unicode_errors = 'strict' # TODO: deprecate (why would response body have errors?) 79 default_conditional_response = False 80 request = None 81 environ = None 82 83 # 84 # __init__, from_file, copy 85 # 86 87 def __init__(self, body=None, status=None, headerlist=None, app_iter=None, 88 content_type=None, conditional_response=None, 89 **kw): 90 if app_iter is None and body is None and ('json_body' in kw or 'json' in kw): 91 if 'json_body' in kw: 92 json_body = kw.pop('json_body') 93 else: 94 json_body = kw.pop('json') 95 body = json.dumps(json_body, separators=(',', ':')) 96 if content_type is None: 97 content_type = 'application/json' 98 if app_iter is None: 99 if body is None: 100 body = b'' 101 elif body is not None: 102 raise TypeError( 103 "You may only give one of the body and app_iter arguments") 104 if status is None: 105 self._status = '200 OK' 106 else: 107 self.status = status 108 if headerlist is None: 109 self._headerlist = [] 110 else: 111 self._headerlist = headerlist 112 self._headers = None 113 if content_type is None: 114 content_type = self.default_content_type 115 charset = None 116 if 'charset' in kw: 117 charset = kw.pop('charset') 118 elif self.default_charset: 119 if (content_type 120 and 'charset=' not in content_type 121 and (content_type == 'text/html' 122 or content_type.startswith('text/') 123 or content_type.startswith('application/xml') 124 or content_type.startswith('application/json') 125 or (content_type.startswith('application/') 126 and (content_type.endswith('+xml') or content_type.endswith('+json'))))): 127 charset = self.default_charset 128 if content_type and charset: 129 content_type += '; charset=' + charset 130 elif self._headerlist and charset: 131 self.charset = charset 132 if not self._headerlist and content_type: 133 self._headerlist.append(('Content-Type', content_type)) 134 if conditional_response is None: 135 self.conditional_response = self.default_conditional_response 136 else: 137 self.conditional_response = bool(conditional_response) 138 if app_iter is None: 139 if isinstance(body, text_type): 140 if charset is None: 141 raise TypeError( 142 "You cannot set the body to a text value without a " 143 "charset") 144 body = body.encode(charset) 145 app_iter = [body] 146 if headerlist is None: 147 self._headerlist.append(('Content-Length', str(len(body)))) 148 else: 149 self.headers['Content-Length'] = str(len(body)) 150 self._app_iter = app_iter 151 for name, value in kw.items(): 152 if not hasattr(self.__class__, name): 153 # Not a basic attribute 154 raise TypeError( 155 "Unexpected keyword: %s=%r" % (name, value)) 156 setattr(self, name, value) 157 158 159 @classmethod 160 def from_file(cls, fp): 161 """Reads a response from a file-like object (it must implement 162 ``.read(size)`` and ``.readline()``). 163 164 It will read up to the end of the response, not the end of the 165 file. 166 167 This reads the response as represented by ``str(resp)``; it 168 may not read every valid HTTP response properly. Responses 169 must have a ``Content-Length``""" 170 headerlist = [] 171 status = fp.readline().strip() 172 is_text = isinstance(status, text_type) 173 if is_text: 174 _colon = ':' 175 else: 176 _colon = b':' 177 while 1: 178 line = fp.readline().strip() 179 if not line: 180 # end of headers 181 break 182 try: 183 header_name, value = line.split(_colon, 1) 184 except ValueError: 185 raise ValueError('Bad header line: %r' % line) 186 value = value.strip() 187 headerlist.append(( 188 native_(header_name, 'latin-1'), 189 native_(value, 'latin-1') 190 )) 191 r = cls( 192 status=status, 193 headerlist=headerlist, 194 app_iter=(), 195 ) 196 body = fp.read(r.content_length or 0) 197 if is_text: 198 r.text = body 199 else: 200 r.body = body 201 return r 202 203 def copy(self): 204 """Makes a copy of the response""" 205 # we need to do this for app_iter to be reusable 206 app_iter = list(self._app_iter) 207 iter_close(self._app_iter) 208 # and this to make sure app_iter instances are different 209 self._app_iter = list(app_iter) 210 return self.__class__( 211 content_type=False, 212 status=self._status, 213 headerlist=self._headerlist[:], 214 app_iter=app_iter, 215 conditional_response=self.conditional_response) 216 217 218 # 219 # __repr__, __str__ 220 # 221 222 def __repr__(self): 223 return '<%s at 0x%x %s>' % (self.__class__.__name__, abs(id(self)), 224 self.status) 225 226 def __str__(self, skip_body=False): 227 parts = [self.status] 228 if not skip_body: 229 # Force enumeration of the body (to set content-length) 230 self.body 231 parts += map('%s: %s'.__mod__, self.headerlist) 232 if not skip_body and self.body: 233 parts += ['', self.text if PY3 else self.body] 234 return '\r\n'.join(parts) 235 236 # 237 # status, status_code/status_int 238 # 239 240 def _status__get(self): 241 """ 242 The status string 243 """ 244 return self._status 245 246 def _status__set(self, value): 247 try: 248 code = int(value) 249 except (ValueError, TypeError): 250 pass 251 else: 252 self.status_code = code 253 return 254 if PY3: # pragma: no cover 255 if isinstance(value, bytes): 256 value = value.decode('ascii') 257 elif isinstance(value, text_type): 258 value = value.encode('ascii') 259 if not isinstance(value, str): 260 raise TypeError( 261 "You must set status to a string or integer (not %s)" 262 % type(value)) 263 264 # Attempt to get the status code itself, if this fails we should fail 265 status_code = int(value.split()[0]) 266 self._status = value 267 268 status = property(_status__get, _status__set, doc=_status__get.__doc__) 269 270 def _status_code__get(self): 271 """ 272 The status as an integer 273 """ 274 return int(self._status.split()[0]) 275 276 def _status_code__set(self, code): 277 try: 278 self._status = '%d %s' % (code, status_reasons[code]) 279 except KeyError: 280 self._status = '%d %s' % (code, status_generic_reasons[code // 100]) 281 282 status_code = status_int = property(_status_code__get, _status_code__set, 283 doc=_status_code__get.__doc__) 284 285 286 # 287 # headerslist, headers 288 # 289 290 def _headerlist__get(self): 291 """ 292 The list of response headers 293 """ 294 return self._headerlist 295 296 def _headerlist__set(self, value): 297 self._headers = None 298 if not isinstance(value, list): 299 if hasattr(value, 'items'): 300 value = value.items() 301 value = list(value) 302 self._headerlist = value 303 304 def _headerlist__del(self): 305 self.headerlist = [] 306 307 headerlist = property(_headerlist__get, _headerlist__set, 308 _headerlist__del, doc=_headerlist__get.__doc__) 309 310 def _headers__get(self): 311 """ 312 The headers in a dictionary-like object 313 """ 314 if self._headers is None: 315 self._headers = ResponseHeaders.view_list(self.headerlist) 316 return self._headers 317 318 def _headers__set(self, value): 319 if hasattr(value, 'items'): 320 value = value.items() 321 self.headerlist = value 322 self._headers = None 323 324 headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__) 325 326 327 # 328 # body 329 # 330 331 def _body__get(self): 332 """ 333 The body of the response, as a ``str``. This will read in the 334 entire app_iter if necessary. 335 """ 336 app_iter = self._app_iter 337 # try: 338 # if len(app_iter) == 1: 339 # return app_iter[0] 340 # except: 341 # pass 342 if isinstance(app_iter, list) and len(app_iter) == 1: 343 return app_iter[0] 344 if app_iter is None: 345 raise AttributeError("No body has been set") 346 try: 347 body = b''.join(app_iter) 348 finally: 349 iter_close(app_iter) 350 if isinstance(body, text_type): 351 raise _error_unicode_in_app_iter(app_iter, body) 352 self._app_iter = [body] 353 if len(body) == 0: 354 # if body-length is zero, we assume it's a HEAD response and 355 # leave content_length alone 356 pass # pragma: no cover (no idea why necessary, it's hit) 357 elif self.content_length is None: 358 self.content_length = len(body) 359 elif self.content_length != len(body): 360 raise AssertionError( 361 "Content-Length is different from actual app_iter length " 362 "(%r!=%r)" 363 % (self.content_length, len(body)) 364 ) 365 return body 366 367 def _body__set(self, value=b''): 368 if not isinstance(value, bytes): 369 if isinstance(value, text_type): 370 msg = ("You cannot set Response.body to a text object " 371 "(use Response.text)") 372 else: 373 msg = ("You can only set the body to a binary type (not %s)" % 374 type(value)) 375 raise TypeError(msg) 376 if self._app_iter is not None: 377 self.content_md5 = None 378 self._app_iter = [value] 379 self.content_length = len(value) 380 381 # def _body__del(self): 382 # self.body = '' 383 # #self.content_length = None 384 385 body = property(_body__get, _body__set, _body__set) 386 387 def _json_body__get(self): 388 """Access the body of the response as JSON""" 389 # Note: UTF-8 is a content-type specific default for JSON: 390 return json.loads(self.body.decode(self.charset or 'UTF-8')) 391 392 def _json_body__set(self, value): 393 self.body = json.dumps(value, separators=(',', ':')).encode(self.charset or 'UTF-8') 394 395 def _json_body__del(self): 396 del self.body 397 398 json = json_body = property(_json_body__get, _json_body__set, _json_body__del) 399 400 401 # 402 # text, unicode_body, ubody 403 # 404 405 def _text__get(self): 406 """ 407 Get/set the text value of the body (using the charset of the 408 Content-Type) 409 """ 410 if not self.charset: 411 raise AttributeError( 412 "You cannot access Response.text unless charset is set") 413 body = self.body 414 return body.decode(self.charset, self.unicode_errors) 415 416 def _text__set(self, value): 417 if not self.charset: 418 raise AttributeError( 419 "You cannot access Response.text unless charset is set") 420 if not isinstance(value, text_type): 421 raise TypeError( 422 "You can only set Response.text to a unicode string " 423 "(not %s)" % type(value)) 424 self.body = value.encode(self.charset) 425 426 def _text__del(self): 427 del self.body 428 429 text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__) 430 431 unicode_body = ubody = property(_text__get, _text__set, _text__del, 432 "Deprecated alias for .text") 433 434 # 435 # body_file, write(text) 436 # 437 438 def _body_file__get(self): 439 """ 440 A file-like object that can be used to write to the 441 body. If you passed in a list app_iter, that app_iter will be 442 modified by writes. 443 """ 444 return ResponseBodyFile(self) 445 446 def _body_file__set(self, file): 447 self.app_iter = iter_file(file) 448 449 def _body_file__del(self): 450 del self.body 451 452 body_file = property(_body_file__get, _body_file__set, _body_file__del, 453 doc=_body_file__get.__doc__) 454 455 def write(self, text): 456 if not isinstance(text, bytes): 457 if not isinstance(text, text_type): 458 msg = "You can only write str to a Response.body_file, not %s" 459 raise TypeError(msg % type(text)) 460 if not self.charset: 461 msg = ("You can only write text to Response if charset has " 462 "been set") 463 raise TypeError(msg) 464 text = text.encode(self.charset) 465 app_iter = self._app_iter 466 if not isinstance(app_iter, list): 467 try: 468 new_app_iter = self._app_iter = list(app_iter) 469 finally: 470 iter_close(app_iter) 471 app_iter = new_app_iter 472 self.content_length = sum(len(chunk) for chunk in app_iter) 473 app_iter.append(text) 474 if self.content_length is not None: 475 self.content_length += len(text) 476 477 478 479 # 480 # app_iter 481 # 482 483 def _app_iter__get(self): 484 """ 485 Returns the app_iter of the response. 486 487 If body was set, this will create an app_iter from that body 488 (a single-item list) 489 """ 490 return self._app_iter 491 492 def _app_iter__set(self, value): 493 if self._app_iter is not None: 494 # Undo the automatically-set content-length 495 self.content_length = None 496 self.content_md5 = None 497 self._app_iter = value 498 499 def _app_iter__del(self): 500 self._app_iter = [] 501 self.content_length = None 502 503 app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del, 504 doc=_app_iter__get.__doc__) 505 506 507 508 # 509 # headers attrs 510 # 511 512 allow = list_header('Allow', '14.7') 513 # TODO: (maybe) support response.vary += 'something' 514 # TODO: same thing for all listy headers 515 vary = list_header('Vary', '14.44') 516 517 content_length = converter( 518 header_getter('Content-Length', '14.17'), 519 parse_int, serialize_int, 'int') 520 521 content_encoding = header_getter('Content-Encoding', '14.11') 522 content_language = list_header('Content-Language', '14.12') 523 content_location = header_getter('Content-Location', '14.14') 524 content_md5 = header_getter('Content-MD5', '14.14') 525 content_disposition = header_getter('Content-Disposition', '19.5.1') 526 527 accept_ranges = header_getter('Accept-Ranges', '14.5') 528 content_range = converter( 529 header_getter('Content-Range', '14.16'), 530 parse_content_range, serialize_content_range, 'ContentRange object') 531 532 date = date_header('Date', '14.18') 533 expires = date_header('Expires', '14.21') 534 last_modified = date_header('Last-Modified', '14.29') 535 536 _etag_raw = header_getter('ETag', '14.19') 537 etag = converter(_etag_raw, 538 parse_etag_response, serialize_etag_response, 539 'Entity tag' 540 ) 541 @property 542 def etag_strong(self): 543 return parse_etag_response(self._etag_raw, strong=True) 544 545 location = header_getter('Location', '14.30') 546 pragma = header_getter('Pragma', '14.32') 547 age = converter( 548 header_getter('Age', '14.6'), 549 parse_int_safe, serialize_int, 'int') 550 551 retry_after = converter( 552 header_getter('Retry-After', '14.37'), 553 parse_date_delta, serialize_date_delta, 'HTTP date or delta seconds') 554 555 server = header_getter('Server', '14.38') 556 557 # TODO: the standard allows this to be a list of challenges 558 www_authenticate = converter( 559 header_getter('WWW-Authenticate', '14.47'), 560 parse_auth, serialize_auth, 561 ) 562 563 564 # 565 # charset 566 # 567 568 def _charset__get(self): 569 """ 570 Get/set the charset (in the Content-Type) 571 """ 572 header = self.headers.get('Content-Type') 573 if not header: 574 return None 575 match = CHARSET_RE.search(header) 576 if match: 577 return match.group(1) 578 return None 579 580 def _charset__set(self, charset): 581 if charset is None: 582 del self.charset 583 return 584 header = self.headers.pop('Content-Type', None) 585 if header is None: 586 raise AttributeError("You cannot set the charset when no " 587 "content-type is defined") 588 match = CHARSET_RE.search(header) 589 if match: 590 header = header[:match.start()] + header[match.end():] 591 header += '; charset=%s' % charset 592 self.headers['Content-Type'] = header 593 594 def _charset__del(self): 595 header = self.headers.pop('Content-Type', None) 596 if header is None: 597 # Don't need to remove anything 598 return 599 match = CHARSET_RE.search(header) 600 if match: 601 header = header[:match.start()] + header[match.end():] 602 self.headers['Content-Type'] = header 603 604 charset = property(_charset__get, _charset__set, _charset__del, 605 doc=_charset__get.__doc__) 606 607 608 # 609 # content_type 610 # 611 612 def _content_type__get(self): 613 """ 614 Get/set the Content-Type header (or None), *without* the 615 charset or any parameters. 616 617 If you include parameters (or ``;`` at all) when setting the 618 content_type, any existing parameters will be deleted; 619 otherwise they will be preserved. 620 """ 621 header = self.headers.get('Content-Type') 622 if not header: 623 return None 624 return header.split(';', 1)[0] 625 626 def _content_type__set(self, value): 627 if not value: 628 self._content_type__del() 629 return 630 if ';' not in value: 631 header = self.headers.get('Content-Type', '') 632 if ';' in header: 633 params = header.split(';', 1)[1] 634 value += ';' + params 635 self.headers['Content-Type'] = value 636 637 def _content_type__del(self): 638 self.headers.pop('Content-Type', None) 639 640 content_type = property(_content_type__get, _content_type__set, 641 _content_type__del, doc=_content_type__get.__doc__) 642 643 644 # 645 # content_type_params 646 # 647 648 def _content_type_params__get(self): 649 """ 650 A dictionary of all the parameters in the content type. 651 652 (This is not a view, set to change, modifications of the dict would not 653 be applied otherwise) 654 """ 655 params = self.headers.get('Content-Type', '') 656 if ';' not in params: 657 return {} 658 params = params.split(';', 1)[1] 659 result = {} 660 for match in _PARAM_RE.finditer(params): 661 result[match.group(1)] = match.group(2) or match.group(3) or '' 662 return result 663 664 def _content_type_params__set(self, value_dict): 665 if not value_dict: 666 del self.content_type_params 667 return 668 params = [] 669 for k, v in sorted(value_dict.items()): 670 if not _OK_PARAM_RE.search(v): 671 v = '"%s"' % v.replace('"', '\\"') 672 params.append('; %s=%s' % (k, v)) 673 ct = self.headers.pop('Content-Type', '').split(';', 1)[0] 674 ct += ''.join(params) 675 self.headers['Content-Type'] = ct 676 677 def _content_type_params__del(self): 678 self.headers['Content-Type'] = self.headers.get( 679 'Content-Type', '').split(';', 1)[0] 680 681 content_type_params = property( 682 _content_type_params__get, 683 _content_type_params__set, 684 _content_type_params__del, 685 _content_type_params__get.__doc__ 686 ) 687 688 689 690 691 # 692 # set_cookie, unset_cookie, delete_cookie, merge_cookies 693 # 694 695 def set_cookie(self, name, value='', max_age=None, 696 path='/', domain=None, secure=False, httponly=False, 697 comment=None, expires=None, overwrite=False): 698 """ 699 Set (add) a cookie for the response. 700 701 Arguments are: 702 703 ``name`` 704 705 The cookie name. 706 707 ``value`` 708 709 The cookie value, which should be a string or ``None``. If 710 ``value`` is ``None``, it's equivalent to calling the 711 :meth:`webob.response.Response.unset_cookie` method for this 712 cookie key (it effectively deletes the cookie on the client). 713 714 ``max_age`` 715 716 An integer representing a number of seconds, ``datetime.timedelta``, 717 or ``None``. This value is used as the ``Max-Age`` of the generated 718 cookie. If ``expires`` is not passed and this value is not 719 ``None``, the ``max_age`` value will also influence the ``Expires`` 720 value of the cookie (``Expires`` will be set to now + max_age). If 721 this value is ``None``, the cookie will not have a ``Max-Age`` value 722 (unless ``expires`` is set). If both ``max_age`` and ``expires`` are 723 set, this value takes precedence. 724 725 ``path`` 726 727 A string representing the cookie ``Path`` value. It defaults to 728 ``/``. 729 730 ``domain`` 731 732 A string representing the cookie ``Domain``, or ``None``. If 733 domain is ``None``, no ``Domain`` value will be sent in the 734 cookie. 735 736 ``secure`` 737 738 A boolean. If it's ``True``, the ``secure`` flag will be sent in 739 the cookie, if it's ``False``, the ``secure`` flag will not be 740 sent in the cookie. 741 742 ``httponly`` 743 744 A boolean. If it's ``True``, the ``HttpOnly`` flag will be sent 745 in the cookie, if it's ``False``, the ``HttpOnly`` flag will not 746 be sent in the cookie. 747 748 ``comment`` 749 750 A string representing the cookie ``Comment`` value, or ``None``. 751 If ``comment`` is ``None``, no ``Comment`` value will be sent in 752 the cookie. 753 754 ``expires`` 755 756 A ``datetime.timedelta`` object representing an amount of time, 757 ``datetime.datetime`` or ``None``. A non-``None`` value is used to 758 generate the ``Expires`` value of the generated cookie. If 759 ``max_age`` is not passed, but this value is not ``None``, it will 760 influence the ``Max-Age`` header. If this value is ``None``, the 761 ``Expires`` cookie value will be unset (unless ``max_age`` is set). 762 If ``max_age`` is set, it will be used to generate the ``expires`` 763 and this value is ignored. 764 765 ``overwrite`` 766 767 If this key is ``True``, before setting the cookie, unset any 768 existing cookie. 769 770 """ 771 if overwrite: 772 self.unset_cookie(name, strict=False) 773 774 # If expires is set, but not max_age we set max_age to expires 775 if not max_age and isinstance(expires, timedelta): 776 max_age = expires 777 778 # expires can also be a datetime 779 if not max_age and isinstance(expires, datetime): 780 max_age = expires - datetime.utcnow() 781 782 value = bytes_(value, 'utf-8') 783 784 cookie = make_cookie(name, value, max_age=max_age, path=path, 785 domain=domain, secure=secure, httponly=httponly, 786 comment=comment) 787 788 self.headerlist.append(('Set-Cookie', cookie)) 789 790 def delete_cookie(self, name, path='/', domain=None): 791 """ 792 Delete a cookie from the client. Note that path and domain must match 793 how the cookie was originally set. 794 795 This sets the cookie to the empty string, and max_age=0 so 796 that it should expire immediately. 797 """ 798 self.set_cookie(name, None, path=path, domain=domain) 799 800 def unset_cookie(self, name, strict=True): 801 """ 802 Unset a cookie with the given name (remove it from the 803 response). 804 """ 805 existing = self.headers.getall('Set-Cookie') 806 if not existing and not strict: 807 return 808 cookies = Cookie() 809 for header in existing: 810 cookies.load(header) 811 if isinstance(name, text_type): 812 name = name.encode('utf8') 813 if name in cookies: 814 del cookies[name] 815 del self.headers['Set-Cookie'] 816 for m in cookies.values(): 817 self.headerlist.append(('Set-Cookie', m.serialize())) 818 elif strict: 819 raise KeyError("No cookie has been set with the name %r" % name) 820 821 822 def merge_cookies(self, resp): 823 """Merge the cookies that were set on this response with the 824 given `resp` object (which can be any WSGI application). 825 826 If the `resp` is a :class:`webob.Response` object, then the 827 other object will be modified in-place. 828 """ 829 if not self.headers.get('Set-Cookie'): 830 return resp 831 if isinstance(resp, Response): 832 for header in self.headers.getall('Set-Cookie'): 833 resp.headers.add('Set-Cookie', header) 834 return resp 835 else: 836 c_headers = [h for h in self.headerlist if 837 h[0].lower() == 'set-cookie'] 838 def repl_app(environ, start_response): 839 def repl_start_response(status, headers, exc_info=None): 840 return start_response(status, headers+c_headers, 841 exc_info=exc_info) 842 return resp(environ, repl_start_response) 843 return repl_app 844 845 846 # 847 # cache_control 848 # 849 850 _cache_control_obj = None 851 852 def _cache_control__get(self): 853 """ 854 Get/set/modify the Cache-Control header (`HTTP spec section 14.9 855 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9>`_) 856 """ 857 value = self.headers.get('cache-control', '') 858 if self._cache_control_obj is None: 859 self._cache_control_obj = CacheControl.parse( 860 value, updates_to=self._update_cache_control, type='response') 861 self._cache_control_obj.header_value = value 862 if self._cache_control_obj.header_value != value: 863 new_obj = CacheControl.parse(value, type='response') 864 self._cache_control_obj.properties.clear() 865 self._cache_control_obj.properties.update(new_obj.properties) 866 self._cache_control_obj.header_value = value 867 return self._cache_control_obj 868 869 def _cache_control__set(self, value): 870 # This actually becomes a copy 871 if not value: 872 value = "" 873 if isinstance(value, dict): 874 value = CacheControl(value, 'response') 875 if isinstance(value, text_type): 876 value = str(value) 877 if isinstance(value, str): 878 if self._cache_control_obj is None: 879 self.headers['Cache-Control'] = value 880 return 881 value = CacheControl.parse(value, 'response') 882 cache = self.cache_control 883 cache.properties.clear() 884 cache.properties.update(value.properties) 885 886 def _cache_control__del(self): 887 self.cache_control = {} 888 889 def _update_cache_control(self, prop_dict): 890 value = serialize_cache_control(prop_dict) 891 if not value: 892 if 'Cache-Control' in self.headers: 893 del self.headers['Cache-Control'] 894 else: 895 self.headers['Cache-Control'] = value 896 897 cache_control = property( 898 _cache_control__get, _cache_control__set, 899 _cache_control__del, doc=_cache_control__get.__doc__) 900 901 902 # 903 # cache_expires 904 # 905 906 def _cache_expires(self, seconds=0, **kw): 907 """ 908 Set expiration on this request. This sets the response to 909 expire in the given seconds, and any other attributes are used 910 for cache_control (e.g., private=True, etc). 911 """ 912 if seconds is True: 913 seconds = 0 914 elif isinstance(seconds, timedelta): 915 seconds = timedelta_to_seconds(seconds) 916 cache_control = self.cache_control 917 if seconds is None: 918 pass 919 elif not seconds: 920 # To really expire something, you have to force a 921 # bunch of these cache control attributes, and IE may 922 # not pay attention to those still so we also set 923 # Expires. 924 cache_control.no_store = True 925 cache_control.no_cache = True 926 cache_control.must_revalidate = True 927 cache_control.max_age = 0 928 cache_control.post_check = 0 929 cache_control.pre_check = 0 930 self.expires = datetime.utcnow() 931 if 'last-modified' not in self.headers: 932 self.last_modified = datetime.utcnow() 933 self.pragma = 'no-cache' 934 else: 935 cache_control.properties.clear() 936 cache_control.max_age = seconds 937 self.expires = datetime.utcnow() + timedelta(seconds=seconds) 938 self.pragma = None 939 for name, value in kw.items(): 940 setattr(cache_control, name, value) 941 942 cache_expires = property(lambda self: self._cache_expires, _cache_expires) 943 944 945 946 # 947 # encode_content, decode_content, md5_etag 948 # 949 950 def encode_content(self, encoding='gzip', lazy=False): 951 """ 952 Encode the content with the given encoding (only gzip and 953 identity are supported). 954 """ 955 assert encoding in ('identity', 'gzip'), \ 956 "Unknown encoding: %r" % encoding 957 if encoding == 'identity': 958 self.decode_content() 959 return 960 if self.content_encoding == 'gzip': 961 return 962 if lazy: 963 self.app_iter = gzip_app_iter(self._app_iter) 964 self.content_length = None 965 else: 966 self.app_iter = list(gzip_app_iter(self._app_iter)) 967 self.content_length = sum(map(len, self._app_iter)) 968 self.content_encoding = 'gzip' 969 970 def decode_content(self): 971 content_encoding = self.content_encoding or 'identity' 972 if content_encoding == 'identity': 973 return 974 if content_encoding not in ('gzip', 'deflate'): 975 raise ValueError( 976 "I don't know how to decode the content %s" % content_encoding) 977 if content_encoding == 'gzip': 978 from gzip import GzipFile 979 from io import BytesIO 980 gzip_f = GzipFile(filename='', mode='r', fileobj=BytesIO(self.body)) 981 self.body = gzip_f.read() 982 self.content_encoding = None 983 gzip_f.close() 984 else: 985 # Weird feature: http://bugs.python.org/issue5784 986 self.body = zlib.decompress(self.body, -15) 987 self.content_encoding = None 988 989 def md5_etag(self, body=None, set_content_md5=False): 990 """ 991 Generate an etag for the response object using an MD5 hash of 992 the body (the body parameter, or ``self.body`` if not given) 993 994 Sets ``self.etag`` 995 If ``set_content_md5`` is True sets ``self.content_md5`` as well 996 """ 997 if body is None: 998 body = self.body 999 md5_digest = md5(body).digest() 1000 md5_digest = b64encode(md5_digest) 1001 md5_digest = md5_digest.replace(b'\n', b'') 1002 md5_digest = native_(md5_digest) 1003 self.etag = md5_digest.strip('=') 1004 if set_content_md5: 1005 self.content_md5 = md5_digest 1006 1007 1008 1009 # 1010 # __call__, conditional_response_app 1011 # 1012 1013 def __call__(self, environ, start_response): 1014 """ 1015 WSGI application interface 1016 """ 1017 if self.conditional_response: 1018 return self.conditional_response_app(environ, start_response) 1019 headerlist = self._abs_headerlist(environ) 1020 start_response(self.status, headerlist) 1021 if environ['REQUEST_METHOD'] == 'HEAD': 1022 # Special case here... 1023 return EmptyResponse(self._app_iter) 1024 return self._app_iter 1025 1026 def _abs_headerlist(self, environ): 1027 """Returns a headerlist, with the Location header possibly 1028 made absolute given the request environ. 1029 """ 1030 headerlist = list(self.headerlist) 1031 for i, (name, value) in enumerate(headerlist): 1032 if name.lower() == 'location': 1033 if SCHEME_RE.search(value): 1034 break 1035 new_location = urlparse.urljoin(_request_uri(environ), value) 1036 headerlist[i] = (name, new_location) 1037 break 1038 return headerlist 1039 1040 _safe_methods = ('GET', 'HEAD') 1041 1042 def conditional_response_app(self, environ, start_response): 1043 """ 1044 Like the normal __call__ interface, but checks conditional headers: 1045 1046 * If-Modified-Since (304 Not Modified; only on GET, HEAD) 1047 * If-None-Match (304 Not Modified; only on GET, HEAD) 1048 * Range (406 Partial Content; only on GET, HEAD) 1049 """ 1050 req = BaseRequest(environ) 1051 headerlist = self._abs_headerlist(environ) 1052 method = environ.get('REQUEST_METHOD', 'GET') 1053 if method in self._safe_methods: 1054 status304 = False 1055 if req.if_none_match and self.etag: 1056 status304 = self.etag in req.if_none_match 1057 elif req.if_modified_since and self.last_modified: 1058 status304 = self.last_modified <= req.if_modified_since 1059 if status304: 1060 start_response('304 Not Modified', filter_headers(headerlist)) 1061 return EmptyResponse(self._app_iter) 1062 if (req.range and self in req.if_range 1063 and self.content_range is None 1064 and method in ('HEAD', 'GET') 1065 and self.status_code == 200 1066 and self.content_length is not None 1067 ): 1068 content_range = req.range.content_range(self.content_length) 1069 if content_range is None: 1070 iter_close(self._app_iter) 1071 body = bytes_("Requested range not satisfiable: %s" % req.range) 1072 headerlist = [ 1073 ('Content-Length', str(len(body))), 1074 ('Content-Range', str(ContentRange(None, None, 1075 self.content_length))), 1076 ('Content-Type', 'text/plain'), 1077 ] + filter_headers(headerlist) 1078 start_response('416 Requested Range Not Satisfiable', 1079 headerlist) 1080 if method == 'HEAD': 1081 return () 1082 return [body] 1083 else: 1084 app_iter = self.app_iter_range(content_range.start, 1085 content_range.stop) 1086 if app_iter is not None: 1087 # the following should be guaranteed by 1088 # Range.range_for_length(length) 1089 assert content_range.start is not None 1090 headerlist = [ 1091 ('Content-Length', 1092 str(content_range.stop - content_range.start)), 1093 ('Content-Range', str(content_range)), 1094 ] + filter_headers(headerlist, ('content-length',)) 1095 start_response('206 Partial Content', headerlist) 1096 if method == 'HEAD': 1097 return EmptyResponse(app_iter) 1098 return app_iter 1099 1100 start_response(self.status, headerlist) 1101 if method == 'HEAD': 1102 return EmptyResponse(self._app_iter) 1103 return self._app_iter 1104 1105 def app_iter_range(self, start, stop): 1106 """ 1107 Return a new app_iter built from the response app_iter, that 1108 serves up only the given ``start:stop`` range. 1109 """ 1110 app_iter = self._app_iter 1111 if hasattr(app_iter, 'app_iter_range'): 1112 return app_iter.app_iter_range(start, stop) 1113 return AppIterRange(app_iter, start, stop) 1114 1115 1116 def filter_headers(hlist, remove_headers=('content-length', 'content-type')): 1117 return [h for h in hlist if (h[0].lower() not in remove_headers)] 1118 1119 1120 def iter_file(file, block_size=1<<18): # 256Kb 1121 while True: 1122 data = file.read(block_size) 1123 if not data: 1124 break 1125 yield data 1126 1127 class ResponseBodyFile(object): 1128 mode = 'wb' 1129 closed = False 1130 1131 def __init__(self, response): 1132 self.response = response 1133 self.write = response.write 1134 1135 def __repr__(self): 1136 return '<body_file for %r>' % self.response 1137 1138 encoding = property( 1139 lambda self: self.response.charset, 1140 doc="The encoding of the file (inherited from response.charset)" 1141 ) 1142 1143 def writelines(self, seq): 1144 for item in seq: 1145 self.write(item) 1146 1147 def close(self): 1148 raise NotImplementedError("Response bodies cannot be closed") 1149 1150 def flush(self): 1151 pass 1152 1153 1154 1155 class AppIterRange(object): 1156 """ 1157 Wraps an app_iter, returning just a range of bytes 1158 """ 1159 1160 def __init__(self, app_iter, start, stop): 1161 assert start >= 0, "Bad start: %r" % start 1162 assert stop is None or (stop >= 0 and stop >= start), ( 1163 "Bad stop: %r" % stop) 1164 self.app_iter = iter(app_iter) 1165 self._pos = 0 # position in app_iter 1166 self.start = start 1167 self.stop = stop 1168 1169 def __iter__(self): 1170 return self 1171 1172 def _skip_start(self): 1173 start, stop = self.start, self.stop 1174 for chunk in self.app_iter: 1175 self._pos += len(chunk) 1176 if self._pos < start: 1177 continue 1178 elif self._pos == start: 1179 return b'' 1180 else: 1181 chunk = chunk[start-self._pos:] 1182 if stop is not None and self._pos > stop: 1183 chunk = chunk[:stop-self._pos] 1184 assert len(chunk) == stop - start 1185 return chunk 1186 else: 1187 raise StopIteration() 1188 1189 1190 def next(self): 1191 if self._pos < self.start: 1192 # need to skip some leading bytes 1193 return self._skip_start() 1194 stop = self.stop 1195 if stop is not None and self._pos >= stop: 1196 raise StopIteration 1197 1198 chunk = next(self.app_iter) 1199 self._pos += len(chunk) 1200 1201 if stop is None or self._pos <= stop: 1202 return chunk 1203 else: 1204 return chunk[:stop-self._pos] 1205 1206 __next__ = next # py3 1207 1208 def close(self): 1209 iter_close(self.app_iter) 1210 1211 1212 class EmptyResponse(object): 1213 """An empty WSGI response. 1214 1215 An iterator that immediately stops. Optionally provides a close 1216 method to close an underlying app_iter it replaces. 1217 """ 1218 1219 def __init__(self, app_iter=None): 1220 if app_iter is not None and hasattr(app_iter, 'close'): 1221 self.close = app_iter.close 1222 1223 def __iter__(self): 1224 return self 1225 1226 def __len__(self): 1227 return 0 1228 1229 def next(self): 1230 raise StopIteration() 1231 1232 __next__ = next # py3 1233 1234 def _request_uri(environ): 1235 """Like wsgiref.url.request_uri, except eliminates :80 ports 1236 1237 Return the full request URI""" 1238 url = environ['wsgi.url_scheme']+'://' 1239 1240 if environ.get('HTTP_HOST'): 1241 url += environ['HTTP_HOST'] 1242 else: 1243 url += environ['SERVER_NAME'] + ':' + environ['SERVER_PORT'] 1244 if url.endswith(':80') and environ['wsgi.url_scheme'] == 'http': 1245 url = url[:-3] 1246 elif url.endswith(':443') and environ['wsgi.url_scheme'] == 'https': 1247 url = url[:-4] 1248 1249 if PY3: # pragma: no cover 1250 script_name = bytes_(environ.get('SCRIPT_NAME', '/'), 'latin-1') 1251 path_info = bytes_(environ.get('PATH_INFO', ''), 'latin-1') 1252 else: 1253 script_name = environ.get('SCRIPT_NAME', '/') 1254 path_info = environ.get('PATH_INFO', '') 1255 1256 url += url_quote(script_name) 1257 qpath_info = url_quote(path_info) 1258 if not 'SCRIPT_NAME' in environ: 1259 url += qpath_info[1:] 1260 else: 1261 url += qpath_info 1262 return url 1263 1264 1265 def iter_close(iter): 1266 if hasattr(iter, 'close'): 1267 iter.close() 1268 1269 def gzip_app_iter(app_iter): 1270 size = 0 1271 crc = zlib.crc32(b"") & 0xffffffff 1272 compress = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS, 1273 zlib.DEF_MEM_LEVEL, 0) 1274 1275 yield _gzip_header 1276 for item in app_iter: 1277 size += len(item) 1278 crc = zlib.crc32(item, crc) & 0xffffffff 1279 1280 # The compress function may return zero length bytes if the input is 1281 # small enough; it buffers the input for the next iteration or for a 1282 # flush. 1283 result = compress.compress(item) 1284 if result: 1285 yield result 1286 1287 # Similarly, flush may also not yield a value. 1288 result = compress.flush() 1289 if result: 1290 yield result 1291 yield struct.pack("<2L", crc, size & 0xffffffff) 1292 1293 def _error_unicode_in_app_iter(app_iter, body): 1294 app_iter_repr = repr(app_iter) 1295 if len(app_iter_repr) > 50: 1296 app_iter_repr = ( 1297 app_iter_repr[:30] + '...' + app_iter_repr[-10:]) 1298 raise TypeError( 1299 'An item of the app_iter (%s) was text, causing a ' 1300 'text body: %r' % (app_iter_repr, body)) 1301