Package wtf :: Module httputil
[hide private]
[frames] | no frames]

Source Code for Module wtf.httputil

  1  # -*- coding: ascii -*- 
  2  # 
  3  # Copyright 2006-2012 
  4  # Andr\xe9 Malo or his licensors, as applicable 
  5  # 
  6  # Licensed under the Apache License, Version 2.0 (the "License"); 
  7  # you may not use this file except in compliance with the License. 
  8  # You may obtain a copy of the License at 
  9  # 
 10  #     http://www.apache.org/licenses/LICENSE-2.0 
 11  # 
 12  # Unless required by applicable law or agreed to in writing, software 
 13  # distributed under the License is distributed on an "AS IS" BASIS, 
 14  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 15  # See the License for the specific language governing permissions and 
 16  # limitations under the License. 
 17  """ 
 18  HTTP utilities 
 19  ============== 
 20   
 21  This module implements various common HTTP utilities. 
 22   
 23  :Variables: 
 24   - `CR`: ASCII CR byte (\\r) 
 25   - `LF`: ASCII LF byte (\\n) 
 26   - `CRLF`: ASCII CRLF sequence (\\r\\n) 
 27   
 28  :Types: 
 29   - `CR`: ``str`` 
 30   - `LF`: ``str`` 
 31   - `CRLF`: ``str`` 
 32  """ 
 33  __author__ = u"Andr\xe9 Malo" 
 34  __docformat__ = "restructuredtext en" 
 35   
 36  import datetime as _datetime 
 37  import re as _re 
 38   
 39  from wtf import Error 
 40   
 41  CR = "\x0D" 
 42  LF = "\x0A" 
 43  CRLF = CR + LF 
