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