##// END OF EJS Templates
vcs: Skip error handling in case of responses from VCSMiddleware.
Martin Bornhold -
r601:e1a806ed default
parent child Browse files
Show More
@@ -1,454 +1,462 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.subscribers import scan_repositories_if_enabled
55 55
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 # this is used to avoid avoid the route lookup overhead in routesmiddleware
61 61 # for certain routes which won't go to pylons to - eg. static files, debugger
62 62 # it is only needed for the pylons migration and can be removed once complete
63 63 class SkippableRoutesMiddleware(RoutesMiddleware):
64 64 """ Routes middleware that allows you to skip prefixes """
65 65
66 66 def __init__(self, *args, **kw):
67 67 self.skip_prefixes = kw.pop('skip_prefixes', [])
68 68 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
69 69
70 70 def __call__(self, environ, start_response):
71 71 for prefix in self.skip_prefixes:
72 72 if environ['PATH_INFO'].startswith(prefix):
73 73 # added to avoid the case when a missing /_static route falls
74 74 # through to pylons and causes an exception as pylons is
75 75 # expecting wsgiorg.routingargs to be set in the environ
76 76 # by RoutesMiddleware.
77 77 if 'wsgiorg.routing_args' not in environ:
78 78 environ['wsgiorg.routing_args'] = (None, {})
79 79 return self.app(environ, start_response)
80 80
81 81 return super(SkippableRoutesMiddleware, self).__call__(
82 82 environ, start_response)
83 83
84 84
85 85 def make_app(global_conf, static_files=True, **app_conf):
86 86 """Create a Pylons WSGI application and return it
87 87
88 88 ``global_conf``
89 89 The inherited configuration for this application. Normally from
90 90 the [DEFAULT] section of the Paste ini file.
91 91
92 92 ``app_conf``
93 93 The application's local configuration. Normally specified in
94 94 the [app:<name>] section of the Paste ini file (where <name>
95 95 defaults to main).
96 96
97 97 """
98 98 # Apply compatibility patches
99 99 patches.kombu_1_5_1_python_2_7_11()
100 100 patches.inspect_getargspec()
101 101
102 102 # Configure the Pylons environment
103 103 config = load_environment(global_conf, app_conf)
104 104
105 105 # The Pylons WSGI app
106 106 app = PylonsApp(config=config)
107 107 if rhodecode.is_test:
108 108 app = csrf.CSRFDetector(app)
109 109
110 110 expected_origin = config.get('expected_origin')
111 111 if expected_origin:
112 112 # The API can be accessed from other Origins.
113 113 app = csrf.OriginChecker(app, expected_origin,
114 114 skip_urls=[routes.util.url_for('api')])
115 115
116 116 # Establish the Registry for this application
117 117 app = RegistryManager(app)
118 118
119 119 app.config = config
120 120
121 121 return app
122 122
123 123
124 124 def make_pyramid_app(global_config, **settings):
125 125 """
126 126 Constructs the WSGI application based on Pyramid and wraps the Pylons based
127 127 application.
128 128
129 129 Specials:
130 130
131 131 * We migrate from Pylons to Pyramid. While doing this, we keep both
132 132 frameworks functional. This involves moving some WSGI middlewares around
133 133 and providing access to some data internals, so that the old code is
134 134 still functional.
135 135
136 136 * The application can also be integrated like a plugin via the call to
137 137 `includeme`. This is accompanied with the other utility functions which
138 138 are called. Changing this should be done with great care to not break
139 139 cases when these fragments are assembled from another place.
140 140
141 141 """
142 142 # The edition string should be available in pylons too, so we add it here
143 143 # before copying the settings.
144 144 settings.setdefault('rhodecode.edition', 'Community Edition')
145 145
146 146 # As long as our Pylons application does expect "unprepared" settings, make
147 147 # sure that we keep an unmodified copy. This avoids unintentional change of
148 148 # behavior in the old application.
149 149 settings_pylons = settings.copy()
150 150
151 151 sanitize_settings_and_apply_defaults(settings)
152 152 config = Configurator(settings=settings)
153 153 add_pylons_compat_data(config.registry, global_config, settings_pylons)
154 154
155 155 load_pyramid_environment(global_config, settings)
156 156
157 157 includeme_first(config)
158 158 includeme(config)
159 159 pyramid_app = config.make_wsgi_app()
160 160 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
161 161 return pyramid_app
162 162
163 163
164 164 def make_not_found_view(config):
165 165 """
166 166 This creates the view which should be registered as not-found-view to
167 167 pyramid. Basically it contains of the old pylons app, converted to a view.
168 168 Additionally it is wrapped by some other middlewares.
169 169 """
170 170 settings = config.registry.settings
171 171 vcs_server_enabled = settings['vcs.server.enable']
172 172
173 173 # Make pylons app from unprepared settings.
174 174 pylons_app = make_app(
175 175 config.registry._pylons_compat_global_config,
176 176 **config.registry._pylons_compat_settings)
177 177 config.registry._pylons_compat_config = pylons_app.config
178 178
179 179 # Appenlight monitoring.
180 180 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
181 181 pylons_app, settings)
182 182
183 183 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find
184 184 # a view to handle the request. Therefore we wrap it around the pylons app.
185 185 if vcs_server_enabled:
186 186 pylons_app = VCSMiddleware(
187 187 pylons_app, settings, appenlight_client, registry=config.registry)
188 188
189 189 pylons_app_as_view = wsgiapp(pylons_app)
190 190
191 191 # Protect from VCS Server error related pages when server is not available
192 192 if not vcs_server_enabled:
193 193 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
194 194
195 195 def pylons_app_with_error_handler(context, request):
196 196 """
197 197 Handle exceptions from rc pylons app:
198 198
199 199 - old webob type exceptions get converted to pyramid exceptions
200 200 - pyramid exceptions are passed to the error handler view
201 201 """
202 def is_vcs_response(request):
203 return True == request.environ.get(
204 'rhodecode.vcs.skip_error_handling')
205
206 def is_webob_error(response):
207 # webob type error responses
208 return (400 <= response.status_int <= 599)
209
202 210 try:
203 211 response = pylons_app_as_view(context, request)
204 if 400 <= response.status_int <= 599: # webob type error responses
212 if is_webob_error(response) and not is_vcs_response(request):
205 213 return error_handler(
206 214 webob_to_pyramid_http_response(response), request)
207 215 except HTTPError as e: # pyramid type exceptions
208 216 return error_handler(e, request)
209 217 except Exception:
210 218 if settings.get('debugtoolbar.enabled', False):
211 219 raise
212 220 return error_handler(HTTPInternalServerError(), request)
213 221 return response
214 222
215 223 return pylons_app_with_error_handler
216 224
217 225
218 226 def add_pylons_compat_data(registry, global_config, settings):
219 227 """
220 228 Attach data to the registry to support the Pylons integration.
221 229 """
222 230 registry._pylons_compat_global_config = global_config
223 231 registry._pylons_compat_settings = settings
224 232
225 233
226 234 def webob_to_pyramid_http_response(webob_response):
227 235 ResponseClass = httpexceptions.status_map[webob_response.status_int]
228 236 pyramid_response = ResponseClass(webob_response.status)
229 237 pyramid_response.status = webob_response.status
230 238 pyramid_response.headers.update(webob_response.headers)
231 239 if pyramid_response.headers['content-type'] == 'text/html':
232 240 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
233 241 return pyramid_response
234 242
235 243
236 244 def error_handler(exception, request):
237 245 # TODO: dan: replace the old pylons error controller with this
238 246 from rhodecode.model.settings import SettingsModel
239 247 from rhodecode.lib.utils2 import AttributeDict
240 248
241 249 try:
242 250 rc_config = SettingsModel().get_all_settings()
243 251 except Exception:
244 252 log.exception('failed to fetch settings')
245 253 rc_config = {}
246 254
247 255 base_response = HTTPInternalServerError()
248 256 # prefer original exception for the response since it may have headers set
249 257 if isinstance(exception, HTTPError):
250 258 base_response = exception
251 259
252 260 c = AttributeDict()
253 261 c.error_message = base_response.status
254 262 c.error_explanation = base_response.explanation or str(base_response)
255 263 c.visual = AttributeDict()
256 264
257 265 c.visual.rhodecode_support_url = (
258 266 request.registry.settings.get('rhodecode_support_url') or
259 267 request.route_url('rhodecode_support')
260 268 )
261 269 c.redirect_time = 0
262 270 c.rhodecode_name = rc_config.get('rhodecode_title', '')
263 271 if not c.rhodecode_name:
264 272 c.rhodecode_name = 'Rhodecode'
265 273
266 274 response = render_to_response(
267 275 '/errors/error_document.html', {'c': c}, request=request,
268 276 response=base_response)
269 277
270 278 return response
271 279
272 280
273 281 def includeme(config):
274 282 settings = config.registry.settings
275 283
276 284 # plugin information
277 285 config.registry.rhodecode_plugins = OrderedDict()
278 286
279 287 config.add_directive(
280 288 'register_rhodecode_plugin', register_rhodecode_plugin)
281 289
282 290 if asbool(settings.get('appenlight', 'false')):
283 291 config.include('appenlight_client.ext.pyramid_tween')
284 292
285 293 # Includes which are required. The application would fail without them.
286 294 config.include('pyramid_mako')
287 295 config.include('pyramid_beaker')
288 296 config.include('rhodecode.channelstream')
289 297 config.include('rhodecode.admin')
290 298 config.include('rhodecode.authentication')
291 299 config.include('rhodecode.integrations')
292 300 config.include('rhodecode.login')
293 301 config.include('rhodecode.tweens')
294 302 config.include('rhodecode.api')
295 303 config.include('rhodecode.svn_support')
296 304 config.add_route(
297 305 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
298 306
299 307 # Add subscribers.
300 308 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
301 309
302 310 # Set the authorization policy.
303 311 authz_policy = ACLAuthorizationPolicy()
304 312 config.set_authorization_policy(authz_policy)
305 313
306 314 # Set the default renderer for HTML templates to mako.
307 315 config.add_mako_renderer('.html')
308 316
309 317 # include RhodeCode plugins
310 318 includes = aslist(settings.get('rhodecode.includes', []))
311 319 for inc in includes:
312 320 config.include(inc)
313 321
314 322 # This is the glue which allows us to migrate in chunks. By registering the
315 323 # pylons based application as the "Not Found" view in Pyramid, we will
316 324 # fallback to the old application each time the new one does not yet know
317 325 # how to handle a request.
318 326 config.add_notfound_view(make_not_found_view(config))
319 327
320 328 if not settings.get('debugtoolbar.enabled', False):
321 329 # if no toolbar, then any exception gets caught and rendered
322 330 config.add_view(error_handler, context=Exception)
323 331
324 332 config.add_view(error_handler, context=HTTPError)
325 333
326 334
327 335 def includeme_first(config):
328 336 # redirect automatic browser favicon.ico requests to correct place
329 337 def favicon_redirect(context, request):
330 338 return redirect(
331 339 request.static_path('rhodecode:public/images/favicon.ico'))
332 340
333 341 config.add_view(favicon_redirect, route_name='favicon')
334 342 config.add_route('favicon', '/favicon.ico')
335 343
336 344 config.add_static_view(
337 345 '_static/deform', 'deform:static')
338 346 config.add_static_view(
339 347 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
340 348
341 349
342 350 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
343 351 """
344 352 Apply outer WSGI middlewares around the application.
345 353
346 354 Part of this has been moved up from the Pylons layer, so that the
347 355 data is also available if old Pylons code is hit through an already ported
348 356 view.
349 357 """
350 358 settings = config.registry.settings
351 359
352 360 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
353 361 pyramid_app = HttpsFixup(pyramid_app, settings)
354 362
355 363 # Add RoutesMiddleware to support the pylons compatibility tween during
356 364 # migration to pyramid.
357 365 pyramid_app = SkippableRoutesMiddleware(
358 366 pyramid_app, config.registry._pylons_compat_config['routes.map'],
359 367 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
360 368
361 369 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
362 370
363 371 if settings['gzip_responses']:
364 372 pyramid_app = make_gzip_middleware(
365 373 pyramid_app, settings, compress_level=1)
366 374
367 375 return pyramid_app
368 376
369 377
370 378 def sanitize_settings_and_apply_defaults(settings):
371 379 """
372 380 Applies settings defaults and does all type conversion.
373 381
374 382 We would move all settings parsing and preparation into this place, so that
375 383 we have only one place left which deals with this part. The remaining parts
376 384 of the application would start to rely fully on well prepared settings.
377 385
378 386 This piece would later be split up per topic to avoid a big fat monster
379 387 function.
380 388 """
381 389
382 390 # Pyramid's mako renderer has to search in the templates folder so that the
383 391 # old templates still work. Ported and new templates are expected to use
384 392 # real asset specifications for the includes.
385 393 mako_directories = settings.setdefault('mako.directories', [
386 394 # Base templates of the original Pylons application
387 395 'rhodecode:templates',
388 396 ])
389 397 log.debug(
390 398 "Using the following Mako template directories: %s",
391 399 mako_directories)
392 400
393 401 # Default includes, possible to change as a user
394 402 pyramid_includes = settings.setdefault('pyramid.includes', [
395 403 'rhodecode.lib.middleware.request_wrapper',
396 404 ])
397 405 log.debug(
398 406 "Using the following pyramid.includes: %s",
399 407 pyramid_includes)
400 408
401 409 # TODO: johbo: Re-think this, usually the call to config.include
402 410 # should allow to pass in a prefix.
403 411 settings.setdefault('rhodecode.api.url', '/_admin/api')
404 412
405 413 # Sanitize generic settings.
406 414 _list_setting(settings, 'default_encoding', 'UTF-8')
407 415 _bool_setting(settings, 'is_test', 'false')
408 416 _bool_setting(settings, 'gzip_responses', 'false')
409 417
410 418 # Call split out functions that sanitize settings for each topic.
411 419 _sanitize_appenlight_settings(settings)
412 420 _sanitize_vcs_settings(settings)
413 421
414 422 return settings
415 423
416 424
417 425 def _sanitize_appenlight_settings(settings):
418 426 _bool_setting(settings, 'appenlight', 'false')
419 427
420 428
421 429 def _sanitize_vcs_settings(settings):
422 430 """
423 431 Applies settings defaults and does type conversion for all VCS related
424 432 settings.
425 433 """
426 434 _string_setting(settings, 'vcs.svn.compatible_version', '')
427 435 _string_setting(settings, 'git_rev_filter', '--all')
428 436 _string_setting(settings, 'vcs.hooks.protocol', 'pyro4')
429 437 _string_setting(settings, 'vcs.server', '')
430 438 _string_setting(settings, 'vcs.server.log_level', 'debug')
431 439 _string_setting(settings, 'vcs.server.protocol', 'pyro4')
432 440 _bool_setting(settings, 'startup.import_repos', 'false')
433 441 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
434 442 _bool_setting(settings, 'vcs.server.enable', 'true')
435 443 _bool_setting(settings, 'vcs.start_server', 'false')
436 444 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
437 445
438 446
439 447 def _bool_setting(settings, name, default):
440 448 settings[name] = asbool(settings.get(name, default))
441 449
442 450
443 451 def _list_setting(settings, name, default):
444 452 raw_value = settings.get(name, default)
445 453
446 454 # Check if we get a setting with the old syntax (comma separated).
447 455 if ',' in raw_value:
448 456 raw_value = raw_value.replace(',', ' ')
449 457
450 458 settings[name] = aslist(raw_value)
451 459
452 460
453 461 def _string_setting(settings, name, default):
454 462 settings[name] = settings.get(name, default).lower()
General Comments 0
You need to be logged in to leave comments. Login now