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