~ubuntu-branches/ubuntu/quantal/nova/quantal-proposed

« back to all changes in this revision

Viewing changes to nova/api/openstack/urlmap.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2012-01-20 11:54:15 UTC
  • mto: This revision was merged to the branch mainline in revision 62.
  • Revision ID: package-import@ubuntu.com-20120120115415-h2ujma9o536o1ut6
Tags: upstream-2012.1~e3~20120120.12170
ImportĀ upstreamĀ versionĀ 2012.1~e3~20120120.12170

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
# Copyright 2011 OpenStack LLC.
 
4
# All Rights Reserved.
 
5
#
 
6
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 
7
#    not use this file except in compliance with the License. You may obtain
 
8
#    a copy of the License at
 
9
#
 
10
#         http://www.apache.org/licenses/LICENSE-2.0
 
11
#
 
12
#    Unless required by applicable law or agreed to in writing, software
 
13
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
14
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
15
#    License for the specific language governing permissions and limitations
 
16
#    under the License.
 
17
 
 
18
import paste.urlmap
 
19
import re
 
20
import urllib2
 
21
 
 
22
from nova import log as logging
 
23
from nova.api.openstack import wsgi
 
24
 
 
25
 
 
26
_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"'
 
27
_option_header_piece_re = re.compile(r';\s*([^\s;=]+|%s)\s*'
 
28
                                     r'(?:=\s*([^;]+|%s))?\s*' %
 
29
    (_quoted_string_re, _quoted_string_re))
 
30
 
 
31
LOG = logging.getLogger('nova.api.openstack.compute.map')
 
32
 
 
33
 
 
34
def unquote_header_value(value):
 
35
    """Unquotes a header value.
 
36
    This does not use the real unquoting but what browsers are actually
 
37
    using for quoting.
 
38
 
 
39
    :param value: the header value to unquote.
 
40
    """
 
41
    if value and value[0] == value[-1] == '"':
 
42
        # this is not the real unquoting, but fixing this so that the
 
43
        # RFC is met will result in bugs with internet explorer and
 
44
        # probably some other browsers as well.  IE for example is
 
45
        # uploading files with "C:\foo\bar.txt" as filename
 
46
        value = value[1:-1]
 
47
    return value
 
48
 
 
49
 
 
50
def parse_list_header(value):
 
51
    """Parse lists as described by RFC 2068 Section 2.
 
52
 
 
53
    In particular, parse comma-separated lists where the elements of
 
54
    the list may include quoted-strings.  A quoted-string could
 
55
    contain a comma.  A non-quoted string could have quotes in the
 
56
    middle.  Quotes are removed automatically after parsing.
 
57
 
 
58
    The return value is a standard :class:`list`:
 
59
 
 
60
    >>> parse_list_header('token, "quoted value"')
 
61
    ['token', 'quoted value']
 
62
 
 
63
    :param value: a string with a list header.
 
64
    :return: :class:`list`
 
65
    """
 
66
    result = []
 
67
    for item in urllib2.parse_http_list(value):
 
68
        if item[:1] == item[-1:] == '"':
 
69
            item = unquote_header_value(item[1:-1])
 
70
        result.append(item)
 
71
    return result
 
72
 
 
73
 
 
74
def parse_options_header(value):
 
75
    """Parse a ``Content-Type`` like header into a tuple with the content
 
76
    type and the options:
 
77
 
 
78
    >>> parse_options_header('Content-Type: text/html; mimetype=text/html')
 
79
    ('Content-Type:', {'mimetype': 'text/html'})
 
80
 
 
81
    :param value: the header to parse.
 
82
    :return: (str, options)
 
83
    """
 
84
    def _tokenize(string):
 
85
        for match in _option_header_piece_re.finditer(string):
 
86
            key, value = match.groups()
 
87
            key = unquote_header_value(key)
 
88
            if value is not None:
 
89
                value = unquote_header_value(value)
 
90
            yield key, value
 
91
 
 
92
    if not value:
 
93
        return '', {}
 
94
 
 
95
    parts = _tokenize(';' + value)
 
96
    name = parts.next()[0]
 
97
    extra = dict(parts)
 
