Show More
Commit Description:
Merge pull request #17 from nattee/master...
Commit Description:
Merge pull request #17 from nattee/master
upgrade to current working snapshot
References:
File last commit:
Show/Diff file:
Action:
lib/assets/Lib/http/cookies.py
| 580 lines
| 20.9 KiB
| text/x-python
| PythonLexer
|
r584 | #!/usr/bin/env python3 | |||
# | ||||
#### | ||||
# Copyright 2000 by Timothy O'Malley <timo@alum.mit.edu> | ||||
# | ||||
# All Rights Reserved | ||||
# | ||||
# Permission to use, copy, modify, and distribute this software | ||||
# and its documentation for any purpose and without fee is hereby | ||||
# granted, provided that the above copyright notice appear in all | ||||
# copies and that both that copyright notice and this permission | ||||
# notice appear in supporting documentation, and that the name of | ||||
# Timothy O'Malley not be used in advertising or publicity | ||||
# pertaining to distribution of the software without specific, written | ||||
# prior permission. | ||||
# | ||||
# Timothy O'Malley DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS | ||||
# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | ||||
# AND FITNESS, IN NO EVENT SHALL Timothy O'Malley BE LIABLE FOR | ||||
# ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | ||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, | ||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS | ||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR | ||||
# PERFORMANCE OF THIS SOFTWARE. | ||||
# | ||||
#### | ||||
# | ||||
# Id: Cookie.py,v 2.29 2000/08/23 05:28:49 timo Exp | ||||
# by Timothy O'Malley <timo@alum.mit.edu> | ||||
# | ||||
# Cookie.py is a Python module for the handling of HTTP | ||||
# cookies as a Python dictionary. See RFC 2109 for more | ||||
# information on cookies. | ||||
# | ||||
# The original idea to treat Cookies as a dictionary came from | ||||
# Dave Mitchell (davem@magnet.com) in 1995, when he released the | ||||
# first version of nscookie.py. | ||||
# | ||||
#### | ||||
r""" | ||||
Here's a sample session to show how to use this module. | ||||
At the moment, this is the only documentation. | ||||
The Basics | ||||
---------- | ||||
Importing is easy... | ||||
>>> from http import cookies | ||||
Most of the time you start by creating a cookie. | ||||
>>> C = cookies.SimpleCookie() | ||||
Once you've created your Cookie, you can add values just as if it were | ||||
a dictionary. | ||||
>>> C = cookies.SimpleCookie() | ||||
>>> C["fig"] = "newton" | ||||
>>> C["sugar"] = "wafer" | ||||
>>> C.output() | ||||
'Set-Cookie: fig=newton\r\nSet-Cookie: sugar=wafer' | ||||
Notice that the printable representation of a Cookie is the | ||||
appropriate format for a Set-Cookie: header. This is the | ||||
default behavior. You can change the header and printed | ||||
attributes by using the .output() function | ||||
>>> C = cookies.SimpleCookie() | ||||
>>> C["rocky"] = "road" | ||||
>>> C["rocky"]["path"] = "/cookie" | ||||
>>> print(C.output(header="Cookie:")) | ||||
Cookie: rocky=road; Path=/cookie | ||||
>>> print(C.output(attrs=[], header="Cookie:")) | ||||
Cookie: rocky=road | ||||
The load() method of a Cookie extracts cookies from a string. In a | ||||
CGI script, you would use this method to extract the cookies from the | ||||
HTTP_COOKIE environment variable. | ||||
>>> C = cookies.SimpleCookie() | ||||
>>> C.load("chips=ahoy; vienna=finger") | ||||
>>> C.output() | ||||
'Set-Cookie: chips=ahoy\r\nSet-Cookie: vienna=finger' | ||||
The load() method is darn-tootin smart about identifying cookies | ||||
within a string. Escaped quotation marks, nested semicolons, and other | ||||
such trickeries do not confuse it. | ||||
>>> C = cookies.SimpleCookie() | ||||
>>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";') | ||||
>>> print(C) | ||||
Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;" | ||||
Each element of the Cookie also supports all of the RFC 2109 | ||||
Cookie attributes. Here's an example which sets the Path | ||||
attribute. | ||||
>>> C = cookies.SimpleCookie() | ||||
>>> C["oreo"] = "doublestuff" | ||||
>>> C["oreo"]["path"] = "/" | ||||
>>> print(C) | ||||
Set-Cookie: oreo=doublestuff; Path=/ | ||||
Each dictionary element has a 'value' attribute, which gives you | ||||
back the value associated with the key. | ||||
>>> C = cookies.SimpleCookie() | ||||
>>> C["twix"] = "none for you" | ||||
>>> C["twix"].value | ||||
'none for you' | ||||
The SimpleCookie expects that all values should be standard strings. | ||||
Just to be sure, SimpleCookie invokes the str() builtin to convert | ||||
the value to a string, when the values are set dictionary-style. | ||||
>>> C = cookies.SimpleCookie() | ||||
>>> C["number"] = 7 | ||||
>>> C["string"] = "seven" | ||||
>>> C["number"].value | ||||
'7' | ||||
>>> C["string"].value | ||||
'seven' | ||||
>>> C.output() | ||||
'Set-Cookie: number=7\r\nSet-Cookie: string=seven' | ||||
Finis. | ||||
""" | ||||
# | ||||
# Import our required modules | ||||
# | ||||
import re | ||||
import string | ||||
__all__ = ["CookieError", "BaseCookie", "SimpleCookie"] | ||||
_nulljoin = ''.join | ||||
_semispacejoin = '; '.join | ||||
_spacejoin = ' '.join | ||||
# | ||||
# Define an exception visible to External modules | ||||
# | ||||
class CookieError(Exception): | ||||
pass | ||||
# These quoting routines conform to the RFC2109 specification, which in | ||||
# turn references the character definitions from RFC2068. They provide | ||||
# a two-way quoting algorithm. Any non-text character is translated | ||||
# into a 4 character sequence: a forward-slash followed by the | ||||
# three-digit octal equivalent of the character. Any '\' or '"' is | ||||
# quoted with a preceeding '\' slash. | ||||
# | ||||
# These are taken from RFC2068 and RFC2109. | ||||
# _LegalChars is the list of chars which don't require "'s | ||||
# _Translator hash-table for fast quoting | ||||
# | ||||
_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" | ||||
_Translator = { | ||||
'\000' : '\\000', '\001' : '\\001', '\002' : '\\002', | ||||
'\003' : '\\003', '\004' : '\\004', '\005' : '\\005', | ||||
'\006' : '\\006', '\007' : '\\007', '\010' : '\\010', | ||||
'\011' : '\\011', '\012' : '\\012', '\013' : '\\013', | ||||
'\014' : '\\014', '\015' : '\\015', '\016' : '\\016', | ||||
'\017' : '\\017', '\020' : '\\020', '\021' : '\\021', | ||||
'\022' : '\\022', '\023' : '\\023', '\024' : '\\024', | ||||
'\025' : '\\025', '\026' : '\\026', '\027' : '\\027', | ||||
'\030' : '\\030', '\031' : '\\031', '\032' : '\\032', | ||||
'\033' : '\\033', '\034' : '\\034', '\035' : '\\035', | ||||
'\036' : '\\036', '\037' : '\\037', | ||||
# Because of the way browsers really handle cookies (as opposed | ||||
# to what the RFC says) we also encode , and ; | ||||
',' : '\\054', ';' : '\\073', | ||||
'"' : '\\"', '\\' : '\\\\', | ||||
'\177' : '\\177', '\200' : '\\200', '\201' : '\\201', | ||||
'\202' : '\\202', '\203' : '\\203', '\204' : '\\204', | ||||
'\205' : '\\205', '\206' : '\\206', '\207' : '\\207', | ||||
'\210' : '\\210', '\211' : '\\211', '\212' : '\\212', | ||||
'\213' : '\\213', '\214' : '\\214', '\215' : '\\215', | ||||
'\216' : '\\216', '\217' : '\\217', '\220' : '\\220', | ||||
'\221' : '\\221', '\222' : '\\222', '\223' : '\\223', | ||||
'\224' : '\\224', '\225' : '\\225', '\226' : '\\226', | ||||
'\227' : '\\227', '\230' : '\\230', '\231' : '\\231', | ||||
'\232' : '\\232', '\233' : '\\233', '\234' : '\\234', | ||||
'\235' : '\\235', '\236' : '\\236', '\237' : '\\237', | ||||
'\240' : '\\240', '\241' : '\\241', '\242' : '\\242', | ||||
'\243' : '\\243', '\244' : '\\244', '\245' : '\\245', | ||||
'\246' : '\\246', '\247' : '\\247', '\250' : '\\250', | ||||
'\251' : '\\251', '\252' : '\\252', '\253' : '\\253', | ||||
'\254' : '\\254', '\255' : '\\255', '\256' : '\\256', | ||||
'\257' : '\\257', '\260' : '\\260', '\261' : '\\261', | ||||
'\262' : '\\262', '\263' : '\\263', '\264' : '\\264', | ||||
'\265' : '\\265', '\266' : '\\266', '\267' : '\\267', | ||||
'\270' : '\\270', '\271' : '\\271', '\272' : '\\272', | ||||
'\273' : '\\273', '\274' : '\\274', '\275' : '\\275', | ||||
'\276' : '\\276', '\277' : '\\277', '\300' : '\\300', | ||||
'\301' : '\\301', '\302' : '\\302', '\303' : '\\303', | ||||
'\304' : '\\304', '\305' : '\\305', '\306' : '\\306', | ||||
'\307' : '\\307', '\310' : '\\310', '\311' : '\\311', | ||||
'\312' : '\\312', '\313' : '\\313', '\314' : '\\314', | ||||
'\315' : '\\315', '\316' : '\\316', '\317' : '\\317', | ||||
'\320' : '\\320', '\321' : '\\321', '\322' : '\\322', | ||||
'\323' : '\\323', '\324' : '\\324', '\325' : '\\325', | ||||
'\326' : '\\326', '\327' : '\\327', '\330' : '\\330', | ||||
'\331' : '\\331', '\332' : '\\332', '\333' : '\\333', | ||||
'\334' : '\\334', '\335' : '\\335', '\336' : '\\336', | ||||
'\337' : '\\337', '\340' : '\\340', '\341' : '\\341', | ||||
'\342' : '\\342', '\343' : '\\343', '\344' : '\\344', | ||||
'\345' : '\\345', '\346' : '\\346', '\347' : '\\347', | ||||
'\350' : '\\350', '\351' : '\\351', '\352' : '\\352', | ||||
'\353' : '\\353', '\354' : '\\354', '\355' : '\\355', | ||||
'\356' : '\\356', '\357' : '\\357', '\360' : '\\360', | ||||
'\361' : '\\361', '\362' : '\\362', '\363' : '\\363', | ||||
'\364' : '\\364', '\365' : '\\365', '\366' : '\\366', | ||||
'\367' : '\\367', '\370' : '\\370', '\371' : '\\371', | ||||
'\372' : '\\372', '\373' : '\\373', '\374' : '\\374', | ||||
'\375' : '\\375', '\376' : '\\376', '\377' : '\\377' | ||||
} | ||||
def _quote(str, LegalChars=_LegalChars): | ||||
r"""Quote a string for use in a cookie header. | ||||
If the string does not need to be double-quoted, then just return the | ||||
string. Otherwise, surround the string in doublequotes and quote | ||||
(with a \) special characters. | ||||
""" | ||||
if all(c in LegalChars for c in str): | ||||
return str | ||||
else: | ||||
return '"' + _nulljoin(_Translator.get(s, s) for s in str) + '"' | ||||
_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") | ||||
_QuotePatt = re.compile(r"[\\].") | ||||
def _unquote(str): | ||||
# If there aren't any doublequotes, | ||||
# then there can't be any special characters. See RFC 2109. | ||||
if len(str) < 2: | ||||
return str | ||||
if str[0] != '"' or str[-1] != '"': | ||||
return str | ||||
# We have to assume that we must decode this string. | ||||
# Down to work. | ||||
# Remove the "s | ||||
str = str[1:-1] | ||||
# Check for special sequences. Examples: | ||||
# \012 --> \n | ||||
# \" --> " | ||||
# | ||||
i = 0 | ||||
n = len(str) | ||||
res = [] | ||||
while 0 <= i < n: | ||||
o_match = _OctalPatt.search(str, i) | ||||
q_match = _QuotePatt.search(str, i) | ||||
if not o_match and not q_match: # Neither matched | ||||
res.append(str[i:]) | ||||
break | ||||
# else: | ||||
j = k = -1 | ||||
if o_match: | ||||
j = o_match.start(0) | ||||
if q_match: | ||||
k = q_match.start(0) | ||||
if q_match and (not o_match or k < j): # QuotePatt matched | ||||
res.append(str[i:k]) | ||||
res.append(str[k+1]) | ||||
i = k + 2 | ||||
else: # OctalPatt matched | ||||
res.append(str[i:j]) | ||||
res.append(chr(int(str[j+1:j+4], 8))) | ||||
i = j + 4 | ||||
return _nulljoin(res) | ||||
# The _getdate() routine is used to set the expiration time in the cookie's HTTP | ||||
# header. By default, _getdate() returns the current time in the appropriate | ||||
# "expires" format for a Set-Cookie header. The one optional argument is an | ||||
# offset from now, in seconds. For example, an offset of -3600 means "one hour | ||||
# ago". The offset may be a floating point number. | ||||
# | ||||
_weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] | ||||
_monthname = [None, | ||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', | ||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] | ||||
def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname): | ||||
from time import gmtime, time | ||||
now = time() | ||||
year, month, day, hh, mm, ss, wd, y, z = gmtime(now + future) | ||||
return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % \ | ||||
(weekdayname[wd], day, monthname[month], year, hh, mm, ss) | ||||
class Morsel(dict): | ||||
"""A class to hold ONE (key, value) pair. | ||||
In a cookie, each such pair may have several attributes, so this class is | ||||
used to keep the attributes associated with the appropriate key,value pair. | ||||
This class also includes a coded_value attribute, which is used to hold | ||||
the network representation of the value. This is most useful when Python | ||||
objects are pickled for network transit. | ||||
""" | ||||
# RFC 2109 lists these attributes as reserved: | ||||
# path comment domain | ||||
# max-age secure version | ||||
# | ||||
# For historical reasons, these attributes are also reserved: | ||||
# expires | ||||
# | ||||
# This is an extension from Microsoft: | ||||
# httponly | ||||
# | ||||
# This dictionary provides a mapping from the lowercase | ||||
# variant on the left to the appropriate traditional | ||||
# formatting on the right. | ||||
_reserved = { | ||||
"expires" : "expires", | ||||
"path" : "Path", | ||||
"comment" : "Comment", | ||||
"domain" : "Domain", | ||||
"max-age" : "Max-Age", | ||||
"secure" : "secure", | ||||
"httponly" : "httponly", | ||||
"version" : "Version", | ||||
} | ||||
_flags = {'secure', 'httponly'} | ||||
def __init__(self): | ||||
# Set defaults | ||||
self.key = self.value = self.coded_value = None | ||||
# Set default attributes | ||||
for key in self._reserved: | ||||
dict.__setitem__(self, key, "") | ||||
def __setitem__(self, K, V): | ||||
K = K.lower() | ||||
if not K in self._reserved: | ||||
raise CookieError("Invalid Attribute %s" % K) | ||||
dict.__setitem__(self, K, V) | ||||
def isReservedKey(self, K): | ||||
return K.lower() in self._reserved | ||||
def set(self, key, val, coded_val, LegalChars=_LegalChars): | ||||
# First we verify that the key isn't a reserved word | ||||
# Second we make sure it only contains legal characters | ||||
if key.lower() in self._reserved: | ||||
raise CookieError("Attempt to set a reserved key: %s" % key) | ||||
if any(c not in LegalChars for c in key): | ||||
raise CookieError("Illegal key value: %s" % key) | ||||
# It's a good key, so save it. | ||||
self.key = key | ||||
self.value = val | ||||
self.coded_value = coded_val | ||||
def output(self, attrs=None, header="Set-Cookie:"): | ||||
return "%s %s" % (header, self.OutputString(attrs)) | ||||
__str__ = output | ||||
def __repr__(self): | ||||
return '<%s: %s=%s>' % (self.__class__.__name__, | ||||
self.key, repr(self.value)) | ||||
def js_output(self, attrs=None): | ||||
# Print javascript | ||||
return """ | ||||
<script type="text/javascript"> | ||||
<!-- begin hiding | ||||
document.cookie = \"%s\"; | ||||
// end hiding --> | ||||
</script> | ||||
""" % (self.OutputString(attrs).replace('"', r'\"')) | ||||
def OutputString(self, attrs=None): | ||||
# Build up our result | ||||
# | ||||
result = [] | ||||
append = result.append | ||||
# First, the key=value pair | ||||
append("%s=%s" % (self.key, self.coded_value)) | ||||
# Now add any defined attributes | ||||
if attrs is None: | ||||
attrs = self._reserved | ||||
items = sorted(self.items()) | ||||
for key, value in items: | ||||
if value == "": | ||||
continue | ||||
if key not in attrs: | ||||
continue | ||||
if key == "expires" and isinstance(value, int): | ||||
append("%s=%s" % (self._reserved[key], _getdate(value))) | ||||
elif key == "max-age" and isinstance(value, int): | ||||
append("%s=%d" % (self._reserved[key], value)) | ||||
elif key == "secure": | ||||
append(str(self._reserved[key])) | ||||
elif key == "httponly": | ||||
append(str(self._reserved[key])) | ||||
else: | ||||
append("%s=%s" % (self._reserved[key], value)) | ||||
# Return the result | ||||
return _semispacejoin(result) | ||||
# | ||||
# Pattern for finding cookie | ||||
# | ||||
# This used to be strict parsing based on the RFC2109 and RFC2068 | ||||
# specifications. I have since discovered that MSIE 3.0x doesn't | ||||
# follow the character rules outlined in those specs. As a | ||||
# result, the parsing rules here are less strict. | ||||
# | ||||
_LegalCharsPatt = r"[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]" | ||||
_CookiePattern = re.compile(r""" | ||||
(?x) # This is a verbose pattern | ||||
(?P<key> # Start of group 'key' | ||||
""" + _LegalCharsPatt + r"""+? # Any word of at least one letter | ||||
) # End of group 'key' | ||||
( # Optional group: there may not be a value. | ||||
\s*=\s* # Equal Sign | ||||
(?P<val> # Start of group 'val' | ||||
"(?:[^\\"]|\\.)*" # Any doublequoted string | ||||
| # or | ||||
\w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT # Special case for "expires" attr | ||||
| # or | ||||
""" + _LegalCharsPatt + r"""* # Any word or empty string | ||||
) # End of group 'val' | ||||
)? # End of optional value group | ||||
\s* # Any number of spaces. | ||||
(\s+|;|$) # Ending either at space, semicolon, or EOS. | ||||
""", re.ASCII) # May be removed if safe. | ||||
# At long last, here is the cookie class. Using this class is almost just like | ||||
# using a dictionary. See this module's docstring for example usage. | ||||
# | ||||
class BaseCookie(dict): | ||||
"""A container class for a set of Morsels.""" | ||||
def value_decode(self, val): | ||||
"""real_value, coded_value = value_decode(STRING) | ||||
Called prior to setting a cookie's value from the network | ||||
representation. The VALUE is the value read from HTTP | ||||
header. | ||||
Override this function to modify the behavior of cookies. | ||||
""" | ||||
return val, val | ||||
def value_encode(self, val): | ||||
"""real_value, coded_value = value_encode(VALUE) | ||||
Called prior to setting a cookie's value from the dictionary | ||||
representation. The VALUE is the value being assigned. | ||||
Override this function to modify the behavior of cookies. | ||||
""" | ||||
strval = str(val) | ||||
return strval, strval | ||||
def __init__(self, input=None): | ||||
if input: | ||||
self.load(input) | ||||
def __set(self, key, real_value, coded_value): | ||||
"""Private method for setting a cookie's value""" | ||||
M = self.get(key, Morsel()) | ||||
M.set(key, real_value, coded_value) | ||||
dict.__setitem__(self, key, M) | ||||
def __setitem__(self, key, value): | ||||
"""Dictionary style assignment.""" | ||||
rval, cval = self.value_encode(value) | ||||
self.__set(key, rval, cval) | ||||
def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"): | ||||
"""Return a string suitable for HTTP.""" | ||||
result = [] | ||||
items = sorted(self.items()) | ||||
for key, value in items: | ||||
result.append(value.output(attrs, header)) | ||||
return sep.join(result) | ||||
__str__ = output | ||||
def __repr__(self): | ||||
l = [] | ||||
items = sorted(self.items()) | ||||
for key, value in items: | ||||
l.append('%s=%s' % (key, repr(value.value))) | ||||
return '<%s: %s>' % (self.__class__.__name__, _spacejoin(l)) | ||||
def js_output(self, attrs=None): | ||||
"""Return a string suitable for JavaScript.""" | ||||
result = [] | ||||
items = sorted(self.items()) | ||||
for key, value in items: | ||||
result.append(value.js_output(attrs)) | ||||
return _nulljoin(result) | ||||
def load(self, rawdata): | ||||
"""Load cookies from a string (presumably HTTP_COOKIE) or | ||||
from a dictionary. Loading cookies from a dictionary 'd' | ||||
is equivalent to calling: | ||||
map(Cookie.__setitem__, d.keys(), d.values()) | ||||
""" | ||||
if isinstance(rawdata, str): | ||||
self.__parse_string(rawdata) | ||||
else: | ||||
# self.update() wouldn't call our custom __setitem__ | ||||
for key, value in rawdata.items(): | ||||
self[key] = value | ||||
return | ||||
def __parse_string(self, str, patt=_CookiePattern): | ||||
i = 0 # Our starting point | ||||
n = len(str) # Length of string | ||||
M = None # current morsel | ||||
while 0 <= i < n: | ||||
# Start looking for a cookie | ||||
match = patt.search(str, i) | ||||
if not match: | ||||
# No more cookies | ||||
break | ||||
key, value = match.group("key"), match.group("val") | ||||
i = match.end(0) | ||||
# Parse the key, value in case it's metainfo | ||||
if key[0] == "$": | ||||
# We ignore attributes which pertain to the cookie | ||||
# mechanism as a whole. See RFC 2109. | ||||
# (Does anyone care?) | ||||
if M: | ||||
M[key[1:]] = value | ||||
elif key.lower() in Morsel._reserved: | ||||
if M: | ||||
if value is None: | ||||
if key.lower() in Morsel._flags: | ||||
M[key] = True | ||||
else: | ||||
M[key] = _unquote(value) | ||||
elif value is not None: | ||||
rval, cval = self.value_decode(value) | ||||
self.__set(key, rval, cval) | ||||
M = self[key] | ||||
class SimpleCookie(BaseCookie): | ||||
""" | ||||
SimpleCookie supports strings as cookie values. When setting | ||||
the value using the dictionary assignment notation, SimpleCookie | ||||
calls the builtin str() to convert the value to a string. Values | ||||
received from HTTP are kept as strings. | ||||
""" | ||||
def value_decode(self, val): | ||||
return _unquote(val), val | ||||
def value_encode(self, val): | ||||
strval = str(val) | ||||
return strval, _quote(strval) | ||||