OLD | NEW |
| (Empty) |
1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.o
rg) | |
2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license
.php | |
3 """ | |
4 Map URL prefixes to WSGI applications. See ``URLMap`` | |
5 """ | |
6 | |
7 import re | |
8 import os | |
9 import cgi | |
10 try: | |
11 # Python 3 | |
12 from collections import MutableMapping as DictMixin | |
13 except ImportError: | |
14 # Python 2 | |
15 from UserDict import DictMixin | |
16 | |
17 from paste import httpexceptions | |
18 | |
19 __all__ = ['URLMap', 'PathProxyURLMap'] | |
20 | |
21 def urlmap_factory(loader, global_conf, **local_conf): | |
22 if 'not_found_app' in local_conf: | |
23 not_found_app = local_conf.pop('not_found_app') | |
24 else: | |
25 not_found_app = global_conf.get('not_found_app') | |
26 if not_found_app: | |
27 not_found_app = loader.get_app(not_found_app, global_conf=global_conf) | |
28 urlmap = URLMap(not_found_app=not_found_app) | |
29 for path, app_name in local_conf.items(): | |
30 path = parse_path_expression(path) | |
31 app = loader.get_app(app_name, global_conf=global_conf) | |
32 urlmap[path] = app | |
33 return urlmap | |
34 | |
35 def parse_path_expression(path): | |
36 """ | |
37 Parses a path expression like 'domain foobar.com port 20 /' or | |
38 just '/foobar' for a path alone. Returns as an address that | |
39 URLMap likes. | |
40 """ | |
41 parts = path.split() | |
42 domain = port = path = None | |
43 while parts: | |
44 if parts[0] == 'domain': | |
45 parts.pop(0) | |
46 if not parts: | |
47 raise ValueError("'domain' must be followed with a domain name") | |
48 if domain: | |
49 raise ValueError("'domain' given twice") | |
50 domain = parts.pop(0) | |
51 elif parts[0] == 'port': | |
52 parts.pop(0) | |
53 if not parts: | |
54 raise ValueError("'port' must be followed with a port number") | |
55 if port: | |
56 raise ValueError("'port' given twice") | |
57 port = parts.pop(0) | |
58 else: | |
59 if path: | |
60 raise ValueError("more than one path given (have %r, got %r)" | |
61 % (path, parts[0])) | |
62 path = parts.pop(0) | |
63 s = '' | |
64 if domain: | |
65 s = 'http://%s' % domain | |
66 if port: | |
67 if not domain: | |
68 raise ValueError("If you give a port, you must also give a domain") | |
69 s += ':' + port | |
70 if path: | |
71 if s: | |
72 s += '/' | |
73 s += path | |
74 return s | |
75 | |
76 class URLMap(DictMixin): | |
77 | |
78 """ | |
79 URLMap instances are dictionary-like object that dispatch to one | |
80 of several applications based on the URL. | |
81 | |
82 The dictionary keys are URLs to match (like | |
83 ``PATH_INFO.startswith(url)``), and the values are applications to | |
84 dispatch to. URLs are matched most-specific-first, i.e., longest | |
85 URL first. The ``SCRIPT_NAME`` and ``PATH_INFO`` environmental | |
86 variables are adjusted to indicate the new context. | |
87 | |
88 URLs can also include domains, like ``http://blah.com/foo``, or as | |
89 tuples ``('blah.com', '/foo')``. This will match domain names; without | |
90 the ``http://domain`` or with a domain of ``None`` any domain will be | |
91 matched (so long as no other explicit domain matches). """ | |
92 | |
93 def __init__(self, not_found_app=None): | |
94 self.applications = [] | |
95 if not not_found_app: | |
96 not_found_app = self.not_found_app | |
97 self.not_found_application = not_found_app | |
98 | |
99 def __len__(self): | |
100 return len(self.applications) | |
101 | |
102 def __iter__(self): | |
103 for app_url, app in self.applications: | |
104 yield app_url | |
105 | |
106 norm_url_re = re.compile('//+') | |
107 domain_url_re = re.compile('^(http|https)://') | |
108 | |
109 def not_found_app(self, environ, start_response): | |
110 mapper = environ.get('paste.urlmap_object') | |
111 if mapper: | |
112 matches = [p for p, a in mapper.applications] | |
113 extra = 'defined apps: %s' % ( | |
114 ',\n '.join(map(repr, matches))) | |
115 else: | |
116 extra = '' | |
117 extra += '\nSCRIPT_NAME: %r' % cgi.escape(environ.get('SCRIPT_NAME')) | |
118 extra += '\nPATH_INFO: %r' % cgi.escape(environ.get('PATH_INFO')) | |
119 extra += '\nHTTP_HOST: %r' % cgi.escape(environ.get('HTTP_HOST')) | |
120 app = httpexceptions.HTTPNotFound( | |
121 environ['PATH_INFO'], | |
122 comment=cgi.escape(extra)).wsgi_application | |
123 return app(environ, start_response) | |
124 | |
125 def normalize_url(self, url, trim=True): | |
126 if isinstance(url, (list, tuple)): | |
127 domain = url[0] | |
128 url = self.normalize_url(url[1])[1] | |
129 return domain, url | |
130 assert (not url or url.startswith('/') | |
131 or self.domain_url_re.search(url)), ( | |
132 "URL fragments must start with / or http:// (you gave %r)" % url) | |
133 match = self.domain_url_re.search(url) | |
134 if match: | |
135 url = url[match.end():] | |
136 if '/' in url: | |
137 domain, url = url.split('/', 1) | |
138 url = '/' + url | |
139 else: | |
140 domain, url = url, '' | |
141 else: | |
142 domain = None | |
143 url = self.norm_url_re.sub('/', url) | |
144 if trim: | |
145 url = url.rstrip('/') | |
146 return domain, url | |
147 | |
148 def sort_apps(self): | |
149 """ | |
150 Make sure applications are sorted with longest URLs first | |
151 """ | |
152 def key(app_desc): | |
153 (domain, url), app = app_desc | |
154 if not domain: | |
155 # Make sure empty domains sort last: | |
156 return '\xff', -len(url) | |
157 else: | |
158 return domain, -len(url) | |
159 apps = [(key(desc), desc) for desc in self.applications] | |
160 apps.sort() | |
161 self.applications = [desc for (sortable, desc) in apps] | |
162 | |
163 def __setitem__(self, url, app): | |
164 if app is None: | |
165 try: | |
166 del self[url] | |
167 except KeyError: | |
168 pass | |
169 return | |
170 dom_url = self.normalize_url(url) | |
171 if dom_url in self: | |
172 del self[dom_url] | |
173 self.applications.append((dom_url, app)) | |
174 self.sort_apps() | |
175 | |
176 def __getitem__(self, url): | |
177 dom_url = self.normalize_url(url) | |
178 for app_url, app in self.applications: | |
179 if app_url == dom_url: | |
180 return app | |
181 raise KeyError( | |
182 "No application with the url %r (domain: %r; existing: %s)" | |
183 % (url[1], url[0] or '*', self.applications)) | |
184 | |
185 def __delitem__(self, url): | |
186 url = self.normalize_url(url) | |
187 for app_url, app in self.applications: | |
188 if app_url == url: | |
189 self.applications.remove((app_url, app)) | |
190 break | |
191 else: | |
192 raise KeyError( | |
193 "No application with the url %r" % (url,)) | |
194 | |
195 def keys(self): | |
196 return [app_url for app_url, app in self.applications] | |
197 | |
198 def __call__(self, environ, start_response): | |
199 host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower() | |
200 if ':' in host: | |
201 host, port = host.split(':', 1) | |
202 else: | |
203 if environ['wsgi.url_scheme'] == 'http': | |
204 port = '80' | |
205 else: | |
206 port = '443' | |
207 path_info = environ.get('PATH_INFO') | |
208 path_info = self.normalize_url(path_info, False)[1] | |
209 for (domain, app_url), app in self.applications: | |
210 if domain and domain != host and domain != host+':'+port: | |
211 continue | |
212 if (path_info == app_url | |
213 or path_info.startswith(app_url + '/')): | |
214 environ['SCRIPT_NAME'] += app_url | |
215 environ['PATH_INFO'] = path_info[len(app_url):] | |
216 return app(environ, start_response) | |
217 environ['paste.urlmap_object'] = self | |
218 return self.not_found_application(environ, start_response) | |
219 | |
220 | |
221 class PathProxyURLMap(object): | |
222 | |
223 """ | |
224 This is a wrapper for URLMap that catches any strings that | |
225 are passed in as applications; these strings are treated as | |
226 filenames (relative to `base_path`) and are passed to the | |
227 callable `builder`, which will return an application. | |
228 | |
229 This is intended for cases when configuration files can be | |
230 treated as applications. | |
231 | |
232 `base_paste_url` is the URL under which all applications added through | |
233 this wrapper must go. Use ``""`` if you want this to not | |
234 change incoming URLs. | |
235 """ | |
236 | |
237 def __init__(self, map, base_paste_url, base_path, builder): | |
238 self.map = map | |
239 self.base_paste_url = self.map.normalize_url(base_paste_url) | |
240 self.base_path = base_path | |
241 self.builder = builder | |
242 | |
243 def __setitem__(self, url, app): | |
244 if isinstance(app, (str, unicode)): | |
245 app_fn = os.path.join(self.base_path, app) | |
246 app = self.builder(app_fn) | |
247 url = self.map.normalize_url(url) | |
248 # @@: This means http://foo.com/bar will potentially | |
249 # match foo.com, but /base_paste_url/bar, which is unintuitive | |
250 url = (url[0] or self.base_paste_url[0], | |
251 self.base_paste_url[1] + url[1]) | |
252 self.map[url] = app | |
253 | |
254 def __getattr__(self, attr): | |
255 return getattr(self.map, attr) | |
256 | |
257 # This is really the only settable attribute | |
258 def not_found_application__get(self): | |
259 return self.map.not_found_application | |
260 def not_found_application__set(self, value): | |
261 self.map.not_found_application = value | |
262 not_found_application = property(not_found_application__get, | |
263 not_found_application__set) | |
OLD | NEW |