| 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 |