root / trunk / twisted / web2 / http_headers.py

Revision 25173, 49.9 kB (checked in by exarkun, 8 months ago)

Merge header-value-quoting-2346

Author: exarkun, itamar
Reviewer: jknight
Fixes: #2346

Fix generation of HTTP header values in web2 for the case where a parameter value
includes a byte which requires quoting. Previously these values would not be
quoted, resulting in output which probably could not be parsed. Now quoting will
be applied if necessary to avoid generating bad output.

Line 
1 # -*- test-case-name: twisted.web2.test.test_http_headers -*-
2 # Copyright (c) 2008 Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 HTTP header representation, parsing, and serialization.
7 """
8
9 import time
10 from calendar import timegm
11 import base64
12 import re
13
14 def dashCapitalize(s):
15     ''' Capitalize a string, making sure to treat - as a word seperator '''
16     return '-'.join([ x.capitalize() for x in s.split('-')])
17
18 # datetime parsing and formatting
19 weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
20 weekdayname_lower = [name.lower() for name in weekdayname]
21 monthname = [None,
22              'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
23              'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
24 monthname_lower = [name and name.lower() for name in monthname]
25
26 # HTTP Header parsing API
27
28 header_case_mapping = {}
29
30 def casemappingify(d):
31     global header_case_mapping
32     newd = dict([(key.lower(),key) for key in d.keys()])
33     header_case_mapping.update(newd)
34
35 def lowerify(d):
36     return dict([(key.lower(),value) for key,value in d.items()])
37
38
39 class HeaderHandler(object):
40     """HeaderHandler manages header generating and parsing functions.
41     """
42     HTTPParsers = {}
43     HTTPGenerators = {}
44
45     def __init__(self, parsers=None, generators=None):
46         """
47         @param parsers: A map of header names to parsing functions.
48         @type parsers: L{dict}
49
50         @param generators: A map of header names to generating functions.
51         @type generators: L{dict}
52         """
53
54         if parsers:
55             self.HTTPParsers.update(parsers)
56         if generators:
57             self.HTTPGenerators.update(generators)
58
59     def parse(self, name, header):
60         """
61         Parse the given header based on its given name.
62
63         @param name: The header name to parse.
64         @type name: C{str}
65
66         @param header: A list of unparsed headers.
67         @type header: C{list} of C{str}
68
69         @return: The return value is the parsed header representation,
70             it is dependent on the header.  See the HTTP Headers document.
71         """
72         parser = self.HTTPParsers.get(name, None)
73         if parser is None:
74             raise ValueError("No header parser for header '%s', either add one or use getHeaderRaw." % (name,))
75
76         try:
77             for p in parser:
78                 # print "Parsing %s: %s(%s)" % (name, repr(p), repr(h))
79                 header = p(header)
80                 # if isinstance(h, types.GeneratorType):
81                 #     h=list(h)
82         except ValueError,v:
83             # print v
84             header=None
85
86         return header
87
88     def generate(self, name, header):
89         """
90         Generate the given header based on its given name.
91
92         @param name: The header name to generate.
93         @type name: C{str}
94
95         @param header: A parsed header, such as the output of
96             L{HeaderHandler}.parse.
97
98         @return: C{list} of C{str} each representing a generated HTTP header.
99         """
100         generator = self.HTTPGenerators.get(name, None)
101
102         if generator is None:
103             # print self.generators
104             raise ValueError("No header generator for header '%s', either add one or use setHeaderRaw." % (name,))
105
106         for g in generator:
107             header = g(header)
108
109         #self._raw_headers[name] = h
110         return header
111
112     def updateParsers(self, parsers):
113         """Update en masse the parser maps.
114
115         @param parsers: Map of header names to parser chains.
116         @type parsers: C{dict}
117         """
118         casemappingify(parsers)
119         self.HTTPParsers.update(lowerify(parsers))
120
121     def addParser(self, name, value):
122         """Add an individual parser chain for the given header.
123
124         @param name: Name of the header to add
125         @type name: C{str}
126
127         @param value: The parser chain
128         @type value: C{str}
129         """
130         self.updateParsers({name: value})
131
132     def updateGenerators(self, generators):
133         """Update en masse the generator maps.
134
135         @param parsers: Map of header names to generator chains.
136         @type parsers: C{dict}
137         """
138         casemappingify(generators)
139         self.HTTPGenerators.update(lowerify(generators))
140
141     def addGenerators(self, name, value):
142         """Add an individual generator chain for the given header.
143
144         @param name: Name of the header to add
145         @type name: C{str}
146
147         @param value: The generator chain
148         @type value: C{str}
149         """
150         self.updateGenerators({name: value})
151
152     def update(self, parsers, generators):
153         """Conveniently update parsers and generators all at once.
154         """
155         self.updateParsers(parsers)
156         self.updateGenerators(generators)
157
158
159 DefaultHTTPHandler = HeaderHandler()
160
161
162 ## HTTP DateTime parser
163 def parseDateTime(dateString):
164     """Convert an HTTP date string (one of three formats) to seconds since epoch."""
165     parts = dateString.split()
166
167     if not parts[0][0:3].lower() in weekdayname_lower:
168         # Weekday is stupid. Might have been omitted.
169         try:
170             return parseDateTime("Sun, "+dateString)
171         except ValueError:
172             # Guess not.
173             pass
174
175     partlen = len(parts)
176     if (partlen == 5 or partlen == 6) and parts[1].isdigit():
177         # 1st date format: Sun, 06 Nov 1994 08:49:37 GMT
178         # (Note: "GMT" is literal, not a variable timezone)
179         # (also handles without "GMT")
180         # This is the normal format
181         day = parts[1]
182         month = parts[2]
183         year = parts[3]
184         time = parts[4]
185     elif (partlen == 3 or partlen == 4) and parts[1].find('-') != -1:
186         # 2nd date format: Sunday, 06-Nov-94 08:49:37 GMT
187         # (Note: "GMT" is literal, not a variable timezone)
188         # (also handles without without "GMT")
189         # Two digit year, yucko.
190         day, month, year = parts[1].split('-')
191         time = parts[2]
192         year=int(year)
193         if year < 69:
194             year = year + 2000
195         elif year < 100:
196             year = year + 1900
197     elif len(parts) == 5:
198         # 3rd date format: Sun Nov  6 08:49:37 1994
199         # ANSI C asctime() format.
200         day = parts[2]
201         month = parts[1]
202         year = parts[4]
203         time = parts[3]
204     else:
205         raise ValueError("Unknown datetime format %r" % dateString)
206
207     day = int(day)
208     month = int(monthname_lower.index(month.lower()))
209     year = int(year)
210     hour, min, sec = map(int, time.split(':'))
211     return int(timegm((year, month, day, hour, min, sec)))
212
213
214 ##### HTTP tokenizer
215 class Token(str):
216     __slots__=[]
217     tokens = {}
218     def __new__(self, char):
219         token = Token.tokens.get(char)
220         if token is None:
221             Token.tokens[char] = token = str.__new__(self, char)
222         return token
223
224     def __repr__(self):
225         return "Token(%s)" % str.__repr__(self)
226
227
228 # RFC 2616 section 2.2
229 http_tokens = " \t\"()<>@,;:\\/[]?={}"
230 http_ctls = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f"
231
232 def tokenize(header, foldCase=True):
233     """Tokenize a string according to normal HTTP header parsing rules.
234
235     In particular:
236      - Whitespace is irrelevant and eaten next to special separator tokens.
237        Its existance (but not amount) is important between character strings.
238      - Quoted string support including embedded backslashes.
239      - Case is insignificant (and thus lowercased), except in quoted strings.
240         (unless foldCase=False)
241      - Multiple headers are concatenated with ','
242
243     NOTE: not all headers can be parsed with this function.
244
245     Takes a raw header value (list of strings), and
246     Returns a generator of strings and Token class instances.
247     """
248     tokens=http_tokens
249     ctls=http_ctls
250
251     string = ",".join(header)
252     list = []
253     start = 0
254     cur = 0
255     quoted = False
256     qpair = False
257     inSpaces = -1
258     qstring = None
259
260     for x in string:
261         if quoted:
262             if qpair:
263                 qpair = False
264                 qstring = qstring+string[start:cur-1]+x
265                 start = cur+1
266             elif x == '\\':
267                 qpair = True
268             elif x == '"':
269                 quoted = False
270                 yield qstring+string[start:cur]
271                 qstring=None
272                 start = cur+1
273         elif x in tokens:
274             if start != cur:
275                 if foldCase:
276                     yield string[start:cur].lower()
277                 else:
278                     yield string[start:cur]
279
280             start = cur+1
281             if x == '"':
282                 quoted = True
283                 qstring = ""
284                 inSpaces = False
285             elif x in " \t":
286                 if inSpaces is False:
287                     inSpaces = True
288             else:
289                 inSpaces = -1
290                 yield Token(x)
291         elif x in ctls:
292             raise ValueError("Invalid control character: %d in header" % ord(x))
293         else:
294             if inSpaces is True:
295                 yield Token(' ')
296                 inSpaces = False
297
298             inSpaces = False
299         cur = cur+1
300
301     if qpair:
302         raise ValueError, "Missing character after '\\'"
303     if quoted:
304         raise ValueError, "Missing end quote"
305
306     if start != cur:
307         if foldCase:
308             yield string[start:cur].lower()
309         else:
310             yield string[start:cur]
311
312 def split(seq, delim):
313     """The same as str.split but works on arbitrary sequences.
314     Too bad it's not builtin to python!"""
315
316     cur = []
317     for item in seq:
318         if item == delim:
319             yield cur
320             cur = []
321         else:
322             cur.append(item)
323     yield cur
324
325 # def find(seq, *args):
326 #     """The same as seq.index but returns -1 if not found, instead
327 #     Too bad it's not builtin to python!"""
328 #     try:
329 #         return seq.index(value, *args)
330 #     except ValueError:
331 #         return -1
332
333
334 def filterTokens(seq):
335     """Filter out instances of Token, leaving only a list of strings.
336
337     Used instead of a more specific parsing method (e.g. splitting on commas)
338     when only strings are expected, so as to be a little lenient.
339
340     Apache does it this way and has some comments about broken clients which
341     forget commas (?), so I'm doing it the same way. It shouldn't
342     hurt anything, in any case.
343     """
344
345     l=[]
346     for x in seq:
347         if not isinstance(x, Token):
348             l.append(x)
349     return l
350
351 ##### parser utilities:
352 def checkSingleToken(tokens):
353     if len(tokens) != 1:
354         raise ValueError, "Expected single token, not %s." % (tokens,)
355     return tokens[0]
356
357 def parseKeyValue(val):
358     if len(val) == 1:
359         return val[0],None
360     elif len(val) == 3 and val[1] == Token('='):
361         return val[0],val[2]
362     raise ValueError, "Expected key or key=value, but got %s." % (val,)
363
364 def parseArgs(field):
365     args=split(field, Token(';'))
366     val = args.next()
367     args = [parseKeyValue(arg) for arg in args]
368     return val,args
369
370 def listParser(fun):
371     """Return a function which applies 'fun' to every element in the
372     comma-separated list"""
373     def listParserHelper(tokens):
374         fields = split(tokens, Token(','))
375         for field in fields:
376             if len(field) != 0:
377                 yield fun(field)
378
379     return listParserHelper
380
381 def last(seq):
382     """Return seq[-1]"""
383
384     return seq[-1]
385
386 ##### Generation utilities
387 def quoteString(s):
388     """
389     Quote a string according to the rules for the I{quoted-string} production
390     in RFC 2616 section 2.2.
391
392     @type s: C{str}
393     @rtype: C{str}
394     """
395     return '"%s"' % s.replace('\\', '\\\\').replace('"', '\\"')
396
397 def listGenerator(fun):
398     """Return a function which applies 'fun' to every element in
399     the given list, then joins the result with generateList"""
400     def listGeneratorHelper(l):
401         return generateList([fun(e) for e in l])
402
403     return listGeneratorHelper
404
405 def generateList(seq):
406     return ", ".join(seq)
407
408 def singleHeader(item):
409     return [item]
410
411 _seperators = re.compile('[' + re.escape(http_tokens) + ']')
412
413 def generateKeyValues(parameters):
414     """
415     Format an iterable of key/value pairs.
416
417     Although each header in HTTP 1.1 redefines the grammar for the formatting
418     of its parameters, the grammar defined by almost all headers conforms to
419     the specification given in RFC 2046.  Note also that RFC 2616 section 19.2
420     note 2 points out that many implementations fail if the value is quoted,
421     therefore this function only quotes the value when it is necessary.
422
423     @param parameters: An iterable of C{tuple} of a C{str} parameter name and
424         C{str} or C{None} parameter value which will be formated.
425
426     @return: The formatted result.
427     @rtype: C{str}
428     """
429     l = []
430     for k, v in parameters:
431         if v is None:
432             l.append('%s' % k)
433         else:
434             if _seperators.search(v) is not None:
435                 v = quoteString(v)
436             l.append('%s=%s' % (k, v))
437     return ";".join(l)
438
439
440 class MimeType(object):
441     def fromString(klass, mimeTypeString):
442         """Generate a MimeType object from the given string.
443
444         @param mimeTypeString: The mimetype to parse
445
446         @return: L{MimeType}
447         """
448         return DefaultHTTPHandler.parse('content-type', [mimeTypeString])
449
450     fromString = classmethod(fromString)
451
452     def __init__(self, mediaType, mediaSubtype, params={}, **kwargs):
453         """
454         @type mediaType: C{str}
455
456         @type mediaSubtype: C{str}
457
458         @type params: C{dict}
459         """
460         self.mediaType = mediaType
461         self.mediaSubtype = mediaSubtype
462         self.params = dict(params)
463
464         if kwargs:
465             self.params.update(kwargs)
466
467     def __eq__(self, other):
468         if not isinstance(other, MimeType): return NotImplemented
469         return (self.mediaType == other.mediaType and
470                 self.mediaSubtype == other.mediaSubtype and
471                 self.params == other.params)
472
473     def __ne__(self, other):
474         return not self.__eq__(other)
475
476     def __repr__(self):
477         return "MimeType(%r, %r, %r)" % (self.mediaType, self.mediaSubtype, self.params)
478
479     def __hash__(self):
480         return hash(self.mediaType)^hash(self.mediaSubtype)^hash(tuple(self.params.iteritems()))
481
482 ##### Specific header parsers.
483 def parseAccept(field):
484     type,args = parseArgs(field)
485
486     if len(type) != 3 or type[1] != Token('/'):
487         raise ValueError, "MIME Type "+str(type)+" invalid."
488
489     # okay, this spec is screwy. A 'q' parameter is used as the separator
490     # between MIME parameters and (as yet undefined) additional HTTP
491     # parameters.
492
493     num = 0
494     for arg in args:
495         if arg[0] == 'q':
496             mimeparams=tuple(args[0:num])
497             params=args[num:]
498             break
499         num = num + 1
500     else:
501         mimeparams=tuple(args)
502         params=[]
503
504     # Default values for parameters:
505     qval = 1.0
506
507     # Parse accept parameters:
508     for param in params:
509         if param[0] =='q':
510             qval = float(param[1])
511         else:
512             # Warn? ignored parameter.
513             pass
514
515     ret = MimeType(type[0],type[2],mimeparams),qval
516     return ret
517
518 def parseAcceptQvalue(field):
519     type,args=parseArgs(field)
520
521     type = checkSingleToken(type)
522
523     qvalue = 1.0 # Default qvalue is 1
524     for arg in args:
525         if arg[0] == 'q':
526             qvalue = float(arg[1])
527     return type,qvalue
528
529 def addDefaultCharset(charsets):
530     if charsets.get('*') is None and charsets.get('iso-8859-1') is None:
531         charsets['iso-8859-1'] = 1.0
532     return charsets
533
534 def addDefaultEncoding(encodings):
535     if encodings.get('*') is None and encodings.get('identity') is None:
536         # RFC doesn't specify a default value for identity, only that it
537         # "is acceptable" if not mentioned. Thus, give it a very low qvalue.
538         encodings['identity'] = .0001
539     return encodings
540
541
542 def parseContentType(header):
543     # Case folding is disabled for this header, because of use of
544     # Content-Type: multipart/form-data; boundary=CaSeFuLsTuFf
545     # So, we need to explicitly .lower() the type/subtype and arg keys.
546
547     type,args = parseArgs(header)
548
549     if len(type) != 3 or type[1] != Token('/'):
550         raise ValueError, "MIME Type "+str(type)+" invalid."
551
552     args = [(kv[0].lower(), kv[1]) for kv in args]
553
554     return MimeType(type[0].lower(), type[2].lower(), tuple(args))
555
556 def parseContentMD5(header):
557     try:
558         return base64.decodestring(header)
559     except Exception,e:
560         raise ValueError(e)
561
562 def parseContentRange(header):
563     """Parse a content-range header into (kind, start, end, realLength).
564
565     realLength might be None if real length is not known ('*').
566     start and end might be None if start,end unspecified (for response code 416)
567     """
568     kind, other = header.strip().split()
569     if kind.lower() != "bytes":
570         raise ValueError("a range of type %r is not supported")
571     startend, realLength = other.split("/")
572     if startend.strip() == '*':
573         start,end=None,None
574     else:
575         start, end = map(int, startend.split("-"))
576     if realLength == "*":
577         realLength = None
578     else:
579         realLength = int(realLength)
580     return (kind, start, end, realLength)
581
582 def parseExpect(field):
583     type,args=parseArgs(field)
584
585     type=parseKeyValue(type)
586     return (type[0], (lambda *args:args)(type[1], *args))
587
588 def parseExpires(header):
589     # """HTTP/1.1 clients and caches MUST treat other invalid date formats,
590     #    especially including the value 0, as in the past (i.e., "already expired")."""
591
592     try:
593         return parseDateTime(header)
594     except ValueError:
595         return 0
596
597 def parseIfModifiedSince(header):
598     # Ancient versions of netscape and *current* versions of MSIE send
599     #   If-Modified-Since: Thu, 05 Aug 2004 12:57:27 GMT; length=123
600     # which is blantantly RFC-violating and not documented anywhere
601     # except bug-trackers for web frameworks.
602
603     # So, we'll just strip off everything after a ';'.
604     return parseDateTime(header.split(';', 1)[0])
605
606 def parseIfRange(headers):
607     try:
608         return ETag.parse(tokenize(headers))
609     except ValueError:
610         return parseDateTime(last(headers))
611
612 def parseRange(range):
613     range = list(range)
614     if len(range) < 3 or range[1] != Token('='):
615         raise ValueError("Invalid range header format: %s" %(range,))
616
617     type=range[0]
618     if type != 'bytes':
619         raise ValueError("Unknown range unit: %s." % (type,))
620     rangeset=split(range[2:], Token(','))
621     ranges = []
622
623     for byterangespec in rangeset:
624         if len(byterangespec) != 1:
625             raise ValueError("Invalid range header format: %s" % (range,))
626         start,end=byterangespec[0].split('-')
627
628         if not start and not end:
629             raise ValueError("Invalid range header format: %s" % (range,))
630
631         if start:
632             start = int(start)
633         else:
634             start = None
635
636         if end:
637             end = int(end)
638         else:
639             end = None
640
641         if start and end and start > end:
642             raise ValueError("Invalid range header, start > end: %s" % (range,))
643         ranges.append((start,end))
644     return type,ranges
645
646 def parseRetryAfter(header):
647     try:
648         # delta seconds
649         return time.time() + int(header)
650     except ValueError:
651         # or datetime
652         return parseDateTime(header)
653
654 # WWW-Authenticate and Authorization
655
656 def parseWWWAuthenticate(tokenized):
657     headers = []
658
659     tokenList = list(tokenized)
660
661     while tokenList:
662         scheme = tokenList.pop(0)
663         challenge = {}
664         last = None
665         kvChallenge = False
666
667         while tokenList:
668             token = tokenList.pop(0)
669             if token == Token('='):
670                 kvChallenge = True
671                 challenge[last] = tokenList.pop(0)
672                 last = None
673
674             elif token == Token(','):
675                 if kvChallenge:
676                     if len(tokenList) > 1 and tokenList[1] != Token('='):
677                         break
678
679                 else:
680                     break
681
682             else:
683                 last = token
684
685         if last and scheme and not challenge and not kvChallenge:
686             challenge = last
687             last = None
688
689         headers.append((scheme, challenge))
690
691     if last and last not in (Token('='), Token(',')):
692         if headers[-1] == (scheme, challenge):
693             scheme = last
694             challenge = {}
695             headers.append((scheme, challenge))
696
697     return headers
698
699 def parseAuthorization(header):
700     scheme, rest = header.split(' ', 1)
701     # this header isn't tokenized because it may eat characters
702     # in the unquoted base64 encoded credentials
703     return scheme.lower(), rest
704
705 #### Header generators
706 def generateAccept(accept):
707     mimeType,q = accept
708
709     out="%s/%s"%(mimeType.mediaType, mimeType.mediaSubtype)
710     if mimeType.params:
711         out+=';'+generateKeyValues(mimeType.params.iteritems())
712
713     if q != 1.0:
714         out+=(';q=%.3f' % (q,)).rstrip('0').rstrip('.')
715
716     return out
717
718 def removeDefaultEncoding(seq):
719     for item in seq:
720         if item[0] != 'identity' or item[1] != .0001:
721             yield item
722
723 def generateAcceptQvalue(keyvalue):
724     if keyvalue[1] == 1.0:
725         return "%s" % keyvalue[0:1]
726     else:
727         return ("%s;q=%.3f" % keyvalue).rstrip('0').rstrip('.')
728
729 def parseCacheControl(kv):
730     k, v = parseKeyValue(kv)
731     if k == 'max-age' or k == 'min-fresh' or k == 's-maxage':
732         # Required integer argument
733         if v is None:
734             v = 0
735         else:
736             v = int(v)
737     elif k == 'max-stale':
738         # Optional integer argument
739         if v is not None:
740             v = int(v)
741     elif k == 'private' or k == 'no-cache':
742         # Optional list argument
743         if v is not None:
744             v = [field.strip().lower() for field in v.split(',')]
745     return k, v
746
747 def generateCacheControl((k, v)):
748     if v is None:
749         return str(k)
750     else:
751         if k == 'no-cache' or k == 'private':
752             # quoted list of values
753             v = quoteString(generateList(
754                 [header_case_mapping.get(name) or dashCapitalize(name) for name in v]))
755         return '%s=%s' % (k,v)
756
757 def generateContentRange(tup):
758     """tup is (type, start, end, len)
759     len can be None.
760     """
761     type, start, end, len = tup
762     if len == None:
763         len = '*'
764     else:
765         len = int(len)
766     if start == None and end == None:
767         startend = '*'
768     else:
769         startend = '%d-%d' % (start, end)
770
771     return '%s %s/%s' % (type, startend, len)
772
773 def generateDateTime(secSinceEpoch):
774     """Convert seconds since epoch to HTTP datetime string."""
775     year, month, day, hh, mm, ss, wd, y, z = time.gmtime(secSinceEpoch)
776     s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
777         weekdayname[wd],
778         day, monthname[month], year,
779         hh, mm, ss)
780     return s
781
782 def generateExpect(item):
783     if item[1][0] is None:
784         out = '%s' % (item[0],)
785     else:
786         out = '%s=%s' % (item[0], item[1][0])
787     if len(item[1]) > 1:
788         out += ';'+generateKeyValues(item[1][1:])
789     return out
790
791 def generateRange(range):
792     def noneOr(s):
793         if s is None:
794             return ''
795         return s
796
797     type,ranges=range
798
799     if type != 'bytes':
800         raise ValueError("Unknown range unit: "+type+".")
801
802     return (type+'='+
803             ','.join(['%s-%s' % (noneOr(startend[0]), noneOr(startend[1]))
804                       for startend in ranges]))
805
806 def generateRetryAfter(when):
807     # always generate delta seconds format
808     return str(int(when - time.time()))
809
810 def generateContentType(mimeType):
811     out="%s/%s"%(mimeType.mediaType, mimeType.mediaSubtype)
812     if mimeType.params:
813         out+=';'+generateKeyValues(mimeType.params.iteritems())
814     return out
815
816 def generateIfRange(dateOrETag):
817     if isinstance(dateOrETag, ETag):
818         return dateOrETag.generate()
819     else:
820         return generateDateTime(dateOrETag)
821
822 # WWW-Authenticate and Authorization
823
824 def generateWWWAuthenticate(headers):
825     _generated = []
826     for seq in headers:
827         scheme, challenge = seq[0], seq[1]
828
829         # If we're going to parse out to something other than a dict
830         # we need to be able to generate from something other than a dict
831
832         try:
833             l = []
834             for k,v in dict(challenge).iteritems():
835                 l.append("%s=%s" % (k, quoteString(v)))
836
837             _generated.append("%s %s" % (scheme, ", ".join(l)))
838         except ValueError:
839             _generated.append("%s %s" % (scheme, challenge))
840
841     return _generated
842
843 def generateAuthorization(seq):
844     return [' '.join(seq)]
845
846
847 ####
848 class ETag(object):
849     def __init__(self, tag, weak=False):
850         self.tag = str(tag)
851         self.weak = weak
852
853     def match(self, other, strongCompare):
854         # Sec 13.3.
855         # The strong comparison function: in order to be considered equal, both
856         #   validators MUST be identical in every way, and both MUST NOT be weak.
857         #
858         # The weak comparison function: in order to be considered equal, both
859         #   validators MUST be identical in every way, but either or both of
860         #   them MAY be tagged as "weak" without affecting the result.
861
862         if not isinstance(other, ETag) or other.tag != self.tag:
863             return False
864
865         if strongCompare and (other.weak or self.weak):
866             return False
867         return True
868
869     def __eq__(self, other):
870         return isinstance(other, ETag) and other.tag == self.tag and other.weak == self.weak
871
872     def __ne__(self, other):
873         return not self.__eq__(other)
874
875     def __repr__(self):
876         return "Etag(%r, weak=%r)" % (self.tag, self.weak)
877
878     def parse(tokens):
879         tokens=tuple(tokens)
880         if len(tokens) == 1 and not isinstance(tokens[0], Token):
881             return ETag(tokens[0])
882
883         if(len(tokens) == 3 and tokens[0] == "w"
884            and tokens[1] == Token('/')):
885             return ETag(tokens[2], weak=True)
886
887         raise ValueError("Invalid ETag.")
888
889     parse=staticmethod(parse)
890
891     def generate(self):
892         if self.weak:
893             return 'W/'+quoteString(self.tag)
894         else:
895             return quoteString(self.tag)
896
897 def parseStarOrETag(tokens):
898     tokens=tuple(tokens)
899     if tokens == ('*',):
900         return '*'
901     else:
902         return ETag.parse(tokens)
903
904 def generateStarOrETag(etag):
905     if etag=='*':
906         return etag
907     else:
908         return etag.generate()
909
910 #### Cookies. Blech!
911 class Cookie(object):
912     # __slots__ = ['name', 'value', 'path', 'domain', 'ports', 'expires', 'discard', 'secure', 'comment', 'commenturl', 'version']
913
914     def __init__(self, name, value, path=None, domain=None, ports=None, expires=None, discard=False, secure=False, comment=None, commenturl=None, version=0):
915         self.name=name
916         self.value=value
917         self.path=path
918         self.domain=domain
919         self.ports=ports
920         self.expires=expires
921         self.discard=discard
922         self.secure=secure
923         self.comment=comment
924         self.commenturl=commenturl
925         self.version=version
926
927     def __repr__(self):
928         s="Cookie(%r=%r" % (self.name, self.value)
929         if self.path is not None: s+=", path=%r" % (self.path,)
930         if self.domain is not None: s+=", domain=%r" % (self.domain,)
931         if self.ports is not None: s+=", ports=%r" % (self.ports,)
932         if self.expires is not None: s+=", expires=%r" % (self.expires,)
933         if self.secure is not False: s+=", secure=%r" % (self.secure,)
934         if self.comment is not None: s+=", comment=%r" % (self.comment,)
935         if self.commenturl is not None: s+=", commenturl=%r" % (self.commenturl,)
936         if self.version != 0: s+=", version=%r" % (self.version,)
937         s+=")"
938         return s
939
940     def __eq__(self, other):
941         return (isinstance(other, Cookie) and
942                 other.path == self.path and
943                 other.domain == self.domain and
944                 other.ports == self.ports and
945                 other.expires == self.expires and
946                 other.secure == self.secure and
947                 other.comment == self.comment and
948                 other.commenturl == self.commenturl and
949                 other.version == self.version)
950
951     def __ne__(self, other):
952         return not self.__eq__(other)
953
954
955 def parseCookie(headers):
956     """Bleargh, the cookie spec sucks.
957     This surely needs interoperability testing.
958     There are two specs that are supported:
959     Version 0) http://wp.netscape.com/newsref/std/cookie_spec.html
960     Version 1) http://www.faqs.org/rfcs/rfc2965.html
961     """
962
963     cookies = []
964     # There can't really be multiple cookie headers according to RFC, because
965     # if multiple headers are allowed, they must be joinable with ",".
966     # Neither new RFC2965 cookies nor old netscape cookies are.
967
968     header = ';'.join(headers)
969     if header[0:8].lower() == "$version":
970         # RFC2965 cookie
971         h=tokenize([header], foldCase=False)
972         r_cookies = split(h, Token(','))
973         for r_cookie in r_cookies:
974             last_cookie = None
975             rr_cookies = split(r_cookie, Token(';'))
976             for cookie in rr_cookies:
977                 nameval = tuple(split(cookie, Token('=')))
978                 if len(nameval) == 2:
979                     (name,), (value,) = nameval
980                 else:
981                     (name,), = nameval
982                     value = None
983
984                 name=name.lower()
985                 if name == '$version':
986                     continue
987                 if name[0] == '$':
988                     if last_cookie is not None:
989                         if name == '$path':
990                             last_cookie.path=value
991                         elif name == '$domain':
992                             last_cookie.domain=value
993                         elif name == '$port':
994                             if value is None:
995                                 last_cookie.ports = ()
996                             else:
997                                 last_cookie.ports=tuple([int(s) for s in value.split(',')])
998                 else:
999                     last_cookie = Cookie(name, value, version=1)
1000                     cookies.append(last_cookie)
1001     else:
1002         # Oldstyle cookies don't do quoted strings or anything sensible.
1003         # All characters are valid for names except ';' and '=', and all
1004         # characters are valid for values except ';'. Spaces are stripped,
1005         # however.
1006         r_cookies = header.split(';')
1007         for r_cookie in r_cookies:
1008             name,value = r_cookie.split('=', 1)
1009             name=name.strip(' \t')
1010             value=value.strip(' \t')
1011
1012             cookies.append(Cookie(name, value))
1013
1014     return cookies
1015
1016 cookie_validname = "[^"+re.escape(http_tokens+http_ctls)+"]*$"
1017 cookie_validname_re = re.compile(cookie_validname)
1018 cookie_validvalue = cookie_validname+'|"([^"]|\\\\")*"$'
1019 cookie_validvalue_re = re.compile(cookie_validvalue)
1020
1021 def generateCookie(cookies):
1022     # There's a fundamental problem with the two cookie specifications.
1023     # They both use the "Cookie" header, and the RFC Cookie header only allows
1024     # one version to be specified. Thus, when you have a collection of V0 and
1025     # V1 cookies, you have to either send them all as V0 or send them all as
1026     # V1.
1027
1028     # I choose to send them all as V1.
1029
1030     # You might think converting a V0 cookie to a V1 cookie would be lossless,
1031     # but you'd be wrong. If you do the conversion, and a V0 parser tries to
1032     # read the cookie, it will see a modified form of the cookie, in cases
1033     # where quotes must be added to conform to proper V1 syntax.
1034     # (as a real example: "Cookie: cartcontents=oid:94680,qty:1,auto:0,esp:y")
1035
1036     # However, that is what we will do, anyways. It has a high probability of
1037     # breaking applications that only handle oldstyle cookies, where some other
1038     # application set a newstyle cookie that is applicable over for site
1039     # (or host), AND where the oldstyle cookie uses a value which is invalid
1040     # syntax in a newstyle cookie.
1041
1042     # Also, the cookie name *cannot* be quoted in V1, so some cookies just
1043     # cannot be converted at all. (e.g. "Cookie: phpAds_capAd[32]=2"). These
1044     # are just dicarded during conversion.
1045
1046     # As this is an unsolvable problem, I will pretend I can just say
1047     # OH WELL, don't do that, or else upgrade your old applications to have
1048     # newstyle cookie parsers.
1049
1050     # I will note offhandedly that there are *many* sites which send V0 cookies
1051     # that are not valid V1 cookie syntax. About 20% for my cookies file.
1052     # However, they do not generally mix them with V1 cookies, so this isn't
1053     # an issue, at least right now. I have not tested to see how many of those
1054     # webapps support RFC2965 V1 cookies. I suspect not many.
1055
1056     max_version = max([cookie.version for cookie in cookies])
1057
1058     if max_version == 0:
1059         # no quoting or anything.
1060         return ';'.join(["%s=%s" % (cookie.name, cookie.value) for cookie in cookies])
1061     else:
1062         str_cookies = ['$Version="1"']
1063         for cookie in cookies:
1064             if cookie.version == 0:
1065                 # Version 0 cookie: we make sure the name and value are valid
1066                 # V1 syntax.
1067
1068                 # If they are, we use them as is. This means in *most* cases,
1069                 # the cookie will look literally the same on output as it did
1070                 # on input.
1071                 # If it isn't a valid name, ignore the cookie.
1072                 # If it isn't a valid value, quote it and hope for the best on
1073                 # the other side.
1074
1075                 if cookie_validname_re.match(cookie.name) is None:
1076                     continue
1077
1078                 value=cookie.value
1079                 if cookie_validvalue_re.match(cookie.value) is None:
1080                     value = quoteString(value)
1081
1082                 str_cookies.append("%s=%s" % (cookie.name, value))
1083             else:
1084                 # V1 cookie, nice and easy
1085                 str_cookies.append("%s=%s" % (cookie.name, quoteString(cookie.value)))
1086
1087             if cookie.path:
1088                 str_cookies.append("$Path=%s" % quoteString(cookie.path))
1089             if cookie.domain:
1090                 str_cookies.append("$Domain=%s" % quoteString(cookie.domain))
1091             if cookie.ports is not None:
1092                 if len(cookie.ports) == 0:
1093                     str_cookies.append("$Port")
1094                 else:
1095                     str_cookies.append("$Port=%s" % quoteString(",".join([str(x) for x in cookie.ports])))
1096         return ';'.join(str_cookies)
1097
1098 def parseSetCookie(headers):
1099     setCookies = []
1100     for header in headers:
1101         try:
1102             parts = header.split(';')
1103             l = []
1104
1105             for part in parts:
1106                 namevalue = part.split('=',1)
1107                 if len(namevalue) == 1:
1108                     name=namevalue[0]
1109                     value=None
1110                 else:
1111                     name,value=namevalue
1112                     value=value.strip(' \t')
1113
1114                 name=name.strip(' \t')
1115
1116                 l.append((name, value))
1117
1118             setCookies.append(makeCookieFromList(l, True))
1119         except ValueError:
1120             # If we can't parse one Set-Cookie, ignore it,
1121             # but not the rest of Set-Cookies.
1122             pass
1123     return setCookies
1124
1125 def parseSetCookie2(toks):
1126     outCookies = []
1127     for cookie in [[parseKeyValue(x) for x in split(y, Token(';'))]
1128                    for y in split(toks, Token(','))]:
1129         try:
1130             outCookies.append(makeCookieFromList(cookie, False))
1131         except ValueError:
1132             # Again, if we can't handle one cookie -- ignore it.
1133             pass
1134     return outCookies
1135
1136 def makeCookieFromList(tup, netscapeFormat):
1137     name, value = tup[0]
1138     if name is None or value is None:
1139         raise ValueError("Cookie has missing name or value")
1140     if name.startswith("$"):
1141         raise ValueError("Invalid cookie name: %r, starts with '$'." % name)
1142     cookie = Cookie(name, value)
1143     hadMaxAge = False
1144
1145     for name,value in tup[1:]:
1146         name = name.lower()
1147
1148         if value is None:
1149             if name in ("discard", "secure"):
1150                 # Boolean attrs
1151                 value = True
1152             elif name != "port":
1153                 # Can be either boolean or explicit
1154                 continue
1155
1156         if name in ("comment", "commenturl", "discard", "domain", "path", "secure"):
1157             # simple cases
1158             setattr(cookie, name, value)
1159         elif name == "expires" and not hadMaxAge:
1160             if netscapeFormat and value[0] == '"' and value[-1] == '"':
1161                 value = value[1:-1]
1162             cookie.expires = parseDateTime(value)
1163         elif name == "max-age":
1164             hadMaxAge = True
1165             cookie.expires = int(value) + time.time()
1166         elif name == "port":
1167             if value is None:
1168                 cookie.ports = ()
1169             else:
1170                 if netscapeFormat and value[0] == '"' and value[-1] == '"':
1171                     value = value[1:-1]
1172                 cookie.ports = tuple([int(s) for s in value.split(',')])
1173         elif name == "version":
1174             cookie.version = int(value)
1175
1176     return cookie
1177
1178
1179 def generateSetCookie(cookies):
1180     setCookies = []
1181     for cookie in cookies:
1182         out = ["%s=%s" % (cookie.name, cookie.value)]
1183         if cookie.expires:
1184             out.append("expires=%s" % generateDateTime(cookie.expires))
1185         if cookie.path:
1186             out.append("path=%s" % cookie.path)
1187         if cookie.domain:
1188             out.append("domain=%s" % cookie.domain)
1189         if cookie.secure:
1190             out.append("secure")
1191
1192         setCookies.append('; '.join(out))
1193     return setCookies
1194
1195 def generateSetCookie2(cookies):
1196     setCookies = []
1197     for cookie in cookies:
1198         out = ["%s=%s" % (cookie.name, quoteString(cookie.value))]
1199         if cookie.comment:
1200             out.append("Comment=%s" % quoteString(cookie.comment))
1201         if cookie.commenturl:
1202             out.append("CommentURL=%s" % quoteString(cookie.commenturl))
1203         if cookie.discard:
1204             out.append("Discard")
1205         if cookie.domain:
1206             out.append("Domain=%s" % quoteString(cookie.domain))
1207         if cookie.expires:
1208             out.append("Max-Age=%s" % (cookie.expires - time.time()))
1209         if cookie.path:
1210             out.append("Path=%s" % quoteString(cookie.path))
1211         if cookie.ports is not None:
1212             if len(cookie.ports) == 0:
1213                 out.append("Port")
1214             else:
1215                 out.append("Port=%s" % quoteString(",".join([str(x) for x in cookie.ports])))
1216         if cookie.secure:
1217             out.append("Secure")
1218         out.append('Version="1"')
1219         setCookies.append('; '.join(out))
1220     return setCookies
1221
1222 def parseDepth(depth):
1223     if depth not in ("0", "1", "infinity"):
1224         raise ValueError("Invalid depth header value: %s" % (depth,))
1225     return depth
1226
1227 def parseOverWrite(overwrite):
1228     if overwrite == "F":
1229         return False
1230     elif overwrite == "T":
1231         return True
1232     raise ValueError("Invalid overwrite header value: %s" % (overwrite,))
1233
1234 def generateOverWrite(overwrite):
1235     if overwrite:
1236         return "T"
1237     else:
1238         return "F"
1239
1240 ##### Random stuff that looks useful.
1241 # def sortMimeQuality(s):
1242 #     def sorter(item1, item2):
1243 #         if item1[0] == '*':
1244 #             if item2[0] == '*':
1245 #                 return 0
1246
1247
1248 # def sortQuality(s):
1249 #     def sorter(item1, item2):
1250 #         if item1[1] < item2[1]:
1251 #             return -1
1252 #         if item1[1] < item2[1]:
1253 #             return 1
1254 #         if item1[0] == item2[0]:
1255 #             return 0
1256
1257
1258 # def getMimeQuality(mimeType, accepts):
1259 #     type,args = parseArgs(mimeType)
1260 #     type=type.split(Token('/'))
1261 #     if len(type) != 2:
1262 #         raise ValueError, "MIME Type "+s+" invalid."
1263
1264 #     for accept in accepts:
1265 #         accept,acceptQual=accept
1266 #         acceptType=accept[0:1]
1267 #         acceptArgs=accept[2]
1268
1269 #         if ((acceptType == type or acceptType == (type[0],'*') or acceptType==('*','*')) and
1270 #             (args == acceptArgs or len(acceptArgs) == 0)):
1271 #             return acceptQual
1272
1273 # def getQuality(type, accepts):
1274 #     qual = accepts.get(type)
1275 #     if qual is not None:
1276 #         return qual
1277
1278 #     return accepts.get('*')
1279
1280 # Headers object
1281 class __RecalcNeeded(object):
1282     def __repr__(self):
1283         return "<RecalcNeeded>"
1284
1285 _RecalcNeeded = __RecalcNeeded()
1286
1287 class Headers(object):
1288     """
1289     This class stores the HTTP headers as both a parsed representation
1290     and the raw string representation. It converts between the two on
1291     demand.
1292     """
1293
1294     def __init__(self, headers=None, rawHeaders=None, handler=DefaultHTTPHandler):
1295         self._raw_headers = {}
1296         self._headers = {}
1297         self.handler = handler
1298         if headers is not None:
1299             for key, value in headers.iteritems():
1300                 self.setHeader(key, value)
1301         if rawHeaders is not None:
1302             for key, value in rawHeaders.iteritems():
1303                 self.setRawHeaders(key, value)
1304
1305     def _setRawHeaders(self, headers):
1306         self._raw_headers = headers
1307         self._headers = {}
1308
1309     def _toParsed(self, name):
1310         r = self._raw_headers.get(name, None)
1311         h = self.handler.parse(name, r)
1312         if h is not None:
1313             self._headers[name] = h
1314         return h
1315
1316     def _toRaw(self, name):
1317         h = self._headers.get(name, None)
1318         r = self.handler.generate(name, h)
1319         if r is not None:
1320             self._raw_headers[name] = r
1321         return r
1322
1323     def hasHeader(self, name):
1324         """Does a header with the given name exist?"""
1325         name=name.lower()
1326         return self._raw_headers.has_key(name)
1327
1328     def getRawHeaders(self, name, default=None):
1329         """Returns a list of headers matching the given name as the raw string given."""
1330
1331         name=name.lower()
1332         raw_header = self._raw_headers.get(name, default)
1333         if raw_header is not _RecalcNeeded:
1334             return raw_header
1335
1336         return self._toRaw(name)
1337
1338     def getHeader(self, name, default=None):
1339         """Ret9urns the parsed representation of the given header.
1340         The exact form of the return value depends on the header in question.
1341
1342         If no parser for the header exists, raise ValueError.
1343
1344         If the header doesn't exist, return default (or None if not specified)
1345         """
1346         name=name.lower()
1347         parsed = self._headers.get(name, default)
1348         if parsed is not _RecalcNeeded:
1349             return parsed
1350         return self._toParsed(name)
1351
1352     def setRawHeaders(self, name, value):
1353         """Sets the raw representation of the given header.
1354         Value should be a list of strings, each being one header of the
1355         given name.
1356         """
1357         name=name.lower()
1358         self._raw_headers[name] = value
1359         self._headers[name] = _RecalcNeeded
1360
1361     def setHeader(self, name, value):
1362         """Sets the parsed representation of the given header.
1363         Value should be a list of objects whose exact form depends
1364         on the header in question.
1365         """
1366         name=name.lower()
1367         self._raw_headers[name] = _RecalcNeeded
1368         self._headers[name] = value
1369
1370     def addRawHeader(self, name, value):
1371         """
1372         Add a raw value to a header that may or may not already exist.
1373         If it exists, add it as a separate header to output; do not
1374         replace anything.
1375         """
1376         name=name.lower()
1377         raw_header = self._raw_headers.get(name)
1378         if raw_header is None:
1379             # No header yet
1380             raw_header = []
1381             self._raw_headers[name] = raw_header
1382         elif raw_header is _RecalcNeeded:
1383             raw_header = self._toRaw(name)
1384
1385         raw_header.append(value)
1386         self._headers[name] = _RecalcNeeded
1387
1388     def removeHeader(self, name):
1389         """Removes the header named."""
1390
1391         name=name.lower()
1392         if self._raw_headers.has_key(name):
1393             del self._raw_headers[name]
1394             del self._headers[name]
1395
1396     def __repr__(self):
1397         return '<Headers: Raw: %s Parsed: %s>'% (self._raw_headers, self._headers)
1398
1399     def canonicalNameCaps(self, name):
1400         """Return the name with the canonical capitalization, if known,
1401         otherwise, Caps-After-Dashes"""
1402         return header_case_mapping.get(name) or dashCapitalize(name)
1403
1404     def getAllRawHeaders(self):
1405         """Return an iterator of key,value pairs of all headers
1406         contained in this object, as strings. The keys are capitalized
1407         in canonical capitalization."""
1408         for k,v in self._raw_headers.iteritems():
1409             if v is _RecalcNeeded:
1410                 v = self._toRaw(k)
1411             yield self.canonicalNameCaps(k), v
1412
1413     def makeImmutable(self):
1414         """Make this header set immutable. All mutating operations will
1415         raise an exception."""
1416         self.setHeader = self.setRawHeaders = self.removeHeader = self._mutateRaise
1417
1418     def _mutateRaise(self, *args):
1419         raise AttributeError("This header object is immutable as the headers have already been sent.")
1420
1421
1422 """The following dicts are all mappings of header to list of operations
1423    to perform. The first operation should generally be 'tokenize' if the
1424    header can be parsed according to the normal tokenization rules. If
1425    it cannot, generally the first thing you want to do is take only the
1426    last instance of the header (in case it was sent multiple times, which
1427    is strictly an error, but we're nice.).
1428    """
1429
1430 iteritems = lambda x: x.iteritems()
1431
1432
1433 parser_general_headers = {
1434     'Cache-Control':(tokenize, listParser(parseCacheControl), dict),
1435     'Connection':(tokenize,filterTokens),
1436     'Date':(last,parseDateTime),
1437 #    'Pragma':tokenize
1438 #    'Trailer':tokenize
1439     'Transfer-Encoding':(tokenize,filterTokens),
1440 #    'Upgrade':tokenize
1441 #    'Via':tokenize,stripComment
1442 #    'Warning':tokenize
1443 }
1444
1445 generator_general_headers = {
1446     'Cache-Control':(iteritems, listGenerator(generateCacheControl), singleHeader),
1447     'Connection':(generateList,singleHeader),
1448     'Date':(generateDateTime,singleHeader),
1449 #    'Pragma':
1450 #    'Trailer':
1451     'Transfer-Encoding':(generateList,singleHeader),
1452 #    'Upgrade':
1453 #    'Via':
1454 #    'Warning':
1455 }
1456
1457 parser_request_headers = {
1458     'Accept': (tokenize, listParser(parseAccept), dict),
1459     'Accept-Charset': (tokenize, listParser(parseAcceptQvalue), dict, addDefaultCharset),
1460     'Accept-Encoding':(tokenize, listParser(parseAcceptQvalue), dict, addDefaultEncoding),
1461     'Accept-Language':(tokenize, listParser(parseAcceptQvalue), dict),
1462     'Authorization': (last, parseAuthorization),
1463     'Cookie':(parseCookie,),
1464     'Expect':(tokenize, listParser(parseExpect), dict),
1465     'From':(last,),
1466     'Host':(last,),
1467     'If-Match':(tokenize, listParser(parseStarOrETag), list),
1468     'If-Modified-Since':(last, parseIfModifiedSince),
1469     'If-None-Match':(tokenize, listParser(parseStarOrETag), list),
1470     'If-Range':(parseIfRange,),
1471     'If-Unmodified-Since':(last,parseDateTime),
1472     'Max-Forwards':(last,int),
1473 #    'Proxy-Authorization':str, # what is "credentials"
1474     'Range':(tokenize, parseRange),
1475     'Referer':(last,str), # TODO: URI object?
1476     'TE':(tokenize, listParser(parseAcceptQvalue), dict),
1477     'User-Agent':(last,str),
1478 }
1479
1480 generator_request_headers = {
1481     'Accept': (iteritems,listGenerator(generateAccept),singleHeader),
1482     'Accept-Charset': (iteritems, listGenerator(generateAcceptQvalue),singleHeader),
1483     'Accept-Encoding': (iteritems, removeDefaultEncoding, listGenerator(generateAcceptQvalue),singleHeader),
1484     'Accept-Language': (iteritems, listGenerator(generateAcceptQvalue),singleHeader),
1485     'Authorization': (generateAuthorization,), # what is "credentials"
1486     'Cookie':(generateCookie,singleHeader),
1487     'Expect':(iteritems, listGenerator(generateExpect), singleHeader),
1488     'From':(str,singleHeader),
1489     'Host':(str,singleHeader),
1490     'If-Match':(listGenerator(generateStarOrETag), singleHeader),
1491     'If-Modified-Since':(generateDateTime,singleHeader),
1492     'If-None-Match':(listGenerator(generateStarOrETag), singleHeader),
1493     'If-Range':(generateIfRange, singleHeader),
1494     'If-Unmodified-Since':(generateDateTime,singleHeader),
1495     'Max-Forwards':(str, singleHeader),
1496 #    'Proxy-Authorization':str, # what is "credentials"
1497     'Range':(generateRange,singleHeader),
1498     'Referer':(str,singleHeader),
1499     'TE': (iteritems, listGenerator(generateAcceptQvalue),singleHeader),
1500     'User-Agent':(str,singleHeader),
1501 }
1502
1503 parser_response_headers = {
1504     'Accept-Ranges':(tokenize, filterTokens),
1505     'Age':(last,int),
1506     'ETag':(tokenize, ETag.parse),
1507     'Location':(last,), # TODO: URI object?
1508 #    'Proxy-Authenticate'
1509     'Retry-After':(last, parseRetryAfter),
1510     'Server':(last,),
1511     'Set-Cookie':(parseSetCookie,),
1512     'Set-Cookie2':(tokenize, parseSetCookie2),
1513     'Vary':(tokenize, filterTokens),
1514     'WWW-Authenticate': (lambda h: tokenize(h, foldCase=False),
1515                          parseWWWAuthenticate,)
1516 }
1517
1518 generator_response_headers = {
1519     'Accept-Ranges':(generateList, singleHeader),
1520     'Age':(str, singleHeader),
1521     'ETag':(ETag.generate, singleHeader),
1522     'Location':(str, singleHeader),
1523 #    'Proxy-Authenticate'
1524     'Retry-After':(generateRetryAfter, singleHeader),
1525     'Server':(str, singleHeader),
1526     'Set-Cookie':(generateSetCookie,),
1527     'Set-Cookie2':(generateSetCookie2,),
1528     'Vary':(generateList, singleHeader),
1529     'WWW-Authenticate':(generateWWWAuthenticate,)
1530 }
1531
1532 parser_entity_headers = {
1533     'Allow':(lambda str:tokenize(str, foldCase=False), filterTokens),
1534     'Content-Encoding':(tokenize, filterTokens),
1535     'Content-Language':(tokenize, filterTokens),
1536     'Content-Length':(last, int),
1537     'Content-Location':(last,), # TODO: URI object?
1538     'Content-MD5':(last, parseContentMD5),
1539     'Content-Range':(last, parseContentRange),
1540     'Content-Type':(lambda str:tokenize(str, foldCase=False), parseContentType),
1541     'Expires':(last, parseExpires),
1542     'Last-Modified':(last, parseDateTime),
1543     }
1544
1545 generator_entity_headers = {
1546     'Allow':(generateList, singleHeader),
1547     'Content-Encoding':(generateList, singleHeader),
1548     'Content-Language':(generateList, singleHeader),
1549     'Content-Length':(str, singleHeader),
1550     'Content-Location':(str, singleHeader),
1551     'Content-MD5':(base64.encodestring, lambda x: x.strip("\n"), singleHeader),
1552     'Content-Range':(generateContentRange, singleHeader),
1553     'Content-Type':(generateContentType, singleHeader),
1554     'Expires':(generateDateTime, singleHeader),
1555     'Last-Modified':(generateDateTime, singleHeader),
1556     }
1557
1558 DefaultHTTPHandler.updateParsers(parser_general_headers)
1559 DefaultHTTPHandler.updateParsers(parser_request_headers)
1560 DefaultHTTPHandler.updateParsers(parser_response_headers)
1561 DefaultHTTPHandler.updateParsers(parser_entity_headers)
1562
1563 DefaultHTTPHandler.updateGenerators(generator_general_headers)
1564 DefaultHTTPHandler.updateGenerators(generator_request_headers)
1565 DefaultHTTPHandler.updateGenerators(generator_response_headers)
1566 DefaultHTTPHandler.updateGenerators(generator_entity_headers)
1567
1568
1569 # casemappingify(DefaultHTTPParsers)
1570 # casemappingify(DefaultHTTPGenerators)
1571
1572 # lowerify(DefaultHTTPParsers)
1573 # lowerify(DefaultHTTPGenerators)
1574
Note: See TracBrowser for help on using the browser.