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