##// END OF EJS Templates
errorpages: convert webob responses better, include status text + headers
dan -
r191:de2c66e3 default
parent child Browse files
Show More
@@ -1,358 +1,367 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 def webob_to_pyramid_http_response(webob_response):
162 ResponseClass = httpexceptions.status_map[webob_response.status_int]
163 pyramid_response = ResponseClass(webob_response.status)
164 pyramid_response.status = webob_response.status
165 pyramid_response.headers.update(webob_response.headers)
166 if pyramid_response.headers['content-type'] == 'text/html':
167 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
168 return pyramid_response
169
170
161 171 def error_handler(exc, request):
162 172 # TODO: dan: replace the old pylons error controller with this
163 173 from rhodecode.model.settings import SettingsModel
164 174 from rhodecode.lib.utils2 import AttributeDict
165 175
166 176 try:
167 177 rc_config = SettingsModel().get_all_settings()
168 178 except Exception:
169 179 log.exception('failed to fetch settings')
170 180 rc_config = {}
171 181
172 182 c = AttributeDict()
173 183 c.error_message = exc.status
174 184 c.error_explanation = exc.explanation or str(exc)
175 185 c.visual = AttributeDict()
176 186
177 187 c.visual.rhodecode_support_url = (
178 188 request.registry.settings.get('rhodecode_support_url') or
179 189 request.route_url('rhodecode_support')
180 190 )
181 191 c.redirect_time = 0
182 192 c.rhodecode_name = rc_config.get('rhodecode_title')
183 193 if not c.rhodecode_name:
184 194 c.rhodecode_name = 'Rhodecode'
185 195
186 196 base_response = HTTPInternalServerError()
187 197 # prefer original exception for the response since it may have headers set
188 198 if isinstance(exc, HTTPError):
189 199 base_response = exc
190 200
191 201 response = render_to_response(
192 202 '/errors/error_document.html', {'c': c}, request=request,
193 203 response=base_response)
194 204
195 205 return response
196 206
197 207
198 208 def includeme(config):
199 209 settings = config.registry.settings
200 210
201 211 # Includes which are required. The application would fail without them.
202 212 config.include('pyramid_mako')
203 213 config.include('pyramid_beaker')
204 214 config.include('rhodecode.authentication')
205 215 config.include('rhodecode.login')
206 216 config.include('rhodecode.tweens')
207 217 config.include('rhodecode.api')
208 218 config.add_route(
209 219 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
210 220
211 221 # Set the authorization policy.
212 222 authz_policy = ACLAuthorizationPolicy()
213 223 config.set_authorization_policy(authz_policy)
214 224
215 225 # Set the default renderer for HTML templates to mako.
216 226 config.add_mako_renderer('.html')
217 227
218 228 # plugin information
219 229 config.registry.rhodecode_plugins = {}
220 230
221 231 config.add_directive(
222 232 'register_rhodecode_plugin', register_rhodecode_plugin)
223 233 # include RhodeCode plugins
224 234 includes = aslist(settings.get('rhodecode.includes', []))
225 235 for inc in includes:
226 236 config.include(inc)
227 237
228 238 pylons_app = make_app(
229 239 config.registry._pylons_compat_global_config,
230 240 **config.registry._pylons_compat_settings)
231 241 config.registry._pylons_compat_config = pylons_app.config
232 242
233 243 pylons_app_as_view = wsgiapp(pylons_app)
234 244
235 245 # Protect from VCS Server error related pages when server is not available
236 246 vcs_server_enabled = asbool(settings.get('vcs.server.enable', 'true'))
237 247 if not vcs_server_enabled:
238 248 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
239 249
240
241 250 def pylons_app_with_error_handler(context, request):
242 251 """
243 252 Handle exceptions from rc pylons app:
244 253
245 254 - old webob type exceptions get converted to pyramid exceptions
246 255 - pyramid exceptions are passed to the error handler view
247 256 """
248 257 try:
249 258 response = pylons_app_as_view(context, request)
250 259 if 400 <= response.status_int <= 599: # webob type error responses
251 ExcClass = httpexceptions.status_map[response.status_int]
252 return error_handler(ExcClass(response.status), request)
260 return error_handler(
261 webob_to_pyramid_http_response(response), request)
253 262 except HTTPError as e: # pyramid type exceptions
254 263 return error_handler(e, request)
255 264
256 265 return response
257 266
258 267 # This is the glue which allows us to migrate in chunks. By registering the
259 268 # pylons based application as the "Not Found" view in Pyramid, we will
260 269 # fallback to the old application each time the new one does not yet know
261 270 # how to handle a request.
262 271 config.add_notfound_view(pylons_app_with_error_handler)
263 272
264 273 config.add_view(error_handler, context=HTTPError) # exceptions in rc pyramid
265 274
266 275 def includeme_last(config):
267 276 """
268 277 The static file catchall needs to be last in the view configuration.
269 278 """
270 279 settings = config.registry.settings
271 280
272 281 # Note: johbo: I would prefer to register a prefix for static files at some
273 282 # point, e.g. move them under '_static/'. This would fully avoid that we
274 283 # can have name clashes with a repository name. Imaging someone calling his
275 284 # repo "css" ;-) Also having an external web server to serve out the static
276 285 # files seems to be easier to set up if they have a common prefix.
277 286 #
278 287 # Example: config.add_static_view('_static', path='rhodecode:public')
279 288 #
280 289 # It might be an option to register both paths for a while and then migrate
281 290 # over to the new location.
282 291
283 292 # Serving static files with a catchall.
284 293 if settings['static_files']:
285 294 config.add_route('catchall_static', '/*subpath')
286 295 config.add_view(
287 296 static_view('rhodecode:public'), route_name='catchall_static')
288 297
289 298
290 299 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
291 300 """
292 301 Apply outer WSGI middlewares around the application.
293 302
294 303 Part of this has been moved up from the Pylons layer, so that the
295 304 data is also available if old Pylons code is hit through an already ported
296 305 view.
297 306 """
298 307 settings = config.registry.settings
299 308
300 309 # Add RoutesMiddleware to support the pylons compatibility tween during
301 310 # migration to pyramid.
302 311 pyramid_app = RoutesMiddleware(
303 312 pyramid_app, config.registry._pylons_compat_config['routes.map'])
304 313
305 314 # TODO: johbo: Don't really see why we enable the gzip middleware when
306 315 # serving static files, might be something that should have its own setting
307 316 # as well?
308 317 if settings['static_files']:
309 318 pyramid_app = make_gzip_middleware(
310 319 pyramid_app, settings, compress_level=1)
311 320
312 321 return pyramid_app
313 322
314 323
315 324 def sanitize_settings_and_apply_defaults(settings):
316 325 """
317 326 Applies settings defaults and does all type conversion.
318 327
319 328 We would move all settings parsing and preparation into this place, so that
320 329 we have only one place left which deals with this part. The remaining parts
321 330 of the application would start to rely fully on well prepared settings.
322 331
323 332 This piece would later be split up per topic to avoid a big fat monster
324 333 function.
325 334 """
326 335
327 336 # Pyramid's mako renderer has to search in the templates folder so that the
328 337 # old templates still work. Ported and new templates are expected to use
329 338 # real asset specifications for the includes.
330 339 mako_directories = settings.setdefault('mako.directories', [
331 340 # Base templates of the original Pylons application
332 341 'rhodecode:templates',
333 342 ])
334 343 log.debug(
335 344 "Using the following Mako template directories: %s",
336 345 mako_directories)
337 346
338 347 # Default includes, possible to change as a user
339 348 pyramid_includes = settings.setdefault('pyramid.includes', [
340 349 'rhodecode.lib.middleware.request_wrapper',
341 350 ])
342 351 log.debug(
343 352 "Using the following pyramid.includes: %s",
344 353 pyramid_includes)
345 354
346 355 # TODO: johbo: Re-think this, usually the call to config.include
347 356 # should allow to pass in a prefix.
348 357 settings.setdefault('rhodecode.api.url', '/_admin/api')
349 358
350 359 _bool_setting(settings, 'vcs.server.enable', 'true')
351 360 _bool_setting(settings, 'static_files', 'true')
352 361 _bool_setting(settings, 'is_test', 'false')
353 362
354 363 return settings
355 364
356 365
357 366 def _bool_setting(settings, name, default):
358 367 settings[name] = asbool(settings.get(name, default))
General Comments 0
You need to be logged in to leave comments. Login now