##// END OF EJS Templates
simplevcs: fixed logic of extraction of base_path
marcink -
r2362:8956a981 default
parent child Browse files
Show More
@@ -1,605 +1,609 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 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 re
27 import re
28 import logging
28 import logging
29 import importlib
29 import importlib
30 from functools import wraps
30 from functools import wraps
31
31
32 import time
32 import time
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 # TODO(marcink): check if we should use webob.exc here ?
34 # TODO(marcink): check if we should use webob.exc here ?
35 from pyramid.httpexceptions import (
35 from pyramid.httpexceptions import (
36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37
37
38 import rhodecode
38 import rhodecode
39 from rhodecode.authentication.base import (
39 from rhodecode.authentication.base import (
40 authenticate, get_perms_cache_manager, VCS_TYPE)
40 authenticate, get_perms_cache_manager, VCS_TYPE)
41 from rhodecode.lib import caches
41 from rhodecode.lib import caches
42 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
42 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
43 from rhodecode.lib.base import (
43 from rhodecode.lib.base import (
44 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
44 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
45 from rhodecode.lib.exceptions import (
45 from rhodecode.lib.exceptions import (
46 HTTPLockedRC, HTTPRequirementError, UserCreationError,
46 HTTPLockedRC, HTTPRequirementError, UserCreationError,
47 NotAllowedToCreateUserError)
47 NotAllowedToCreateUserError)
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.middleware import appenlight
49 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware.utils import scm_app_http
50 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.backends import base
54 from rhodecode.lib.vcs.backends import base
55 from rhodecode.model import meta
55 from rhodecode.model import meta
56 from rhodecode.model.db import User, Repository, PullRequest
56 from rhodecode.model.db import User, Repository, PullRequest
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.pull_request import PullRequestModel
58 from rhodecode.model.pull_request import PullRequestModel
59 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
59 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 def initialize_generator(factory):
64 def initialize_generator(factory):
65 """
65 """
66 Initializes the returned generator by draining its first element.
66 Initializes the returned generator by draining its first element.
67
67
68 This can be used to give a generator an initializer, which is the code
68 This can be used to give a generator an initializer, which is the code
69 up to the first yield statement. This decorator enforces that the first
69 up to the first yield statement. This decorator enforces that the first
70 produced element has the value ``"__init__"`` to make its special
70 produced element has the value ``"__init__"`` to make its special
71 purpose very explicit in the using code.
71 purpose very explicit in the using code.
72 """
72 """
73
73
74 @wraps(factory)
74 @wraps(factory)
75 def wrapper(*args, **kwargs):
75 def wrapper(*args, **kwargs):
76 gen = factory(*args, **kwargs)
76 gen = factory(*args, **kwargs)
77 try:
77 try:
78 init = gen.next()
78 init = gen.next()
79 except StopIteration:
79 except StopIteration:
80 raise ValueError('Generator must yield at least one element.')
80 raise ValueError('Generator must yield at least one element.')
81 if init != "__init__":
81 if init != "__init__":
82 raise ValueError('First yielded element must be "__init__".')
82 raise ValueError('First yielded element must be "__init__".')
83 return gen
83 return gen
84 return wrapper
84 return wrapper
85
85
86
86
87 class SimpleVCS(object):
87 class SimpleVCS(object):
88 """Common functionality for SCM HTTP handlers."""
88 """Common functionality for SCM HTTP handlers."""
89
89
90 SCM = 'unknown'
90 SCM = 'unknown'
91
91
92 acl_repo_name = None
92 acl_repo_name = None
93 url_repo_name = None
93 url_repo_name = None
94 vcs_repo_name = None
94 vcs_repo_name = None
95
95
96 # We have to handle requests to shadow repositories different than requests
96 # We have to handle requests to shadow repositories different than requests
97 # to normal repositories. Therefore we have to distinguish them. To do this
97 # to normal repositories. Therefore we have to distinguish them. To do this
98 # we use this regex which will match only on URLs pointing to shadow
98 # we use this regex which will match only on URLs pointing to shadow
99 # repositories.
99 # repositories.
100 shadow_repo_re = re.compile(
100 shadow_repo_re = re.compile(
101 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
101 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
102 '(?P<target>{slug_pat})/' # target repo
102 '(?P<target>{slug_pat})/' # target repo
103 'pull-request/(?P<pr_id>\d+)/' # pull request
103 'pull-request/(?P<pr_id>\d+)/' # pull request
104 'repository$' # shadow repo
104 'repository$' # shadow repo
105 .format(slug_pat=SLUG_RE.pattern))
105 .format(slug_pat=SLUG_RE.pattern))
106
106
107 def __init__(self, config, registry):
107 def __init__(self, config, registry):
108 self.registry = registry
108 self.registry = registry
109 self.config = config
109 self.config = config
110 # re-populated by specialized middleware
110 # re-populated by specialized middleware
111 self.repo_vcs_config = base.Config()
111 self.repo_vcs_config = base.Config()
112 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
112 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
113
113
114 registry.rhodecode_settings = self.rhodecode_settings
114 registry.rhodecode_settings = self.rhodecode_settings
115 # authenticate this VCS request using authfunc
115 # authenticate this VCS request using authfunc
116 auth_ret_code_detection = \
116 auth_ret_code_detection = \
117 str2bool(self.config.get('auth_ret_code_detection', False))
117 str2bool(self.config.get('auth_ret_code_detection', False))
118 self.authenticate = BasicAuth(
118 self.authenticate = BasicAuth(
119 '', authenticate, registry, config.get('auth_ret_code'),
119 '', authenticate, registry, config.get('auth_ret_code'),
120 auth_ret_code_detection)
120 auth_ret_code_detection)
121 self.ip_addr = '0.0.0.0'
121 self.ip_addr = '0.0.0.0'
122
122
123 @property
123 @property
124 def base_path(self):
124 def base_path(self):
125 return self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
125 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
126 if not settings_path:
127 # try, maybe we passed in explicitly as config option
128 settings_path = self.config.get('base_path')
129 return settings_path
126
130
127 def set_repo_names(self, environ):
131 def set_repo_names(self, environ):
128 """
132 """
129 This will populate the attributes acl_repo_name, url_repo_name,
133 This will populate the attributes acl_repo_name, url_repo_name,
130 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
134 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
131 shadow) repositories all names are equal. In case of requests to a
135 shadow) repositories all names are equal. In case of requests to a
132 shadow repository the acl-name points to the target repo of the pull
136 shadow repository the acl-name points to the target repo of the pull
133 request and the vcs-name points to the shadow repo file system path.
137 request and the vcs-name points to the shadow repo file system path.
134 The url-name is always the URL used by the vcs client program.
138 The url-name is always the URL used by the vcs client program.
135
139
136 Example in case of a shadow repo:
140 Example in case of a shadow repo:
137 acl_repo_name = RepoGroup/MyRepo
141 acl_repo_name = RepoGroup/MyRepo
138 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
142 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
139 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
143 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
140 """
144 """
141 # First we set the repo name from URL for all attributes. This is the
145 # First we set the repo name from URL for all attributes. This is the
142 # default if handling normal (non shadow) repo requests.
146 # default if handling normal (non shadow) repo requests.
143 self.url_repo_name = self._get_repository_name(environ)
147 self.url_repo_name = self._get_repository_name(environ)
144 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
148 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
145 self.is_shadow_repo = False
149 self.is_shadow_repo = False
146
150
147 # Check if this is a request to a shadow repository.
151 # Check if this is a request to a shadow repository.
148 match = self.shadow_repo_re.match(self.url_repo_name)
152 match = self.shadow_repo_re.match(self.url_repo_name)
149 if match:
153 if match:
150 match_dict = match.groupdict()
154 match_dict = match.groupdict()
151
155
152 # Build acl repo name from regex match.
156 # Build acl repo name from regex match.
153 acl_repo_name = safe_unicode('{groups}{target}'.format(
157 acl_repo_name = safe_unicode('{groups}{target}'.format(
154 groups=match_dict['groups'] or '',
158 groups=match_dict['groups'] or '',
155 target=match_dict['target']))
159 target=match_dict['target']))
156
160
157 # Retrieve pull request instance by ID from regex match.
161 # Retrieve pull request instance by ID from regex match.
158 pull_request = PullRequest.get(match_dict['pr_id'])
162 pull_request = PullRequest.get(match_dict['pr_id'])
159
163
160 # Only proceed if we got a pull request and if acl repo name from
164 # Only proceed if we got a pull request and if acl repo name from
161 # URL equals the target repo name of the pull request.
165 # URL equals the target repo name of the pull request.
162 if pull_request and (acl_repo_name ==
166 if pull_request and (acl_repo_name ==
163 pull_request.target_repo.repo_name):
167 pull_request.target_repo.repo_name):
164 # Get file system path to shadow repository.
168 # Get file system path to shadow repository.
165 workspace_id = PullRequestModel()._workspace_id(pull_request)
169 workspace_id = PullRequestModel()._workspace_id(pull_request)
166 target_vcs = pull_request.target_repo.scm_instance()
170 target_vcs = pull_request.target_repo.scm_instance()
167 vcs_repo_name = target_vcs._get_shadow_repository_path(
171 vcs_repo_name = target_vcs._get_shadow_repository_path(
168 workspace_id)
172 workspace_id)
169
173
170 # Store names for later usage.
174 # Store names for later usage.
171 self.vcs_repo_name = vcs_repo_name
175 self.vcs_repo_name = vcs_repo_name
172 self.acl_repo_name = acl_repo_name
176 self.acl_repo_name = acl_repo_name
173 self.is_shadow_repo = True
177 self.is_shadow_repo = True
174
178
175 log.debug('Setting all VCS repository names: %s', {
179 log.debug('Setting all VCS repository names: %s', {
176 'acl_repo_name': self.acl_repo_name,
180 'acl_repo_name': self.acl_repo_name,
177 'url_repo_name': self.url_repo_name,
181 'url_repo_name': self.url_repo_name,
178 'vcs_repo_name': self.vcs_repo_name,
182 'vcs_repo_name': self.vcs_repo_name,
179 })
183 })
180
184
181 @property
185 @property
182 def scm_app(self):
186 def scm_app(self):
183 custom_implementation = self.config['vcs.scm_app_implementation']
187 custom_implementation = self.config['vcs.scm_app_implementation']
184 if custom_implementation == 'http':
188 if custom_implementation == 'http':
185 log.info('Using HTTP implementation of scm app.')
189 log.info('Using HTTP implementation of scm app.')
186 scm_app_impl = scm_app_http
190 scm_app_impl = scm_app_http
187 else:
191 else:
188 log.info('Using custom implementation of scm_app: "{}"'.format(
192 log.info('Using custom implementation of scm_app: "{}"'.format(
189 custom_implementation))
193 custom_implementation))
190 scm_app_impl = importlib.import_module(custom_implementation)
194 scm_app_impl = importlib.import_module(custom_implementation)
191 return scm_app_impl
195 return scm_app_impl
192
196
193 def _get_by_id(self, repo_name):
197 def _get_by_id(self, repo_name):
194 """
198 """
195 Gets a special pattern _<ID> from clone url and tries to replace it
199 Gets a special pattern _<ID> from clone url and tries to replace it
196 with a repository_name for support of _<ID> non changeable urls
200 with a repository_name for support of _<ID> non changeable urls
197 """
201 """
198
202
199 data = repo_name.split('/')
203 data = repo_name.split('/')
200 if len(data) >= 2:
204 if len(data) >= 2:
201 from rhodecode.model.repo import RepoModel
205 from rhodecode.model.repo import RepoModel
202 by_id_match = RepoModel().get_repo_by_id(repo_name)
206 by_id_match = RepoModel().get_repo_by_id(repo_name)
203 if by_id_match:
207 if by_id_match:
204 data[1] = by_id_match.repo_name
208 data[1] = by_id_match.repo_name
205
209
206 return safe_str('/'.join(data))
210 return safe_str('/'.join(data))
207
211
208 def _invalidate_cache(self, repo_name):
212 def _invalidate_cache(self, repo_name):
209 """
213 """
210 Set's cache for this repository for invalidation on next access
214 Set's cache for this repository for invalidation on next access
211
215
212 :param repo_name: full repo name, also a cache key
216 :param repo_name: full repo name, also a cache key
213 """
217 """
214 ScmModel().mark_for_invalidation(repo_name)
218 ScmModel().mark_for_invalidation(repo_name)
215
219
216 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
220 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
217 db_repo = Repository.get_by_repo_name(repo_name)
221 db_repo = Repository.get_by_repo_name(repo_name)
218 if not db_repo:
222 if not db_repo:
219 log.debug('Repository `%s` not found inside the database.',
223 log.debug('Repository `%s` not found inside the database.',
220 repo_name)
224 repo_name)
221 return False
225 return False
222
226
223 if db_repo.repo_type != scm_type:
227 if db_repo.repo_type != scm_type:
224 log.warning(
228 log.warning(
225 'Repository `%s` have incorrect scm_type, expected %s got %s',
229 'Repository `%s` have incorrect scm_type, expected %s got %s',
226 repo_name, db_repo.repo_type, scm_type)
230 repo_name, db_repo.repo_type, scm_type)
227 return False
231 return False
228
232
229 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
233 return is_valid_repo(repo_name, base_path, explicit_scm=scm_type)
230
234
231 def valid_and_active_user(self, user):
235 def valid_and_active_user(self, user):
232 """
236 """
233 Checks if that user is not empty, and if it's actually object it checks
237 Checks if that user is not empty, and if it's actually object it checks
234 if he's active.
238 if he's active.
235
239
236 :param user: user object or None
240 :param user: user object or None
237 :return: boolean
241 :return: boolean
238 """
242 """
239 if user is None:
243 if user is None:
240 return False
244 return False
241
245
242 elif user.active:
246 elif user.active:
243 return True
247 return True
244
248
245 return False
249 return False
246
250
247 @property
251 @property
248 def is_shadow_repo_dir(self):
252 def is_shadow_repo_dir(self):
249 return os.path.isdir(self.vcs_repo_name)
253 return os.path.isdir(self.vcs_repo_name)
250
254
251 def _check_permission(self, action, user, repo_name, ip_addr=None,
255 def _check_permission(self, action, user, repo_name, ip_addr=None,
252 plugin_id='', plugin_cache_active=False, cache_ttl=0):
256 plugin_id='', plugin_cache_active=False, cache_ttl=0):
253 """
257 """
254 Checks permissions using action (push/pull) user and repository
258 Checks permissions using action (push/pull) user and repository
255 name. If plugin_cache and ttl is set it will use the plugin which
259 name. If plugin_cache and ttl is set it will use the plugin which
256 authenticated the user to store the cached permissions result for N
260 authenticated the user to store the cached permissions result for N
257 amount of seconds as in cache_ttl
261 amount of seconds as in cache_ttl
258
262
259 :param action: push or pull action
263 :param action: push or pull action
260 :param user: user instance
264 :param user: user instance
261 :param repo_name: repository name
265 :param repo_name: repository name
262 """
266 """
263
267
264 # get instance of cache manager configured for a namespace
268 # get instance of cache manager configured for a namespace
265 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
269 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
266 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
270 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
267 plugin_id, plugin_cache_active, cache_ttl)
271 plugin_id, plugin_cache_active, cache_ttl)
268
272
269 # for environ based password can be empty, but then the validation is
273 # for environ based password can be empty, but then the validation is
270 # on the server that fills in the env data needed for authentication
274 # on the server that fills in the env data needed for authentication
271 _perm_calc_hash = caches.compute_key_from_params(
275 _perm_calc_hash = caches.compute_key_from_params(
272 plugin_id, action, user.user_id, repo_name, ip_addr)
276 plugin_id, action, user.user_id, repo_name, ip_addr)
273
277
274 # _authenticate is a wrapper for .auth() method of plugin.
278 # _authenticate is a wrapper for .auth() method of plugin.
275 # it checks if .auth() sends proper data.
279 # it checks if .auth() sends proper data.
276 # For RhodeCodeExternalAuthPlugin it also maps users to
280 # For RhodeCodeExternalAuthPlugin it also maps users to
277 # Database and maps the attributes returned from .auth()
281 # Database and maps the attributes returned from .auth()
278 # to RhodeCode database. If this function returns data
282 # to RhodeCode database. If this function returns data
279 # then auth is correct.
283 # then auth is correct.
280 start = time.time()
284 start = time.time()
281 log.debug('Running plugin `%s` permissions check', plugin_id)
285 log.debug('Running plugin `%s` permissions check', plugin_id)
282
286
283 def perm_func():
287 def perm_func():
284 """
288 """
285 This function is used internally in Cache of Beaker to calculate
289 This function is used internally in Cache of Beaker to calculate
286 Results
290 Results
287 """
291 """
288 log.debug('auth: calculating permission access now...')
292 log.debug('auth: calculating permission access now...')
289 # check IP
293 # check IP
290 inherit = user.inherit_default_permissions
294 inherit = user.inherit_default_permissions
291 ip_allowed = AuthUser.check_ip_allowed(
295 ip_allowed = AuthUser.check_ip_allowed(
292 user.user_id, ip_addr, inherit_from_default=inherit)
296 user.user_id, ip_addr, inherit_from_default=inherit)
293 if ip_allowed:
297 if ip_allowed:
294 log.info('Access for IP:%s allowed', ip_addr)
298 log.info('Access for IP:%s allowed', ip_addr)
295 else:
299 else:
296 return False
300 return False
297
301
298 if action == 'push':
302 if action == 'push':
299 perms = ('repository.write', 'repository.admin')
303 perms = ('repository.write', 'repository.admin')
300 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
304 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
301 return False
305 return False
302
306
303 else:
307 else:
304 # any other action need at least read permission
308 # any other action need at least read permission
305 perms = (
309 perms = (
306 'repository.read', 'repository.write', 'repository.admin')
310 'repository.read', 'repository.write', 'repository.admin')
307 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
311 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
308 return False
312 return False
309
313
310 return True
314 return True
311
315
312 if plugin_cache_active:
316 if plugin_cache_active:
313 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
317 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
314 perm_result = cache_manager.get(
318 perm_result = cache_manager.get(
315 _perm_calc_hash, createfunc=perm_func)
319 _perm_calc_hash, createfunc=perm_func)
316 else:
320 else:
317 perm_result = perm_func()
321 perm_result = perm_func()
318
322
319 auth_time = time.time() - start
323 auth_time = time.time() - start
320 log.debug('Permissions for plugin `%s` completed in %.3fs, '
324 log.debug('Permissions for plugin `%s` completed in %.3fs, '
321 'expiration time of fetched cache %.1fs.',
325 'expiration time of fetched cache %.1fs.',
322 plugin_id, auth_time, cache_ttl)
326 plugin_id, auth_time, cache_ttl)
323
327
324 return perm_result
328 return perm_result
325
329
326 def _check_ssl(self, environ, start_response):
330 def _check_ssl(self, environ, start_response):
327 """
331 """
328 Checks the SSL check flag and returns False if SSL is not present
332 Checks the SSL check flag and returns False if SSL is not present
329 and required True otherwise
333 and required True otherwise
330 """
334 """
331 org_proto = environ['wsgi._org_proto']
335 org_proto = environ['wsgi._org_proto']
332 # check if we have SSL required ! if not it's a bad request !
336 # check if we have SSL required ! if not it's a bad request !
333 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
337 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
334 if require_ssl and org_proto == 'http':
338 if require_ssl and org_proto == 'http':
335 log.debug('proto is %s and SSL is required BAD REQUEST !',
339 log.debug('proto is %s and SSL is required BAD REQUEST !',
336 org_proto)
340 org_proto)
337 return False
341 return False
338 return True
342 return True
339
343
340 def __call__(self, environ, start_response):
344 def __call__(self, environ, start_response):
341 try:
345 try:
342 return self._handle_request(environ, start_response)
346 return self._handle_request(environ, start_response)
343 except Exception:
347 except Exception:
344 log.exception("Exception while handling request")
348 log.exception("Exception while handling request")
345 appenlight.track_exception(environ)
349 appenlight.track_exception(environ)
346 return HTTPInternalServerError()(environ, start_response)
350 return HTTPInternalServerError()(environ, start_response)
347 finally:
351 finally:
348 meta.Session.remove()
352 meta.Session.remove()
349
353
350 def _handle_request(self, environ, start_response):
354 def _handle_request(self, environ, start_response):
351
355
352 if not self._check_ssl(environ, start_response):
356 if not self._check_ssl(environ, start_response):
353 reason = ('SSL required, while RhodeCode was unable '
357 reason = ('SSL required, while RhodeCode was unable '
354 'to detect this as SSL request')
358 'to detect this as SSL request')
355 log.debug('User not allowed to proceed, %s', reason)
359 log.debug('User not allowed to proceed, %s', reason)
356 return HTTPNotAcceptable(reason)(environ, start_response)
360 return HTTPNotAcceptable(reason)(environ, start_response)
357
361
358 if not self.url_repo_name:
362 if not self.url_repo_name:
359 log.warning('Repository name is empty: %s', self.url_repo_name)
363 log.warning('Repository name is empty: %s', self.url_repo_name)
360 # failed to get repo name, we fail now
364 # failed to get repo name, we fail now
361 return HTTPNotFound()(environ, start_response)
365 return HTTPNotFound()(environ, start_response)
362 log.debug('Extracted repo name is %s', self.url_repo_name)
366 log.debug('Extracted repo name is %s', self.url_repo_name)
363
367
364 ip_addr = get_ip_addr(environ)
368 ip_addr = get_ip_addr(environ)
365 user_agent = get_user_agent(environ)
369 user_agent = get_user_agent(environ)
366 username = None
370 username = None
367
371
368 # skip passing error to error controller
372 # skip passing error to error controller
369 environ['pylons.status_code_redirect'] = True
373 environ['pylons.status_code_redirect'] = True
370
374
371 # ======================================================================
375 # ======================================================================
372 # GET ACTION PULL or PUSH
376 # GET ACTION PULL or PUSH
373 # ======================================================================
377 # ======================================================================
374 action = self._get_action(environ)
378 action = self._get_action(environ)
375
379
376 # ======================================================================
380 # ======================================================================
377 # Check if this is a request to a shadow repository of a pull request.
381 # Check if this is a request to a shadow repository of a pull request.
378 # In this case only pull action is allowed.
382 # In this case only pull action is allowed.
379 # ======================================================================
383 # ======================================================================
380 if self.is_shadow_repo and action != 'pull':
384 if self.is_shadow_repo and action != 'pull':
381 reason = 'Only pull action is allowed for shadow repositories.'
385 reason = 'Only pull action is allowed for shadow repositories.'
382 log.debug('User not allowed to proceed, %s', reason)
386 log.debug('User not allowed to proceed, %s', reason)
383 return HTTPNotAcceptable(reason)(environ, start_response)
387 return HTTPNotAcceptable(reason)(environ, start_response)
384
388
385 # Check if the shadow repo actually exists, in case someone refers
389 # Check if the shadow repo actually exists, in case someone refers
386 # to it, and it has been deleted because of successful merge.
390 # to it, and it has been deleted because of successful merge.
387 if self.is_shadow_repo and not self.is_shadow_repo_dir:
391 if self.is_shadow_repo and not self.is_shadow_repo_dir:
388 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
392 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
389 self.is_shadow_repo_dir)
393 self.is_shadow_repo_dir)
390 return HTTPNotFound()(environ, start_response)
394 return HTTPNotFound()(environ, start_response)
391
395
392 # ======================================================================
396 # ======================================================================
393 # CHECK ANONYMOUS PERMISSION
397 # CHECK ANONYMOUS PERMISSION
394 # ======================================================================
398 # ======================================================================
395 if action in ['pull', 'push']:
399 if action in ['pull', 'push']:
396 anonymous_user = User.get_default_user()
400 anonymous_user = User.get_default_user()
397 username = anonymous_user.username
401 username = anonymous_user.username
398 if anonymous_user.active:
402 if anonymous_user.active:
399 # ONLY check permissions if the user is activated
403 # ONLY check permissions if the user is activated
400 anonymous_perm = self._check_permission(
404 anonymous_perm = self._check_permission(
401 action, anonymous_user, self.acl_repo_name, ip_addr)
405 action, anonymous_user, self.acl_repo_name, ip_addr)
402 else:
406 else:
403 anonymous_perm = False
407 anonymous_perm = False
404
408
405 if not anonymous_user.active or not anonymous_perm:
409 if not anonymous_user.active or not anonymous_perm:
406 if not anonymous_user.active:
410 if not anonymous_user.active:
407 log.debug('Anonymous access is disabled, running '
411 log.debug('Anonymous access is disabled, running '
408 'authentication')
412 'authentication')
409
413
410 if not anonymous_perm:
414 if not anonymous_perm:
411 log.debug('Not enough credentials to access this '
415 log.debug('Not enough credentials to access this '
412 'repository as anonymous user')
416 'repository as anonymous user')
413
417
414 username = None
418 username = None
415 # ==============================================================
419 # ==============================================================
416 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
420 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
417 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
421 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
418 # ==============================================================
422 # ==============================================================
419
423
420 # try to auth based on environ, container auth methods
424 # try to auth based on environ, container auth methods
421 log.debug('Running PRE-AUTH for container based authentication')
425 log.debug('Running PRE-AUTH for container based authentication')
422 pre_auth = authenticate(
426 pre_auth = authenticate(
423 '', '', environ, VCS_TYPE, registry=self.registry,
427 '', '', environ, VCS_TYPE, registry=self.registry,
424 acl_repo_name=self.acl_repo_name)
428 acl_repo_name=self.acl_repo_name)
425 if pre_auth and pre_auth.get('username'):
429 if pre_auth and pre_auth.get('username'):
426 username = pre_auth['username']
430 username = pre_auth['username']
427 log.debug('PRE-AUTH got %s as username', username)
431 log.debug('PRE-AUTH got %s as username', username)
428 if pre_auth:
432 if pre_auth:
429 log.debug('PRE-AUTH successful from %s',
433 log.debug('PRE-AUTH successful from %s',
430 pre_auth.get('auth_data', {}).get('_plugin'))
434 pre_auth.get('auth_data', {}).get('_plugin'))
431
435
432 # If not authenticated by the container, running basic auth
436 # If not authenticated by the container, running basic auth
433 # before inject the calling repo_name for special scope checks
437 # before inject the calling repo_name for special scope checks
434 self.authenticate.acl_repo_name = self.acl_repo_name
438 self.authenticate.acl_repo_name = self.acl_repo_name
435
439
436 plugin_cache_active, cache_ttl = False, 0
440 plugin_cache_active, cache_ttl = False, 0
437 plugin = None
441 plugin = None
438 if not username:
442 if not username:
439 self.authenticate.realm = self.authenticate.get_rc_realm()
443 self.authenticate.realm = self.authenticate.get_rc_realm()
440
444
441 try:
445 try:
442 auth_result = self.authenticate(environ)
446 auth_result = self.authenticate(environ)
443 except (UserCreationError, NotAllowedToCreateUserError) as e:
447 except (UserCreationError, NotAllowedToCreateUserError) as e:
444 log.error(e)
448 log.error(e)
445 reason = safe_str(e)
449 reason = safe_str(e)
446 return HTTPNotAcceptable(reason)(environ, start_response)
450 return HTTPNotAcceptable(reason)(environ, start_response)
447
451
448 if isinstance(auth_result, dict):
452 if isinstance(auth_result, dict):
449 AUTH_TYPE.update(environ, 'basic')
453 AUTH_TYPE.update(environ, 'basic')
450 REMOTE_USER.update(environ, auth_result['username'])
454 REMOTE_USER.update(environ, auth_result['username'])
451 username = auth_result['username']
455 username = auth_result['username']
452 plugin = auth_result.get('auth_data', {}).get('_plugin')
456 plugin = auth_result.get('auth_data', {}).get('_plugin')
453 log.info(
457 log.info(
454 'MAIN-AUTH successful for user `%s` from %s plugin',
458 'MAIN-AUTH successful for user `%s` from %s plugin',
455 username, plugin)
459 username, plugin)
456
460
457 plugin_cache_active, cache_ttl = auth_result.get(
461 plugin_cache_active, cache_ttl = auth_result.get(
458 'auth_data', {}).get('_ttl_cache') or (False, 0)
462 'auth_data', {}).get('_ttl_cache') or (False, 0)
459 else:
463 else:
460 return auth_result.wsgi_application(
464 return auth_result.wsgi_application(
461 environ, start_response)
465 environ, start_response)
462
466
463
467
464 # ==============================================================
468 # ==============================================================
465 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
469 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
466 # ==============================================================
470 # ==============================================================
467 user = User.get_by_username(username)
471 user = User.get_by_username(username)
468 if not self.valid_and_active_user(user):
472 if not self.valid_and_active_user(user):
469 return HTTPForbidden()(environ, start_response)
473 return HTTPForbidden()(environ, start_response)
470 username = user.username
474 username = user.username
471 user.update_lastactivity()
475 user.update_lastactivity()
472 meta.Session().commit()
476 meta.Session().commit()
473
477
474 # check user attributes for password change flag
478 # check user attributes for password change flag
475 user_obj = user
479 user_obj = user
476 if user_obj and user_obj.username != User.DEFAULT_USER and \
480 if user_obj and user_obj.username != User.DEFAULT_USER and \
477 user_obj.user_data.get('force_password_change'):
481 user_obj.user_data.get('force_password_change'):
478 reason = 'password change required'
482 reason = 'password change required'
479 log.debug('User not allowed to authenticate, %s', reason)
483 log.debug('User not allowed to authenticate, %s', reason)
480 return HTTPNotAcceptable(reason)(environ, start_response)
484 return HTTPNotAcceptable(reason)(environ, start_response)
481
485
482 # check permissions for this repository
486 # check permissions for this repository
483 perm = self._check_permission(
487 perm = self._check_permission(
484 action, user, self.acl_repo_name, ip_addr,
488 action, user, self.acl_repo_name, ip_addr,
485 plugin, plugin_cache_active, cache_ttl)
489 plugin, plugin_cache_active, cache_ttl)
486 if not perm:
490 if not perm:
487 return HTTPForbidden()(environ, start_response)
491 return HTTPForbidden()(environ, start_response)
488
492
489 # extras are injected into UI object and later available
493 # extras are injected into UI object and later available
490 # in hooks executed by RhodeCode
494 # in hooks executed by RhodeCode
491 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
495 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
492 extras = vcs_operation_context(
496 extras = vcs_operation_context(
493 environ, repo_name=self.acl_repo_name, username=username,
497 environ, repo_name=self.acl_repo_name, username=username,
494 action=action, scm=self.SCM, check_locking=check_locking,
498 action=action, scm=self.SCM, check_locking=check_locking,
495 is_shadow_repo=self.is_shadow_repo
499 is_shadow_repo=self.is_shadow_repo
496 )
500 )
497
501
498 # ======================================================================
502 # ======================================================================
499 # REQUEST HANDLING
503 # REQUEST HANDLING
500 # ======================================================================
504 # ======================================================================
501 repo_path = os.path.join(
505 repo_path = os.path.join(
502 safe_str(self.base_path), safe_str(self.vcs_repo_name))
506 safe_str(self.base_path), safe_str(self.vcs_repo_name))
503 log.debug('Repository path is %s', repo_path)
507 log.debug('Repository path is %s', repo_path)
504
508
505 fix_PATH()
509 fix_PATH()
506
510
507 log.info(
511 log.info(
508 '%s action on %s repo "%s" by "%s" from %s %s',
512 '%s action on %s repo "%s" by "%s" from %s %s',
509 action, self.SCM, safe_str(self.url_repo_name),
513 action, self.SCM, safe_str(self.url_repo_name),
510 safe_str(username), ip_addr, user_agent)
514 safe_str(username), ip_addr, user_agent)
511
515
512 return self._generate_vcs_response(
516 return self._generate_vcs_response(
513 environ, start_response, repo_path, extras, action)
517 environ, start_response, repo_path, extras, action)
514
518
515 @initialize_generator
519 @initialize_generator
516 def _generate_vcs_response(
520 def _generate_vcs_response(
517 self, environ, start_response, repo_path, extras, action):
521 self, environ, start_response, repo_path, extras, action):
518 """
522 """
519 Returns a generator for the response content.
523 Returns a generator for the response content.
520
524
521 This method is implemented as a generator, so that it can trigger
525 This method is implemented as a generator, so that it can trigger
522 the cache validation after all content sent back to the client. It
526 the cache validation after all content sent back to the client. It
523 also handles the locking exceptions which will be triggered when
527 also handles the locking exceptions which will be triggered when
524 the first chunk is produced by the underlying WSGI application.
528 the first chunk is produced by the underlying WSGI application.
525 """
529 """
526 callback_daemon, extras = self._prepare_callback_daemon(extras)
530 callback_daemon, extras = self._prepare_callback_daemon(extras)
527 config = self._create_config(extras, self.acl_repo_name)
531 config = self._create_config(extras, self.acl_repo_name)
528 log.debug('HOOKS extras is %s', extras)
532 log.debug('HOOKS extras is %s', extras)
529 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
533 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
530
534
531 try:
535 try:
532 with callback_daemon:
536 with callback_daemon:
533 try:
537 try:
534 response = app(environ, start_response)
538 response = app(environ, start_response)
535 finally:
539 finally:
536 # This statement works together with the decorator
540 # This statement works together with the decorator
537 # "initialize_generator" above. The decorator ensures that
541 # "initialize_generator" above. The decorator ensures that
538 # we hit the first yield statement before the generator is
542 # we hit the first yield statement before the generator is
539 # returned back to the WSGI server. This is needed to
543 # returned back to the WSGI server. This is needed to
540 # ensure that the call to "app" above triggers the
544 # ensure that the call to "app" above triggers the
541 # needed callback to "start_response" before the
545 # needed callback to "start_response" before the
542 # generator is actually used.
546 # generator is actually used.
543 yield "__init__"
547 yield "__init__"
544
548
545 for chunk in response:
549 for chunk in response:
546 yield chunk
550 yield chunk
547 except Exception as exc:
551 except Exception as exc:
548 # TODO: martinb: Exceptions are only raised in case of the Pyro4
552 # TODO: martinb: Exceptions are only raised in case of the Pyro4
549 # backend. Refactor this except block after dropping Pyro4 support.
553 # backend. Refactor this except block after dropping Pyro4 support.
550 # TODO: johbo: Improve "translating" back the exception.
554 # TODO: johbo: Improve "translating" back the exception.
551 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
555 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
552 exc = HTTPLockedRC(*exc.args)
556 exc = HTTPLockedRC(*exc.args)
553 _code = rhodecode.CONFIG.get('lock_ret_code')
557 _code = rhodecode.CONFIG.get('lock_ret_code')
554 log.debug('Repository LOCKED ret code %s!', (_code,))
558 log.debug('Repository LOCKED ret code %s!', (_code,))
555 elif getattr(exc, '_vcs_kind', None) == 'requirement':
559 elif getattr(exc, '_vcs_kind', None) == 'requirement':
556 log.debug(
560 log.debug(
557 'Repository requires features unknown to this Mercurial')
561 'Repository requires features unknown to this Mercurial')
558 exc = HTTPRequirementError(*exc.args)
562 exc = HTTPRequirementError(*exc.args)
559 else:
563 else:
560 raise
564 raise
561
565
562 for chunk in exc(environ, start_response):
566 for chunk in exc(environ, start_response):
563 yield chunk
567 yield chunk
564 finally:
568 finally:
565 # invalidate cache on push
569 # invalidate cache on push
566 try:
570 try:
567 if action == 'push':
571 if action == 'push':
568 self._invalidate_cache(self.url_repo_name)
572 self._invalidate_cache(self.url_repo_name)
569 finally:
573 finally:
570 meta.Session.remove()
574 meta.Session.remove()
571
575
572 def _get_repository_name(self, environ):
576 def _get_repository_name(self, environ):
573 """Get repository name out of the environmnent
577 """Get repository name out of the environmnent
574
578
575 :param environ: WSGI environment
579 :param environ: WSGI environment
576 """
580 """
577 raise NotImplementedError()
581 raise NotImplementedError()
578
582
579 def _get_action(self, environ):
583 def _get_action(self, environ):
580 """Map request commands into a pull or push command.
584 """Map request commands into a pull or push command.
581
585
582 :param environ: WSGI environment
586 :param environ: WSGI environment
583 """
587 """
584 raise NotImplementedError()
588 raise NotImplementedError()
585
589
586 def _create_wsgi_app(self, repo_path, repo_name, config):
590 def _create_wsgi_app(self, repo_path, repo_name, config):
587 """Return the WSGI app that will finally handle the request."""
591 """Return the WSGI app that will finally handle the request."""
588 raise NotImplementedError()
592 raise NotImplementedError()
589
593
590 def _create_config(self, extras, repo_name):
594 def _create_config(self, extras, repo_name):
591 """Create a safe config representation."""
595 """Create a safe config representation."""
592 raise NotImplementedError()
596 raise NotImplementedError()
593
597
594 def _prepare_callback_daemon(self, extras):
598 def _prepare_callback_daemon(self, extras):
595 return prepare_callback_daemon(
599 return prepare_callback_daemon(
596 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
600 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
597 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
601 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
598
602
599
603
600 def _should_check_locking(query_string):
604 def _should_check_locking(query_string):
601 # this is kind of hacky, but due to how mercurial handles client-server
605 # this is kind of hacky, but due to how mercurial handles client-server
602 # server see all operation on commit; bookmarks, phases and
606 # server see all operation on commit; bookmarks, phases and
603 # obsolescence marker in different transaction, we don't want to check
607 # obsolescence marker in different transaction, we don't want to check
604 # locking on those
608 # locking on those
605 return query_string not in ['cmd=listkeys']
609 return query_string not in ['cmd=listkeys']
@@ -1,233 +1,241 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 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 from webob.exc import HTTPNotFound
27 from webob.exc import HTTPNotFound
28
28
29 import rhodecode
29 import rhodecode
30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
32 from rhodecode.lib.middleware.simplehg import SimpleHg
32 from rhodecode.lib.middleware.simplehg import SimpleHg
33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
34 from rhodecode.model.settings import VcsSettingsModel
34 from rhodecode.model.settings import VcsSettingsModel
35
35
36 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
37
37
38 VCS_TYPE_KEY = '_rc_vcs_type'
38 VCS_TYPE_KEY = '_rc_vcs_type'
39 VCS_TYPE_SKIP = '_rc_vcs_skip'
39 VCS_TYPE_SKIP = '_rc_vcs_skip'
40
40
41
41
42 def is_git(environ):
42 def is_git(environ):
43 """
43 """
44 Returns True if requests should be handled by GIT wsgi middleware
44 Returns True if requests should be handled by GIT wsgi middleware
45 """
45 """
46 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
46 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
47 log.debug(
47 log.debug(
48 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
48 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
49 is_git_path is not None)
49 is_git_path is not None)
50
50
51 return is_git_path
51 return is_git_path
52
52
53
53
54 def is_hg(environ):
54 def is_hg(environ):
55 """
55 """
56 Returns True if requests target is mercurial server - header
56 Returns True if requests target is mercurial server - header
57 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
57 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
58 """
58 """
59 is_hg_path = False
59 is_hg_path = False
60
60
61 http_accept = environ.get('HTTP_ACCEPT')
61 http_accept = environ.get('HTTP_ACCEPT')
62
62
63 if http_accept and http_accept.startswith('application/mercurial'):
63 if http_accept and http_accept.startswith('application/mercurial'):
64 query = urlparse.parse_qs(environ['QUERY_STRING'])
64 query = urlparse.parse_qs(environ['QUERY_STRING'])
65 if 'cmd' in query:
65 if 'cmd' in query:
66 is_hg_path = True
66 is_hg_path = True
67
67
68 log.debug(
68 log.debug(
69 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
69 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
70 is_hg_path)
70 is_hg_path)
71
71
72 return is_hg_path
72 return is_hg_path
73
73
74
74
75 def is_svn(environ):
75 def is_svn(environ):
76 """
76 """
77 Returns True if requests target is Subversion server
77 Returns True if requests target is Subversion server
78 """
78 """
79 http_dav = environ.get('HTTP_DAV', '')
79 http_dav = environ.get('HTTP_DAV', '')
80 magic_path_segment = rhodecode.CONFIG.get(
80 magic_path_segment = rhodecode.CONFIG.get(
81 'rhodecode_subversion_magic_path', '/!svn')
81 'rhodecode_subversion_magic_path', '/!svn')
82 is_svn_path = (
82 is_svn_path = (
83 'subversion' in http_dav or
83 'subversion' in http_dav or
84 magic_path_segment in environ['PATH_INFO'])
84 magic_path_segment in environ['PATH_INFO'])
85 log.debug(
85 log.debug(
86 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
86 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
87 is_svn_path)
87 is_svn_path)
88
88
89 return is_svn_path
89 return is_svn_path
90
90
91
91
92 class GunzipMiddleware(object):
92 class GunzipMiddleware(object):
93 """
93 """
94 WSGI middleware that unzips gzip-encoded requests before
94 WSGI middleware that unzips gzip-encoded requests before
95 passing on to the underlying application.
95 passing on to the underlying application.
96 """
96 """
97
97
98 def __init__(self, application):
98 def __init__(self, application):
99 self.app = application
99 self.app = application
100
100
101 def __call__(self, environ, start_response):
101 def __call__(self, environ, start_response):
102 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
102 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
103
103
104 if b'gzip' in accepts_encoding_header:
104 if b'gzip' in accepts_encoding_header:
105 log.debug('gzip detected, now running gunzip wrapper')
105 log.debug('gzip detected, now running gunzip wrapper')
106 wsgi_input = environ['wsgi.input']
106 wsgi_input = environ['wsgi.input']
107
107
108 if not hasattr(environ['wsgi.input'], 'seek'):
108 if not hasattr(environ['wsgi.input'], 'seek'):
109 # The gzip implementation in the standard library of Python 2.x
109 # The gzip implementation in the standard library of Python 2.x
110 # requires the '.seek()' and '.tell()' methods to be available
110 # requires the '.seek()' and '.tell()' methods to be available
111 # on the input stream. Read the data into a temporary file to
111 # on the input stream. Read the data into a temporary file to
112 # work around this limitation.
112 # work around this limitation.
113
113
114 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
114 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
115 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
115 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
116 wsgi_input.seek(0)
116 wsgi_input.seek(0)
117
117
118 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
118 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
119 # since we "Ungzipped" the content we say now it's no longer gzip
119 # since we "Ungzipped" the content we say now it's no longer gzip
120 # content encoding
120 # content encoding
121 del environ['HTTP_CONTENT_ENCODING']
121 del environ['HTTP_CONTENT_ENCODING']
122
122
123 # content length has changes ? or i'm not sure
123 # content length has changes ? or i'm not sure
124 if 'CONTENT_LENGTH' in environ:
124 if 'CONTENT_LENGTH' in environ:
125 del environ['CONTENT_LENGTH']
125 del environ['CONTENT_LENGTH']
126 else:
126 else:
127 log.debug('content not gzipped, gzipMiddleware passing '
127 log.debug('content not gzipped, gzipMiddleware passing '
128 'request further')
128 'request further')
129 return self.app(environ, start_response)
129 return self.app(environ, start_response)
130
130
131
131
132 def is_vcs_call(environ):
132 def is_vcs_call(environ):
133 if VCS_TYPE_KEY in environ:
133 if VCS_TYPE_KEY in environ:
134 raw_type = environ[VCS_TYPE_KEY]
134 raw_type = environ[VCS_TYPE_KEY]
135 return raw_type and raw_type != VCS_TYPE_SKIP
135 return raw_type and raw_type != VCS_TYPE_SKIP
136 return False
136 return False
137
137
138
138
139 def detect_vcs_request(environ, backends):
139 def detect_vcs_request(environ, backends):
140 checks = {
140 checks = {
141 'hg': (is_hg, SimpleHg),
141 'hg': (is_hg, SimpleHg),
142 'git': (is_git, SimpleGit),
142 'git': (is_git, SimpleGit),
143 'svn': (is_svn, SimpleSvn),
143 'svn': (is_svn, SimpleSvn),
144 }
144 }
145 handler = None
145 handler = None
146
146
147 if VCS_TYPE_KEY in environ:
147 if VCS_TYPE_KEY in environ:
148 raw_type = environ[VCS_TYPE_KEY]
148 raw_type = environ[VCS_TYPE_KEY]
149 if raw_type == VCS_TYPE_SKIP:
149 if raw_type == VCS_TYPE_SKIP:
150 log.debug('got `skip` marker for vcs detection, skipping...')
150 log.debug('got `skip` marker for vcs detection, skipping...')
151 return handler
151 return handler
152
152
153 _check, handler = checks.get(raw_type) or [None, None]
153 _check, handler = checks.get(raw_type) or [None, None]
154 if handler:
154 if handler:
155 log.debug('got handler:%s from environ', handler)
155 log.debug('got handler:%s from environ', handler)
156
156
157 if not handler:
157 if not handler:
158 log.debug('checking if request is of VCS type in order: %s', backends)
158 log.debug('checking if request is of VCS type in order: %s', backends)
159 for vcs_type in backends:
159 for vcs_type in backends:
160 vcs_check, _handler = checks[vcs_type]
160 vcs_check, _handler = checks[vcs_type]
161 if vcs_check(environ):
161 if vcs_check(environ):
162 log.debug('vcs handler found %s', _handler)
162 log.debug('vcs handler found %s', _handler)
163 handler = _handler
163 handler = _handler
164 break
164 break
165
165
166 return handler
166 return handler
167
167
168
168
169 class VCSMiddleware(object):
169 class VCSMiddleware(object):
170
170
171 def __init__(self, app, registry, config, appenlight_client):
171 def __init__(self, app, registry, config, appenlight_client):
172 self.application = app
172 self.application = app
173 self.registry = registry
173 self.registry = registry
174 self.config = config
174 self.config = config
175 self.appenlight_client = appenlight_client
175 self.appenlight_client = appenlight_client
176 self.use_gzip = True
176 self.use_gzip = True
177 # order in which we check the middlewares, based on vcs.backends config
177 # order in which we check the middlewares, based on vcs.backends config
178 self.check_middlewares = config['vcs.backends']
178 self.check_middlewares = config['vcs.backends']
179
179
180 def vcs_config(self, repo_name=None):
180 def vcs_config(self, repo_name=None):
181 """
181 """
182 returns serialized VcsSettings
182 returns serialized VcsSettings
183 """
183 """
184 return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
184 try:
185 return VcsSettingsModel(
186 repo=repo_name).get_ui_settings_as_config_obj()
187 except Exception:
188 pass
185
189
186 def wrap_in_gzip_if_enabled(self, app, config):
190 def wrap_in_gzip_if_enabled(self, app, config):
187 if self.use_gzip:
191 if self.use_gzip:
188 app = GunzipMiddleware(app)
192 app = GunzipMiddleware(app)
189 return app
193 return app
190
194
191 def _get_handler_app(self, environ):
195 def _get_handler_app(self, environ):
192 app = None
196 app = None
193 log.debug('VCSMiddleware: detecting vcs type.')
197 log.debug('VCSMiddleware: detecting vcs type.')
194 handler = detect_vcs_request(environ, self.check_middlewares)
198 handler = detect_vcs_request(environ, self.check_middlewares)
195 if handler:
199 if handler:
196 app = handler(self.config, self.registry)
200 app = handler(self.config, self.registry)
197
201
198 return app
202 return app
199
203
200 def __call__(self, environ, start_response):
204 def __call__(self, environ, start_response):
201 # check if we handle one of interesting protocols, optionally extract
205 # check if we handle one of interesting protocols, optionally extract
202 # specific vcsSettings and allow changes of how things are wrapped
206 # specific vcsSettings and allow changes of how things are wrapped
203 vcs_handler = self._get_handler_app(environ)
207 vcs_handler = self._get_handler_app(environ)
204 if vcs_handler:
208 if vcs_handler:
205 # translate the _REPO_ID into real repo NAME for usage
209 # translate the _REPO_ID into real repo NAME for usage
206 # in middleware
210 # in middleware
207 environ['PATH_INFO'] = vcs_handler._get_by_id(environ['PATH_INFO'])
211 environ['PATH_INFO'] = vcs_handler._get_by_id(environ['PATH_INFO'])
208
212
209 # Set acl, url and vcs repo names.
213 # Set acl, url and vcs repo names.
210 vcs_handler.set_repo_names(environ)
214 vcs_handler.set_repo_names(environ)
211
215
216 # register repo config back to the handler
217 vcs_conf = self.vcs_config(vcs_handler.acl_repo_name)
218 # maybe damaged/non existent settings. We still want to
219 # pass that point to validate on is_valid_and_existing_repo
220 # and return proper HTTP Code back to client
221 if vcs_conf:
222 vcs_handler.repo_vcs_config = vcs_conf
223
212 # check for type, presence in database and on filesystem
224 # check for type, presence in database and on filesystem
213 if not vcs_handler.is_valid_and_existing_repo(
225 if not vcs_handler.is_valid_and_existing_repo(
214 vcs_handler.acl_repo_name,
226 vcs_handler.acl_repo_name,
215 vcs_handler.base_path,
227 vcs_handler.base_path,
216 vcs_handler.SCM):
228 vcs_handler.SCM):
217 return HTTPNotFound()(environ, start_response)
229 return HTTPNotFound()(environ, start_response)
218
230
219 environ['REPO_NAME'] = vcs_handler.url_repo_name
231 environ['REPO_NAME'] = vcs_handler.url_repo_name
220
232
221 # register repo config back to the handler
222 vcs_handler.repo_vcs_config = self.vcs_config(
223 vcs_handler.acl_repo_name)
224
225 # Wrap handler in middlewares if they are enabled.
233 # Wrap handler in middlewares if they are enabled.
226 vcs_handler = self.wrap_in_gzip_if_enabled(
234 vcs_handler = self.wrap_in_gzip_if_enabled(
227 vcs_handler, self.config)
235 vcs_handler, self.config)
228 vcs_handler, _ = wrap_in_appenlight_if_enabled(
236 vcs_handler, _ = wrap_in_appenlight_if_enabled(
229 vcs_handler, self.config, self.appenlight_client)
237 vcs_handler, self.config, self.appenlight_client)
230
238
231 return vcs_handler(environ, start_response)
239 return vcs_handler(environ, start_response)
232
240
233 return self.application(environ, start_response)
241 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now