##// END OF EJS Templates
wsgi-stack: Use the pylons error handling middleware.
Martin Bornhold -
r945:c9704d4f default
parent child Browse files
Show More
@@ -1,505 +1,463 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Pylons middleware initialization
23 23 """
24 24 import logging
25 25 from collections import OrderedDict
26 26
27 27 from paste.registry import RegistryManager
28 28 from paste.gzipper import make_gzip_middleware
29 29 from pylons.wsgiapp import PylonsApp
30 30 from pyramid.authorization import ACLAuthorizationPolicy
31 31 from pyramid.config import Configurator
32 32 from pyramid.settings import asbool, aslist
33 33 from pyramid.wsgi import wsgiapp
34 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError, HTTPFound
34 from pyramid.httpexceptions import (
35 HTTPError, HTTPInternalServerError, HTTPFound)
35 36 from pyramid.events import ApplicationCreated
36 import pyramid.httpexceptions as httpexceptions
37 37 from pyramid.renderers import render_to_response
38 38 from routes.middleware import RoutesMiddleware
39 39 import routes.util
40 40
41 41 import rhodecode
42 42 from rhodecode.model import meta
43 43 from rhodecode.config import patches
44 44 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 45 from rhodecode.config.environment import (
46 46 load_environment, load_pyramid_environment)
47 from rhodecode.lib.exceptions import VCSServerUnavailable
48 from rhodecode.lib.vcs.exceptions import VCSCommunicationError
49 47 from rhodecode.lib.middleware import csrf
50 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 from rhodecode.lib.middleware.error_handling import (
50 PylonsErrorHandlingMiddleware)
51 51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
52 52 from rhodecode.lib.middleware.vcs import VCSMiddleware
53 53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
55 55 from rhodecode.subscribers import scan_repositories_if_enabled
56 56
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 # this is used to avoid avoid the route lookup overhead in routesmiddleware
62 62 # for certain routes which won't go to pylons to - eg. static files, debugger
63 63 # it is only needed for the pylons migration and can be removed once complete
64 64 class SkippableRoutesMiddleware(RoutesMiddleware):
65 65 """ Routes middleware that allows you to skip prefixes """
66 66
67 67 def __init__(self, *args, **kw):
68 68 self.skip_prefixes = kw.pop('skip_prefixes', [])
69 69 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
70 70
71 71 def __call__(self, environ, start_response):
72 72 for prefix in self.skip_prefixes:
73 73 if environ['PATH_INFO'].startswith(prefix):
74 74 # added to avoid the case when a missing /_static route falls
75 75 # through to pylons and causes an exception as pylons is
76 76 # expecting wsgiorg.routingargs to be set in the environ
77 77 # by RoutesMiddleware.
78 78 if 'wsgiorg.routing_args' not in environ:
79 79 environ['wsgiorg.routing_args'] = (None, {})
80 80 return self.app(environ, start_response)
81 81
82 82 return super(SkippableRoutesMiddleware, self).__call__(
83 83 environ, start_response)
84 84
85 85
86 86 def make_app(global_conf, static_files=True, **app_conf):
87 87 """Create a Pylons WSGI application and return it
88 88
89 89 ``global_conf``
90 90 The inherited configuration for this application. Normally from
91 91 the [DEFAULT] section of the Paste ini file.
92 92
93 93 ``app_conf``
94 94 The application's local configuration. Normally specified in
95 95 the [app:<name>] section of the Paste ini file (where <name>
96 96 defaults to main).
97 97
98 98 """
99 99 # Apply compatibility patches
100 100 patches.kombu_1_5_1_python_2_7_11()
101 101 patches.inspect_getargspec()
102 102
103 103 # Configure the Pylons environment
104 104 config = load_environment(global_conf, app_conf)
105 105
106 106 # The Pylons WSGI app
107 107 app = PylonsApp(config=config)
108 108 if rhodecode.is_test:
109 109 app = csrf.CSRFDetector(app)
110 110
111 111 expected_origin = config.get('expected_origin')
112 112 if expected_origin:
113 113 # The API can be accessed from other Origins.
114 114 app = csrf.OriginChecker(app, expected_origin,
115 115 skip_urls=[routes.util.url_for('api')])
116 116
117 117 # Establish the Registry for this application
118 118 app = RegistryManager(app)
119 119
120 120 app.config = config
121 121
122 122 return app
123 123
124 124
125 125 def make_pyramid_app(global_config, **settings):
126 126 """
127 127 Constructs the WSGI application based on Pyramid and wraps the Pylons based
128 128 application.
129 129
130 130 Specials:
131 131
132 132 * We migrate from Pylons to Pyramid. While doing this, we keep both
133 133 frameworks functional. This involves moving some WSGI middlewares around
134 134 and providing access to some data internals, so that the old code is
135 135 still functional.
136 136
137 137 * The application can also be integrated like a plugin via the call to
138 138 `includeme`. This is accompanied with the other utility functions which
139 139 are called. Changing this should be done with great care to not break
140 140 cases when these fragments are assembled from another place.
141 141
142 142 """
143 143 # The edition string should be available in pylons too, so we add it here
144 144 # before copying the settings.
145 145 settings.setdefault('rhodecode.edition', 'Community Edition')
146 146
147 147 # As long as our Pylons application does expect "unprepared" settings, make
148 148 # sure that we keep an unmodified copy. This avoids unintentional change of
149 149 # behavior in the old application.
150 150 settings_pylons = settings.copy()
151 151
152 152 sanitize_settings_and_apply_defaults(settings)
153 153 config = Configurator(settings=settings)
154 154 add_pylons_compat_data(config.registry, global_config, settings_pylons)
155 155
156 156 load_pyramid_environment(global_config, settings)
157 157
158 158 includeme_first(config)
159 159 includeme(config)
160 160 pyramid_app = config.make_wsgi_app()
161 161 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
162 162 pyramid_app.config = config
163 163
164 164 # creating the app uses a connection - return it after we are done
165 165 meta.Session.remove()
166 166
167 167 return pyramid_app
168 168
169 169
170 170 def make_not_found_view(config):
171 171 """
172 172 This creates the view which should be registered as not-found-view to
173 173 pyramid. Basically it contains of the old pylons app, converted to a view.
174 174 Additionally it is wrapped by some other middlewares.
175 175 """
176 176 settings = config.registry.settings
177 177 vcs_server_enabled = settings['vcs.server.enable']
178 178
179 179 # Make pylons app from unprepared settings.
180 180 pylons_app = make_app(
181 181 config.registry._pylons_compat_global_config,
182 182 **config.registry._pylons_compat_settings)
183 183 config.registry._pylons_compat_config = pylons_app.config
184 184
185 185 # Appenlight monitoring.
186 186 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
187 187 pylons_app, settings)
188 188
189 189 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find
190 190 # a view to handle the request. Therefore we wrap it around the pylons app.
191 191 if vcs_server_enabled:
192 192 pylons_app = VCSMiddleware(
193 193 pylons_app, settings, appenlight_client, registry=config.registry)
194 194
195 pylons_app_as_view = wsgiapp(pylons_app)
196
197 def pylons_app_with_error_handler(context, request):
198 """
199 Handle exceptions from rc pylons app:
200
201 - old webob type exceptions get converted to pyramid exceptions
202 - pyramid exceptions are passed to the error handler view
203 """
204 def is_vcs_response(response):
205 return 'X-RhodeCode-Backend' in response.headers
206
207 def is_http_error(response):
208 # webob type error responses
209 return (400 <= response.status_int <= 599)
210
211 def is_error_handling_needed(response):
212 return is_http_error(response) and not is_vcs_response(response)
195 # Add an error handling middleware to convert errors from the old pylons
196 # app into a proper error page response.
197 reraise = (settings.get('debugtoolbar.enabled', False) or
198 rhodecode.disable_error_handler)
199 pylons_app = PylonsErrorHandlingMiddleware(
200 pylons_app, error_handler, reraise)
213 201
214 try:
215 response = pylons_app_as_view(context, request)
216 if is_error_handling_needed(response):
217 response = webob_to_pyramid_http_response(response)
218 return error_handler(response, request)
219 except HTTPError as e: # pyramid type exceptions
220 return error_handler(e, request)
221 except Exception as e:
222 log.exception(e)
223
224 if (settings.get('debugtoolbar.enabled', False) or
225 rhodecode.disable_error_handler):
226 raise
227
228 if isinstance(e, VCSCommunicationError):
229 return error_handler(VCSServerUnavailable(), request)
230
231 return error_handler(HTTPInternalServerError(), request)
232
233 return response
234
235 return pylons_app_with_error_handler
202 # Convert WSGI app to pyramid view and return it.
203 return wsgiapp(pylons_app)
236 204
237 205
238 206 def add_pylons_compat_data(registry, global_config, settings):
239 207 """
240 208 Attach data to the registry to support the Pylons integration.
241 209 """
242 210 registry._pylons_compat_global_config = global_config
243 211 registry._pylons_compat_settings = settings
244 212
245 213
246 def webob_to_pyramid_http_response(webob_response):
247 ResponseClass = httpexceptions.status_map[webob_response.status_int]
248 pyramid_response = ResponseClass(webob_response.status)
249 pyramid_response.status = webob_response.status
250 pyramid_response.headers.update(webob_response.headers)
251 if pyramid_response.headers['content-type'] == 'text/html':
252 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
253 return pyramid_response
254
255
256 214 def error_handler(exception, request):
257 215 from rhodecode.model.settings import SettingsModel
258 216 from rhodecode.lib.utils2 import AttributeDict
259 217
260 218 try:
261 219 rc_config = SettingsModel().get_all_settings()
262 220 except Exception:
263 221 log.exception('failed to fetch settings')
264 222 rc_config = {}
265 223
266 224 base_response = HTTPInternalServerError()
267 225 # prefer original exception for the response since it may have headers set
268 226 if isinstance(exception, HTTPError):
269 227 base_response = exception
270 228
271 229 c = AttributeDict()
272 230 c.error_message = base_response.status
273 231 c.error_explanation = base_response.explanation or str(base_response)
274 232 c.visual = AttributeDict()
275 233
276 234 c.visual.rhodecode_support_url = (
277 235 request.registry.settings.get('rhodecode_support_url') or
278 236 request.route_url('rhodecode_support')
279 237 )
280 238 c.redirect_time = 0
281 239 c.rhodecode_name = rc_config.get('rhodecode_title', '')
282 240 if not c.rhodecode_name:
283 241 c.rhodecode_name = 'Rhodecode'
284 242
285 243 c.causes = []
286 244 if hasattr(base_response, 'causes'):
287 245 c.causes = base_response.causes
288 246
289 247 response = render_to_response(
290 248 '/errors/error_document.html', {'c': c}, request=request,
291 249 response=base_response)
292 250
293 251 return response
294 252
295 253
296 254 def includeme(config):
297 255 settings = config.registry.settings
298 256
299 257 # plugin information
300 258 config.registry.rhodecode_plugins = OrderedDict()
301 259
302 260 config.add_directive(
303 261 'register_rhodecode_plugin', register_rhodecode_plugin)
304 262
305 263 if asbool(settings.get('appenlight', 'false')):
306 264 config.include('appenlight_client.ext.pyramid_tween')
307 265
308 266 # Includes which are required. The application would fail without them.
309 267 config.include('pyramid_mako')
310 268 config.include('pyramid_beaker')
311 269 config.include('rhodecode.channelstream')
312 270 config.include('rhodecode.admin')
313 271 config.include('rhodecode.authentication')
314 272 config.include('rhodecode.integrations')
315 273 config.include('rhodecode.login')
316 274 config.include('rhodecode.tweens')
317 275 config.include('rhodecode.api')
318 276 config.include('rhodecode.svn_support')
319 277 config.add_route(
320 278 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
321 279
322 280 # Add subscribers.
323 281 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
324 282
325 283 # Set the authorization policy.
326 284 authz_policy = ACLAuthorizationPolicy()
327 285 config.set_authorization_policy(authz_policy)
328 286
329 287 # Set the default renderer for HTML templates to mako.
330 288 config.add_mako_renderer('.html')
331 289
332 290 # include RhodeCode plugins
333 291 includes = aslist(settings.get('rhodecode.includes', []))
334 292 for inc in includes:
335 293 config.include(inc)
336 294
337 295 # This is the glue which allows us to migrate in chunks. By registering the
338 296 # pylons based application as the "Not Found" view in Pyramid, we will
339 297 # fallback to the old application each time the new one does not yet know
340 298 # how to handle a request.
341 299 config.add_notfound_view(make_not_found_view(config))
342 300
343 301 if not settings.get('debugtoolbar.enabled', False):
344 302 # if no toolbar, then any exception gets caught and rendered
345 303 config.add_view(error_handler, context=Exception)
346 304
347 305 config.add_view(error_handler, context=HTTPError)
348 306
349 307
350 308 def includeme_first(config):
351 309 # redirect automatic browser favicon.ico requests to correct place
352 310 def favicon_redirect(context, request):
353 311 return HTTPFound(
354 312 request.static_path('rhodecode:public/images/favicon.ico'))
355 313
356 314 config.add_view(favicon_redirect, route_name='favicon')
357 315 config.add_route('favicon', '/favicon.ico')
358 316
359 317 config.add_static_view(
360 318 '_static/deform', 'deform:static')
361 319 config.add_static_view(
362 320 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
363 321
364 322
365 323 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
366 324 """
367 325 Apply outer WSGI middlewares around the application.
368 326
369 327 Part of this has been moved up from the Pylons layer, so that the
370 328 data is also available if old Pylons code is hit through an already ported
371 329 view.
372 330 """
373 331 settings = config.registry.settings
374 332
375 333 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
376 334 pyramid_app = HttpsFixup(pyramid_app, settings)
377 335
378 336 # Add RoutesMiddleware to support the pylons compatibility tween during
379 337 # migration to pyramid.
380 338 pyramid_app = SkippableRoutesMiddleware(
381 339 pyramid_app, config.registry._pylons_compat_config['routes.map'],
382 340 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
383 341
384 342 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
385 343
386 344 if settings['gzip_responses']:
387 345 pyramid_app = make_gzip_middleware(
388 346 pyramid_app, settings, compress_level=1)
389 347
390 348
391 349 # this should be the outer most middleware in the wsgi stack since
392 350 # middleware like Routes make database calls
393 351 def pyramid_app_with_cleanup(environ, start_response):
394 352 try:
395 353 return pyramid_app(environ, start_response)
396 354 finally:
397 355 # Dispose current database session and rollback uncommitted
398 356 # transactions.
399 357 meta.Session.remove()
400 358
401 359 # In a single threaded mode server, on non sqlite db we should have
402 360 # '0 Current Checked out connections' at the end of a request,
403 361 # if not, then something, somewhere is leaving a connection open
404 362 pool = meta.Base.metadata.bind.engine.pool
405 363 log.debug('sa pool status: %s', pool.status())
406 364
407 365
408 366 return pyramid_app_with_cleanup
409 367
410 368
411 369 def sanitize_settings_and_apply_defaults(settings):
412 370 """
413 371 Applies settings defaults and does all type conversion.
414 372
415 373 We would move all settings parsing and preparation into this place, so that
416 374 we have only one place left which deals with this part. The remaining parts
417 375 of the application would start to rely fully on well prepared settings.
418 376
419 377 This piece would later be split up per topic to avoid a big fat monster
420 378 function.
421 379 """
422 380
423 381 # Pyramid's mako renderer has to search in the templates folder so that the
424 382 # old templates still work. Ported and new templates are expected to use
425 383 # real asset specifications for the includes.
426 384 mako_directories = settings.setdefault('mako.directories', [
427 385 # Base templates of the original Pylons application
428 386 'rhodecode:templates',
429 387 ])
430 388 log.debug(
431 389 "Using the following Mako template directories: %s",
432 390 mako_directories)
433 391
434 392 # Default includes, possible to change as a user
435 393 pyramid_includes = settings.setdefault('pyramid.includes', [
436 394 'rhodecode.lib.middleware.request_wrapper',
437 395 ])
438 396 log.debug(
439 397 "Using the following pyramid.includes: %s",
440 398 pyramid_includes)
441 399
442 400 # TODO: johbo: Re-think this, usually the call to config.include
443 401 # should allow to pass in a prefix.
444 402 settings.setdefault('rhodecode.api.url', '/_admin/api')
445 403
446 404 # Sanitize generic settings.
447 405 _list_setting(settings, 'default_encoding', 'UTF-8')
448 406 _bool_setting(settings, 'is_test', 'false')
449 407 _bool_setting(settings, 'gzip_responses', 'false')
450 408
451 409 # Call split out functions that sanitize settings for each topic.
452 410 _sanitize_appenlight_settings(settings)
453 411 _sanitize_vcs_settings(settings)
454 412
455 413 return settings
456 414
457 415
458 416 def _sanitize_appenlight_settings(settings):
459 417 _bool_setting(settings, 'appenlight', 'false')
460 418
461 419
462 420 def _sanitize_vcs_settings(settings):
463 421 """
464 422 Applies settings defaults and does type conversion for all VCS related
465 423 settings.
466 424 """
467 425 _string_setting(settings, 'vcs.svn.compatible_version', '')
468 426 _string_setting(settings, 'git_rev_filter', '--all')
469 427 _string_setting(settings, 'vcs.hooks.protocol', 'pyro4')
470 428 _string_setting(settings, 'vcs.server', '')
471 429 _string_setting(settings, 'vcs.server.log_level', 'debug')
472 430 _string_setting(settings, 'vcs.server.protocol', 'pyro4')
473 431 _bool_setting(settings, 'startup.import_repos', 'false')
474 432 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
475 433 _bool_setting(settings, 'vcs.server.enable', 'true')
476 434 _bool_setting(settings, 'vcs.start_server', 'false')
477 435 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
478 436 _int_setting(settings, 'vcs.connection_timeout', 3600)
479 437
480 438
481 439 def _int_setting(settings, name, default):
482 440 settings[name] = int(settings.get(name, default))
483 441
484 442
485 443 def _bool_setting(settings, name, default):
486 444 input = settings.get(name, default)
487 445 if isinstance(input, unicode):
488 446 input = input.encode('utf8')
489 447 settings[name] = asbool(input)
490 448
491 449
492 450 def _list_setting(settings, name, default):
493 451 raw_value = settings.get(name, default)
494 452
495 453 old_separator = ','
496 454 if old_separator in raw_value:
497 455 # If we get a comma separated list, pass it to our own function.
498 456 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
499 457 else:
500 458 # Otherwise we assume it uses pyramids space/newline separation.
501 459 settings[name] = aslist(raw_value)
502 460
503 461
504 462 def _string_setting(settings, name, default):
505 463 settings[name] = settings.get(name, default).lower()
General Comments 0
You need to be logged in to leave comments. Login now