1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
47 """ Base header parse error """
48
50 """ A header line is invalid """
51
53 """ The headers are incomplete """
54
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()],
77 'month': self.months[stamp.month],
78 'sep': [' ', '-'][bool(cookie)],
79 }
80 make_date.wdays = (
81 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'
82 )
83 make_date.months = (None,
84 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
85 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec',
86 )
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):
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:
123 break
124 match = self.HEADER_MATCH(line)
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
132
133
134
135 read_headers.HEADER_MATCH = _re.compile(
136 r'''(?P<name>[-!#$%&'*+.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'''
137 r'''^_`abcdefghijklmnopqrstuvwxyz|~]+)\s*:\s*(?P<value>.*)$''',
138 _re.X).match
142 """ Interface for Cookie codecs """
143
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
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
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
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
309
310 @staticmethod
311 - def _int(key, value):
312 """ Integer translator """
313 return "%s=%d" % (key, int(value))
314
315 @staticmethod
317 """ Boolean translator """
318 if value:
319 return key
320 return None
321
323 """ String translator """
324 return "%s=%s" % (key, self._encode(value))
325
326 @staticmethod
328 """ Unquoted string translator """
329 return "%s=%s" % (key, value)
330
331
332 -def make_cookie(name, value, codec=None, **kwargs):
333 """
334 Make a cookie
335
336 The is a simple interface to the `CookieMaker` class. See there for
337 detailed information.
338
339 :Parameters:
340 - `name`: Cookie name
341 - `value`: Cookie value
342 - `codec`: Value codec. If unset or ``None``, the identity codec is
343 applied.
344 - `kwargs`: Cookie attributes
345
346 :Types:
347 - `name`: ``str``
348 - `value`: ``str``
349 - `codec`: `CookieCodecInterface`
350 - `kwargs`: ``dict``
351
352 :return: The cookie string
353 :rtype: ``str``
354
355 :Exceptions:
356 - `ValueError`: Invalid name or values given
357 - `TypeError`: Unrecognized attributes given
358 """
359 return CookieMaker(codec)(name, value, **kwargs)
360