1# Copyright 2012 Google Inc.
2#
3# Permission is hereby granted, free of charge, to any person obtaining a
4# copy of this software and associated documentation files (the
5# "Software"), to deal in the Software without restriction, including
6# without limitation the rights to use, copy, modify, merge, publish, dis-
7# tribute, sublicense, and/or sell copies of the Software, and to permit
8# persons to whom the Software is furnished to do so, subject to the fol-
9# lowing conditions:
10#
11# The above copyright notice and this permission notice shall be included
12# in all copies or substantial portions of the Software.
13#
14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20# IN THE SOFTWARE.
21
22import types
23from boto.gs.user import User
24from boto.exception import InvalidCorsError
25from xml.sax import handler
26
27# Relevant tags for the CORS XML document.
28CORS_CONFIG = 'CorsConfig'
29CORS        = 'Cors'
30ORIGINS     = 'Origins'
31ORIGIN      = 'Origin'
32METHODS     = 'Methods'
33METHOD      = 'Method'
34HEADERS     = 'ResponseHeaders'
35HEADER      = 'ResponseHeader'
36MAXAGESEC   = 'MaxAgeSec'
37
38class Cors(handler.ContentHandler):
39    """Encapsulates the CORS configuration XML document"""
40    def __init__(self):
41        # List of CORS elements found within a CorsConfig element.
42        self.cors = []
43        # List of collections (e.g. Methods, ResponseHeaders, Origins)
44        # found within a CORS element. We use a list of lists here
45        # instead of a dictionary because the collections need to be
46        # preserved in the order in which they appear in the input XML
47        # document (and Python dictionary keys are inherently unordered).
48        # The elements on this list are two element tuples of the form
49        # (collection name, [list of collection contents]).
50        self.collections = []
51        # Lists of elements within a collection. Again a list is needed to
52        # preserve ordering but also because the same element may appear
53        # multiple times within a collection.
54        self.elements = []
55        # Dictionary mapping supported collection names to element types
56        # which may be contained within each.
57        self.legal_collections = {
58            ORIGINS : [ORIGIN],
59            METHODS : [METHOD],
60            HEADERS : [HEADER],
61            MAXAGESEC: []
62        }
63        # List of supported element types within any collection, used for
64        # checking validadity of a parsed element name.
65        self.legal_elements = [ORIGIN, METHOD, HEADER]
66
67        self.parse_level = 0
68        self.collection = None
69        self.element = None
70
71    def validateParseLevel(self, tag, level):
72        """Verify parse level for a given tag."""
73        if self.parse_level != level:
74            raise InvalidCorsError('Invalid tag %s at parse level %d: ' %
75                                   (tag, self.parse_level))
76
77    def startElement(self, name, attrs, connection):
78        """SAX XML logic for parsing new element found."""
79        if name == CORS_CONFIG:
80            self.validateParseLevel(name, 0)
81            self.parse_level += 1;
82        elif name == CORS:
83            self.validateParseLevel(name, 1)
84            self.parse_level += 1;
85        elif name in self.legal_collections:
86            self.validateParseLevel(name, 2)
87            self.parse_level += 1;
88            self.collection = name
89        elif name in self.legal_elements:
90            self.validateParseLevel(name, 3)
91            # Make sure this tag is found inside a collection tag.
92            if self.collection is None:
93                raise InvalidCorsError('Tag %s found outside collection' % name)
94            # Make sure this tag is allowed for the current collection tag.
95            if name not in self.legal_collections[self.collection]:
96                raise InvalidCorsError('Tag %s not allowed in %s collection' %
97                                       (name, self.collection))
98            self.element = name
99        else:
100            raise InvalidCorsError('Unsupported tag ' + name)
101
102    def endElement(self, name, value, connection):
103        """SAX XML logic for parsing new element found."""
104        if name == CORS_CONFIG:
105            self.validateParseLevel(name, 1)
106            self.parse_level -= 1;
107        elif name == CORS:
108            self.validateParseLevel(name, 2)
109            self.parse_level -= 1;
110            # Terminating a CORS element, save any collections we found
111            # and re-initialize collections list.
112            self.cors.append(self.collections)
113            self.collections = []
114        elif name in self.legal_collections:
115            self.validateParseLevel(name, 3)
116            if name != self.collection:
117              raise InvalidCorsError('Mismatched start and end tags (%s/%s)' %
118                                     (self.collection, name))
119            self.parse_level -= 1;
120            if not self.legal_collections[name]:
121              # If this collection doesn't contain any sub-elements, store
122              # a tuple of name and this tag's element value.
123              self.collections.append((name, value.strip()))
124            else:
125              # Otherwise, we're terminating a collection of sub-elements,
126              # so store a tuple of name and list of contained elements.
127              self.collections.append((name, self.elements))
128            self.elements = []
129            self.collection = None
130        elif name in self.legal_elements:
131            self.validateParseLevel(name, 3)
132            # Make sure this tag is found inside a collection tag.
133            if self.collection is None:
134                raise InvalidCorsError('Tag %s found outside collection' % name)
135            # Make sure this end tag is allowed for the current collection tag.
136            if name not in self.legal_collections[self.collection]:
137                raise InvalidCorsError('Tag %s not allowed in %s collection' %
138                                       (name, self.collection))
139            if name != self.element:
140              raise InvalidCorsError('Mismatched start and end tags (%s/%s)' %
141                                     (self.element, name))
142            # Terminating an element tag, add it to the list of elements
143            # for the current collection.
144            self.elements.append((name, value.strip()))
145            self.element = None
146        else:
147            raise InvalidCorsError('Unsupported end tag ' + name)
148
149    def to_xml(self):
150        """Convert CORS object into XML string representation."""
151        s = '<' + CORS_CONFIG + '>'
152        for collections in self.cors:
153          s += '<' + CORS + '>'
154          for (collection, elements_or_value) in collections:
155            assert collection is not None
156            s += '<' + collection + '>'
157            # If collection elements has type string, append atomic value,
158            # otherwise, append sequence of values in named tags.
159            if isinstance(elements_or_value, str):
160              s += elements_or_value
161            else:
162              for (name, value) in elements_or_value:
163                assert name is not None
164                assert value is not None
165                s += '<' + name + '>' + value + '</' + name + '>'
166            s += '</' + collection + '>'
167          s += '</' + CORS + '>'
168        s += '</' + CORS_CONFIG + '>'
169        return s
170