98
    return name, extra
 
99
 
 
100
 
 
101
class Accept(object):
 
102
    def __init__(self, value):
 
103
        self._content_types = [parse_options_header(v) for v in
 
104
                               parse_list_header(value)]
 
105
 
 
106
    def best_match(self, supported_content_types):
 
107
        # FIXME: Should we have a more sophisticated matching algorithm that
 
108
        # takes into account the version as well?
 
109
        best_quality = -1
 
110
        best_content_type = None
 
111
        best_params = {}
 
112
        best_match = '*/*'
 
113
 
 
114
        for content_type in supported_content_types:
 
115
            for content_mask, params in self._content_types:
 
116
                try:
 
117
                    quality = float(params.get('q', 1))
 
118
                except ValueError:
 
119
                    continue
 
120
 
 
121
                if quality < best_quality:
 
122
                    continue
 
123
                elif best_quality == quality:
 
124
                    if best_match.count('*') <= content_mask.count('*'):
 
125
                        continue
 
126
 
 
127
                if self._match_mask(content_mask, content_type):
 
128
                    best_quality = quality
 
129
                    best_content_type = content_type
 
130
                    best_params = params
 
131
                    best_match = content_mask
 
132
 
 
133
        return best_content_type, best_params
 
134
 
 
135
    def content_type_params(self, best_content_type):
 
136
        """Find parameters in Accept header for given content type."""
 
137
        for content_type, params in self._content_types:
 
138
            if best_content_type == content_type:
 
139
                return params
 
140
 
 
141
        return {}
 
142
 
 
143
    def _match_mask(self, mask, content_type):
 
144
        if '*' not in mask:
 
145
            return content_type == mask
 
146
        if mask == '*/*':
 
147
            return True
 
148
        mask_major = mask[:-2]
 
149
        content_type_major = content_type.split('/', 1)[0]
 
150
        return content_type_major == mask_major
 
151
 
 
152
 
 
153
def urlmap_factory(loader, global_conf, **local_conf):
 
154
    if 'not_found_app' in local_conf:
 
155
        not_found_app = local_conf.pop('not_found_app')
 
156
    else:
 
157
        not_found_app = global_conf.get('not_found_app')
 
158
    if not_found_app:
 
159
        not_found_app = loader.get_app(not_found_app, global_conf=global_conf)
 
160
    urlmap = URLMap(not_found_app=not_found_app)
 
161
    for path, app_name in local_conf.items():
 
162
        path = paste.urlmap.parse_path_expression(path)
 
163
        app = loader.get_app(app_name, global_conf=global_conf)
 
164
        urlmap[path] = app
 
165
    return urlmap
 
166
 
 
167
 
 
168
class URLMap(paste.urlmap.URLMap):
 
169
    def _match(self, host, port, path_info):
 
170
        """Find longest match for a given URL path."""
 
171
        for (domain, app_url), app in self.applications:
 
172
            if domain and domain != host and domain != host + ':' + port:
 
173
                continue
 
174
            if (path_info == app_url
 
175
                or path_info.startswith(app_url + '/')):
 
176
                return app, app_url
 
177
 
 
178
        return None, None
 
179
 
 
180
    def _set_script_name(self, app, app_url):
 
181
        def wrap(environ, start_response):
 
182
            environ['SCRIPT_NAME'] += app_url
 
183
            return app(environ, start_response)
 
184
 
 
185
        return wrap
 
186
 
 
187
    def _munge_path(self, app, path_info, app_url):
 
188
        def wrap(environ, start_response):
 
189
            environ['SCRIPT_NAME'] += app_url
 
190
            environ['PATH_INFO'] = path_info[len(app_url):]
 
191
            return app(environ, start_response)
 
192
 
 
193
        return wrap
 
194
 
 
195
    def _path_strategy(self, host, port, path_info):
 
196
        """Check path suffix for MIME type and path prefix for API version."""
 
197
        mime_type = app = app_url = None
 
198
 
 
199
        parts = path_info.rsplit('.', 1)
 
200
        if len(parts) > 1:
 
201
            possible_type = 'application/' + parts[1]
 
202
            if possible_type in wsgi.SUPPORTED_CONTENT_TYPES:
 
