##// END OF EJS Templates
vcs: Move VCSMiddleware up to pyramid layer as wrapper around pylons app....
Martin Bornhold -
r581:5c1ed3a7 default
parent child Browse files
Show More
@@ -1,409 +1,423 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.static import static_view
33 33 from pyramid.settings import asbool, aslist
34 34 from pyramid.wsgi import wsgiapp
35 35 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError
36 36 from pylons.controllers.util import abort, redirect
37 37 from pyramid.events import ApplicationCreated
38 38 import pyramid.httpexceptions as httpexceptions
39 39 from pyramid.renderers import render_to_response, render
40 40 from routes.middleware import RoutesMiddleware
41 41 import routes.util
42 42
43 43 import rhodecode
44 44 import rhodecode.integrations # do not remove this as it registers celery tasks
45 45 from rhodecode.config import patches
46 46 from rhodecode.config.routing import STATIC_FILE_PREFIX
47 47 from rhodecode.config.environment import (
48 48 load_environment, load_pyramid_environment)
49 49 from rhodecode.lib.middleware import csrf
50 50 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
51 51 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
52 52 from rhodecode.lib.middleware.https_fixup import HttpsFixup
53 53 from rhodecode.lib.middleware.vcs import VCSMiddleware
54 54 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
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, full_stack=True, 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 ``full_stack``
94 94 Whether or not this application provides a full WSGI stack (by
95 95 default, meaning it handles its own exceptions and errors).
96 96 Disable full_stack when this application is "managed" by
97 97 another WSGI middleware.
98 98
99 99 ``app_conf``
100 100 The application's local configuration. Normally specified in
101 101 the [app:<name>] section of the Paste ini file (where <name>
102 102 defaults to main).
103 103
104 104 """
105 105 # Apply compatibility patches
106 106 patches.kombu_1_5_1_python_2_7_11()
107 107 patches.inspect_getargspec()
108 108
109 109 # Configure the Pylons environment
110 110 config = load_environment(global_conf, app_conf)
111 111
112 112 # The Pylons WSGI app
113 113 app = PylonsApp(config=config)
114 114 if rhodecode.is_test:
115 115 app = csrf.CSRFDetector(app)
116 116
117 117 expected_origin = config.get('expected_origin')
118 118 if expected_origin:
119 119 # The API can be accessed from other Origins.
120 120 app = csrf.OriginChecker(app, expected_origin,
121 121 skip_urls=[routes.util.url_for('api')])
122 122
123 123
124 124 if asbool(full_stack):
125 125
126 126 # Appenlight monitoring and error handler
127 127 app, appenlight_client = wrap_in_appenlight_if_enabled(app, config)
128 128
129 # we want our low level middleware to get to the request ASAP. We don't
130 # need any pylons stack middleware in them
131 app = VCSMiddleware(app, config, appenlight_client)
132
133 129 # Establish the Registry for this application
134 130 app = RegistryManager(app)
135 131
136 132 app.config = config
137 133
138 134 return app
139 135
140 136
141 137 def make_pyramid_app(global_config, **settings):
142 138 """
143 139 Constructs the WSGI application based on Pyramid and wraps the Pylons based
144 140 application.
145 141
146 142 Specials:
147 143
148 144 * We migrate from Pylons to Pyramid. While doing this, we keep both
149 145 frameworks functional. This involves moving some WSGI middlewares around
150 146 and providing access to some data internals, so that the old code is
151 147 still functional.
152 148
153 149 * The application can also be integrated like a plugin via the call to
154 150 `includeme`. This is accompanied with the other utility functions which
155 151 are called. Changing this should be done with great care to not break
156 152 cases when these fragments are assembled from another place.
157 153
158 154 """
159 155 # The edition string should be available in pylons too, so we add it here
160 156 # before copying the settings.
161 157 settings.setdefault('rhodecode.edition', 'Community Edition')
162 158
163 159 # As long as our Pylons application does expect "unprepared" settings, make
164 160 # sure that we keep an unmodified copy. This avoids unintentional change of
165 161 # behavior in the old application.
166 162 settings_pylons = settings.copy()
167 163
168 164 sanitize_settings_and_apply_defaults(settings)
169 165 config = Configurator(settings=settings)
170 166 add_pylons_compat_data(config.registry, global_config, settings_pylons)
171 167
172 168 load_pyramid_environment(global_config, settings)
173 169
174 170 includeme_first(config)
175 171 includeme(config)
176 172 pyramid_app = config.make_wsgi_app()
177 173 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
178 174 return pyramid_app
179 175
180 176
177 def make_not_found_view(config):
178 """
179 This creates the view shich should be registered as not-found-view to
180 pyramid. Basically it contains of the old pylons app, converted to a view.
181 Additionally it is wrapped by some other middlewares.
182 """
183 settings = config.registry.settings
184
185 # Make pylons app from unprepared settings.
186 pylons_app = make_app(
187 config.registry._pylons_compat_global_config,
188 **config.registry._pylons_compat_settings)
189 config.registry._pylons_compat_config = pylons_app.config
190
191 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find
192 # a view to handle the request. Therefore we wrap it around the pylons app
193 # and it will be added as not found view.
194 if settings['vcs.server.enable']:
195 pylons_app = VCSMiddleware(
196 pylons_app, settings, None, registry=config.registry)
197
198 pylons_app_as_view = wsgiapp(pylons_app)
199
200 # Protect from VCS Server error related pages when server is not available
201 vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true'))
202 if not vcs_server_enabled:
203 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
204
205 def pylons_app_with_error_handler(context, request):
206 """
207 Handle exceptions from rc pylons app:
208
209 - old webob type exceptions get converted to pyramid exceptions
210 - pyramid exceptions are passed to the error handler view
211 """
212 try:
213 response = pylons_app_as_view(context, request)
214 if 400 <= response.status_int <= 599: # webob type error responses
215 return error_handler(
216 webob_to_pyramid_http_response(response), request)
217 except HTTPError as e: # pyramid type exceptions
218 return error_handler(e, request)
219 except Exception:
220 if settings.get('debugtoolbar.enabled', False):
221 raise
222 return error_handler(HTTPInternalServerError(), request)
223 return response
224
225 return pylons_app_with_error_handler
226
227
181 228 def add_pylons_compat_data(registry, global_config, settings):
182 229 """
183 230 Attach data to the registry to support the Pylons integration.
184 231 """
185 232 registry._pylons_compat_global_config = global_config
186 233 registry._pylons_compat_settings = settings
187 234
188 235
189 236 def webob_to_pyramid_http_response(webob_response):
190 237 ResponseClass = httpexceptions.status_map[webob_response.status_int]
191 238 pyramid_response = ResponseClass(webob_response.status)
192 239 pyramid_response.status = webob_response.status
193 240 pyramid_response.headers.update(webob_response.headers)
194 241 if pyramid_response.headers['content-type'] == 'text/html':
195 242 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
196 243 return pyramid_response
197 244
198 245
199 246 def error_handler(exception, request):
200 247 # TODO: dan: replace the old pylons error controller with this
201 248 from rhodecode.model.settings import SettingsModel
202 249 from rhodecode.lib.utils2 import AttributeDict
203 250
204 251 try:
205 252 rc_config = SettingsModel().get_all_settings()
206 253 except Exception:
207 254 log.exception('failed to fetch settings')
208 255 rc_config = {}
209 256
210 257 base_response = HTTPInternalServerError()
211 258 # prefer original exception for the response since it may have headers set
212 259 if isinstance(exception, HTTPError):
213 260 base_response = exception
214 261
215 262 c = AttributeDict()
216 263 c.error_message = base_response.status
217 264 c.error_explanation = base_response.explanation or str(base_response)
218 265 c.visual = AttributeDict()
219 266
220 267 c.visual.rhodecode_support_url = (
221 268 request.registry.settings.get('rhodecode_support_url') or
222 269 request.route_url('rhodecode_support')
223 270 )
224 271 c.redirect_time = 0
225 272 c.rhodecode_name = rc_config.get('rhodecode_title', '')
226 273 if not c.rhodecode_name:
227 274 c.rhodecode_name = 'Rhodecode'
228 275
229 276 response = render_to_response(
230 277 '/errors/error_document.html', {'c': c}, request=request,
231 278 response=base_response)
232 279
233 280 return response
234 281
235 282
236 283 def includeme(config):
237 284 settings = config.registry.settings
238 285
239 286 # plugin information
240 287 config.registry.rhodecode_plugins = OrderedDict()
241 288
242 289 config.add_directive(
243 290 'register_rhodecode_plugin', register_rhodecode_plugin)
244 291
245 292 if asbool(settings.get('appenlight', 'false')):
246 293 config.include('appenlight_client.ext.pyramid_tween')
247 294
248 295 # Includes which are required. The application would fail without them.
249 296 config.include('pyramid_mako')
250 297 config.include('pyramid_beaker')
251 298 config.include('rhodecode.channelstream')
252 299 config.include('rhodecode.admin')
253 300 config.include('rhodecode.authentication')
254 301 config.include('rhodecode.integrations')
255 302 config.include('rhodecode.login')
256 303 config.include('rhodecode.tweens')
257 304 config.include('rhodecode.api')
258 305 config.include('rhodecode.svn_support')
259 306 config.add_route(
260 307 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
261 308
262 309 # Add subscribers.
263 310 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
264 311
265 312 # Set the authorization policy.
266 313 authz_policy = ACLAuthorizationPolicy()
267 314 config.set_authorization_policy(authz_policy)
268 315
269 316 # Set the default renderer for HTML templates to mako.
270 317 config.add_mako_renderer('.html')
271 318
272 319 # include RhodeCode plugins
273 320 includes = aslist(settings.get('rhodecode.includes', []))
274 321 for inc in includes:
275 322 config.include(inc)
276 323
277 pylons_app = make_app(
278 config.registry._pylons_compat_global_config,
279 **config.registry._pylons_compat_settings)
280 config.registry._pylons_compat_config = pylons_app.config
281
282 pylons_app_as_view = wsgiapp(pylons_app)
283
284 # Protect from VCS Server error related pages when server is not available
285 vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true'))
286 if not vcs_server_enabled:
287 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
288
289
290 def pylons_app_with_error_handler(context, request):
291 """
292 Handle exceptions from rc pylons app:
293
294 - old webob type exceptions get converted to pyramid exceptions
295 - pyramid exceptions are passed to the error handler view
296 """
297 try:
298 response = pylons_app_as_view(context, request)
299 if 400 <= response.status_int <= 599: # webob type error responses
300 return error_handler(
301 webob_to_pyramid_http_response(response), request)
302 except HTTPError as e: # pyramid type exceptions
303 return error_handler(e, request)
304 except Exception:
305 if settings.get('debugtoolbar.enabled', False):
306 raise
307 return error_handler(HTTPInternalServerError(), request)
308 return response
309
310 324 # This is the glue which allows us to migrate in chunks. By registering the
311 325 # pylons based application as the "Not Found" view in Pyramid, we will
312 326 # fallback to the old application each time the new one does not yet know
313 327 # how to handle a request.
314 config.add_notfound_view(pylons_app_with_error_handler)
328 config.add_notfound_view(make_not_found_view(config))
315 329
316 330 if not settings.get('debugtoolbar.enabled', False):
317 331 # if no toolbar, then any exception gets caught and rendered
318 332 config.add_view(error_handler, context=Exception)
319 333
320 334 config.add_view(error_handler, context=HTTPError)
321 335
322 336
323 337 def includeme_first(config):
324 338 # redirect automatic browser favicon.ico requests to correct place
325 339 def favicon_redirect(context, request):
326 340 return redirect(
327 341 request.static_path('rhodecode:public/images/favicon.ico'))
328 342
329 343 config.add_view(favicon_redirect, route_name='favicon')
330 344 config.add_route('favicon', '/favicon.ico')
331 345
332 346 config.add_static_view(
333 347 '_static/deform', 'deform:static')
334 348 config.add_static_view(
335 349 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
336 350
337 351 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
338 352 """
339 353 Apply outer WSGI middlewares around the application.
340 354
341 355 Part of this has been moved up from the Pylons layer, so that the
342 356 data is also available if old Pylons code is hit through an already ported
343 357 view.
344 358 """
345 359 settings = config.registry.settings
346 360
347 361 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
348 362 pyramid_app = HttpsFixup(pyramid_app, settings)
349 363
350 364 # Add RoutesMiddleware to support the pylons compatibility tween during
351 365 # migration to pyramid.
352 366 pyramid_app = SkippableRoutesMiddleware(
353 367 pyramid_app, config.registry._pylons_compat_config['routes.map'],
354 368 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
355 369
356 370 if asbool(settings.get('appenlight', 'false')):
357 371 pyramid_app, _ = wrap_in_appenlight_if_enabled(
358 372 pyramid_app, config.registry._pylons_compat_config)
359 373
360 374 if asbool(settings.get('gzip_responses', 'true')):
361 375 pyramid_app = make_gzip_middleware(
362 376 pyramid_app, settings, compress_level=1)
363 377
364 378 return pyramid_app
365 379
366 380
367 381 def sanitize_settings_and_apply_defaults(settings):
368 382 """
369 383 Applies settings defaults and does all type conversion.
370 384
371 385 We would move all settings parsing and preparation into this place, so that
372 386 we have only one place left which deals with this part. The remaining parts
373 387 of the application would start to rely fully on well prepared settings.
374 388
375 389 This piece would later be split up per topic to avoid a big fat monster
376 390 function.
377 391 """
378 392
379 393 # Pyramid's mako renderer has to search in the templates folder so that the
380 394 # old templates still work. Ported and new templates are expected to use
381 395 # real asset specifications for the includes.
382 396 mako_directories = settings.setdefault('mako.directories', [
383 397 # Base templates of the original Pylons application
384 398 'rhodecode:templates',
385 399 ])
386 400 log.debug(
387 401 "Using the following Mako template directories: %s",
388 402 mako_directories)
389 403
390 404 # Default includes, possible to change as a user
391 405 pyramid_includes = settings.setdefault('pyramid.includes', [
392 406 'rhodecode.lib.middleware.request_wrapper',
393 407 ])
394 408 log.debug(
395 409 "Using the following pyramid.includes: %s",
396 410 pyramid_includes)
397 411
398 412 # TODO: johbo: Re-think this, usually the call to config.include
399 413 # should allow to pass in a prefix.
400 414 settings.setdefault('rhodecode.api.url', '/_admin/api')
401 415
402 416 _bool_setting(settings, 'vcs.server.enable', 'true')
403 417 _bool_setting(settings, 'is_test', 'false')
404 418
405 419 return settings
406 420
407 421
408 422 def _bool_setting(settings, name, default):
409 423 settings[name] = asbool(settings.get(name, default))
General Comments 0
You need to be logged in to leave comments. Login now