##// END OF EJS Templates
core: let pyramid handle tracebacks for all exceptions....
marcink -
r1311:69141fb5 default
parent child Browse files
Show More
@@ -1,483 +1,485 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 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 34 from pyramid.httpexceptions import (
35 35 HTTPError, HTTPInternalServerError, HTTPFound)
36 36 from pyramid.events import ApplicationCreated
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 47 from rhodecode.lib.middleware import csrf
48 48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 49 from rhodecode.lib.middleware.error_handling import (
50 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 pylons app is executed inside of the pyramid 404 exception handler.
190 190 # Exceptions which are raised inside of it are not handled by pyramid
191 191 # again. Therefore we add a middleware that invokes the error handler in
192 192 # case of an exception or error response. This way we return proper error
193 193 # HTML pages in case of an error.
194 194 reraise = (settings.get('debugtoolbar.enabled', False) or
195 195 rhodecode.disable_error_handler)
196 196 pylons_app = PylonsErrorHandlingMiddleware(
197 197 pylons_app, error_handler, reraise)
198 198
199 199 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find a
200 200 # view to handle the request. Therefore it is wrapped around the pylons
201 201 # app. It has to be outside of the error handling otherwise error responses
202 202 # from the vcsserver are converted to HTML error pages. This confuses the
203 203 # command line tools and the user won't get a meaningful error message.
204 204 if vcs_server_enabled:
205 205 pylons_app = VCSMiddleware(
206 206 pylons_app, settings, appenlight_client, registry=config.registry)
207 207
208 208 # Convert WSGI app to pyramid view and return it.
209 209 return wsgiapp(pylons_app)
210 210
211 211
212 212 def add_pylons_compat_data(registry, global_config, settings):
213 213 """
214 214 Attach data to the registry to support the Pylons integration.
215 215 """
216 216 registry._pylons_compat_global_config = global_config
217 217 registry._pylons_compat_settings = settings
218 218
219 219
220 220 def error_handler(exception, request):
221 221 from rhodecode.model.settings import SettingsModel
222 222 from rhodecode.lib.utils2 import AttributeDict
223 223
224 224 try:
225 225 rc_config = SettingsModel().get_all_settings()
226 226 except Exception:
227 227 log.exception('failed to fetch settings')
228 228 rc_config = {}
229 229
230 log.exception(
231 'error occurred handling this request for path: %s', request.path)
230 232 base_response = HTTPInternalServerError()
231 233 # prefer original exception for the response since it may have headers set
232 234 if isinstance(exception, HTTPError):
233 235 base_response = exception
234 236
235 237 c = AttributeDict()
236 238 c.error_message = base_response.status
237 239 c.error_explanation = base_response.explanation or str(base_response)
238 240 c.visual = AttributeDict()
239 241
240 242 c.visual.rhodecode_support_url = (
241 243 request.registry.settings.get('rhodecode_support_url') or
242 244 request.route_url('rhodecode_support')
243 245 )
244 246 c.redirect_time = 0
245 247 c.rhodecode_name = rc_config.get('rhodecode_title', '')
246 248 if not c.rhodecode_name:
247 249 c.rhodecode_name = 'Rhodecode'
248 250
249 251 c.causes = []
250 252 if hasattr(base_response, 'causes'):
251 253 c.causes = base_response.causes
252 254
253 255 response = render_to_response(
254 256 '/errors/error_document.mako', {'c': c}, request=request,
255 257 response=base_response)
256 258
257 259 return response
258 260
259 261
260 262 def includeme(config):
261 263 settings = config.registry.settings
262 264
263 265 # plugin information
264 266 config.registry.rhodecode_plugins = OrderedDict()
265 267
266 268 config.add_directive(
267 269 'register_rhodecode_plugin', register_rhodecode_plugin)
268 270
269 271 if asbool(settings.get('appenlight', 'false')):
270 272 config.include('appenlight_client.ext.pyramid_tween')
271 273
272 274 # Includes which are required. The application would fail without them.
273 275 config.include('pyramid_mako')
274 276 config.include('pyramid_beaker')
275 277 config.include('rhodecode.channelstream')
276 278 config.include('rhodecode.admin')
277 279 config.include('rhodecode.authentication')
278 280 config.include('rhodecode.integrations')
279 281 config.include('rhodecode.login')
280 282 config.include('rhodecode.tweens')
281 283 config.include('rhodecode.api')
282 284 config.include('rhodecode.svn_support')
283 285 config.add_route(
284 286 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
285 287
286 288 config.add_translation_dirs('rhodecode:i18n/')
287 289 settings['default_locale_name'] = settings.get('lang', 'en')
288 290
289 291 # Add subscribers.
290 292 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
291 293
292 294 # Set the authorization policy.
293 295 authz_policy = ACLAuthorizationPolicy()
294 296 config.set_authorization_policy(authz_policy)
295 297
296 298 # Set the default renderer for HTML templates to mako.
297 299 config.add_mako_renderer('.html')
298 300
299 301 # include RhodeCode plugins
300 302 includes = aslist(settings.get('rhodecode.includes', []))
301 303 for inc in includes:
302 304 config.include(inc)
303 305
304 306 # This is the glue which allows us to migrate in chunks. By registering the
305 307 # pylons based application as the "Not Found" view in Pyramid, we will
306 308 # fallback to the old application each time the new one does not yet know
307 309 # how to handle a request.
308 310 config.add_notfound_view(make_not_found_view(config))
309 311
310 312 if not settings.get('debugtoolbar.enabled', False):
311 313 # if no toolbar, then any exception gets caught and rendered
312 314 config.add_view(error_handler, context=Exception)
313 315
314 316 config.add_view(error_handler, context=HTTPError)
315 317
316 318
317 319 def includeme_first(config):
318 320 # redirect automatic browser favicon.ico requests to correct place
319 321 def favicon_redirect(context, request):
320 322 return HTTPFound(
321 323 request.static_path('rhodecode:public/images/favicon.ico'))
322 324
323 325 config.add_view(favicon_redirect, route_name='favicon')
324 326 config.add_route('favicon', '/favicon.ico')
325 327
326 328 config.add_static_view(
327 329 '_static/deform', 'deform:static')
328 330 config.add_static_view(
329 331 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
330 332
331 333
332 334 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
333 335 """
334 336 Apply outer WSGI middlewares around the application.
335 337
336 338 Part of this has been moved up from the Pylons layer, so that the
337 339 data is also available if old Pylons code is hit through an already ported
338 340 view.
339 341 """
340 342 settings = config.registry.settings
341 343
342 344 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
343 345 pyramid_app = HttpsFixup(pyramid_app, settings)
344 346
345 347 # Add RoutesMiddleware to support the pylons compatibility tween during
346 348 # migration to pyramid.
347 349 pyramid_app = SkippableRoutesMiddleware(
348 350 pyramid_app, config.registry._pylons_compat_config['routes.map'],
349 351 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
350 352
351 353 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
352 354
353 355 if settings['gzip_responses']:
354 356 pyramid_app = make_gzip_middleware(
355 357 pyramid_app, settings, compress_level=1)
356 358
357 359
358 360 # this should be the outer most middleware in the wsgi stack since
359 361 # middleware like Routes make database calls
360 362 def pyramid_app_with_cleanup(environ, start_response):
361 363 try:
362 364 return pyramid_app(environ, start_response)
363 365 finally:
364 366 # Dispose current database session and rollback uncommitted
365 367 # transactions.
366 368 meta.Session.remove()
367 369
368 370 # In a single threaded mode server, on non sqlite db we should have
369 371 # '0 Current Checked out connections' at the end of a request,
370 372 # if not, then something, somewhere is leaving a connection open
371 373 pool = meta.Base.metadata.bind.engine.pool
372 374 log.debug('sa pool status: %s', pool.status())
373 375
374 376
375 377 return pyramid_app_with_cleanup
376 378
377 379
378 380 def sanitize_settings_and_apply_defaults(settings):
379 381 """
380 382 Applies settings defaults and does all type conversion.
381 383
382 384 We would move all settings parsing and preparation into this place, so that
383 385 we have only one place left which deals with this part. The remaining parts
384 386 of the application would start to rely fully on well prepared settings.
385 387
386 388 This piece would later be split up per topic to avoid a big fat monster
387 389 function.
388 390 """
389 391
390 392 # Pyramid's mako renderer has to search in the templates folder so that the
391 393 # old templates still work. Ported and new templates are expected to use
392 394 # real asset specifications for the includes.
393 395 mako_directories = settings.setdefault('mako.directories', [
394 396 # Base templates of the original Pylons application
395 397 'rhodecode:templates',
396 398 ])
397 399 log.debug(
398 400 "Using the following Mako template directories: %s",
399 401 mako_directories)
400 402
401 403 # Default includes, possible to change as a user
402 404 pyramid_includes = settings.setdefault('pyramid.includes', [
403 405 'rhodecode.lib.middleware.request_wrapper',
404 406 ])
405 407 log.debug(
406 408 "Using the following pyramid.includes: %s",
407 409 pyramid_includes)
408 410
409 411 # TODO: johbo: Re-think this, usually the call to config.include
410 412 # should allow to pass in a prefix.
411 413 settings.setdefault('rhodecode.api.url', '/_admin/api')
412 414
413 415 # Sanitize generic settings.
414 416 _list_setting(settings, 'default_encoding', 'UTF-8')
415 417 _bool_setting(settings, 'is_test', 'false')
416 418 _bool_setting(settings, 'gzip_responses', 'false')
417 419
418 420 # Call split out functions that sanitize settings for each topic.
419 421 _sanitize_appenlight_settings(settings)
420 422 _sanitize_vcs_settings(settings)
421 423
422 424 return settings
423 425
424 426
425 427 def _sanitize_appenlight_settings(settings):
426 428 _bool_setting(settings, 'appenlight', 'false')
427 429
428 430
429 431 def _sanitize_vcs_settings(settings):
430 432 """
431 433 Applies settings defaults and does type conversion for all VCS related
432 434 settings.
433 435 """
434 436 _string_setting(settings, 'vcs.svn.compatible_version', '')
435 437 _string_setting(settings, 'git_rev_filter', '--all')
436 438 _string_setting(settings, 'vcs.hooks.protocol', 'http')
437 439 _string_setting(settings, 'vcs.scm_app_implementation', 'http')
438 440 _string_setting(settings, 'vcs.server', '')
439 441 _string_setting(settings, 'vcs.server.log_level', 'debug')
440 442 _string_setting(settings, 'vcs.server.protocol', 'http')
441 443 _bool_setting(settings, 'startup.import_repos', 'false')
442 444 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
443 445 _bool_setting(settings, 'vcs.server.enable', 'true')
444 446 _bool_setting(settings, 'vcs.start_server', 'false')
445 447 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
446 448 _int_setting(settings, 'vcs.connection_timeout', 3600)
447 449
448 450 # Support legacy values of vcs.scm_app_implementation. Legacy
449 451 # configurations may use 'rhodecode.lib.middleware.utils.scm_app_http'
450 452 # which is now mapped to 'http'.
451 453 scm_app_impl = settings['vcs.scm_app_implementation']
452 454 if scm_app_impl == 'rhodecode.lib.middleware.utils.scm_app_http':
453 455 settings['vcs.scm_app_implementation'] = 'http'
454 456
455 457
456 458 def _int_setting(settings, name, default):
457 459 settings[name] = int(settings.get(name, default))
458 460
459 461
460 462 def _bool_setting(settings, name, default):
461 463 input = settings.get(name, default)
462 464 if isinstance(input, unicode):
463 465 input = input.encode('utf8')
464 466 settings[name] = asbool(input)
465 467
466 468
467 469 def _list_setting(settings, name, default):
468 470 raw_value = settings.get(name, default)
469 471
470 472 old_separator = ','
471 473 if old_separator in raw_value:
472 474 # If we get a comma separated list, pass it to our own function.
473 475 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
474 476 else:
475 477 # Otherwise we assume it uses pyramids space/newline separation.
476 478 settings[name] = aslist(raw_value)
477 479
478 480
479 481 def _string_setting(settings, name, default, lower=True):
480 482 value = settings.get(name, default)
481 483 if lower:
482 484 value = value.lower()
483 485 settings[name] = value
@@ -1,99 +1,98 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 import logging
23 23 from pyramid import httpexceptions
24 24 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError
25 25 from pyramid.threadlocal import get_current_request
26 26
27 27 from rhodecode.lib.exceptions import VCSServerUnavailable
28 28 from rhodecode.lib.vcs.exceptions import VCSCommunicationError
29 29
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 class PylonsErrorHandlingMiddleware(object):
35 35 """
36 36 This middleware is wrapped around the old pylons application to catch
37 37 errors and invoke our error handling view to return a proper error page.
38 38 """
39 39 def __init__(self, app, error_view, reraise=False):
40 40 self.app = app
41 41 self.error_view = error_view
42 42 self._reraise = reraise
43 43
44 44 def __call__(self, environ, start_response):
45 45 # We need to use the pyramid request here instead of creating a custom
46 46 # instance from the environ because this request maybe passed to the
47 47 # error handler view which is a pyramid view and expects a pyramid
48 48 # request which has been processed by the pyramid router.
49 49 request = get_current_request()
50 50
51 51 response = self.handle_request(request)
52 52 return response(environ, start_response)
53 53
54 54 def is_http_error(self, response):
55 55 # webob type error responses
56 56 return (400 <= response.status_int <= 599)
57 57
58 58 def reraise(self):
59 59 return self._reraise
60 60
61 61 def handle_request(self, request):
62 62 """
63 63 Calls the underlying WSGI app (typically the old RhodeCode pylons app)
64 64 and returns the response if no error happened. In case of an error it
65 65 invokes the error handling view to return a proper error page as
66 66 response.
67 67
68 68 - old webob type exceptions get converted to pyramid exceptions
69 69 - pyramid exceptions are passed to the error handler view
70 70 """
71 71 try:
72 72 response = request.get_response(self.app)
73 73 if self.is_http_error(response):
74 74 response = webob_to_pyramid_http_response(response)
75 75 return self.error_view(response, request)
76 76 except HTTPError as e: # pyramid type exceptions
77 77 return self.error_view(e, request)
78 78 except Exception as e:
79 log.exception(e)
80 79
81 80 if self.reraise():
82 81 raise
83 82
84 83 if isinstance(e, VCSCommunicationError):
85 84 return self.error_view(VCSServerUnavailable(), request)
86 85
87 86 return self.error_view(HTTPInternalServerError(), request)
88 87
89 88 return response
90 89
91 90
92 91 def webob_to_pyramid_http_response(webob_response):
93 92 ResponseClass = httpexceptions.status_map[webob_response.status_int]
94 93 pyramid_response = ResponseClass(webob_response.status)
95 94 pyramid_response.status = webob_response.status
96 95 pyramid_response.headers.update(webob_response.headers)
97 96 if pyramid_response.headers['content-type'] == 'text/html':
98 97 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
99 98 return pyramid_response
General Comments 0
You need to be logged in to leave comments. Login now