##// END OF EJS Templates
vcs: Use response header to decide if error handling is needed.
Martin Bornhold -
r609:24f4767a default
parent child Browse files
Show More
@@ -1,468 +1,470 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 HTTPError, HTTPInternalServerError
35 35 from pylons.controllers.util import redirect
36 36 from pyramid.events import ApplicationCreated
37 37 import pyramid.httpexceptions as httpexceptions
38 38 from pyramid.renderers import render_to_response
39 39 from routes.middleware import RoutesMiddleware
40 40 import routes.util
41 41
42 42 import rhodecode
43 43 import rhodecode.integrations # do not remove this as it registers celery tasks
44 44 from rhodecode.config import patches
45 45 from rhodecode.config.routing import STATIC_FILE_PREFIX
46 46 from rhodecode.config.environment import (
47 47 load_environment, load_pyramid_environment)
48 48 from rhodecode.lib.middleware import csrf
49 49 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
50 50 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
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 return pyramid_app
163 163
164 164
165 165 def make_not_found_view(config):
166 166 """
167 167 This creates the view which should be registered as not-found-view to
168 168 pyramid. Basically it contains of the old pylons app, converted to a view.
169 169 Additionally it is wrapped by some other middlewares.
170 170 """
171 171 settings = config.registry.settings
172 172 vcs_server_enabled = settings['vcs.server.enable']
173 173
174 174 # Make pylons app from unprepared settings.
175 175 pylons_app = make_app(
176 176 config.registry._pylons_compat_global_config,
177 177 **config.registry._pylons_compat_settings)
178 178 config.registry._pylons_compat_config = pylons_app.config
179 179
180 180 # Appenlight monitoring.
181 181 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
182 182 pylons_app, settings)
183 183
184 184 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find
185 185 # a view to handle the request. Therefore we wrap it around the pylons app.
186 186 if vcs_server_enabled:
187 187 pylons_app = VCSMiddleware(
188 188 pylons_app, settings, appenlight_client, registry=config.registry)
189 189
190 190 pylons_app_as_view = wsgiapp(pylons_app)
191 191
192 192 # Protect from VCS Server error related pages when server is not available
193 193 if not vcs_server_enabled:
194 194 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
195 195
196 196 def pylons_app_with_error_handler(context, request):
197 197 """
198 198 Handle exceptions from rc pylons app:
199 199
200 200 - old webob type exceptions get converted to pyramid exceptions
201 201 - pyramid exceptions are passed to the error handler view
202 202 """
203 def is_vcs_request(request):
204 return True == request.environ.get(
205 'rhodecode.vcs.skip_error_handling')
203 def is_vcs_response(response):
204 return 'X-RhodeCode-Backend' in response.headers
206 205
207 def is_webob_error(response):
206 def is_http_error(response):
208 207 # webob type error responses
209 208 return (400 <= response.status_int <= 599)
210 209
210 def is_error_handling_needed(response):
211 return is_http_error(response) and not is_vcs_response(response)
212
211 213 try:
212 214 response = pylons_app_as_view(context, request)
213 if is_webob_error(response) and not is_vcs_request(request):
214 return error_handler(
215 webob_to_pyramid_http_response(response), request)
215 if is_error_handling_needed(response):
216 response = webob_to_pyramid_http_response(response)
217 return error_handler(response, request)
216 218 except HTTPError as e: # pyramid type exceptions
217 219 return error_handler(e, request)
218 220 except Exception:
219 221 if settings.get('debugtoolbar.enabled', False):
220 222 raise
221 223 return error_handler(HTTPInternalServerError(), request)
222 224 return response
223 225
224 226 return pylons_app_with_error_handler
225 227
226 228
227 229 def add_pylons_compat_data(registry, global_config, settings):
228 230 """
229 231 Attach data to the registry to support the Pylons integration.
230 232 """
231 233 registry._pylons_compat_global_config = global_config
232 234 registry._pylons_compat_settings = settings
233 235
234 236
235 237 def webob_to_pyramid_http_response(webob_response):
236 238 ResponseClass = httpexceptions.status_map[webob_response.status_int]
237 239 pyramid_response = ResponseClass(webob_response.status)
238 240 pyramid_response.status = webob_response.status
239 241 pyramid_response.headers.update(webob_response.headers)
240 242 if pyramid_response.headers['content-type'] == 'text/html':
241 243 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
242 244 return pyramid_response
243 245
244 246
245 247 def error_handler(exception, request):
246 248 # TODO: dan: replace the old pylons error controller with this
247 249 from rhodecode.model.settings import SettingsModel
248 250 from rhodecode.lib.utils2 import AttributeDict
249 251
250 252 try:
251 253 rc_config = SettingsModel().get_all_settings()
252 254 except Exception:
253 255 log.exception('failed to fetch settings')
254 256 rc_config = {}
255 257
256 258 base_response = HTTPInternalServerError()
257 259 # prefer original exception for the response since it may have headers set
258 260 if isinstance(exception, HTTPError):
259 261 base_response = exception
260 262
261 263 c = AttributeDict()
262 264 c.error_message = base_response.status
263 265 c.error_explanation = base_response.explanation or str(base_response)
264 266 c.visual = AttributeDict()
265 267
266 268 c.visual.rhodecode_support_url = (
267 269 request.registry.settings.get('rhodecode_support_url') or
268 270 request.route_url('rhodecode_support')
269 271 )
270 272 c.redirect_time = 0
271 273 c.rhodecode_name = rc_config.get('rhodecode_title', '')
272 274 if not c.rhodecode_name:
273 275 c.rhodecode_name = 'Rhodecode'
274 276
275 277 response = render_to_response(
276 278 '/errors/error_document.html', {'c': c}, request=request,
277 279 response=base_response)
278 280
279 281 return response
280 282
281 283
282 284 def includeme(config):
283 285 settings = config.registry.settings
284 286
285 287 # plugin information
286 288 config.registry.rhodecode_plugins = OrderedDict()
287 289
288 290 config.add_directive(
289 291 'register_rhodecode_plugin', register_rhodecode_plugin)
290 292
291 293 if asbool(settings.get('appenlight', 'false')):
292 294 config.include('appenlight_client.ext.pyramid_tween')
293 295
294 296 # Includes which are required. The application would fail without them.
295 297 config.include('pyramid_mako')
296 298 config.include('pyramid_beaker')
297 299 config.include('rhodecode.channelstream')
298 300 config.include('rhodecode.admin')
299 301 config.include('rhodecode.authentication')
300 302 config.include('rhodecode.integrations')
301 303 config.include('rhodecode.login')
302 304 config.include('rhodecode.tweens')
303 305 config.include('rhodecode.api')
304 306 config.include('rhodecode.svn_support')
305 307 config.add_route(
306 308 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
307 309
308 310 # Add subscribers.
309 311 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
310 312
311 313 # Set the authorization policy.
312 314 authz_policy = ACLAuthorizationPolicy()
313 315 config.set_authorization_policy(authz_policy)
314 316
315 317 # Set the default renderer for HTML templates to mako.
316 318 config.add_mako_renderer('.html')
317 319
318 320 # include RhodeCode plugins
319 321 includes = aslist(settings.get('rhodecode.includes', []))
320 322 for inc in includes:
321 323 config.include(inc)
322 324
323 325 # This is the glue which allows us to migrate in chunks. By registering the
324 326 # pylons based application as the "Not Found" view in Pyramid, we will
325 327 # fallback to the old application each time the new one does not yet know
326 328 # how to handle a request.
327 329 config.add_notfound_view(make_not_found_view(config))
328 330
329 331 if not settings.get('debugtoolbar.enabled', False):
330 332 # if no toolbar, then any exception gets caught and rendered
331 333 config.add_view(error_handler, context=Exception)
332 334
333 335 config.add_view(error_handler, context=HTTPError)
334 336
335 337
336 338 def includeme_first(config):
337 339 # redirect automatic browser favicon.ico requests to correct place
338 340 def favicon_redirect(context, request):
339 341 return redirect(
340 342 request.static_path('rhodecode:public/images/favicon.ico'))
341 343
342 344 config.add_view(favicon_redirect, route_name='favicon')
343 345 config.add_route('favicon', '/favicon.ico')
344 346
345 347 config.add_static_view(
346 348 '_static/deform', 'deform:static')
347 349 config.add_static_view(
348 350 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
349 351
350 352
351 353 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
352 354 """
353 355 Apply outer WSGI middlewares around the application.
354 356
355 357 Part of this has been moved up from the Pylons layer, so that the
356 358 data is also available if old Pylons code is hit through an already ported
357 359 view.
358 360 """
359 361 settings = config.registry.settings
360 362
361 363 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
362 364 pyramid_app = HttpsFixup(pyramid_app, settings)
363 365
364 366 # Add RoutesMiddleware to support the pylons compatibility tween during
365 367 # migration to pyramid.
366 368 pyramid_app = SkippableRoutesMiddleware(
367 369 pyramid_app, config.registry._pylons_compat_config['routes.map'],
368 370 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
369 371
370 372 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
371 373
372 374 if settings['gzip_responses']:
373 375 pyramid_app = make_gzip_middleware(
374 376 pyramid_app, settings, compress_level=1)
375 377
376 378 return pyramid_app
377 379
378 380
379 381 def sanitize_settings_and_apply_defaults(settings):
380 382 """
381 383 Applies settings defaults and does all type conversion.
382 384
383 385 We would move all settings parsing and preparation into this place, so that
384 386 we have only one place left which deals with this part. The remaining parts
385 387 of the application would start to rely fully on well prepared settings.
386 388
387 389 This piece would later be split up per topic to avoid a big fat monster
388 390 function.
389 391 """
390 392
391 393 # Pyramid's mako renderer has to search in the templates folder so that the
392 394 # old templates still work. Ported and new templates are expected to use
393 395 # real asset specifications for the includes.
394 396 mako_directories = settings.setdefault('mako.directories', [
395 397 # Base templates of the original Pylons application
396 398 'rhodecode:templates',
397 399 ])
398 400 log.debug(
399 401 "Using the following Mako template directories: %s",
400 402 mako_directories)
401 403
402 404 # Default includes, possible to change as a user
403 405 pyramid_includes = settings.setdefault('pyramid.includes', [
404 406 'rhodecode.lib.middleware.request_wrapper',
405 407 ])
406 408 log.debug(
407 409 "Using the following pyramid.includes: %s",
408 410 pyramid_includes)
409 411
410 412 # TODO: johbo: Re-think this, usually the call to config.include
411 413 # should allow to pass in a prefix.
412 414 settings.setdefault('rhodecode.api.url', '/_admin/api')
413 415
414 416 # Sanitize generic settings.
415 417 _list_setting(settings, 'default_encoding', 'UTF-8')
416 418 _bool_setting(settings, 'is_test', 'false')
417 419 _bool_setting(settings, 'gzip_responses', 'false')
418 420
419 421 # Call split out functions that sanitize settings for each topic.
420 422 _sanitize_appenlight_settings(settings)
421 423 _sanitize_vcs_settings(settings)
422 424
423 425 return settings
424 426
425 427
426 428 def _sanitize_appenlight_settings(settings):
427 429 _bool_setting(settings, 'appenlight', 'false')
428 430
429 431
430 432 def _sanitize_vcs_settings(settings):
431 433 """
432 434 Applies settings defaults and does type conversion for all VCS related
433 435 settings.
434 436 """
435 437 _string_setting(settings, 'vcs.svn.compatible_version', '')
436 438 _string_setting(settings, 'git_rev_filter', '--all')
437 439 _string_setting(settings, 'vcs.hooks.protocol', 'pyro4')
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', 'pyro4')
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
447 449
448 450 def _bool_setting(settings, name, default):
449 451 input = settings.get(name, default)
450 452 if isinstance(input, unicode):
451 453 input = input.encode('utf8')
452 454 settings[name] = asbool(input)
453 455
454 456
455 457 def _list_setting(settings, name, default):
456 458 raw_value = settings.get(name, default)
457 459
458 460 old_separator = ','
459 461 if old_separator in raw_value:
460 462 # If we get a comma separated list, pass it to our own function.
461 463 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
462 464 else:
463 465 # Otherwise we assume it uses pyramids space/newline separation.
464 466 settings[name] = aslist(raw_value)
465 467
466 468
467 469 def _string_setting(settings, name, default):
468 470 settings[name] = settings.get(name, default).lower()
General Comments 0
You need to be logged in to leave comments. Login now