44 45 46 -class HeaderError(Error):
47 """ Base header parse error """
48
49 -class InvalidHeaderLine(HeaderError):
50 """ A header line is invalid """
51
52 -class IncompleteHeaders(HeaderError):
53 """ The headers are incomplete """
54
55 56 -def make_date(stamp=None, cookie=False):
57 """ 58 Make a HTTP date 59 60 :Parameters: 61 - `stamp`: The UTC timestamp to process. If omitted or ``None``, the 62 current time is taken 63 64 :Types: 65 - `stamp`: ``datetime.datetime`` 66 67 :return: The HTTP date string 68 :rtype: ``str`` 69 """ 70 self = make_date 71 if stamp is None: 72 stamp = _datetime.datetime.utcnow() 73 return stamp.strftime( 74 "%%(wday)s, %d%%(sep)s%%(month)s%%(sep)s%Y %H:%M:%S GMT" 75 ) % { 76 'wday': self.wdays[stamp.weekday()], # pylint: disable = E1101 77 'month': self.months[stamp.month], # pylint: disable = E1101 78 'sep': [' ', '-'][bool(cookie)], 79 }
80 make_date.wdays = ( # pylint: disable = W0612 81 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun' 82 ) 83 make_date.months = (None, # pylint: disable = W0612 84 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 85 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec', 86 )
87 88 89 -def read_headers(stream):
90 """ 91 Read MIME headers from stream 92 93 :Parameters: 94 - `stream`: The stream to read from 95 96 :Types: 97 - `stream`: ``file`` 98 99 :return: Dictionary of lists of headers (``{'name': ['val', ...], ...}``) 100 :rtype: ``dict`` 101 102 :Exceptions: 103 - `httputil.InvalidHeaderLine`: Unparsable header line 104 - `httputil.IncompleteHeaders`: Stream ended before the final empty line 105 """ 106 headers = {} 107 self, name, values = read_headers, None, None 108 while True: 109 line = stream.readline() 110 if not line: 111 raise IncompleteHeaders("Headers not completed") 112 line = line[:-1 - line.endswith(CRLF)] 113 if self.CONT_MATCH(line): # pylint: disable = E1101 114 if name is None: 115 raise InvalidHeaderLine( 116 "Continuation line without line to continue") 117 values.append(line.lstrip()) 118 continue 119 elif name is not None: 120 headers.setdefault(name.lower(), [] 121 ).append(" ".join(values)) 122 if not line: # empty separator line, finished reading 123 break 124 match = self.HEADER_MATCH(line) # pylint: disable = E1101 125 if not match: 126 raise InvalidHeaderLine("Invalid header line format") 127 name, value = match.group('name', 'value') 128 values = [value] 129 return headers
130 131 read_headers.CONT_MATCH = _re.compile(r'\s').match # pylint: disable = W0612 132 # token chars from rfc 2616: 133 # ''.join(c for c in map(chr, range(33, 127)) 134 # if c not in '()<>@,;:\\"/[]?={}') 135 read_headers.HEADER_MATCH = _re.compile( # pylint: disable = W0612 136 r'''(?P<name>[-!#$%&'*+.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ''' 137 r'''^_`abcdefghijklmnopqrstuvwxyz|~]+)\s*:\s*(?P<value>.*)$''', 138 _re.X).match
139 140 141 -class CookieCodecInterface(object):
142 """ Interface for Cookie codecs """ 143
144 - def encode(self, value):
145 """ 146 Encode the cookie value to a 7bit US-ASCII string 147 148 This method is also responsible for quoting the value if necessary. 149 150 :Parameters: 151 - `value`: The value to encode 152 153 :Types: 154 - `value`: any 155 156 :return: The encoded value 157 :rtype: ``str`` 158 """
159
160 - def decode(self, value):
161 """ 162 Decode the cookie value from 7bit US-ASCII string 163 164 :Parameters: 165 - `value`: The cookie string (as submitted) 166 167 :Types: 168 - `value`: ``str`` 169 170 :return: The decoded value 171 :rtype: any 172 173 :Exceptions: 174 - `ValueError`: The value could not be decoded properly 175 """
176
177 178 -class CookieMaker(object):
179 """ 180 Cookie maker helper class 181 182 :CVariables: 183 - `UNSAFE_SEARCH`: Unsafe character search function 184 - `_ATTR`: Attribute spelling and type getter 185 - `KEYS`: Valid attribute keys 186 187 :IVariables: 188 - `_encode`: Value encoder 189 190 :Types: 191 - `UNSAFE_SEARCH`: ``callable`` 192 - `_ATTR`: ``callable`` 193 - `KEYS`: ``tuple`` 194 - `_encode`: ``callable`` 195 """ 196 UNSAFE_SEARCH = _re.compile(r"[^a-zA-Z\d!#$%&'*+.^_`|~-]").search 197 _ATTR = dict( 198 expires=("expires", 'date'), 199 path= ("Path", 'ustring'), 200 comment=("Comment", 'string'), 201 domain= ("Domain", 'ustring'), 202 max_age=("Max-Age", 'int'), 203 secure= ("secure", 'bool'), 204 version=("Version", 'string'), 205 ) 206 KEYS = tuple(sorted(_ATTR.keys())) 207 _ATTR = _ATTR.get 208
209 - def __init__(self, codec=None):
210 """ 211 Initialization 212 213 :Parameters: 214 - `codec`: Cookie codec to apply. If unset or ``None``, an identity 215 codec is applied (leaving 8bit chars as-is) 216 217 :Types: 218 - `codec`: `CookieCodecInterface` 219 """ 220 if codec is None: 221 encode = lambda x: x 222 else: 223 encode = codec.encode 224 self._encode = encode
225
226 - def __call__(self, name, value, **kwargs):
227 """ 228 Create the cookie string 229 230 Cookie parameters are given in kwargs. Valid keys are listed in 231 `KEYS`. ``None``-values are ignored. Here are short descriptions of 232 the valid parameters: 233 234 ``comment`` 235 Cookie comment (``str``) 236 ``domain`` 237 Valid domain (``str``) 238 ``expires`` 239 Expire time of the cookie (``datetime.datetime``). If unset 240 or ``None`` the cookie is dropped when the browser is closed. 241 See also the ``max_age`` keyword. 242 ``max_age`` 243 Max age of the cookie in seconds (``int``). If set, make sure it 244 matches the expiry time. The difference is that expires will be 245 transformed to a HTTP date, while max-age will stay an integer. 246 The expires parameter is the older one and better understood by 247 the clients out there. For that reason if you set max_age only, 248 expires will be set automatically to ``now + max_age``. If unset 249 or ``None`` the cookie will be dropped when the browser is closed. 250 ``path`` 251 Valid URL base path for the cookie. It should always be set to a 252 reasonable path (at least ``/``), otherwise the cookie will only 253 be valid for the current URL and below. 254 ``secure`` 255 Whether this is an SSL-only cookie or not (``bool``) 256 ``version`` 257 Cookie spec version (``int``). See `RFC 2965`_ 258 259 .. _RFC 2965: http://www.ietf.org/rfc/rfc2965.txt 260 261 :Parameters: 262 - `name`: Cookie name 263 - `value`: Cookie value (if a codec was given, the type should be 264 applicable for the codec encoder). 265 - `kwargs`: Cookie parameters 266 267 :Types: 268 - `name`: ``str`` 269 - `value`: ``str`` 270 - `kwargs`: ``dict`` 271 272 :return: The cookie string 273 :rtype: ``str`` 274 275 :Exceptions: 276 - `ValueError`: Invalid name or values given 277 - `TypeError`: Unrecognized attributes given 278 """ 279 if self.UNSAFE_SEARCH(name): 280 raise ValueError("%r is unsafe as key" % (name,)) 281 elif name.lower().replace('-', '_') in self.KEYS: 282 raise ValueError("%s is a reserved attribute and cannot be used " 283 "as name" % (name,)) 284 items = ["%s=%s" % (str(name), str(self._encode(value)))] 285 if kwargs.get('max_age') is not None: 286 kwargs['max_age'] = max(0, kwargs['max_age']) 287 if kwargs.get('expires') is None: 288 kwargs['expires'] = ( 289 _datetime.datetime.utcnow() + 290 _datetime.timedelta(seconds=kwargs['max_age']) 291 ) 292 293 for key in self.KEYS: 294 if key in kwargs: 295 val = kwargs.pop(key) 296 if val is not None: 297 key, translator = self._ATTR(key) 298 value = getattr(self, '_' + translator)(key, val) 299 if value is not None: 300 items.append(str(value)) 301 if kwargs: 302 raise TypeError("Unrecognized keywords: %r" % (kwargs.keys(),)) 303 return "; ".join(item for item in items if item is not None)
304 305 @staticmethod
306 - def _date(key, value):
307 """ Date translator """ 308 return "%s=%s" % (key, make_date(value, cookie=True))
309 310 @staticmethod
311 - def _int(key, value):
312 """ Integer translator """ 313 return "%s=%d" % (key, int(value))
314 315 @staticmethod
316 - def _bool(key, value):
317 """ Boolean translator """ 318 if value: 319 return key 320 return None
321
322 - def _string(self, key, value):
323 """ String translator """ 324 return "%s=%s" % (key, self._encode(value))
325 326 @staticmethod
327 - def _ustring(key, value):
328 """ Unquoted string translator """ 329 return "%s=%s" % (key, value)
330 360