203
                mime_type = possible_type
 
204
 
 
205
        parts = path_info.split('/')
 
206
        if len(parts) > 1:
 
207
            possible_app, possible_app_url = self._match(host, port, path_info)
 
208
            # Don't use prefix if it ends up matching default
 
209
            if possible_app and possible_app_url:
 
210
                app_url = possible_app_url
 
211
                app = self._munge_path(possible_app, path_info, app_url)
 
212
 
 
213
        return mime_type, app, app_url
 
214
 
 
215
    def _content_type_strategy(self, host, port, environ):
 
216
        """Check Content-Type header for API version."""
 
217
        app = None
 
218
        params = parse_options_header(environ.get('CONTENT_TYPE', ''))[1]
 
219
        if 'version' in params:
 
220
            app, app_url = self._match(host, port, '/v' + params['version'])
 
221
            if app:
 
222
                app = self._set_script_name(app, app_url)
 
223
 
 
224
        return app
 
225
 
 
226
    def _accept_strategy(self, host, port, environ, supported_content_types):
 
227
        """Check Accept header for best matching MIME type and API version."""
 
228
        accept = Accept(environ.get('HTTP_ACCEPT', ''))
 
229
 
 
230
        app = None
 
231
 
 
232
        # Find the best match in the Accept header
 
233
        mime_type, params = accept.best_match(supported_content_types)
 
234
        if 'version' in params:
 
235
            app, app_url = self._match(host, port, '/v' + params['version'])
 
236
            if app:
 
237
                app = self._set_script_name(app, app_url)
 
238
 
 
239
        return mime_type, app
 
240
 
 
241
    def __call__(self, environ, start_response):
 
242
        host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower()
 
243
        if ':' in host:
 
244
            host, port = host.split(':', 1)
 
245
        else:
 
246
            if environ['wsgi.url_scheme'] == 'http':
 
247
                port = '80'
 
248
            else:
 
249
                port = '443'
 
250
 
 
251
        path_info = environ['PATH_INFO']
 
252
        path_info = self.normalize_url(path_info, False)[1]
 
253
 
 
254
        # The MIME type for the response is determined in one of two ways:
 
255
        # 1) URL path suffix (eg /servers/detail.json)
 
256
        # 2) Accept header (eg application/json;q=0.8, application/xml;q=0.2)
 
257
 
 
258
        # The API version is determined in one of three ways:
 
259
        # 1) URL path prefix (eg /v1.1/tenant/servers/detail)
 
260
        # 2) Content-Type header (eg application/json;version=1.1)
 
261
        # 3) Accept header (eg application/json;q=0.8;version=1.1)
 
262
 
 
263
        supported_content_types = list(wsgi.SUPPORTED_CONTENT_TYPES)
 
264
 
 
265
        mime_type, app, app_url = self._path_strategy(host, port, path_info)
 
266
 
 
267
        # Accept application/atom+xml for the index query of each API
 
268
        # version mount point as well as the root index
 
269
        if (app_url and app_url + '/' == path_info) or path_info == '/':
 
270
            supported_content_types.append('application/atom+xml')
 
271
 
 
272
        if not app:
 
273
            app = self._content_type_strategy(host, port, environ)
 
274
 
 
275
        if not mime_type or not app:
 
276
            possible_mime_type, possible_app = self._accept_strategy(
 
277
                    host, port, environ, supported_content_types)
 
278
            if possible_mime_type and not mime_type:
 
279
                mime_type = possible_mime_type
 
280
            if possible_app and not app:
 
281
                app = possible_app
 
282
 
 
283
        if not mime_type:
 
284
            mime_type = 'application/json'
 
285
 
 
286
        if not app:
 
287
            # Didn't match a particular version, probably matches default
 
288
            app, app_url = self._match(host, port, path_info)
 
289
            if app:
 
290
                app = self._munge_path(app, path_info, app_url)
 
291
 
 
292
        if app:
 
293
            environ['nova.best_content_type'] = mime_type
 
294
            return app(environ, start_response)
 
295
 
 
296
        environ['paste.urlmap_object'] = self
 
297
        return self.not_found_application(environ, start_response)