##// END OF EJS Templates
db: move Session.remove to outer wsgi layer and also add it...
dan -
r669:d3c76065 stable
parent child Browse files
Show More
@@ -1,474 +1,497 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 Pylons middleware initialization
22 Pylons middleware initialization
23 """
23 """
24 import logging
24 import logging
25 from collections import OrderedDict
25 from collections import OrderedDict
26
26
27 from paste.registry import RegistryManager
27 from paste.registry import RegistryManager
28 from paste.gzipper import make_gzip_middleware
28 from paste.gzipper import make_gzip_middleware
29 from pylons.wsgiapp import PylonsApp
29 from pylons.wsgiapp import PylonsApp
30 from pyramid.authorization import ACLAuthorizationPolicy
30 from pyramid.authorization import ACLAuthorizationPolicy
31 from pyramid.config import Configurator
31 from pyramid.config import Configurator
32 from pyramid.settings import asbool, aslist
32 from pyramid.settings import asbool, aslist
33 from pyramid.wsgi import wsgiapp
33 from pyramid.wsgi import wsgiapp
34 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError, HTTPFound
34 from pyramid.httpexceptions import HTTPError, HTTPInternalServerError, HTTPFound
35 from pyramid.events import ApplicationCreated
35 from pyramid.events import ApplicationCreated
36 import pyramid.httpexceptions as httpexceptions
36 import pyramid.httpexceptions as httpexceptions
37 from pyramid.renderers import render_to_response
37 from pyramid.renderers import render_to_response
38 from routes.middleware import RoutesMiddleware
38 from routes.middleware import RoutesMiddleware
39 import routes.util
39 import routes.util
40
40
41 import rhodecode
41 import rhodecode
42 from rhodecode.model import meta
42 from rhodecode.config import patches
43 from rhodecode.config import patches
43 from rhodecode.config.routing import STATIC_FILE_PREFIX
44 from rhodecode.config.routing import STATIC_FILE_PREFIX
44 from rhodecode.config.environment import (
45 from rhodecode.config.environment import (
45 load_environment, load_pyramid_environment)
46 load_environment, load_pyramid_environment)
46 from rhodecode.lib.middleware import csrf
47 from rhodecode.lib.middleware import csrf
47 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
48 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
49 from rhodecode.lib.middleware.disable_vcs import DisableVCSPagesWrapper
49 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 from rhodecode.lib.middleware.https_fixup import HttpsFixup
50 from rhodecode.lib.middleware.vcs import VCSMiddleware
51 from rhodecode.lib.middleware.vcs import VCSMiddleware
51 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
52 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
53 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
53 from rhodecode.subscribers import scan_repositories_if_enabled
54 from rhodecode.subscribers import scan_repositories_if_enabled
54
55
55
56
56 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
57
58
58
59
59 # this is used to avoid avoid the route lookup overhead in routesmiddleware
60 # this is used to avoid avoid the route lookup overhead in routesmiddleware
60 # for certain routes which won't go to pylons to - eg. static files, debugger
61 # for certain routes which won't go to pylons to - eg. static files, debugger
61 # it is only needed for the pylons migration and can be removed once complete
62 # it is only needed for the pylons migration and can be removed once complete
62 class SkippableRoutesMiddleware(RoutesMiddleware):
63 class SkippableRoutesMiddleware(RoutesMiddleware):
63 """ Routes middleware that allows you to skip prefixes """
64 """ Routes middleware that allows you to skip prefixes """
64
65
65 def __init__(self, *args, **kw):
66 def __init__(self, *args, **kw):
66 self.skip_prefixes = kw.pop('skip_prefixes', [])
67 self.skip_prefixes = kw.pop('skip_prefixes', [])
67 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
68 super(SkippableRoutesMiddleware, self).__init__(*args, **kw)
68
69
69 def __call__(self, environ, start_response):
70 def __call__(self, environ, start_response):
70 for prefix in self.skip_prefixes:
71 for prefix in self.skip_prefixes:
71 if environ['PATH_INFO'].startswith(prefix):
72 if environ['PATH_INFO'].startswith(prefix):
72 # added to avoid the case when a missing /_static route falls
73 # added to avoid the case when a missing /_static route falls
73 # through to pylons and causes an exception as pylons is
74 # through to pylons and causes an exception as pylons is
74 # expecting wsgiorg.routingargs to be set in the environ
75 # expecting wsgiorg.routingargs to be set in the environ
75 # by RoutesMiddleware.
76 # by RoutesMiddleware.
76 if 'wsgiorg.routing_args' not in environ:
77 if 'wsgiorg.routing_args' not in environ:
77 environ['wsgiorg.routing_args'] = (None, {})
78 environ['wsgiorg.routing_args'] = (None, {})
78 return self.app(environ, start_response)
79 return self.app(environ, start_response)
79
80
80 return super(SkippableRoutesMiddleware, self).__call__(
81 return super(SkippableRoutesMiddleware, self).__call__(
81 environ, start_response)
82 environ, start_response)
82
83
83
84
84 def make_app(global_conf, static_files=True, **app_conf):
85 def make_app(global_conf, static_files=True, **app_conf):
85 """Create a Pylons WSGI application and return it
86 """Create a Pylons WSGI application and return it
86
87
87 ``global_conf``
88 ``global_conf``
88 The inherited configuration for this application. Normally from
89 The inherited configuration for this application. Normally from
89 the [DEFAULT] section of the Paste ini file.
90 the [DEFAULT] section of the Paste ini file.
90
91
91 ``app_conf``
92 ``app_conf``
92 The application's local configuration. Normally specified in
93 The application's local configuration. Normally specified in
93 the [app:<name>] section of the Paste ini file (where <name>
94 the [app:<name>] section of the Paste ini file (where <name>
94 defaults to main).
95 defaults to main).
95
96
96 """
97 """
97 # Apply compatibility patches
98 # Apply compatibility patches
98 patches.kombu_1_5_1_python_2_7_11()
99 patches.kombu_1_5_1_python_2_7_11()
99 patches.inspect_getargspec()
100 patches.inspect_getargspec()
100
101
101 # Configure the Pylons environment
102 # Configure the Pylons environment
102 config = load_environment(global_conf, app_conf)
103 config = load_environment(global_conf, app_conf)
103
104
104 # The Pylons WSGI app
105 # The Pylons WSGI app
105 app = PylonsApp(config=config)
106 app = PylonsApp(config=config)
106 if rhodecode.is_test:
107 if rhodecode.is_test:
107 app = csrf.CSRFDetector(app)
108 app = csrf.CSRFDetector(app)
108
109
109 expected_origin = config.get('expected_origin')
110 expected_origin = config.get('expected_origin')
110 if expected_origin:
111 if expected_origin:
111 # The API can be accessed from other Origins.
112 # The API can be accessed from other Origins.
112 app = csrf.OriginChecker(app, expected_origin,
113 app = csrf.OriginChecker(app, expected_origin,
113 skip_urls=[routes.util.url_for('api')])
114 skip_urls=[routes.util.url_for('api')])
114
115
115 # Establish the Registry for this application
116 # Establish the Registry for this application
116 app = RegistryManager(app)
117 app = RegistryManager(app)
117
118
118 app.config = config
119 app.config = config
119
120
120 return app
121 return app
121
122
122
123
123 def make_pyramid_app(global_config, **settings):
124 def make_pyramid_app(global_config, **settings):
124 """
125 """
125 Constructs the WSGI application based on Pyramid and wraps the Pylons based
126 Constructs the WSGI application based on Pyramid and wraps the Pylons based
126 application.
127 application.
127
128
128 Specials:
129 Specials:
129
130
130 * We migrate from Pylons to Pyramid. While doing this, we keep both
131 * We migrate from Pylons to Pyramid. While doing this, we keep both
131 frameworks functional. This involves moving some WSGI middlewares around
132 frameworks functional. This involves moving some WSGI middlewares around
132 and providing access to some data internals, so that the old code is
133 and providing access to some data internals, so that the old code is
133 still functional.
134 still functional.
134
135
135 * The application can also be integrated like a plugin via the call to
136 * The application can also be integrated like a plugin via the call to
136 `includeme`. This is accompanied with the other utility functions which
137 `includeme`. This is accompanied with the other utility functions which
137 are called. Changing this should be done with great care to not break
138 are called. Changing this should be done with great care to not break
138 cases when these fragments are assembled from another place.
139 cases when these fragments are assembled from another place.
139
140
140 """
141 """
141 # The edition string should be available in pylons too, so we add it here
142 # The edition string should be available in pylons too, so we add it here
142 # before copying the settings.
143 # before copying the settings.
143 settings.setdefault('rhodecode.edition', 'Community Edition')
144 settings.setdefault('rhodecode.edition', 'Community Edition')
144
145
145 # As long as our Pylons application does expect "unprepared" settings, make
146 # As long as our Pylons application does expect "unprepared" settings, make
146 # sure that we keep an unmodified copy. This avoids unintentional change of
147 # sure that we keep an unmodified copy. This avoids unintentional change of
147 # behavior in the old application.
148 # behavior in the old application.
148 settings_pylons = settings.copy()
149 settings_pylons = settings.copy()
149
150
150 sanitize_settings_and_apply_defaults(settings)
151 sanitize_settings_and_apply_defaults(settings)
151 config = Configurator(settings=settings)
152 config = Configurator(settings=settings)
152 add_pylons_compat_data(config.registry, global_config, settings_pylons)
153 add_pylons_compat_data(config.registry, global_config, settings_pylons)
153
154
154 load_pyramid_environment(global_config, settings)
155 load_pyramid_environment(global_config, settings)
155
156
156 includeme_first(config)
157 includeme_first(config)
157 includeme(config)
158 includeme(config)
158 pyramid_app = config.make_wsgi_app()
159 pyramid_app = config.make_wsgi_app()
159 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
160 pyramid_app = wrap_app_in_wsgi_middlewares(pyramid_app, config)
160 pyramid_app.config = config
161 pyramid_app.config = config
162
163 # creating the app uses a connection - return it after we are done
164 meta.Session.remove()
165
161 return pyramid_app
166 return pyramid_app
162
167
163
168
164 def make_not_found_view(config):
169 def make_not_found_view(config):
165 """
170 """
166 This creates the view which should be registered as not-found-view to
171 This creates the view which should be registered as not-found-view to
167 pyramid. Basically it contains of the old pylons app, converted to a view.
172 pyramid. Basically it contains of the old pylons app, converted to a view.
168 Additionally it is wrapped by some other middlewares.
173 Additionally it is wrapped by some other middlewares.
169 """
174 """
170 settings = config.registry.settings
175 settings = config.registry.settings
171 vcs_server_enabled = settings['vcs.server.enable']
176 vcs_server_enabled = settings['vcs.server.enable']
172
177
173 # Make pylons app from unprepared settings.
178 # Make pylons app from unprepared settings.
174 pylons_app = make_app(
179 pylons_app = make_app(
175 config.registry._pylons_compat_global_config,
180 config.registry._pylons_compat_global_config,
176 **config.registry._pylons_compat_settings)
181 **config.registry._pylons_compat_settings)
177 config.registry._pylons_compat_config = pylons_app.config
182 config.registry._pylons_compat_config = pylons_app.config
178
183
179 # Appenlight monitoring.
184 # Appenlight monitoring.
180 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
185 pylons_app, appenlight_client = wrap_in_appenlight_if_enabled(
181 pylons_app, settings)
186 pylons_app, settings)
182
187
183 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find
188 # The VCSMiddleware shall operate like a fallback if pyramid doesn't find
184 # a view to handle the request. Therefore we wrap it around the pylons app.
189 # a view to handle the request. Therefore we wrap it around the pylons app.
185 if vcs_server_enabled:
190 if vcs_server_enabled:
186 pylons_app = VCSMiddleware(
191 pylons_app = VCSMiddleware(
187 pylons_app, settings, appenlight_client, registry=config.registry)
192 pylons_app, settings, appenlight_client, registry=config.registry)
188
193
189 pylons_app_as_view = wsgiapp(pylons_app)
194 pylons_app_as_view = wsgiapp(pylons_app)
190
195
191 # Protect from VCS Server error related pages when server is not available
196 # Protect from VCS Server error related pages when server is not available
192 if not vcs_server_enabled:
197 if not vcs_server_enabled:
193 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
198 pylons_app_as_view = DisableVCSPagesWrapper(pylons_app_as_view)
194
199
195 def pylons_app_with_error_handler(context, request):
200 def pylons_app_with_error_handler(context, request):
196 """
201 """
197 Handle exceptions from rc pylons app:
202 Handle exceptions from rc pylons app:
198
203
199 - old webob type exceptions get converted to pyramid exceptions
204 - old webob type exceptions get converted to pyramid exceptions
200 - pyramid exceptions are passed to the error handler view
205 - pyramid exceptions are passed to the error handler view
201 """
206 """
202 def is_vcs_response(response):
207 def is_vcs_response(response):
203 return 'X-RhodeCode-Backend' in response.headers
208 return 'X-RhodeCode-Backend' in response.headers
204
209
205 def is_http_error(response):
210 def is_http_error(response):
206 # webob type error responses
211 # webob type error responses
207 return (400 <= response.status_int <= 599)
212 return (400 <= response.status_int <= 599)
208
213
209 def is_error_handling_needed(response):
214 def is_error_handling_needed(response):
210 return is_http_error(response) and not is_vcs_response(response)
215 return is_http_error(response) and not is_vcs_response(response)
211
216
212 try:
217 try:
213 response = pylons_app_as_view(context, request)
218 response = pylons_app_as_view(context, request)
214 if is_error_handling_needed(response):
219 if is_error_handling_needed(response):
215 response = webob_to_pyramid_http_response(response)
220 response = webob_to_pyramid_http_response(response)
216 return error_handler(response, request)
221 return error_handler(response, request)
217 except HTTPError as e: # pyramid type exceptions
222 except HTTPError as e: # pyramid type exceptions
218 return error_handler(e, request)
223 return error_handler(e, request)
219 except Exception:
224 except Exception:
220 if settings.get('debugtoolbar.enabled', False):
225 if settings.get('debugtoolbar.enabled', False):
221 raise
226 raise
222 return error_handler(HTTPInternalServerError(), request)
227 return error_handler(HTTPInternalServerError(), request)
223 return response
228 return response
224
229
225 return pylons_app_with_error_handler
230 return pylons_app_with_error_handler
226
231
227
232
228 def add_pylons_compat_data(registry, global_config, settings):
233 def add_pylons_compat_data(registry, global_config, settings):
229 """
234 """
230 Attach data to the registry to support the Pylons integration.
235 Attach data to the registry to support the Pylons integration.
231 """
236 """
232 registry._pylons_compat_global_config = global_config
237 registry._pylons_compat_global_config = global_config
233 registry._pylons_compat_settings = settings
238 registry._pylons_compat_settings = settings
234
239
235
240
236 def webob_to_pyramid_http_response(webob_response):
241 def webob_to_pyramid_http_response(webob_response):
237 ResponseClass = httpexceptions.status_map[webob_response.status_int]
242 ResponseClass = httpexceptions.status_map[webob_response.status_int]
238 pyramid_response = ResponseClass(webob_response.status)
243 pyramid_response = ResponseClass(webob_response.status)
239 pyramid_response.status = webob_response.status
244 pyramid_response.status = webob_response.status
240 pyramid_response.headers.update(webob_response.headers)
245 pyramid_response.headers.update(webob_response.headers)
241 if pyramid_response.headers['content-type'] == 'text/html':
246 if pyramid_response.headers['content-type'] == 'text/html':
242 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
247 pyramid_response.headers['content-type'] = 'text/html; charset=UTF-8'
243 return pyramid_response
248 return pyramid_response
244
249
245
250
246 def error_handler(exception, request):
251 def error_handler(exception, request):
247 # TODO: dan: replace the old pylons error controller with this
252 # TODO: dan: replace the old pylons error controller with this
248 from rhodecode.model.settings import SettingsModel
253 from rhodecode.model.settings import SettingsModel
249 from rhodecode.lib.utils2 import AttributeDict
254 from rhodecode.lib.utils2 import AttributeDict
250
255
251 try:
256 try:
252 rc_config = SettingsModel().get_all_settings()
257 rc_config = SettingsModel().get_all_settings()
253 except Exception:
258 except Exception:
254 log.exception('failed to fetch settings')
259 log.exception('failed to fetch settings')
255 rc_config = {}
260 rc_config = {}
256
261
257 base_response = HTTPInternalServerError()
262 base_response = HTTPInternalServerError()
258 # prefer original exception for the response since it may have headers set
263 # prefer original exception for the response since it may have headers set
259 if isinstance(exception, HTTPError):
264 if isinstance(exception, HTTPError):
260 base_response = exception
265 base_response = exception
261
266
262 c = AttributeDict()
267 c = AttributeDict()
263 c.error_message = base_response.status
268 c.error_message = base_response.status
264 c.error_explanation = base_response.explanation or str(base_response)
269 c.error_explanation = base_response.explanation or str(base_response)
265 c.visual = AttributeDict()
270 c.visual = AttributeDict()
266
271
267 c.visual.rhodecode_support_url = (
272 c.visual.rhodecode_support_url = (
268 request.registry.settings.get('rhodecode_support_url') or
273 request.registry.settings.get('rhodecode_support_url') or
269 request.route_url('rhodecode_support')
274 request.route_url('rhodecode_support')
270 )
275 )
271 c.redirect_time = 0
276 c.redirect_time = 0
272 c.rhodecode_name = rc_config.get('rhodecode_title', '')
277 c.rhodecode_name = rc_config.get('rhodecode_title', '')
273 if not c.rhodecode_name:
278 if not c.rhodecode_name:
274 c.rhodecode_name = 'Rhodecode'
279 c.rhodecode_name = 'Rhodecode'
275
280
276 response = render_to_response(
281 response = render_to_response(
277 '/errors/error_document.html', {'c': c}, request=request,
282 '/errors/error_document.html', {'c': c}, request=request,
278 response=base_response)
283 response=base_response)
279
284
280 return response
285 return response
281
286
282
287
283 def includeme(config):
288 def includeme(config):
284 settings = config.registry.settings
289 settings = config.registry.settings
285
290
286 # plugin information
291 # plugin information
287 config.registry.rhodecode_plugins = OrderedDict()
292 config.registry.rhodecode_plugins = OrderedDict()
288
293
289 config.add_directive(
294 config.add_directive(
290 'register_rhodecode_plugin', register_rhodecode_plugin)
295 'register_rhodecode_plugin', register_rhodecode_plugin)
291
296
292 if asbool(settings.get('appenlight', 'false')):
297 if asbool(settings.get('appenlight', 'false')):
293 config.include('appenlight_client.ext.pyramid_tween')
298 config.include('appenlight_client.ext.pyramid_tween')
294
299
295 # Includes which are required. The application would fail without them.
300 # Includes which are required. The application would fail without them.
296 config.include('pyramid_mako')
301 config.include('pyramid_mako')
297 config.include('pyramid_beaker')
302 config.include('pyramid_beaker')
298 config.include('rhodecode.channelstream')
303 config.include('rhodecode.channelstream')
299 config.include('rhodecode.admin')
304 config.include('rhodecode.admin')
300 config.include('rhodecode.authentication')
305 config.include('rhodecode.authentication')
301 config.include('rhodecode.integrations')
306 config.include('rhodecode.integrations')
302 config.include('rhodecode.login')
307 config.include('rhodecode.login')
303 config.include('rhodecode.tweens')
308 config.include('rhodecode.tweens')
304 config.include('rhodecode.api')
309 config.include('rhodecode.api')
305 config.include('rhodecode.svn_support')
310 config.include('rhodecode.svn_support')
306 config.add_route(
311 config.add_route(
307 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
312 'rhodecode_support', 'https://rhodecode.com/help/', static=True)
308
313
309 # Add subscribers.
314 # Add subscribers.
310 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
315 config.add_subscriber(scan_repositories_if_enabled, ApplicationCreated)
311
316
312 # Set the authorization policy.
317 # Set the authorization policy.
313 authz_policy = ACLAuthorizationPolicy()
318 authz_policy = ACLAuthorizationPolicy()
314 config.set_authorization_policy(authz_policy)
319 config.set_authorization_policy(authz_policy)
315
320
316 # Set the default renderer for HTML templates to mako.
321 # Set the default renderer for HTML templates to mako.
317 config.add_mako_renderer('.html')
322 config.add_mako_renderer('.html')
318
323
319 # include RhodeCode plugins
324 # include RhodeCode plugins
320 includes = aslist(settings.get('rhodecode.includes', []))
325 includes = aslist(settings.get('rhodecode.includes', []))
321 for inc in includes:
326 for inc in includes:
322 config.include(inc)
327 config.include(inc)
323
328
324 # This is the glue which allows us to migrate in chunks. By registering the
329 # This is the glue which allows us to migrate in chunks. By registering the
325 # pylons based application as the "Not Found" view in Pyramid, we will
330 # pylons based application as the "Not Found" view in Pyramid, we will
326 # fallback to the old application each time the new one does not yet know
331 # fallback to the old application each time the new one does not yet know
327 # how to handle a request.
332 # how to handle a request.
328 config.add_notfound_view(make_not_found_view(config))
333 config.add_notfound_view(make_not_found_view(config))
329
334
330 if not settings.get('debugtoolbar.enabled', False):
335 if not settings.get('debugtoolbar.enabled', False):
331 # if no toolbar, then any exception gets caught and rendered
336 # if no toolbar, then any exception gets caught and rendered
332 config.add_view(error_handler, context=Exception)
337 config.add_view(error_handler, context=Exception)
333
338
334 config.add_view(error_handler, context=HTTPError)
339 config.add_view(error_handler, context=HTTPError)
335
340
336
341
337 def includeme_first(config):
342 def includeme_first(config):
338 # redirect automatic browser favicon.ico requests to correct place
343 # redirect automatic browser favicon.ico requests to correct place
339 def favicon_redirect(context, request):
344 def favicon_redirect(context, request):
340 return HTTPFound(
345 return HTTPFound(
341 request.static_path('rhodecode:public/images/favicon.ico'))
346 request.static_path('rhodecode:public/images/favicon.ico'))
342
347
343 config.add_view(favicon_redirect, route_name='favicon')
348 config.add_view(favicon_redirect, route_name='favicon')
344 config.add_route('favicon', '/favicon.ico')
349 config.add_route('favicon', '/favicon.ico')
345
350
346 config.add_static_view(
351 config.add_static_view(
347 '_static/deform', 'deform:static')
352 '_static/deform', 'deform:static')
348 config.add_static_view(
353 config.add_static_view(
349 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
354 '_static/rhodecode', path='rhodecode:public', cache_max_age=3600 * 24)
350
355
351
356
352 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
357 def wrap_app_in_wsgi_middlewares(pyramid_app, config):
353 """
358 """
354 Apply outer WSGI middlewares around the application.
359 Apply outer WSGI middlewares around the application.
355
360
356 Part of this has been moved up from the Pylons layer, so that the
361 Part of this has been moved up from the Pylons layer, so that the
357 data is also available if old Pylons code is hit through an already ported
362 data is also available if old Pylons code is hit through an already ported
358 view.
363 view.
359 """
364 """
360 settings = config.registry.settings
365 settings = config.registry.settings
361
366
362 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
367 # enable https redirects based on HTTP_X_URL_SCHEME set by proxy
363 pyramid_app = HttpsFixup(pyramid_app, settings)
368 pyramid_app = HttpsFixup(pyramid_app, settings)
364
369
365 # Add RoutesMiddleware to support the pylons compatibility tween during
370 # Add RoutesMiddleware to support the pylons compatibility tween during
366 # migration to pyramid.
371 # migration to pyramid.
367 pyramid_app = SkippableRoutesMiddleware(
372 pyramid_app = SkippableRoutesMiddleware(
368 pyramid_app, config.registry._pylons_compat_config['routes.map'],
373 pyramid_app, config.registry._pylons_compat_config['routes.map'],
369 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
374 skip_prefixes=(STATIC_FILE_PREFIX, '/_debug_toolbar'))
370
375
371 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
376 pyramid_app, _ = wrap_in_appenlight_if_enabled(pyramid_app, settings)
372
377
373 if settings['gzip_responses']:
378 if settings['gzip_responses']:
374 pyramid_app = make_gzip_middleware(
379 pyramid_app = make_gzip_middleware(
375 pyramid_app, settings, compress_level=1)
380 pyramid_app, settings, compress_level=1)
376
381
377 return pyramid_app
382
383 # this should be the outer most middleware in the wsgi stack since
384 # middleware like Routes make database calls
385 def pyramid_app_with_cleanup(environ, start_response):
386 try:
387 return pyramid_app(environ, start_response)
388 finally:
389 # Dispose current database session and rollback uncommitted
390 # transactions.
391 meta.Session.remove()
392
393 # In a single threaded mode server, on non sqlite db we should have
394 # '0 Current Checked out connections' at the end of a request,
395 # if not, then something, somewhere is leaving a connection open
396 pool = meta.Base.metadata.bind.engine.pool
397 log.debug('sa pool status: %s', pool.status())
398
399
400 return pyramid_app_with_cleanup
378
401
379
402
380 def sanitize_settings_and_apply_defaults(settings):
403 def sanitize_settings_and_apply_defaults(settings):
381 """
404 """
382 Applies settings defaults and does all type conversion.
405 Applies settings defaults and does all type conversion.
383
406
384 We would move all settings parsing and preparation into this place, so that
407 We would move all settings parsing and preparation into this place, so that
385 we have only one place left which deals with this part. The remaining parts
408 we have only one place left which deals with this part. The remaining parts
386 of the application would start to rely fully on well prepared settings.
409 of the application would start to rely fully on well prepared settings.
387
410
388 This piece would later be split up per topic to avoid a big fat monster
411 This piece would later be split up per topic to avoid a big fat monster
389 function.
412 function.
390 """
413 """
391
414
392 # Pyramid's mako renderer has to search in the templates folder so that the
415 # Pyramid's mako renderer has to search in the templates folder so that the
393 # old templates still work. Ported and new templates are expected to use
416 # old templates still work. Ported and new templates are expected to use
394 # real asset specifications for the includes.
417 # real asset specifications for the includes.
395 mako_directories = settings.setdefault('mako.directories', [
418 mako_directories = settings.setdefault('mako.directories', [
396 # Base templates of the original Pylons application
419 # Base templates of the original Pylons application
397 'rhodecode:templates',
420 'rhodecode:templates',
398 ])
421 ])
399 log.debug(
422 log.debug(
400 "Using the following Mako template directories: %s",
423 "Using the following Mako template directories: %s",
401 mako_directories)
424 mako_directories)
402
425
403 # Default includes, possible to change as a user
426 # Default includes, possible to change as a user
404 pyramid_includes = settings.setdefault('pyramid.includes', [
427 pyramid_includes = settings.setdefault('pyramid.includes', [
405 'rhodecode.lib.middleware.request_wrapper',
428 'rhodecode.lib.middleware.request_wrapper',
406 ])
429 ])
407 log.debug(
430 log.debug(
408 "Using the following pyramid.includes: %s",
431 "Using the following pyramid.includes: %s",
409 pyramid_includes)
432 pyramid_includes)
410
433
411 # TODO: johbo: Re-think this, usually the call to config.include
434 # TODO: johbo: Re-think this, usually the call to config.include
412 # should allow to pass in a prefix.
435 # should allow to pass in a prefix.
413 settings.setdefault('rhodecode.api.url', '/_admin/api')
436 settings.setdefault('rhodecode.api.url', '/_admin/api')
414
437
415 # Sanitize generic settings.
438 # Sanitize generic settings.
416 _list_setting(settings, 'default_encoding', 'UTF-8')
439 _list_setting(settings, 'default_encoding', 'UTF-8')
417 _bool_setting(settings, 'is_test', 'false')
440 _bool_setting(settings, 'is_test', 'false')
418 _bool_setting(settings, 'gzip_responses', 'false')
441 _bool_setting(settings, 'gzip_responses', 'false')
419
442
420 # Call split out functions that sanitize settings for each topic.
443 # Call split out functions that sanitize settings for each topic.
421 _sanitize_appenlight_settings(settings)
444 _sanitize_appenlight_settings(settings)
422 _sanitize_vcs_settings(settings)
445 _sanitize_vcs_settings(settings)
423
446
424 return settings
447 return settings
425
448
426
449
427 def _sanitize_appenlight_settings(settings):
450 def _sanitize_appenlight_settings(settings):
428 _bool_setting(settings, 'appenlight', 'false')
451 _bool_setting(settings, 'appenlight', 'false')
429
452
430
453
431 def _sanitize_vcs_settings(settings):
454 def _sanitize_vcs_settings(settings):
432 """
455 """
433 Applies settings defaults and does type conversion for all VCS related
456 Applies settings defaults and does type conversion for all VCS related
434 settings.
457 settings.
435 """
458 """
436 _string_setting(settings, 'vcs.svn.compatible_version', '')
459 _string_setting(settings, 'vcs.svn.compatible_version', '')
437 _string_setting(settings, 'git_rev_filter', '--all')
460 _string_setting(settings, 'git_rev_filter', '--all')
438 _string_setting(settings, 'vcs.hooks.protocol', 'pyro4')
461 _string_setting(settings, 'vcs.hooks.protocol', 'pyro4')
439 _string_setting(settings, 'vcs.server', '')
462 _string_setting(settings, 'vcs.server', '')
440 _string_setting(settings, 'vcs.server.log_level', 'debug')
463 _string_setting(settings, 'vcs.server.log_level', 'debug')
441 _string_setting(settings, 'vcs.server.protocol', 'pyro4')
464 _string_setting(settings, 'vcs.server.protocol', 'pyro4')
442 _bool_setting(settings, 'startup.import_repos', 'false')
465 _bool_setting(settings, 'startup.import_repos', 'false')
443 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
466 _bool_setting(settings, 'vcs.hooks.direct_calls', 'false')
444 _bool_setting(settings, 'vcs.server.enable', 'true')
467 _bool_setting(settings, 'vcs.server.enable', 'true')
445 _bool_setting(settings, 'vcs.start_server', 'false')
468 _bool_setting(settings, 'vcs.start_server', 'false')
446 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
469 _list_setting(settings, 'vcs.backends', 'hg, git, svn')
447 _int_setting(settings, 'vcs.connection_timeout', 3600)
470 _int_setting(settings, 'vcs.connection_timeout', 3600)
448
471
449
472
450 def _int_setting(settings, name, default):
473 def _int_setting(settings, name, default):
451 settings[name] = int(settings.get(name, default))
474 settings[name] = int(settings.get(name, default))
452
475
453
476
454 def _bool_setting(settings, name, default):
477 def _bool_setting(settings, name, default):
455 input = settings.get(name, default)
478 input = settings.get(name, default)
456 if isinstance(input, unicode):
479 if isinstance(input, unicode):
457 input = input.encode('utf8')
480 input = input.encode('utf8')
458 settings[name] = asbool(input)
481 settings[name] = asbool(input)
459
482
460
483
461 def _list_setting(settings, name, default):
484 def _list_setting(settings, name, default):
462 raw_value = settings.get(name, default)
485 raw_value = settings.get(name, default)
463
486
464 old_separator = ','
487 old_separator = ','
465 if old_separator in raw_value:
488 if old_separator in raw_value:
466 # If we get a comma separated list, pass it to our own function.
489 # If we get a comma separated list, pass it to our own function.
467 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
490 settings[name] = rhodecode_aslist(raw_value, sep=old_separator)
468 else:
491 else:
469 # Otherwise we assume it uses pyramids space/newline separation.
492 # Otherwise we assume it uses pyramids space/newline separation.
470 settings[name] = aslist(raw_value)
493 settings[name] = aslist(raw_value)
471
494
472
495
473 def _string_setting(settings, name, default):
496 def _string_setting(settings, name, default):
474 settings[name] = settings.get(name, default).lower()
497 settings[name] = settings.get(name, default).lower()
@@ -1,274 +1,278 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import json
21 import json
22 import logging
22 import logging
23 import urlparse
23 import urlparse
24 import threading
24 import threading
25 from BaseHTTPServer import BaseHTTPRequestHandler
25 from BaseHTTPServer import BaseHTTPRequestHandler
26 from SocketServer import TCPServer
26 from SocketServer import TCPServer
27 from routes.util import URLGenerator
27 from routes.util import URLGenerator
28
28
29 import Pyro4
29 import Pyro4
30 import pylons
30 import pylons
31 import rhodecode
31 import rhodecode
32
32
33 from rhodecode.model import meta
33 from rhodecode.lib import hooks_base
34 from rhodecode.lib import hooks_base
34 from rhodecode.lib.utils2 import (
35 from rhodecode.lib.utils2 import (
35 AttributeDict, safe_str, get_routes_generator_for_server_url)
36 AttributeDict, safe_str, get_routes_generator_for_server_url)
36
37
37
38
38 log = logging.getLogger(__name__)
39 log = logging.getLogger(__name__)
39
40
40
41
41 class HooksHttpHandler(BaseHTTPRequestHandler):
42 class HooksHttpHandler(BaseHTTPRequestHandler):
42 def do_POST(self):
43 def do_POST(self):
43 method, extras = self._read_request()
44 method, extras = self._read_request()
44 try:
45 try:
45 result = self._call_hook(method, extras)
46 result = self._call_hook(method, extras)
46 except Exception as e:
47 except Exception as e:
47 result = {
48 result = {
48 'exception': e.__class__.__name__,
49 'exception': e.__class__.__name__,
49 'exception_args': e.args
50 'exception_args': e.args
50 }
51 }
51 self._write_response(result)
52 self._write_response(result)
52
53
53 def _read_request(self):
54 def _read_request(self):
54 length = int(self.headers['Content-Length'])
55 length = int(self.headers['Content-Length'])
55 body = self.rfile.read(length).decode('utf-8')
56 body = self.rfile.read(length).decode('utf-8')
56 data = json.loads(body)
57 data = json.loads(body)
57 return data['method'], data['extras']
58 return data['method'], data['extras']
58
59
59 def _write_response(self, result):
60 def _write_response(self, result):
60 self.send_response(200)
61 self.send_response(200)
61 self.send_header("Content-type", "text/json")
62 self.send_header("Content-type", "text/json")
62 self.end_headers()
63 self.end_headers()
63 self.wfile.write(json.dumps(result))
64 self.wfile.write(json.dumps(result))
64
65
65 def _call_hook(self, method, extras):
66 def _call_hook(self, method, extras):
66 hooks = Hooks()
67 hooks = Hooks()
67 result = getattr(hooks, method)(extras)
68 try:
69 result = getattr(hooks, method)(extras)
70 finally:
71 meta.Session.remove()
68 return result
72 return result
69
73
70 def log_message(self, format, *args):
74 def log_message(self, format, *args):
71 """
75 """
72 This is an overriden method of BaseHTTPRequestHandler which logs using
76 This is an overriden method of BaseHTTPRequestHandler which logs using
73 logging library instead of writing directly to stderr.
77 logging library instead of writing directly to stderr.
74 """
78 """
75
79
76 message = format % args
80 message = format % args
77
81
78 # TODO: mikhail: add different log levels support
82 # TODO: mikhail: add different log levels support
79 log.debug(
83 log.debug(
80 "%s - - [%s] %s", self.client_address[0],
84 "%s - - [%s] %s", self.client_address[0],
81 self.log_date_time_string(), message)
85 self.log_date_time_string(), message)
82
86
83
87
84 class DummyHooksCallbackDaemon(object):
88 class DummyHooksCallbackDaemon(object):
85 def __init__(self):
89 def __init__(self):
86 self.hooks_module = Hooks.__module__
90 self.hooks_module = Hooks.__module__
87
91
88 def __enter__(self):
92 def __enter__(self):
89 log.debug('Running dummy hooks callback daemon')
93 log.debug('Running dummy hooks callback daemon')
90 return self
94 return self
91
95
92 def __exit__(self, exc_type, exc_val, exc_tb):
96 def __exit__(self, exc_type, exc_val, exc_tb):
93 log.debug('Exiting dummy hooks callback daemon')
97 log.debug('Exiting dummy hooks callback daemon')
94
98
95
99
96 class ThreadedHookCallbackDaemon(object):
100 class ThreadedHookCallbackDaemon(object):
97
101
98 _callback_thread = None
102 _callback_thread = None
99 _daemon = None
103 _daemon = None
100 _done = False
104 _done = False
101
105
102 def __init__(self):
106 def __init__(self):
103 self._prepare()
107 self._prepare()
104
108
105 def __enter__(self):
109 def __enter__(self):
106 self._run()
110 self._run()
107 return self
111 return self
108
112
109 def __exit__(self, exc_type, exc_val, exc_tb):
113 def __exit__(self, exc_type, exc_val, exc_tb):
110 self._stop()
114 self._stop()
111
115
112 def _prepare(self):
116 def _prepare(self):
113 raise NotImplementedError()
117 raise NotImplementedError()
114
118
115 def _run(self):
119 def _run(self):
116 raise NotImplementedError()
120 raise NotImplementedError()
117
121
118 def _stop(self):
122 def _stop(self):
119 raise NotImplementedError()
123 raise NotImplementedError()
120
124
121
125
122 class Pyro4HooksCallbackDaemon(ThreadedHookCallbackDaemon):
126 class Pyro4HooksCallbackDaemon(ThreadedHookCallbackDaemon):
123 """
127 """
124 Context manager which will run a callback daemon in a background thread.
128 Context manager which will run a callback daemon in a background thread.
125 """
129 """
126
130
127 hooks_uri = None
131 hooks_uri = None
128
132
129 def _prepare(self):
133 def _prepare(self):
130 log.debug("Preparing callback daemon and registering hook object")
134 log.debug("Preparing callback daemon and registering hook object")
131 self._daemon = Pyro4.Daemon()
135 self._daemon = Pyro4.Daemon()
132 hooks_interface = Hooks()
136 hooks_interface = Hooks()
133 self.hooks_uri = str(self._daemon.register(hooks_interface))
137 self.hooks_uri = str(self._daemon.register(hooks_interface))
134 log.debug("Hooks uri is: %s", self.hooks_uri)
138 log.debug("Hooks uri is: %s", self.hooks_uri)
135
139
136 def _run(self):
140 def _run(self):
137 log.debug("Running event loop of callback daemon in background thread")
141 log.debug("Running event loop of callback daemon in background thread")
138 callback_thread = threading.Thread(
142 callback_thread = threading.Thread(
139 target=self._daemon.requestLoop,
143 target=self._daemon.requestLoop,
140 kwargs={'loopCondition': lambda: not self._done})
144 kwargs={'loopCondition': lambda: not self._done})
141 callback_thread.daemon = True
145 callback_thread.daemon = True
142 callback_thread.start()
146 callback_thread.start()
143 self._callback_thread = callback_thread
147 self._callback_thread = callback_thread
144
148
145 def _stop(self):
149 def _stop(self):
146 log.debug("Waiting for background thread to finish.")
150 log.debug("Waiting for background thread to finish.")
147 self._done = True
151 self._done = True
148 self._callback_thread.join()
152 self._callback_thread.join()
149 self._daemon.close()
153 self._daemon.close()
150 self._daemon = None
154 self._daemon = None
151 self._callback_thread = None
155 self._callback_thread = None
152
156
153
157
154 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
158 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
155 """
159 """
156 Context manager which will run a callback daemon in a background thread.
160 Context manager which will run a callback daemon in a background thread.
157 """
161 """
158
162
159 hooks_uri = None
163 hooks_uri = None
160
164
161 IP_ADDRESS = '127.0.0.1'
165 IP_ADDRESS = '127.0.0.1'
162
166
163 # From Python docs: Polling reduces our responsiveness to a shutdown
167 # From Python docs: Polling reduces our responsiveness to a shutdown
164 # request and wastes cpu at all other times.
168 # request and wastes cpu at all other times.
165 POLL_INTERVAL = 0.1
169 POLL_INTERVAL = 0.1
166
170
167 def _prepare(self):
171 def _prepare(self):
168 log.debug("Preparing callback daemon and registering hook object")
172 log.debug("Preparing callback daemon and registering hook object")
169
173
170 self._done = False
174 self._done = False
171 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
175 self._daemon = TCPServer((self.IP_ADDRESS, 0), HooksHttpHandler)
172 _, port = self._daemon.server_address
176 _, port = self._daemon.server_address
173 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
177 self.hooks_uri = '{}:{}'.format(self.IP_ADDRESS, port)
174
178
175 log.debug("Hooks uri is: %s", self.hooks_uri)
179 log.debug("Hooks uri is: %s", self.hooks_uri)
176
180
177 def _run(self):
181 def _run(self):
178 log.debug("Running event loop of callback daemon in background thread")
182 log.debug("Running event loop of callback daemon in background thread")
179 callback_thread = threading.Thread(
183 callback_thread = threading.Thread(
180 target=self._daemon.serve_forever,
184 target=self._daemon.serve_forever,
181 kwargs={'poll_interval': self.POLL_INTERVAL})
185 kwargs={'poll_interval': self.POLL_INTERVAL})
182 callback_thread.daemon = True
186 callback_thread.daemon = True
183 callback_thread.start()
187 callback_thread.start()
184 self._callback_thread = callback_thread
188 self._callback_thread = callback_thread
185
189
186 def _stop(self):
190 def _stop(self):
187 log.debug("Waiting for background thread to finish.")
191 log.debug("Waiting for background thread to finish.")
188 self._daemon.shutdown()
192 self._daemon.shutdown()
189 self._callback_thread.join()
193 self._callback_thread.join()
190 self._daemon = None
194 self._daemon = None
191 self._callback_thread = None
195 self._callback_thread = None
192
196
193
197
194 def prepare_callback_daemon(extras, protocol=None, use_direct_calls=False):
198 def prepare_callback_daemon(extras, protocol=None, use_direct_calls=False):
195 callback_daemon = None
199 callback_daemon = None
196 protocol = protocol.lower() if protocol else None
200 protocol = protocol.lower() if protocol else None
197
201
198 if use_direct_calls:
202 if use_direct_calls:
199 callback_daemon = DummyHooksCallbackDaemon()
203 callback_daemon = DummyHooksCallbackDaemon()
200 extras['hooks_module'] = callback_daemon.hooks_module
204 extras['hooks_module'] = callback_daemon.hooks_module
201 else:
205 else:
202 if protocol == 'pyro4':
206 if protocol == 'pyro4':
203 callback_daemon = Pyro4HooksCallbackDaemon()
207 callback_daemon = Pyro4HooksCallbackDaemon()
204 elif protocol == 'http':
208 elif protocol == 'http':
205 callback_daemon = HttpHooksCallbackDaemon()
209 callback_daemon = HttpHooksCallbackDaemon()
206 else:
210 else:
207 log.error('Unsupported callback daemon protocol "%s"', protocol)
211 log.error('Unsupported callback daemon protocol "%s"', protocol)
208 raise Exception('Unsupported callback daemon protocol.')
212 raise Exception('Unsupported callback daemon protocol.')
209
213
210 extras['hooks_uri'] = callback_daemon.hooks_uri
214 extras['hooks_uri'] = callback_daemon.hooks_uri
211 extras['hooks_protocol'] = protocol
215 extras['hooks_protocol'] = protocol
212
216
213 return callback_daemon, extras
217 return callback_daemon, extras
214
218
215
219
216 class Hooks(object):
220 class Hooks(object):
217 """
221 """
218 Exposes the hooks for remote call backs
222 Exposes the hooks for remote call backs
219 """
223 """
220
224
221 @Pyro4.callback
225 @Pyro4.callback
222 def repo_size(self, extras):
226 def repo_size(self, extras):
223 log.debug("Called repo_size of Hooks object")
227 log.debug("Called repo_size of Hooks object")
224 return self._call_hook(hooks_base.repo_size, extras)
228 return self._call_hook(hooks_base.repo_size, extras)
225
229
226 @Pyro4.callback
230 @Pyro4.callback
227 def pre_pull(self, extras):
231 def pre_pull(self, extras):
228 log.debug("Called pre_pull of Hooks object")
232 log.debug("Called pre_pull of Hooks object")
229 return self._call_hook(hooks_base.pre_pull, extras)
233 return self._call_hook(hooks_base.pre_pull, extras)
230
234
231 @Pyro4.callback
235 @Pyro4.callback
232 def post_pull(self, extras):
236 def post_pull(self, extras):
233 log.debug("Called post_pull of Hooks object")
237 log.debug("Called post_pull of Hooks object")
234 return self._call_hook(hooks_base.post_pull, extras)
238 return self._call_hook(hooks_base.post_pull, extras)
235
239
236 @Pyro4.callback
240 @Pyro4.callback
237 def pre_push(self, extras):
241 def pre_push(self, extras):
238 log.debug("Called pre_push of Hooks object")
242 log.debug("Called pre_push of Hooks object")
239 return self._call_hook(hooks_base.pre_push, extras)
243 return self._call_hook(hooks_base.pre_push, extras)
240
244
241 @Pyro4.callback
245 @Pyro4.callback
242 def post_push(self, extras):
246 def post_push(self, extras):
243 log.debug("Called post_push of Hooks object")
247 log.debug("Called post_push of Hooks object")
244 return self._call_hook(hooks_base.post_push, extras)
248 return self._call_hook(hooks_base.post_push, extras)
245
249
246 def _call_hook(self, hook, extras):
250 def _call_hook(self, hook, extras):
247 extras = AttributeDict(extras)
251 extras = AttributeDict(extras)
248 pylons_router = get_routes_generator_for_server_url(extras.server_url)
252 pylons_router = get_routes_generator_for_server_url(extras.server_url)
249 pylons.url._push_object(pylons_router)
253 pylons.url._push_object(pylons_router)
250
254
251 try:
255 try:
252 result = hook(extras)
256 result = hook(extras)
253 except Exception as error:
257 except Exception as error:
254 log.exception('Exception when handling hook %s', hook)
258 log.exception('Exception when handling hook %s', hook)
255 error_args = error.args
259 error_args = error.args
256 return {
260 return {
257 'status': 128,
261 'status': 128,
258 'output': '',
262 'output': '',
259 'exception': type(error).__name__,
263 'exception': type(error).__name__,
260 'exception_args': error_args,
264 'exception_args': error_args,
261 }
265 }
262 finally:
266 finally:
263 pylons.url._pop_object()
267 pylons.url._pop_object()
264
268
265 return {
269 return {
266 'status': result.status,
270 'status': result.status,
267 'output': result.output,
271 'output': result.output,
268 }
272 }
269
273
270 def __enter__(self):
274 def __enter__(self):
271 return self
275 return self
272
276
273 def __exit__(self, exc_type, exc_val, exc_tb):
277 def __exit__(self, exc_type, exc_val, exc_tb):
274 pass
278 pass
@@ -1,445 +1,448 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2016 RhodeCode GmbH
3 # Copyright (C) 2014-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import logging
27 import logging
28 import importlib
28 import importlib
29 from functools import wraps
29 from functools import wraps
30
30
31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 from webob.exc import (
32 from webob.exc import (
33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34
34
35 import rhodecode
35 import rhodecode
36 from rhodecode.authentication.base import authenticate, VCS_TYPE
36 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 from rhodecode.lib.exceptions import (
39 from rhodecode.lib.exceptions import (
40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 NotAllowedToCreateUserError)
41 NotAllowedToCreateUserError)
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 from rhodecode.lib.middleware import appenlight
43 from rhodecode.lib.middleware import appenlight
44 from rhodecode.lib.middleware.utils import scm_app
44 from rhodecode.lib.middleware.utils import scm_app
45 from rhodecode.lib.utils import (
45 from rhodecode.lib.utils import (
46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
48 from rhodecode.lib.vcs.conf import settings as vcs_settings
48 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 from rhodecode.model import meta
49 from rhodecode.model import meta
50 from rhodecode.model.db import User, Repository
50 from rhodecode.model.db import User, Repository
51 from rhodecode.model.scm import ScmModel
51 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.settings import SettingsModel
52 from rhodecode.model.settings import SettingsModel
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 def initialize_generator(factory):
57 def initialize_generator(factory):
58 """
58 """
59 Initializes the returned generator by draining its first element.
59 Initializes the returned generator by draining its first element.
60
60
61 This can be used to give a generator an initializer, which is the code
61 This can be used to give a generator an initializer, which is the code
62 up to the first yield statement. This decorator enforces that the first
62 up to the first yield statement. This decorator enforces that the first
63 produced element has the value ``"__init__"`` to make its special
63 produced element has the value ``"__init__"`` to make its special
64 purpose very explicit in the using code.
64 purpose very explicit in the using code.
65 """
65 """
66
66
67 @wraps(factory)
67 @wraps(factory)
68 def wrapper(*args, **kwargs):
68 def wrapper(*args, **kwargs):
69 gen = factory(*args, **kwargs)
69 gen = factory(*args, **kwargs)
70 try:
70 try:
71 init = gen.next()
71 init = gen.next()
72 except StopIteration:
72 except StopIteration:
73 raise ValueError('Generator must yield at least one element.')
73 raise ValueError('Generator must yield at least one element.')
74 if init != "__init__":
74 if init != "__init__":
75 raise ValueError('First yielded element must be "__init__".')
75 raise ValueError('First yielded element must be "__init__".')
76 return gen
76 return gen
77 return wrapper
77 return wrapper
78
78
79
79
80 class SimpleVCS(object):
80 class SimpleVCS(object):
81 """Common functionality for SCM HTTP handlers."""
81 """Common functionality for SCM HTTP handlers."""
82
82
83 SCM = 'unknown'
83 SCM = 'unknown'
84
84
85 def __init__(self, application, config, registry):
85 def __init__(self, application, config, registry):
86 self.registry = registry
86 self.registry = registry
87 self.application = application
87 self.application = application
88 self.config = config
88 self.config = config
89 # base path of repo locations
89 # base path of repo locations
90 self.basepath = get_rhodecode_base_path()
90 self.basepath = get_rhodecode_base_path()
91 # authenticate this VCS request using authfunc
91 # authenticate this VCS request using authfunc
92 auth_ret_code_detection = \
92 auth_ret_code_detection = \
93 str2bool(self.config.get('auth_ret_code_detection', False))
93 str2bool(self.config.get('auth_ret_code_detection', False))
94 self.authenticate = BasicAuth(
94 self.authenticate = BasicAuth(
95 '', authenticate, registry, config.get('auth_ret_code'),
95 '', authenticate, registry, config.get('auth_ret_code'),
96 auth_ret_code_detection)
96 auth_ret_code_detection)
97 self.ip_addr = '0.0.0.0'
97 self.ip_addr = '0.0.0.0'
98
98
99 @property
99 @property
100 def scm_app(self):
100 def scm_app(self):
101 custom_implementation = self.config.get('vcs.scm_app_implementation')
101 custom_implementation = self.config.get('vcs.scm_app_implementation')
102 if custom_implementation and custom_implementation != 'pyro4':
102 if custom_implementation and custom_implementation != 'pyro4':
103 log.info(
103 log.info(
104 "Using custom implementation of scm_app: %s",
104 "Using custom implementation of scm_app: %s",
105 custom_implementation)
105 custom_implementation)
106 scm_app_impl = importlib.import_module(custom_implementation)
106 scm_app_impl = importlib.import_module(custom_implementation)
107 else:
107 else:
108 scm_app_impl = scm_app
108 scm_app_impl = scm_app
109 return scm_app_impl
109 return scm_app_impl
110
110
111 def _get_by_id(self, repo_name):
111 def _get_by_id(self, repo_name):
112 """
112 """
113 Gets a special pattern _<ID> from clone url and tries to replace it
113 Gets a special pattern _<ID> from clone url and tries to replace it
114 with a repository_name for support of _<ID> non changable urls
114 with a repository_name for support of _<ID> non changable urls
115
115
116 :param repo_name:
116 :param repo_name:
117 """
117 """
118
118
119 data = repo_name.split('/')
119 data = repo_name.split('/')
120 if len(data) >= 2:
120 if len(data) >= 2:
121 from rhodecode.model.repo import RepoModel
121 from rhodecode.model.repo import RepoModel
122 by_id_match = RepoModel().get_repo_by_id(repo_name)
122 by_id_match = RepoModel().get_repo_by_id(repo_name)
123 if by_id_match:
123 if by_id_match:
124 data[1] = by_id_match.repo_name
124 data[1] = by_id_match.repo_name
125
125
126 return safe_str('/'.join(data))
126 return safe_str('/'.join(data))
127
127
128 def _invalidate_cache(self, repo_name):
128 def _invalidate_cache(self, repo_name):
129 """
129 """
130 Set's cache for this repository for invalidation on next access
130 Set's cache for this repository for invalidation on next access
131
131
132 :param repo_name: full repo name, also a cache key
132 :param repo_name: full repo name, also a cache key
133 """
133 """
134 ScmModel().mark_for_invalidation(repo_name)
134 ScmModel().mark_for_invalidation(repo_name)
135
135
136 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
136 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
137 db_repo = Repository.get_by_repo_name(repo_name)
137 db_repo = Repository.get_by_repo_name(repo_name)
138 if not db_repo:
138 if not db_repo:
139 log.debug('Repository `%s` not found inside the database.',
139 log.debug('Repository `%s` not found inside the database.',
140 repo_name)
140 repo_name)
141 return False
141 return False
142
142
143 if db_repo.repo_type != scm_type:
143 if db_repo.repo_type != scm_type:
144 log.warning(
144 log.warning(
145 'Repository `%s` have incorrect scm_type, expected %s got %s',
145 'Repository `%s` have incorrect scm_type, expected %s got %s',
146 repo_name, db_repo.repo_type, scm_type)
146 repo_name, db_repo.repo_type, scm_type)
147 return False
147 return False
148
148
149 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
149 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
150
150
151 def valid_and_active_user(self, user):
151 def valid_and_active_user(self, user):
152 """
152 """
153 Checks if that user is not empty, and if it's actually object it checks
153 Checks if that user is not empty, and if it's actually object it checks
154 if he's active.
154 if he's active.
155
155
156 :param user: user object or None
156 :param user: user object or None
157 :return: boolean
157 :return: boolean
158 """
158 """
159 if user is None:
159 if user is None:
160 return False
160 return False
161
161
162 elif user.active:
162 elif user.active:
163 return True
163 return True
164
164
165 return False
165 return False
166
166
167 def _check_permission(self, action, user, repo_name, ip_addr=None):
167 def _check_permission(self, action, user, repo_name, ip_addr=None):
168 """
168 """
169 Checks permissions using action (push/pull) user and repository
169 Checks permissions using action (push/pull) user and repository
170 name
170 name
171
171
172 :param action: push or pull action
172 :param action: push or pull action
173 :param user: user instance
173 :param user: user instance
174 :param repo_name: repository name
174 :param repo_name: repository name
175 """
175 """
176 # check IP
176 # check IP
177 inherit = user.inherit_default_permissions
177 inherit = user.inherit_default_permissions
178 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
178 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
179 inherit_from_default=inherit)
179 inherit_from_default=inherit)
180 if ip_allowed:
180 if ip_allowed:
181 log.info('Access for IP:%s allowed', ip_addr)
181 log.info('Access for IP:%s allowed', ip_addr)
182 else:
182 else:
183 return False
183 return False
184
184
185 if action == 'push':
185 if action == 'push':
186 if not HasPermissionAnyMiddleware('repository.write',
186 if not HasPermissionAnyMiddleware('repository.write',
187 'repository.admin')(user,
187 'repository.admin')(user,
188 repo_name):
188 repo_name):
189 return False
189 return False
190
190
191 else:
191 else:
192 # any other action need at least read permission
192 # any other action need at least read permission
193 if not HasPermissionAnyMiddleware('repository.read',
193 if not HasPermissionAnyMiddleware('repository.read',
194 'repository.write',
194 'repository.write',
195 'repository.admin')(user,
195 'repository.admin')(user,
196 repo_name):
196 repo_name):
197 return False
197 return False
198
198
199 return True
199 return True
200
200
201 def _check_ssl(self, environ, start_response):
201 def _check_ssl(self, environ, start_response):
202 """
202 """
203 Checks the SSL check flag and returns False if SSL is not present
203 Checks the SSL check flag and returns False if SSL is not present
204 and required True otherwise
204 and required True otherwise
205 """
205 """
206 org_proto = environ['wsgi._org_proto']
206 org_proto = environ['wsgi._org_proto']
207 # check if we have SSL required ! if not it's a bad request !
207 # check if we have SSL required ! if not it's a bad request !
208 require_ssl = str2bool(
208 require_ssl = str2bool(
209 SettingsModel().get_ui_by_key('push_ssl').ui_value)
209 SettingsModel().get_ui_by_key('push_ssl').ui_value)
210 if require_ssl and org_proto == 'http':
210 if require_ssl and org_proto == 'http':
211 log.debug('proto is %s and SSL is required BAD REQUEST !',
211 log.debug('proto is %s and SSL is required BAD REQUEST !',
212 org_proto)
212 org_proto)
213 return False
213 return False
214 return True
214 return True
215
215
216 def __call__(self, environ, start_response):
216 def __call__(self, environ, start_response):
217 try:
217 try:
218 return self._handle_request(environ, start_response)
218 return self._handle_request(environ, start_response)
219 except Exception:
219 except Exception:
220 log.exception("Exception while handling request")
220 log.exception("Exception while handling request")
221 appenlight.track_exception(environ)
221 appenlight.track_exception(environ)
222 return HTTPInternalServerError()(environ, start_response)
222 return HTTPInternalServerError()(environ, start_response)
223 finally:
223 finally:
224 meta.Session.remove()
224 meta.Session.remove()
225
225
226 def _handle_request(self, environ, start_response):
226 def _handle_request(self, environ, start_response):
227
227
228 if not self._check_ssl(environ, start_response):
228 if not self._check_ssl(environ, start_response):
229 reason = ('SSL required, while RhodeCode was unable '
229 reason = ('SSL required, while RhodeCode was unable '
230 'to detect this as SSL request')
230 'to detect this as SSL request')
231 log.debug('User not allowed to proceed, %s', reason)
231 log.debug('User not allowed to proceed, %s', reason)
232 return HTTPNotAcceptable(reason)(environ, start_response)
232 return HTTPNotAcceptable(reason)(environ, start_response)
233
233
234 ip_addr = get_ip_addr(environ)
234 ip_addr = get_ip_addr(environ)
235 username = None
235 username = None
236
236
237 # skip passing error to error controller
237 # skip passing error to error controller
238 environ['pylons.status_code_redirect'] = True
238 environ['pylons.status_code_redirect'] = True
239
239
240 # ======================================================================
240 # ======================================================================
241 # EXTRACT REPOSITORY NAME FROM ENV
241 # EXTRACT REPOSITORY NAME FROM ENV
242 # ======================================================================
242 # ======================================================================
243 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
243 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
244 repo_name = self._get_repository_name(environ)
244 repo_name = self._get_repository_name(environ)
245 environ['REPO_NAME'] = repo_name
245 environ['REPO_NAME'] = repo_name
246 log.debug('Extracted repo name is %s', repo_name)
246 log.debug('Extracted repo name is %s', repo_name)
247
247
248 # check for type, presence in database and on filesystem
248 # check for type, presence in database and on filesystem
249 if not self.is_valid_and_existing_repo(
249 if not self.is_valid_and_existing_repo(
250 repo_name, self.basepath, self.SCM):
250 repo_name, self.basepath, self.SCM):
251 return HTTPNotFound()(environ, start_response)
251 return HTTPNotFound()(environ, start_response)
252
252
253 # ======================================================================
253 # ======================================================================
254 # GET ACTION PULL or PUSH
254 # GET ACTION PULL or PUSH
255 # ======================================================================
255 # ======================================================================
256 action = self._get_action(environ)
256 action = self._get_action(environ)
257
257
258 # ======================================================================
258 # ======================================================================
259 # CHECK ANONYMOUS PERMISSION
259 # CHECK ANONYMOUS PERMISSION
260 # ======================================================================
260 # ======================================================================
261 if action in ['pull', 'push']:
261 if action in ['pull', 'push']:
262 anonymous_user = User.get_default_user()
262 anonymous_user = User.get_default_user()
263 username = anonymous_user.username
263 username = anonymous_user.username
264 if anonymous_user.active:
264 if anonymous_user.active:
265 # ONLY check permissions if the user is activated
265 # ONLY check permissions if the user is activated
266 anonymous_perm = self._check_permission(
266 anonymous_perm = self._check_permission(
267 action, anonymous_user, repo_name, ip_addr)
267 action, anonymous_user, repo_name, ip_addr)
268 else:
268 else:
269 anonymous_perm = False
269 anonymous_perm = False
270
270
271 if not anonymous_user.active or not anonymous_perm:
271 if not anonymous_user.active or not anonymous_perm:
272 if not anonymous_user.active:
272 if not anonymous_user.active:
273 log.debug('Anonymous access is disabled, running '
273 log.debug('Anonymous access is disabled, running '
274 'authentication')
274 'authentication')
275
275
276 if not anonymous_perm:
276 if not anonymous_perm:
277 log.debug('Not enough credentials to access this '
277 log.debug('Not enough credentials to access this '
278 'repository as anonymous user')
278 'repository as anonymous user')
279
279
280 username = None
280 username = None
281 # ==============================================================
281 # ==============================================================
282 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
282 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
283 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
283 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
284 # ==============================================================
284 # ==============================================================
285
285
286 # try to auth based on environ, container auth methods
286 # try to auth based on environ, container auth methods
287 log.debug('Running PRE-AUTH for container based authentication')
287 log.debug('Running PRE-AUTH for container based authentication')
288 pre_auth = authenticate(
288 pre_auth = authenticate(
289 '', '', environ, VCS_TYPE, registry=self.registry)
289 '', '', environ, VCS_TYPE, registry=self.registry)
290 if pre_auth and pre_auth.get('username'):
290 if pre_auth and pre_auth.get('username'):
291 username = pre_auth['username']
291 username = pre_auth['username']
292 log.debug('PRE-AUTH got %s as username', username)
292 log.debug('PRE-AUTH got %s as username', username)
293
293
294 # If not authenticated by the container, running basic auth
294 # If not authenticated by the container, running basic auth
295 if not username:
295 if not username:
296 self.authenticate.realm = get_rhodecode_realm()
296 self.authenticate.realm = get_rhodecode_realm()
297
297
298 try:
298 try:
299 result = self.authenticate(environ)
299 result = self.authenticate(environ)
300 except (UserCreationError, NotAllowedToCreateUserError) as e:
300 except (UserCreationError, NotAllowedToCreateUserError) as e:
301 log.error(e)
301 log.error(e)
302 reason = safe_str(e)
302 reason = safe_str(e)
303 return HTTPNotAcceptable(reason)(environ, start_response)
303 return HTTPNotAcceptable(reason)(environ, start_response)
304
304
305 if isinstance(result, str):
305 if isinstance(result, str):
306 AUTH_TYPE.update(environ, 'basic')
306 AUTH_TYPE.update(environ, 'basic')
307 REMOTE_USER.update(environ, result)
307 REMOTE_USER.update(environ, result)
308 username = result
308 username = result
309 else:
309 else:
310 return result.wsgi_application(environ, start_response)
310 return result.wsgi_application(environ, start_response)
311
311
312 # ==============================================================
312 # ==============================================================
313 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
313 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
314 # ==============================================================
314 # ==============================================================
315 user = User.get_by_username(username)
315 user = User.get_by_username(username)
316 if not self.valid_and_active_user(user):
316 if not self.valid_and_active_user(user):
317 return HTTPForbidden()(environ, start_response)
317 return HTTPForbidden()(environ, start_response)
318 username = user.username
318 username = user.username
319 user.update_lastactivity()
319 user.update_lastactivity()
320 meta.Session().commit()
320 meta.Session().commit()
321
321
322 # check user attributes for password change flag
322 # check user attributes for password change flag
323 user_obj = user
323 user_obj = user
324 if user_obj and user_obj.username != User.DEFAULT_USER and \
324 if user_obj and user_obj.username != User.DEFAULT_USER and \
325 user_obj.user_data.get('force_password_change'):
325 user_obj.user_data.get('force_password_change'):
326 reason = 'password change required'
326 reason = 'password change required'
327 log.debug('User not allowed to authenticate, %s', reason)
327 log.debug('User not allowed to authenticate, %s', reason)
328 return HTTPNotAcceptable(reason)(environ, start_response)
328 return HTTPNotAcceptable(reason)(environ, start_response)
329
329
330 # check permissions for this repository
330 # check permissions for this repository
331 perm = self._check_permission(action, user, repo_name, ip_addr)
331 perm = self._check_permission(action, user, repo_name, ip_addr)
332 if not perm:
332 if not perm:
333 return HTTPForbidden()(environ, start_response)
333 return HTTPForbidden()(environ, start_response)
334
334
335 # extras are injected into UI object and later available
335 # extras are injected into UI object and later available
336 # in hooks executed by rhodecode
336 # in hooks executed by rhodecode
337 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
337 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
338 extras = vcs_operation_context(
338 extras = vcs_operation_context(
339 environ, repo_name=repo_name, username=username,
339 environ, repo_name=repo_name, username=username,
340 action=action, scm=self.SCM,
340 action=action, scm=self.SCM,
341 check_locking=check_locking)
341 check_locking=check_locking)
342
342
343 # ======================================================================
343 # ======================================================================
344 # REQUEST HANDLING
344 # REQUEST HANDLING
345 # ======================================================================
345 # ======================================================================
346 str_repo_name = safe_str(repo_name)
346 str_repo_name = safe_str(repo_name)
347 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
347 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
348 log.debug('Repository path is %s', repo_path)
348 log.debug('Repository path is %s', repo_path)
349
349
350 fix_PATH()
350 fix_PATH()
351
351
352 log.info(
352 log.info(
353 '%s action on %s repo "%s" by "%s" from %s',
353 '%s action on %s repo "%s" by "%s" from %s',
354 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
354 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
355
355
356 return self._generate_vcs_response(
356 return self._generate_vcs_response(
357 environ, start_response, repo_path, repo_name, extras, action)
357 environ, start_response, repo_path, repo_name, extras, action)
358
358
359 @initialize_generator
359 @initialize_generator
360 def _generate_vcs_response(
360 def _generate_vcs_response(
361 self, environ, start_response, repo_path, repo_name, extras,
361 self, environ, start_response, repo_path, repo_name, extras,
362 action):
362 action):
363 """
363 """
364 Returns a generator for the response content.
364 Returns a generator for the response content.
365
365
366 This method is implemented as a generator, so that it can trigger
366 This method is implemented as a generator, so that it can trigger
367 the cache validation after all content sent back to the client. It
367 the cache validation after all content sent back to the client. It
368 also handles the locking exceptions which will be triggered when
368 also handles the locking exceptions which will be triggered when
369 the first chunk is produced by the underlying WSGI application.
369 the first chunk is produced by the underlying WSGI application.
370 """
370 """
371 callback_daemon, extras = self._prepare_callback_daemon(extras)
371 callback_daemon, extras = self._prepare_callback_daemon(extras)
372 config = self._create_config(extras, repo_name)
372 config = self._create_config(extras, repo_name)
373 log.debug('HOOKS extras is %s', extras)
373 log.debug('HOOKS extras is %s', extras)
374 app = self._create_wsgi_app(repo_path, repo_name, config)
374 app = self._create_wsgi_app(repo_path, repo_name, config)
375
375
376 try:
376 try:
377 with callback_daemon:
377 with callback_daemon:
378 try:
378 try:
379 response = app(environ, start_response)
379 response = app(environ, start_response)
380 finally:
380 finally:
381 # This statement works together with the decorator
381 # This statement works together with the decorator
382 # "initialize_generator" above. The decorator ensures that
382 # "initialize_generator" above. The decorator ensures that
383 # we hit the first yield statement before the generator is
383 # we hit the first yield statement before the generator is
384 # returned back to the WSGI server. This is needed to
384 # returned back to the WSGI server. This is needed to
385 # ensure that the call to "app" above triggers the
385 # ensure that the call to "app" above triggers the
386 # needed callback to "start_response" before the
386 # needed callback to "start_response" before the
387 # generator is actually used.
387 # generator is actually used.
388 yield "__init__"
388 yield "__init__"
389
389
390 for chunk in response:
390 for chunk in response:
391 yield chunk
391 yield chunk
392 except Exception as exc:
392 except Exception as exc:
393 # TODO: johbo: Improve "translating" back the exception.
393 # TODO: johbo: Improve "translating" back the exception.
394 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
394 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
395 exc = HTTPLockedRC(*exc.args)
395 exc = HTTPLockedRC(*exc.args)
396 _code = rhodecode.CONFIG.get('lock_ret_code')
396 _code = rhodecode.CONFIG.get('lock_ret_code')
397 log.debug('Repository LOCKED ret code %s!', (_code,))
397 log.debug('Repository LOCKED ret code %s!', (_code,))
398 elif getattr(exc, '_vcs_kind', None) == 'requirement':
398 elif getattr(exc, '_vcs_kind', None) == 'requirement':
399 log.debug(
399 log.debug(
400 'Repository requires features unknown to this Mercurial')
400 'Repository requires features unknown to this Mercurial')
401 exc = HTTPRequirementError(*exc.args)
401 exc = HTTPRequirementError(*exc.args)
402 else:
402 else:
403 raise
403 raise
404
404
405 for chunk in exc(environ, start_response):
405 for chunk in exc(environ, start_response):
406 yield chunk
406 yield chunk
407 finally:
407 finally:
408 # invalidate cache on push
408 # invalidate cache on push
409 if action == 'push':
409 try:
410 self._invalidate_cache(repo_name)
410 if action == 'push':
411 self._invalidate_cache(repo_name)
412 finally:
413 meta.Session.remove()
411
414
412 def _get_repository_name(self, environ):
415 def _get_repository_name(self, environ):
413 """Get repository name out of the environmnent
416 """Get repository name out of the environmnent
414
417
415 :param environ: WSGI environment
418 :param environ: WSGI environment
416 """
419 """
417 raise NotImplementedError()
420 raise NotImplementedError()
418
421
419 def _get_action(self, environ):
422 def _get_action(self, environ):
420 """Map request commands into a pull or push command.
423 """Map request commands into a pull or push command.
421
424
422 :param environ: WSGI environment
425 :param environ: WSGI environment
423 """
426 """
424 raise NotImplementedError()
427 raise NotImplementedError()
425
428
426 def _create_wsgi_app(self, repo_path, repo_name, config):
429 def _create_wsgi_app(self, repo_path, repo_name, config):
427 """Return the WSGI app that will finally handle the request."""
430 """Return the WSGI app that will finally handle the request."""
428 raise NotImplementedError()
431 raise NotImplementedError()
429
432
430 def _create_config(self, extras, repo_name):
433 def _create_config(self, extras, repo_name):
431 """Create a Pyro safe config representation."""
434 """Create a Pyro safe config representation."""
432 raise NotImplementedError()
435 raise NotImplementedError()
433
436
434 def _prepare_callback_daemon(self, extras):
437 def _prepare_callback_daemon(self, extras):
435 return prepare_callback_daemon(
438 return prepare_callback_daemon(
436 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
439 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
437 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
440 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
438
441
439
442
440 def _should_check_locking(query_string):
443 def _should_check_locking(query_string):
441 # this is kind of hacky, but due to how mercurial handles client-server
444 # this is kind of hacky, but due to how mercurial handles client-server
442 # server see all operation on commit; bookmarks, phases and
445 # server see all operation on commit; bookmarks, phases and
443 # obsolescence marker in different transaction, we don't want to check
446 # obsolescence marker in different transaction, we don't want to check
444 # locking on those
447 # locking on those
445 return query_string not in ['cmd=listkeys']
448 return query_string not in ['cmd=listkeys']
@@ -1,86 +1,81 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import logging
22 import logging
23 import pylons
23 import pylons
24 import rhodecode
24 import rhodecode
25
25
26 from pylons.i18n.translation import _get_translator
26 from pylons.i18n.translation import _get_translator
27 from pylons.util import ContextObj
27 from pylons.util import ContextObj
28 from routes.util import URLGenerator
28 from routes.util import URLGenerator
29
29
30 from rhodecode.lib.base import attach_context_attributes, get_auth_user
30 from rhodecode.lib.base import attach_context_attributes, get_auth_user
31 from rhodecode.model import meta
31 from rhodecode.model import meta
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 def pylons_compatibility_tween_factory(handler, registry):
36 def pylons_compatibility_tween_factory(handler, registry):
37 def pylons_compatibility_tween(request):
37 def pylons_compatibility_tween(request):
38 """
38 """
39 While migrating from pylons to pyramid we need to call some pylons code
39 While migrating from pylons to pyramid we need to call some pylons code
40 from pyramid. For example while rendering an old template that uses the
40 from pyramid. For example while rendering an old template that uses the
41 'c' or 'h' objects. This tween sets up the needed pylons globals.
41 'c' or 'h' objects. This tween sets up the needed pylons globals.
42 """
42 """
43 try:
43 config = rhodecode.CONFIG
44 config = rhodecode.CONFIG
44 environ = request.environ
45 environ = request.environ
45 session = request.session
46 session = request.session
46 session_key = (config['pylons.environ_config']
47 session_key = (config['pylons.environ_config']
47 .get('session', 'beaker.session'))
48 .get('session', 'beaker.session'))
49
48
50 # Setup pylons globals.
49 # Setup pylons globals.
51 pylons.config._push_object(config)
50 pylons.config._push_object(config)
52 pylons.request._push_object(request)
51 pylons.request._push_object(request)
53 pylons.session._push_object(session)
52 pylons.session._push_object(session)
54 environ[session_key] = session
53 environ[session_key] = session
55 pylons.url._push_object(URLGenerator(config['routes.map'],
54 pylons.url._push_object(URLGenerator(config['routes.map'],
56 environ))
55 environ))
57
56
58 # TODO: Maybe we should use the language from pyramid.
57 # TODO: Maybe we should use the language from pyramid.
59 translator = _get_translator(config.get('lang'))
58 translator = _get_translator(config.get('lang'))
60 pylons.translator._push_object(translator)
59 pylons.translator._push_object(translator)
61
62 # Get the rhodecode auth user object and make it available.
63 auth_user = get_auth_user(environ)
64 request.user = auth_user
65 environ['rc_auth_user'] = auth_user
66
60
67 # Setup the pylons context object ('c')
61 # Get the rhodecode auth user object and make it available.
68 context = ContextObj()
62 auth_user = get_auth_user(environ)
69 context.rhodecode_user = auth_user
63 request.user = auth_user
70 attach_context_attributes(context, request)
64 environ['rc_auth_user'] = auth_user
71 pylons.tmpl_context._push_object(context)
65
72 return handler(request)
66 # Setup the pylons context object ('c')
73 finally:
67 context = ContextObj()
74 # Dispose current database session and rollback uncommitted
68 context.rhodecode_user = auth_user
75 # transactions.
69 attach_context_attributes(context, request)
76 meta.Session.remove()
70 pylons.tmpl_context._push_object(context)
71 return handler(request)
77
72
78 return pylons_compatibility_tween
73 return pylons_compatibility_tween
79
74
80
75
81 def includeme(config):
76 def includeme(config):
82 config.add_subscriber('rhodecode.subscribers.add_renderer_globals',
77 config.add_subscriber('rhodecode.subscribers.add_renderer_globals',
83 'pyramid.events.BeforeRender')
78 'pyramid.events.BeforeRender')
84 config.add_subscriber('rhodecode.subscribers.add_localizer',
79 config.add_subscriber('rhodecode.subscribers.add_localizer',
85 'pyramid.events.NewRequest')
80 'pyramid.events.NewRequest')
86 config.add_tween('rhodecode.tweens.pylons_compatibility_tween_factory')
81 config.add_tween('rhodecode.tweens.pylons_compatibility_tween_factory')
General Comments 0
You need to be logged in to leave comments. Login now