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