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