##// END OF EJS Templates
scm-app: allow to set documented pyro4 as scm_app_implementation....
marcink -
r632:064401fe default
parent child Browse files
Show More
@@ -1,445 +1,445 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:
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 if action == 'push':
410 self._invalidate_cache(repo_name)
410 self._invalidate_cache(repo_name)
411
411
412 def _get_repository_name(self, environ):
412 def _get_repository_name(self, environ):
413 """Get repository name out of the environmnent
413 """Get repository name out of the environmnent
414
414
415 :param environ: WSGI environment
415 :param environ: WSGI environment
416 """
416 """
417 raise NotImplementedError()
417 raise NotImplementedError()
418
418
419 def _get_action(self, environ):
419 def _get_action(self, environ):
420 """Map request commands into a pull or push command.
420 """Map request commands into a pull or push command.
421
421
422 :param environ: WSGI environment
422 :param environ: WSGI environment
423 """
423 """
424 raise NotImplementedError()
424 raise NotImplementedError()
425
425
426 def _create_wsgi_app(self, repo_path, repo_name, config):
426 def _create_wsgi_app(self, repo_path, repo_name, config):
427 """Return the WSGI app that will finally handle the request."""
427 """Return the WSGI app that will finally handle the request."""
428 raise NotImplementedError()
428 raise NotImplementedError()
429
429
430 def _create_config(self, extras, repo_name):
430 def _create_config(self, extras, repo_name):
431 """Create a Pyro safe config representation."""
431 """Create a Pyro safe config representation."""
432 raise NotImplementedError()
432 raise NotImplementedError()
433
433
434 def _prepare_callback_daemon(self, extras):
434 def _prepare_callback_daemon(self, extras):
435 return prepare_callback_daemon(
435 return prepare_callback_daemon(
436 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
436 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
437 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
437 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
438
438
439
439
440 def _should_check_locking(query_string):
440 def _should_check_locking(query_string):
441 # this is kind of hacky, but due to how mercurial handles client-server
441 # this is kind of hacky, but due to how mercurial handles client-server
442 # server see all operation on commit; bookmarks, phases and
442 # server see all operation on commit; bookmarks, phases and
443 # obsolescence marker in different transaction, we don't want to check
443 # obsolescence marker in different transaction, we don't want to check
444 # locking on those
444 # locking on those
445 return query_string not in ['cmd=listkeys']
445 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now