##// END OF EJS Templates
wsgi-stack: Add a more meaningful comment why we insert the error middleware.
Martin Bornhold -
r946:cc879238 default
parent child Browse files
Show More
@@ -1,463 +1,466 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 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 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 # Add an error handling middleware to convert errors from the old pylons
196 # app into a proper error page response.
195 # The pylons app is executed inside of the pyramid 404 exception handler.
196 # Exceptions which are raised inside of it are not handled by pyramid
197 # again. Therefore we add a middleware that invokes the error handler in
198 # case of an exception or error response. This way we return proper error
199 # HTML pages in case of an error.
197 200 reraise = (settings.get('debugtoolbar.enabled', False) or
198 201 rhodecode.disable_error_handler)
199 202 pylons_app = PylonsErrorHandlingMiddleware(
200 203 pylons_app, error_handler, reraise)
201 204
202 205 # Convert WSGI app to pyramid view and return it.
203 206 return wsgiapp(pylons_app)
204 207
205 208
206 209 def add_pylons_compat_data(registry, global_config, settings):
207 210 """
208 211 Attach data to the registry to support the Pylons integration.
209 212 """
210 213 registry._pylons_compat_global_config = global_config
211 214 registry._pylons_compat_settings = settings
212 215
213 216
214 217 def error_handler(exception, request):
215 218 from rhodecode.model.settings import SettingsModel
216 219 from rhodecode.lib.utils2 import AttributeDict
217 220
218 221 try:
219 222 rc_config = SettingsModel().get_all_settings()
220 223 except Exception:
221 224 log.exception('failed to fetch settings')
222 225 rc_config = {}
223 226
224 227 base_response = HTTPInternalServerError()
225 228 # prefer original exception for the response since it may have headers set
226 229 if isinstance(exception, HTTPError):
227 230 base_response = exception
228 231
229 232 c = AttributeDict()
230 233 c.error_message = base_response.status
231 234 c.error_explanation = base_response.explanation or str(base_response)
232 235 c.visual = AttributeDict()
233 236
234 237 c.visual.rhodecode_support_url = (
235 238 request.registry.settings.get('rhodecode_support_url') or
236 239 request.route_url('rhodecode_support')
237 240 )
238 241 c.redirect_time = 0
239 242 c.rhodecode_name = rc_config.get('rhodecode_title', '')
240 243 if not c.rhodecode_name:
241 244 c.rhodecode_name = 'Rhodecode'
242 245
243 246 c.causes = []
244 247 if hasattr(base_response, 'causes'):
245 248 c.causes = base_response.causes
246 249
247 250 response = render_to_response(
248 251 '/errors/error_document.html', {'c': c}, request=request,
249 252 response=base_response)
250 253
251 254 return response
252 255
253 256
254 257 def includeme(config):
255 258 settings = config.registry.settings
256 259
257 260 # plugin information
258 261 config.registry.rhodecode_plugins = OrderedDict()
259 262
260 263 config.add_directive(
261 264 'register_rhodecode_plugin', register_rhodecode_plugin)
262 265
263 266 if asbool(settings.get('appenlight', 'false')):
264 267 config.include('appenlight_client.ext.pyramid_tween')
265 268
266 269 # Includes which are required. The application would fail without them.
267 270 config.include('pyramid_mako')
268 271 config.include('pyramid_beaker')
269 272 config.include('rhodecode.channelstream')
270 273 config.include('rhodecode.admin')
271 274 config.include('rhodecode.authentication')
272 275 config.include('rhodecode.integrations')
273 276 config.include('rhodecode.login')
274 277 config.include('rhodecode.tweens')
275 278 config.include('rhodecode.api')
276 279 config.include('rhodecode.svn_support')
277 280 config.add_route(
278 281 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
279 282
280 283 # Add subscribers.
281 284 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
282 285
283 286 # Set the authorization policy.
284 287 authz_policy = ACLAuthorizationPolicy()
285 288 config.set_authorization_policy(authz_policy)
286 289
287 290 # Set the default renderer for HTML templates to mako.
288 291 config.add_mako_renderer('.html')
289 292
290 293 # include RhodeCode plugins
291 294 includes = aslist(settings.get('rhodecode.includes', []))
292 295 for inc in includes:
293 296 config.include(inc)
294 297
295 298 # This is the glue which allows us to migrate in chunks. By registering the
296 299 # pylons based application as the "Not Found" view in Pyramid, we will
297 300 # fallback to the old application each time the new one does not yet know
298 301 # how to handle a request.
299 302 config.add_notfound_view(make_not_found_view(config))
300 303
301 304 if not settings.get('debugtoolbar.enabled', False):
302 305 # if no toolbar, then any exception gets caught and rendered
303 306 config.add_view(error_handler, context=Exception)
304 307
305 308 config.add_view(error_handler, context=HTTPError)
306 309
307 310
308 311 def includeme_first(config):
309 312 # redirect automatic browser favicon.ico requests to correct place
310 313 def favicon_redirect(context, request):
311 314 return HTTPFound(
312 315 request.static_path('rhodecode:public/images/favicon.ico'))
313 316
314 317 config.add_view(favicon_redirect, route_name='favicon')
315 318 config.add_route('favicon', '/favicon.ico')
316 319
317 320 config.add_static_view(
318 321 '_static/deform', 'deform:static')
319 322 config.add_static_view(
320 323 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
321 324
322 325
323 326 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
324 327 """
325 328 Apply outer WSGI middlewares around the application.
326 329
327 330 Part of this has been moved up from the Pylons layer, so that the
328 331 data is also available if old Pylons code is hit through an already ported
329 332 view.
330 333 """
331 334 settings = config.registry.settings
332 335
333 336 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
334 337 pyramid_app = HttpsFixup(pyramid_app, settings)
335 338
336 339 # Add RoutesMiddleware to support the pylons compatibility tween during
337 340 # migration to pyramid.
338 341 pyramid_app = SkippableRoutesMiddleware(
339 342 pyramid_app, config.registry._pylons_compat_config['routes.map'],
340 343 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
341 344
342 345 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
343 346
344 347 if settings['gzip_responses']:
345 348 pyramid_app = make_gzip_middleware(
346 349 pyramid_app, settings, compress_level=1)
347 350
348 351
349 352 # this should be the outer most middleware in the wsgi stack since
350 353 # middleware like Routes make database calls
351 354 def pyramid_app_with_cleanup(environ, start_response):
352 355 try:
353 356 return pyramid_app(environ, start_response)
354 357 finally:
355 358 # Dispose current database session and rollback uncommitted
356 359 # transactions.
357 360 meta.Session.remove()
358 361
359 362 # In a single threaded mode server, on non sqlite db we should have
360 363 # '0 Current Checked out connections' at the end of a request,
361 364 # if not, then something, somewhere is leaving a connection open
362 365 pool = meta.Base.metadata.bind.engine.pool
363 366 log.debug('sa pool status: %s', pool.status())
364 367
365 368
366 369 return pyramid_app_with_cleanup
367 370
368 371
369 372 def sanitize_settings_and_apply_defaults(settings):
370 373 """
371 374 Applies settings defaults and does all type conversion.
372 375
373 376 We would move all settings parsing and preparation into this place, so that
374 377 we have only one place left which deals with this part. The remaining parts
375 378 of the application would start to rely fully on well prepared settings.
376 379
377 380 This piece would later be split up per topic to avoid a big fat monster
378 381 function.
379 382 """
380 383
381 384 # Pyramid's mako renderer has to search in the templates folder so that the
382 385 # old templates still work. Ported and new templates are expected to use
383 386 # real asset specifications for the includes.
384 387 mako_directories = settings.setdefault('mako.directories', [
385 388 # Base templates of the original Pylons application
386 389 'rhodecode:templates',
387 390 ])
388 391 log.debug(
389 392 "Using the following Mako template directories: %s",
390 393 mako_directories)
391 394
392 395 # Default includes, possible to change as a user
393 396 pyramid_includes = settings.setdefault('pyramid.includes', [
394 397 'rhodecode.lib.middleware.request_wrapper',
395 398 ])
396 399 log.debug(
397 400 "Using the following pyramid.includes: %s",
398 401 pyramid_includes)
399 402
400 403 # TODO: johbo: Re-think this, usually the call to config.include
401 404 # should allow to pass in a prefix.
402 405 settings.setdefault('rhodecode.api.url', '/_admin/api')
403 406
404 407 # Sanitize generic settings.
405 408 _list_setting(settings, 'default_encoding', 'UTF-8')
406 409 _bool_setting(settings, 'is_test', 'false')
407 410 _bool_setting(settings, 'gzip_responses', 'false')
408 411
409 412 # Call split out functions that sanitize settings for each topic.
410 413 _sanitize_appenlight_settings(settings)
411 414 _sanitize_vcs_settings(settings)
412 415
413 416 return settings
414 417
415 418
416 419 def _sanitize_appenlight_settings(settings):
417 420 _bool_setting(settings, 'appenlight', 'false')
418 421
419 422
420 423 def _sanitize_vcs_settings(settings):
421 424 """
422 425 Applies settings defaults and does type conversion for all VCS related
423 426 settings.
424 427 """
425 428 _string_setting(settings, 'vcs.svn.compatible_version', '')
426 429 _string_setting(settings, 'git_rev_filter', '--all')
427 430 _string_setting(settings, 'vcs.hooks.protocol', 'pyro4')
428 431 _string_setting(settings, 'vcs.server', '')
429 432 _string_setting(settings, 'vcs.server.log_level', 'debug')
430 433 _string_setting(settings, 'vcs.server.protocol', 'pyro4')
431 434 _bool_setting(settings, 'startup.import_repos', 'false')
432 435 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
433 436 _bool_setting(settings, 'vcs.server.enable', 'true')
434 437 _bool_setting(settings, 'vcs.start_server', 'false')
435 438 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
436 439 _int_setting(settings, 'vcs.connection_timeout', 3600)
437 440
438 441
439 442 def _int_setting(settings, name, default):
440 443 settings[name] = int(settings.get(name, default))
441 444
442 445
443 446 def _bool_setting(settings, name, default):
444 447 input = settings.get(name, default)
445 448 if isinstance(input, unicode):
446 449 input = input.encode('utf8')
447 450 settings[name] = asbool(input)
448 451
449 452
450 453 def _list_setting(settings, name, default):
451 454 raw_value = settings.get(name, default)
452 455
453 456 old_separator = ','
454 457 if old_separator in raw_value:
455 458 # If we get a comma separated list, pass it to our own function.
456 459 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
457 460 else:
458 461 # Otherwise we assume it uses pyramids space/newline separation.
459 462 settings[name] = aslist(raw_value)
460 463
461 464
462 465 def _string_setting(settings, name, default):
463 466 settings[name] = settings.get(name, default).lower()
General Comments 0
You need to be logged in to leave comments. Login now