Handle nested comments in the User-Agent string. ... (quixote/http_request.py)

Neil Schemenauer <nascheme-fVcApmY9cLvQ3/1i3zOLAti2O/[email protected]> Mon, 30 Dec 2002 19:55:29 -0500
Newsgroups gmane.comp.web.quixote.cvs
Message-ID <[email protected]>
Update of /home/cvs/quixote
In directory hewson:/tmp/cvs-serv10362

Modified Files:
	http_request.py 
Log Message:
Handle nested comments in the User-Agent string.  Some versions of MSIE
use that feature and it's allowed by the specification.  


Index: http_request.py
===================================================================
RCS file: /home/cvs/quixote/http_request.py,v
retrieving revision 1.54
retrieving revision 1.55
diff -u -d -r1.54 -r1.55
--- http_request.py	20 Nov 2002 19:43:22 -0000	1.54
+++ http_request.py	31 Dec 2002 00:55:26 -0000	1.55
@@ -47,17 +47,11 @@
 #   CHAR = any 7-bit US ASCII character (0-127)
 #   separators are  ( ) < > @ , ; : \ " / [ ] ? = { }
 #
-# The comment RE is a simplification and not to spec -- it doesn't
-# allow for backslash-quoting within comments or nested comments!
-#
-# The user_agent RE is also a simplification; it only looks for
-# one "product", possibly followed by a comment.
+# The user_agent RE is a simplification; it only looks for one "product",
+# possibly followed by a comment.
 _http_token_pat = r'[^\x00-\x20\(\)\<\>\@\,\;\:\\\"\/\[\]\?\=\{\}\x7F-\xFF]+'
 _http_product_pat = r'(%s)(?:/(%s))?' % (_http_token_pat, _http_token_pat)
 _http_product_re = re.compile(_http_product_pat)
-_http_comment_pat = r'\(([^\(\)]*)\)'
-_http_useragent_re = re.compile(r'%s(?:\s+%s)?'
-                                % (_http_product_pat, _http_comment_pat))
 _comment_delim_re = re.compile(r';\s*')
 
 
@@ -381,26 +375,51 @@
             return (None, None)
 
         # The syntax for "User-Agent" in RFC 2616 is fairly simple:
-        #   User-Agent = "User-Agent" ":" 1*( product | comment )
-        #   product    = token ["/" product-version]
-        #   product-version = token
-        #   token      = 1*<any CHAR except CTLs or separators>
         #
-        # The _http_useragent_re handles the most-commonly-used
-        # subset of this syntax, namely
+        #  User-Agent      = "User-Agent" ":" 1*( product | comment )
+        #  product         = token ["/" product-version ]
+        #  product-version = token
+        #  comment         = "(" *( ctext | comment ) ")"
+        #  ctext           = <any TEXT excluding "(" and ")">
+        #  token           = 1*<any CHAR except CTLs or tspecials>
+        #  tspecials       = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" |
+        #                    "\" | <"> | "/" | "[" | "]" | "?" | "=" | "{" |
+        #                    "}" | SP | HT
+        #
+        # This function handles the most-commonly-used subset of this syntax,
+        # namely
         #   User-Agent = "User-Agent" ":" product 1*SP [comment]
         # ie. one product string followed by an optional comment;
         # anything after that first comment is ignored.  This should be
         # enough to distinguish Mozilla/Netscape, MSIE, Opera, and
         # Konqueror.
 
-        ua_match = _http_useragent_re.match(ua)
-        if not ua_match:
+        m = _http_product_re.match(ua)
+        if not m:
             import sys
             sys.stderr.write("couldn't parse User-Agent header: %r\n" % ua)
             return (None, None)
 
-        (name, version, comment) = ua_match.groups()
+        name, version = m.groups()
+        ua = ua[m.end():].lstrip()
+        
+        if ua.startswith('('):
+            # we need to handle nested comments since MSIE uses them
+            depth = 1
+            chars = []
+            for c in ua[1:]:
+                if c == '(':
+                    depth += 1
+                elif c == ')':
+                    depth -= 1
+                    if depth == 0:
+                        break
+                elif depth == 1:
+                    # nested comments are discarded
+                    chars.append(c)
+            comment = ''.join(chars)
+        else:
+            comment = ''
         if comment:
             comment_chunks = _comment_delim_re.split(comment)
         else: