##// END OF EJS Templates
vcs: add translation of _REPO_ID into real repo name into more generic...
marcink -
r755:75333e72 default
parent child Browse files
Show More
@@ -1,452 +1,450 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.lib.vcs.backends import base
49 from rhodecode.lib.vcs.backends import base
50 from rhodecode.model import meta
50 from rhodecode.model import meta
51 from rhodecode.model.db import User, Repository
51 from rhodecode.model.db import User, Repository
52 from rhodecode.model.scm import ScmModel
52 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.settings import SettingsModel
53 from rhodecode.model.settings import SettingsModel
54
54
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58
58
59 def initialize_generator(factory):
59 def initialize_generator(factory):
60 """
60 """
61 Initializes the returned generator by draining its first element.
61 Initializes the returned generator by draining its first element.
62
62
63 This can be used to give a generator an initializer, which is the code
63 This can be used to give a generator an initializer, which is the code
64 up to the first yield statement. This decorator enforces that the first
64 up to the first yield statement. This decorator enforces that the first
65 produced element has the value ``"__init__"`` to make its special
65 produced element has the value ``"__init__"`` to make its special
66 purpose very explicit in the using code.
66 purpose very explicit in the using code.
67 """
67 """
68
68
69 @wraps(factory)
69 @wraps(factory)
70 def wrapper(*args, **kwargs):
70 def wrapper(*args, **kwargs):
71 gen = factory(*args, **kwargs)
71 gen = factory(*args, **kwargs)
72 try:
72 try:
73 init = gen.next()
73 init = gen.next()
74 except StopIteration:
74 except StopIteration:
75 raise ValueError('Generator must yield at least one element.')
75 raise ValueError('Generator must yield at least one element.')
76 if init != "__init__":
76 if init != "__init__":
77 raise ValueError('First yielded element must be "__init__".')
77 raise ValueError('First yielded element must be "__init__".')
78 return gen
78 return gen
79 return wrapper
79 return wrapper
80
80
81
81
82 class SimpleVCS(object):
82 class SimpleVCS(object):
83 """Common functionality for SCM HTTP handlers."""
83 """Common functionality for SCM HTTP handlers."""
84
84
85 SCM = 'unknown'
85 SCM = 'unknown'
86
86
87 def __init__(self, application, config, registry):
87 def __init__(self, application, config, registry):
88 self.registry = registry
88 self.registry = registry
89 self.application = application
89 self.application = application
90 self.config = config
90 self.config = config
91 # re-populated by specialized middlewares
91 # re-populated by specialized middlewares
92 self.repo_vcs_config = base.Config()
92 self.repo_vcs_config = base.Config()
93 # base path of repo locations
93 # base path of repo locations
94 self.basepath = get_rhodecode_base_path()
94 self.basepath = get_rhodecode_base_path()
95 # authenticate this VCS request using authfunc
95 # authenticate this VCS request using authfunc
96 auth_ret_code_detection = \
96 auth_ret_code_detection = \
97 str2bool(self.config.get('auth_ret_code_detection', False))
97 str2bool(self.config.get('auth_ret_code_detection', False))
98 self.authenticate = BasicAuth(
98 self.authenticate = BasicAuth(
99 '', authenticate, registry, config.get('auth_ret_code'),
99 '', authenticate, registry, config.get('auth_ret_code'),
100 auth_ret_code_detection)
100 auth_ret_code_detection)
101 self.ip_addr = '0.0.0.0'
101 self.ip_addr = '0.0.0.0'
102
102
103 @property
103 @property
104 def scm_app(self):
104 def scm_app(self):
105 custom_implementation = self.config.get('vcs.scm_app_implementation')
105 custom_implementation = self.config.get('vcs.scm_app_implementation')
106 if custom_implementation and custom_implementation != 'pyro4':
106 if custom_implementation and custom_implementation != 'pyro4':
107 log.info(
107 log.info(
108 "Using custom implementation of scm_app: %s",
108 "Using custom implementation of scm_app: %s",
109 custom_implementation)
109 custom_implementation)
110 scm_app_impl = importlib.import_module(custom_implementation)
110 scm_app_impl = importlib.import_module(custom_implementation)
111 else:
111 else:
112 scm_app_impl = scm_app
112 scm_app_impl = scm_app
113 return scm_app_impl
113 return scm_app_impl
114
114
115 def _get_by_id(self, repo_name):
115 def _get_by_id(self, repo_name):
116 """
116 """
117 Gets a special pattern _<ID> from clone url and tries to replace it
117 Gets a special pattern _<ID> from clone url and tries to replace it
118 with a repository_name for support of _<ID> non changable urls
118 with a repository_name for support of _<ID> non changable urls
119
119
120 :param repo_name:
120 :param repo_name:
121 """
121 """
122
122
123 data = repo_name.split('/')
123 data = repo_name.split('/')
124 if len(data) >= 2:
124 if len(data) >= 2:
125 from rhodecode.model.repo import RepoModel
125 from rhodecode.model.repo import RepoModel
126 by_id_match = RepoModel().get_repo_by_id(repo_name)
126 by_id_match = RepoModel().get_repo_by_id(repo_name)
127 if by_id_match:
127 if by_id_match:
128 data[1] = by_id_match.repo_name
128 data[1] = by_id_match.repo_name
129
129
130 return safe_str('/'.join(data))
130 return safe_str('/'.join(data))
131
131
132 def _invalidate_cache(self, repo_name):
132 def _invalidate_cache(self, repo_name):
133 """
133 """
134 Set's cache for this repository for invalidation on next access
134 Set's cache for this repository for invalidation on next access
135
135
136 :param repo_name: full repo name, also a cache key
136 :param repo_name: full repo name, also a cache key
137 """
137 """
138 ScmModel().mark_for_invalidation(repo_name)
138 ScmModel().mark_for_invalidation(repo_name)
139
139
140 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
140 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
141 db_repo = Repository.get_by_repo_name(repo_name)
141 db_repo = Repository.get_by_repo_name(repo_name)
142 if not db_repo:
142 if not db_repo:
143 log.debug('Repository `%s` not found inside the database.',
143 log.debug('Repository `%s` not found inside the database.',
144 repo_name)
144 repo_name)
145 return False
145 return False
146
146
147 if db_repo.repo_type != scm_type:
147 if db_repo.repo_type != scm_type:
148 log.warning(
148 log.warning(
149 'Repository `%s` have incorrect scm_type, expected %s got %s',
149 'Repository `%s` have incorrect scm_type, expected %s got %s',
150 repo_name, db_repo.repo_type, scm_type)
150 repo_name, db_repo.repo_type, scm_type)
151 return False
151 return False
152
152
153 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
153 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
154
154
155 def valid_and_active_user(self, user):
155 def valid_and_active_user(self, user):
156 """
156 """
157 Checks if that user is not empty, and if it's actually object it checks
157 Checks if that user is not empty, and if it's actually object it checks
158 if he's active.
158 if he's active.
159
159
160 :param user: user object or None
160 :param user: user object or None
161 :return: boolean
161 :return: boolean
162 """
162 """
163 if user is None:
163 if user is None:
164 return False
164 return False
165
165
166 elif user.active:
166 elif user.active:
167 return True
167 return True
168
168
169 return False
169 return False
170
170
171 def _check_permission(self, action, user, repo_name, ip_addr=None):
171 def _check_permission(self, action, user, repo_name, ip_addr=None):
172 """
172 """
173 Checks permissions using action (push/pull) user and repository
173 Checks permissions using action (push/pull) user and repository
174 name
174 name
175
175
176 :param action: push or pull action
176 :param action: push or pull action
177 :param user: user instance
177 :param user: user instance
178 :param repo_name: repository name
178 :param repo_name: repository name
179 """
179 """
180 # check IP
180 # check IP
181 inherit = user.inherit_default_permissions
181 inherit = user.inherit_default_permissions
182 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
182 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
183 inherit_from_default=inherit)
183 inherit_from_default=inherit)
184 if ip_allowed:
184 if ip_allowed:
185 log.info('Access for IP:%s allowed', ip_addr)
185 log.info('Access for IP:%s allowed', ip_addr)
186 else:
186 else:
187 return False
187 return False
188
188
189 if action == 'push':
189 if action == 'push':
190 if not HasPermissionAnyMiddleware('repository.write',
190 if not HasPermissionAnyMiddleware('repository.write',
191 'repository.admin')(user,
191 'repository.admin')(user,
192 repo_name):
192 repo_name):
193 return False
193 return False
194
194
195 else:
195 else:
196 # any other action need at least read permission
196 # any other action need at least read permission
197 if not HasPermissionAnyMiddleware('repository.read',
197 if not HasPermissionAnyMiddleware('repository.read',
198 'repository.write',
198 'repository.write',
199 'repository.admin')(user,
199 'repository.admin')(user,
200 repo_name):
200 repo_name):
201 return False
201 return False
202
202
203 return True
203 return True
204
204
205 def _check_ssl(self, environ, start_response):
205 def _check_ssl(self, environ, start_response):
206 """
206 """
207 Checks the SSL check flag and returns False if SSL is not present
207 Checks the SSL check flag and returns False if SSL is not present
208 and required True otherwise
208 and required True otherwise
209 """
209 """
210 org_proto = environ['wsgi._org_proto']
210 org_proto = environ['wsgi._org_proto']
211 # check if we have SSL required ! if not it's a bad request !
211 # check if we have SSL required ! if not it's a bad request !
212 require_ssl = str2bool(
212 require_ssl = str2bool(
213 SettingsModel().get_ui_by_key('push_ssl').ui_value)
213 SettingsModel().get_ui_by_key('push_ssl').ui_value)
214 if require_ssl and org_proto == 'http':
214 if require_ssl and org_proto == 'http':
215 log.debug('proto is %s and SSL is required BAD REQUEST !',
215 log.debug('proto is %s and SSL is required BAD REQUEST !',
216 org_proto)
216 org_proto)
217 return False
217 return False
218 return True
218 return True
219
219
220 def __call__(self, environ, start_response):
220 def __call__(self, environ, start_response):
221 try:
221 try:
222 return self._handle_request(environ, start_response)
222 return self._handle_request(environ, start_response)
223 except Exception:
223 except Exception:
224 log.exception("Exception while handling request")
224 log.exception("Exception while handling request")
225 appenlight.track_exception(environ)
225 appenlight.track_exception(environ)
226 return HTTPInternalServerError()(environ, start_response)
226 return HTTPInternalServerError()(environ, start_response)
227 finally:
227 finally:
228 meta.Session.remove()
228 meta.Session.remove()
229
229
230 def _handle_request(self, environ, start_response):
230 def _handle_request(self, environ, start_response):
231
231
232 if not self._check_ssl(environ, start_response):
232 if not self._check_ssl(environ, start_response):
233 reason = ('SSL required, while RhodeCode was unable '
233 reason = ('SSL required, while RhodeCode was unable '
234 'to detect this as SSL request')
234 'to detect this as SSL request')
235 log.debug('User not allowed to proceed, %s', reason)
235 log.debug('User not allowed to proceed, %s', reason)
236 return HTTPNotAcceptable(reason)(environ, start_response)
236 return HTTPNotAcceptable(reason)(environ, start_response)
237
237
238 ip_addr = get_ip_addr(environ)
238 ip_addr = get_ip_addr(environ)
239 username = None
239 username = None
240
240
241 # skip passing error to error controller
241 # skip passing error to error controller
242 environ['pylons.status_code_redirect'] = True
242 environ['pylons.status_code_redirect'] = True
243
243
244 # ======================================================================
244 # ======================================================================
245 # EXTRACT REPOSITORY NAME FROM ENV
245 # EXTRACT REPOSITORY NAME FROM ENV SET IN `class VCSMiddleware`
246 # ======================================================================
246 # ======================================================================
247 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
247 repo_name = environ['REPO_NAME']
248 repo_name = self._get_repository_name(environ)
249 environ['REPO_NAME'] = repo_name
250 log.debug('Extracted repo name is %s', repo_name)
248 log.debug('Extracted repo name is %s', repo_name)
251
249
252 # check for type, presence in database and on filesystem
250 # check for type, presence in database and on filesystem
253 if not self.is_valid_and_existing_repo(
251 if not self.is_valid_and_existing_repo(
254 repo_name, self.basepath, self.SCM):
252 repo_name, self.basepath, self.SCM):
255 return HTTPNotFound()(environ, start_response)
253 return HTTPNotFound()(environ, start_response)
256
254
257 # ======================================================================
255 # ======================================================================
258 # GET ACTION PULL or PUSH
256 # GET ACTION PULL or PUSH
259 # ======================================================================
257 # ======================================================================
260 action = self._get_action(environ)
258 action = self._get_action(environ)
261
259
262 # ======================================================================
260 # ======================================================================
263 # CHECK ANONYMOUS PERMISSION
261 # CHECK ANONYMOUS PERMISSION
264 # ======================================================================
262 # ======================================================================
265 if action in ['pull', 'push']:
263 if action in ['pull', 'push']:
266 anonymous_user = User.get_default_user()
264 anonymous_user = User.get_default_user()
267 username = anonymous_user.username
265 username = anonymous_user.username
268 if anonymous_user.active:
266 if anonymous_user.active:
269 # ONLY check permissions if the user is activated
267 # ONLY check permissions if the user is activated
270 anonymous_perm = self._check_permission(
268 anonymous_perm = self._check_permission(
271 action, anonymous_user, repo_name, ip_addr)
269 action, anonymous_user, repo_name, ip_addr)
272 else:
270 else:
273 anonymous_perm = False
271 anonymous_perm = False
274
272
275 if not anonymous_user.active or not anonymous_perm:
273 if not anonymous_user.active or not anonymous_perm:
276 if not anonymous_user.active:
274 if not anonymous_user.active:
277 log.debug('Anonymous access is disabled, running '
275 log.debug('Anonymous access is disabled, running '
278 'authentication')
276 'authentication')
279
277
280 if not anonymous_perm:
278 if not anonymous_perm:
281 log.debug('Not enough credentials to access this '
279 log.debug('Not enough credentials to access this '
282 'repository as anonymous user')
280 'repository as anonymous user')
283
281
284 username = None
282 username = None
285 # ==============================================================
283 # ==============================================================
286 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
284 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
287 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
285 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
288 # ==============================================================
286 # ==============================================================
289
287
290 # try to auth based on environ, container auth methods
288 # try to auth based on environ, container auth methods
291 log.debug('Running PRE-AUTH for container based authentication')
289 log.debug('Running PRE-AUTH for container based authentication')
292 pre_auth = authenticate(
290 pre_auth = authenticate(
293 '', '', environ, VCS_TYPE, registry=self.registry)
291 '', '', environ, VCS_TYPE, registry=self.registry)
294 if pre_auth and pre_auth.get('username'):
292 if pre_auth and pre_auth.get('username'):
295 username = pre_auth['username']
293 username = pre_auth['username']
296 log.debug('PRE-AUTH got %s as username', username)
294 log.debug('PRE-AUTH got %s as username', username)
297
295
298 # If not authenticated by the container, running basic auth
296 # If not authenticated by the container, running basic auth
299 if not username:
297 if not username:
300 self.authenticate.realm = get_rhodecode_realm()
298 self.authenticate.realm = get_rhodecode_realm()
301
299
302 try:
300 try:
303 result = self.authenticate(environ)
301 result = self.authenticate(environ)
304 except (UserCreationError, NotAllowedToCreateUserError) as e:
302 except (UserCreationError, NotAllowedToCreateUserError) as e:
305 log.error(e)
303 log.error(e)
306 reason = safe_str(e)
304 reason = safe_str(e)
307 return HTTPNotAcceptable(reason)(environ, start_response)
305 return HTTPNotAcceptable(reason)(environ, start_response)
308
306
309 if isinstance(result, str):
307 if isinstance(result, str):
310 AUTH_TYPE.update(environ, 'basic')
308 AUTH_TYPE.update(environ, 'basic')
311 REMOTE_USER.update(environ, result)
309 REMOTE_USER.update(environ, result)
312 username = result
310 username = result
313 else:
311 else:
314 return result.wsgi_application(environ, start_response)
312 return result.wsgi_application(environ, start_response)
315
313
316 # ==============================================================
314 # ==============================================================
317 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
315 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
318 # ==============================================================
316 # ==============================================================
319 user = User.get_by_username(username)
317 user = User.get_by_username(username)
320 if not self.valid_and_active_user(user):
318 if not self.valid_and_active_user(user):
321 return HTTPForbidden()(environ, start_response)
319 return HTTPForbidden()(environ, start_response)
322 username = user.username
320 username = user.username
323 user.update_lastactivity()
321 user.update_lastactivity()
324 meta.Session().commit()
322 meta.Session().commit()
325
323
326 # check user attributes for password change flag
324 # check user attributes for password change flag
327 user_obj = user
325 user_obj = user
328 if user_obj and user_obj.username != User.DEFAULT_USER and \
326 if user_obj and user_obj.username != User.DEFAULT_USER and \
329 user_obj.user_data.get('force_password_change'):
327 user_obj.user_data.get('force_password_change'):
330 reason = 'password change required'
328 reason = 'password change required'
331 log.debug('User not allowed to authenticate, %s', reason)
329 log.debug('User not allowed to authenticate, %s', reason)
332 return HTTPNotAcceptable(reason)(environ, start_response)
330 return HTTPNotAcceptable(reason)(environ, start_response)
333
331
334 # check permissions for this repository
332 # check permissions for this repository
335 perm = self._check_permission(action, user, repo_name, ip_addr)
333 perm = self._check_permission(action, user, repo_name, ip_addr)
336 if not perm:
334 if not perm:
337 return HTTPForbidden()(environ, start_response)
335 return HTTPForbidden()(environ, start_response)
338
336
339 # extras are injected into UI object and later available
337 # extras are injected into UI object and later available
340 # in hooks executed by rhodecode
338 # in hooks executed by rhodecode
341 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
339 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
342 extras = vcs_operation_context(
340 extras = vcs_operation_context(
343 environ, repo_name=repo_name, username=username,
341 environ, repo_name=repo_name, username=username,
344 action=action, scm=self.SCM,
342 action=action, scm=self.SCM,
345 check_locking=check_locking)
343 check_locking=check_locking)
346
344
347 # ======================================================================
345 # ======================================================================
348 # REQUEST HANDLING
346 # REQUEST HANDLING
349 # ======================================================================
347 # ======================================================================
350 str_repo_name = safe_str(repo_name)
348 str_repo_name = safe_str(repo_name)
351 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
349 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
352 log.debug('Repository path is %s', repo_path)
350 log.debug('Repository path is %s', repo_path)
353
351
354 fix_PATH()
352 fix_PATH()
355
353
356 log.info(
354 log.info(
357 '%s action on %s repo "%s" by "%s" from %s',
355 '%s action on %s repo "%s" by "%s" from %s',
358 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
356 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
359
357
360 return self._generate_vcs_response(
358 return self._generate_vcs_response(
361 environ, start_response, repo_path, repo_name, extras, action)
359 environ, start_response, repo_path, repo_name, extras, action)
362
360
363 @initialize_generator
361 @initialize_generator
364 def _generate_vcs_response(
362 def _generate_vcs_response(
365 self, environ, start_response, repo_path, repo_name, extras,
363 self, environ, start_response, repo_path, repo_name, extras,
366 action):
364 action):
367 """
365 """
368 Returns a generator for the response content.
366 Returns a generator for the response content.
369
367
370 This method is implemented as a generator, so that it can trigger
368 This method is implemented as a generator, so that it can trigger
371 the cache validation after all content sent back to the client. It
369 the cache validation after all content sent back to the client. It
372 also handles the locking exceptions which will be triggered when
370 also handles the locking exceptions which will be triggered when
373 the first chunk is produced by the underlying WSGI application.
371 the first chunk is produced by the underlying WSGI application.
374 """
372 """
375 callback_daemon, extras = self._prepare_callback_daemon(extras)
373 callback_daemon, extras = self._prepare_callback_daemon(extras)
376 config = self._create_config(extras, repo_name)
374 config = self._create_config(extras, repo_name)
377 log.debug('HOOKS extras is %s', extras)
375 log.debug('HOOKS extras is %s', extras)
378 app = self._create_wsgi_app(repo_path, repo_name, config)
376 app = self._create_wsgi_app(repo_path, repo_name, config)
379
377
380 try:
378 try:
381 with callback_daemon:
379 with callback_daemon:
382 try:
380 try:
383 response = app(environ, start_response)
381 response = app(environ, start_response)
384 finally:
382 finally:
385 # This statement works together with the decorator
383 # This statement works together with the decorator
386 # "initialize_generator" above. The decorator ensures that
384 # "initialize_generator" above. The decorator ensures that
387 # we hit the first yield statement before the generator is
385 # we hit the first yield statement before the generator is
388 # returned back to the WSGI server. This is needed to
386 # returned back to the WSGI server. This is needed to
389 # ensure that the call to "app" above triggers the
387 # ensure that the call to "app" above triggers the
390 # needed callback to "start_response" before the
388 # needed callback to "start_response" before the
391 # generator is actually used.
389 # generator is actually used.
392 yield "__init__"
390 yield "__init__"
393
391
394 for chunk in response:
392 for chunk in response:
395 yield chunk
393 yield chunk
396 except Exception as exc:
394 except Exception as exc:
397 # TODO: johbo: Improve "translating" back the exception.
395 # TODO: johbo: Improve "translating" back the exception.
398 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
396 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
399 exc = HTTPLockedRC(*exc.args)
397 exc = HTTPLockedRC(*exc.args)
400 _code = rhodecode.CONFIG.get('lock_ret_code')
398 _code = rhodecode.CONFIG.get('lock_ret_code')
401 log.debug('Repository LOCKED ret code %s!', (_code,))
399 log.debug('Repository LOCKED ret code %s!', (_code,))
402 elif getattr(exc, '_vcs_kind', None) == 'requirement':
400 elif getattr(exc, '_vcs_kind', None) == 'requirement':
403 log.debug(
401 log.debug(
404 'Repository requires features unknown to this Mercurial')
402 'Repository requires features unknown to this Mercurial')
405 exc = HTTPRequirementError(*exc.args)
403 exc = HTTPRequirementError(*exc.args)
406 else:
404 else:
407 raise
405 raise
408
406
409 for chunk in exc(environ, start_response):
407 for chunk in exc(environ, start_response):
410 yield chunk
408 yield chunk
411 finally:
409 finally:
412 # invalidate cache on push
410 # invalidate cache on push
413 try:
411 try:
414 if action == 'push':
412 if action == 'push':
415 self._invalidate_cache(repo_name)
413 self._invalidate_cache(repo_name)
416 finally:
414 finally:
417 meta.Session.remove()
415 meta.Session.remove()
418
416
419 def _get_repository_name(self, environ):
417 def _get_repository_name(self, environ):
420 """Get repository name out of the environmnent
418 """Get repository name out of the environmnent
421
419
422 :param environ: WSGI environment
420 :param environ: WSGI environment
423 """
421 """
424 raise NotImplementedError()
422 raise NotImplementedError()
425
423
426 def _get_action(self, environ):
424 def _get_action(self, environ):
427 """Map request commands into a pull or push command.
425 """Map request commands into a pull or push command.
428
426
429 :param environ: WSGI environment
427 :param environ: WSGI environment
430 """
428 """
431 raise NotImplementedError()
429 raise NotImplementedError()
432
430
433 def _create_wsgi_app(self, repo_path, repo_name, config):
431 def _create_wsgi_app(self, repo_path, repo_name, config):
434 """Return the WSGI app that will finally handle the request."""
432 """Return the WSGI app that will finally handle the request."""
435 raise NotImplementedError()
433 raise NotImplementedError()
436
434
437 def _create_config(self, extras, repo_name):
435 def _create_config(self, extras, repo_name):
438 """Create a Pyro safe config representation."""
436 """Create a Pyro safe config representation."""
439 raise NotImplementedError()
437 raise NotImplementedError()
440
438
441 def _prepare_callback_daemon(self, extras):
439 def _prepare_callback_daemon(self, extras):
442 return prepare_callback_daemon(
440 return prepare_callback_daemon(
443 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
441 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
444 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
442 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
445
443
446
444
447 def _should_check_locking(query_string):
445 def _should_check_locking(query_string):
448 # this is kind of hacky, but due to how mercurial handles client-server
446 # this is kind of hacky, but due to how mercurial handles client-server
449 # server see all operation on commit; bookmarks, phases and
447 # server see all operation on commit; bookmarks, phases and
450 # obsolescence marker in different transaction, we don't want to check
448 # obsolescence marker in different transaction, we don't want to check
451 # locking on those
449 # locking on those
452 return query_string not in ['cmd=listkeys']
450 return query_string not in ['cmd=listkeys']
@@ -1,178 +1,183 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 gzip
21 import gzip
22 import shutil
22 import shutil
23 import logging
23 import logging
24 import tempfile
24 import tempfile
25 import urlparse
25 import urlparse
26
26
27 import rhodecode
27 import rhodecode
28 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
28 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
29 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
29 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
30 from rhodecode.lib.middleware.simplehg import SimpleHg
30 from rhodecode.lib.middleware.simplehg import SimpleHg
31 from rhodecode.lib.middleware.simplesvn import SimpleSvn
31 from rhodecode.lib.middleware.simplesvn import SimpleSvn
32 from rhodecode.lib.vcs.backends import base
32 from rhodecode.lib.vcs.backends import base
33 from rhodecode.model.settings import VcsSettingsModel
33 from rhodecode.model.settings import VcsSettingsModel
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 def is_git(environ):
38 def is_git(environ):
39 """
39 """
40 Returns True if requests should be handled by GIT wsgi middleware
40 Returns True if requests should be handled by GIT wsgi middleware
41 """
41 """
42 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
42 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
43 log.debug(
43 log.debug(
44 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
44 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
45 is_git_path is not None)
45 is_git_path is not None)
46
46
47 return is_git_path
47 return is_git_path
48
48
49
49
50 def is_hg(environ):
50 def is_hg(environ):
51 """
51 """
52 Returns True if requests target is mercurial server - header
52 Returns True if requests target is mercurial server - header
53 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
53 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
54 """
54 """
55 is_hg_path = False
55 is_hg_path = False
56
56
57 http_accept = environ.get('HTTP_ACCEPT')
57 http_accept = environ.get('HTTP_ACCEPT')
58
58
59 if http_accept and http_accept.startswith('application/mercurial'):
59 if http_accept and http_accept.startswith('application/mercurial'):
60 query = urlparse.parse_qs(environ['QUERY_STRING'])
60 query = urlparse.parse_qs(environ['QUERY_STRING'])
61 if 'cmd' in query:
61 if 'cmd' in query:
62 is_hg_path = True
62 is_hg_path = True
63
63
64 log.debug(
64 log.debug(
65 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
65 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
66 is_hg_path)
66 is_hg_path)
67
67
68 return is_hg_path
68 return is_hg_path
69
69
70
70
71 def is_svn(environ):
71 def is_svn(environ):
72 """
72 """
73 Returns True if requests target is Subversion server
73 Returns True if requests target is Subversion server
74 """
74 """
75 http_dav = environ.get('HTTP_DAV', '')
75 http_dav = environ.get('HTTP_DAV', '')
76 magic_path_segment = rhodecode.CONFIG.get(
76 magic_path_segment = rhodecode.CONFIG.get(
77 'rhodecode_subversion_magic_path', '/!svn')
77 'rhodecode_subversion_magic_path', '/!svn')
78 is_svn_path = (
78 is_svn_path = (
79 'subversion' in http_dav or
79 'subversion' in http_dav or
80 magic_path_segment in environ['PATH_INFO'])
80 magic_path_segment in environ['PATH_INFO'])
81 log.debug(
81 log.debug(
82 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
82 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
83 is_svn_path)
83 is_svn_path)
84
84
85 return is_svn_path
85 return is_svn_path
86
86
87
87
88 class GunzipMiddleware(object):
88 class GunzipMiddleware(object):
89 """
89 """
90 WSGI middleware that unzips gzip-encoded requests before
90 WSGI middleware that unzips gzip-encoded requests before
91 passing on to the underlying application.
91 passing on to the underlying application.
92 """
92 """
93
93
94 def __init__(self, application):
94 def __init__(self, application):
95 self.app = application
95 self.app = application
96
96
97 def __call__(self, environ, start_response):
97 def __call__(self, environ, start_response):
98 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
98 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
99
99
100 if b'gzip' in accepts_encoding_header:
100 if b'gzip' in accepts_encoding_header:
101 log.debug('gzip detected, now running gunzip wrapper')
101 log.debug('gzip detected, now running gunzip wrapper')
102 wsgi_input = environ['wsgi.input']
102 wsgi_input = environ['wsgi.input']
103
103
104 if not hasattr(environ['wsgi.input'], 'seek'):
104 if not hasattr(environ['wsgi.input'], 'seek'):
105 # The gzip implementation in the standard library of Python 2.x
105 # The gzip implementation in the standard library of Python 2.x
106 # requires the '.seek()' and '.tell()' methods to be available
106 # requires the '.seek()' and '.tell()' methods to be available
107 # on the input stream. Read the data into a temporary file to
107 # on the input stream. Read the data into a temporary file to
108 # work around this limitation.
108 # work around this limitation.
109
109
110 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
110 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
111 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
111 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
112 wsgi_input.seek(0)
112 wsgi_input.seek(0)
113
113
114 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
114 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
115 # since we "Ungzipped" the content we say now it's no longer gzip
115 # since we "Ungzipped" the content we say now it's no longer gzip
116 # content encoding
116 # content encoding
117 del environ['HTTP_CONTENT_ENCODING']
117 del environ['HTTP_CONTENT_ENCODING']
118
118
119 # content length has changes ? or i'm not sure
119 # content length has changes ? or i'm not sure
120 if 'CONTENT_LENGTH' in environ:
120 if 'CONTENT_LENGTH' in environ:
121 del environ['CONTENT_LENGTH']
121 del environ['CONTENT_LENGTH']
122 else:
122 else:
123 log.debug('content not gzipped, gzipMiddleware passing '
123 log.debug('content not gzipped, gzipMiddleware passing '
124 'request further')
124 'request further')
125 return self.app(environ, start_response)
125 return self.app(environ, start_response)
126
126
127
127
128 class VCSMiddleware(object):
128 class VCSMiddleware(object):
129
129
130 def __init__(self, app, config, appenlight_client, registry):
130 def __init__(self, app, config, appenlight_client, registry):
131 self.application = app
131 self.application = app
132 self.config = config
132 self.config = config
133 self.appenlight_client = appenlight_client
133 self.appenlight_client = appenlight_client
134 self.registry = registry
134 self.registry = registry
135 self.repo_vcs_config = base.Config()
135 self.repo_vcs_config = base.Config()
136 self.use_gzip = True
136 self.use_gzip = True
137
137
138 def vcs_config(self, repo_name=None):
138 def vcs_config(self, repo_name=None):
139 """
139 """
140 returns serialized VcsSettings
140 returns serialized VcsSettings
141 """
141 """
142 return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
142 return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
143
143
144 def wrap_in_gzip_if_enabled(self, app):
144 def wrap_in_gzip_if_enabled(self, app):
145 if self.use_gzip:
145 if self.use_gzip:
146 app = GunzipMiddleware(app)
146 app = GunzipMiddleware(app)
147 return app
147 return app
148
148
149 def _get_handler_app(self, environ):
149 def _get_handler_app(self, environ):
150 app = None
150 app = None
151
151
152 if is_hg(environ):
152 if is_hg(environ):
153 app = SimpleHg(self.application, self.config, self.registry)
153 app = SimpleHg(self.application, self.config, self.registry)
154
154
155 if is_git(environ):
155 if is_git(environ):
156 app = SimpleGit(self.application, self.config, self.registry)
156 app = SimpleGit(self.application, self.config, self.registry)
157
157
158 if is_svn(environ):
158 if is_svn(environ):
159 app = SimpleSvn(self.application, self.config, self.registry)
159 app = SimpleSvn(self.application, self.config, self.registry)
160
160
161 if app:
161 if app:
162 # translate the _REPO_ID into real repo NAME for usage
163 # in midddleware
164 environ['PATH_INFO'] = app._get_by_id(environ['PATH_INFO'])
162 repo_name = app._get_repository_name(environ)
165 repo_name = app._get_repository_name(environ)
166 environ['REPO_NAME'] = repo_name
167
163 self.repo_vcs_config = self.vcs_config(repo_name)
168 self.repo_vcs_config = self.vcs_config(repo_name)
164 app.repo_vcs_config = self.repo_vcs_config
169 app.repo_vcs_config = self.repo_vcs_config
165
170
166 app = self.wrap_in_gzip_if_enabled(app)
171 app = self.wrap_in_gzip_if_enabled(app)
167 app, _ = wrap_in_appenlight_if_enabled(
172 app, _ = wrap_in_appenlight_if_enabled(
168 app, self.config, self.appenlight_client)
173 app, self.config, self.appenlight_client)
169
174
170 return app
175 return app
171
176
172 def __call__(self, environ, start_response):
177 def __call__(self, environ, start_response):
173 # check if we handle one of interesting protocols ?
178 # check if we handle one of interesting protocols ?
174 vcs_handler = self._get_handler_app(environ)
179 vcs_handler = self._get_handler_app(environ)
175 if vcs_handler:
180 if vcs_handler:
176 return vcs_handler(environ, start_response)
181 return vcs_handler(environ, start_response)
177
182
178 return self.application(environ, start_response)
183 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now