Home | History | Annotate | Download | only in webob
      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