##// END OF EJS Templates
commits: disable shadow repo callback daemon
marcink -
r3932:04ab971e default
parent child Browse files
Show More
@@ -1,675 +1,678 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2019 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 from StringIO import StringIO
32 32 from lxml import etree
33 33
34 34 import time
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 36
37 37 from pyramid.httpexceptions import (
38 38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 39 from zope.cachedescriptors.property import Lazy as LazyProperty
40 40
41 41 import rhodecode
42 42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 43 from rhodecode.lib import rc_cache
44 44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 45 from rhodecode.lib.base import (
46 46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 47 from rhodecode.lib.exceptions import (UserCreationError, 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
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def extract_svn_txn_id(acl_repo_name, data):
66 66 """
67 67 Helper method for extraction of svn txn_id from submitted XML data during
68 68 POST operations
69 69 """
70 70 try:
71 71 root = etree.fromstring(data)
72 72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
73 73 for el in root:
74 74 if el.tag == '{DAV:}source':
75 75 for sub_el in el:
76 76 if sub_el.tag == '{DAV:}href':
77 77 match = pat.search(sub_el.text)
78 78 if match:
79 79 svn_tx_id = match.groupdict()['txn_id']
80 80 txn_id = rc_cache.utils.compute_key_from_params(
81 81 acl_repo_name, svn_tx_id)
82 82 return txn_id
83 83 except Exception:
84 84 log.exception('Failed to extract txn_id')
85 85
86 86
87 87 def initialize_generator(factory):
88 88 """
89 89 Initializes the returned generator by draining its first element.
90 90
91 91 This can be used to give a generator an initializer, which is the code
92 92 up to the first yield statement. This decorator enforces that the first
93 93 produced element has the value ``"__init__"`` to make its special
94 94 purpose very explicit in the using code.
95 95 """
96 96
97 97 @wraps(factory)
98 98 def wrapper(*args, **kwargs):
99 99 gen = factory(*args, **kwargs)
100 100 try:
101 101 init = gen.next()
102 102 except StopIteration:
103 103 raise ValueError('Generator must yield at least one element.')
104 104 if init != "__init__":
105 105 raise ValueError('First yielded element must be "__init__".')
106 106 return gen
107 107 return wrapper
108 108
109 109
110 110 class SimpleVCS(object):
111 111 """Common functionality for SCM HTTP handlers."""
112 112
113 113 SCM = 'unknown'
114 114
115 115 acl_repo_name = None
116 116 url_repo_name = None
117 117 vcs_repo_name = None
118 118 rc_extras = {}
119 119
120 120 # We have to handle requests to shadow repositories different than requests
121 121 # to normal repositories. Therefore we have to distinguish them. To do this
122 122 # we use this regex which will match only on URLs pointing to shadow
123 123 # repositories.
124 124 shadow_repo_re = re.compile(
125 125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
126 126 '(?P<target>{slug_pat})/' # target repo
127 127 'pull-request/(?P<pr_id>\d+)/' # pull request
128 128 'repository$' # shadow repo
129 129 .format(slug_pat=SLUG_RE.pattern))
130 130
131 131 def __init__(self, config, registry):
132 132 self.registry = registry
133 133 self.config = config
134 134 # re-populated by specialized middleware
135 135 self.repo_vcs_config = base.Config()
136 136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
137 137
138 138 registry.rhodecode_settings = self.rhodecode_settings
139 139 # authenticate this VCS request using authfunc
140 140 auth_ret_code_detection = \
141 141 str2bool(self.config.get('auth_ret_code_detection', False))
142 142 self.authenticate = BasicAuth(
143 143 '', authenticate, registry, config.get('auth_ret_code'),
144 144 auth_ret_code_detection)
145 145 self.ip_addr = '0.0.0.0'
146 146
147 147 @LazyProperty
148 148 def global_vcs_config(self):
149 149 try:
150 150 return VcsSettingsModel().get_ui_settings_as_config_obj()
151 151 except Exception:
152 152 return base.Config()
153 153
154 154 @property
155 155 def base_path(self):
156 156 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
157 157
158 158 if not settings_path:
159 159 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
160 160
161 161 if not settings_path:
162 162 # try, maybe we passed in explicitly as config option
163 163 settings_path = self.config.get('base_path')
164 164
165 165 if not settings_path:
166 166 raise ValueError('FATAL: base_path is empty')
167 167 return settings_path
168 168
169 169 def set_repo_names(self, environ):
170 170 """
171 171 This will populate the attributes acl_repo_name, url_repo_name,
172 172 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
173 173 shadow) repositories all names are equal. In case of requests to a
174 174 shadow repository the acl-name points to the target repo of the pull
175 175 request and the vcs-name points to the shadow repo file system path.
176 176 The url-name is always the URL used by the vcs client program.
177 177
178 178 Example in case of a shadow repo:
179 179 acl_repo_name = RepoGroup/MyRepo
180 180 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
181 181 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
182 182 """
183 183 # First we set the repo name from URL for all attributes. This is the
184 184 # default if handling normal (non shadow) repo requests.
185 185 self.url_repo_name = self._get_repository_name(environ)
186 186 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
187 187 self.is_shadow_repo = False
188 188
189 189 # Check if this is a request to a shadow repository.
190 190 match = self.shadow_repo_re.match(self.url_repo_name)
191 191 if match:
192 192 match_dict = match.groupdict()
193 193
194 194 # Build acl repo name from regex match.
195 195 acl_repo_name = safe_unicode('{groups}{target}'.format(
196 196 groups=match_dict['groups'] or '',
197 197 target=match_dict['target']))
198 198
199 199 # Retrieve pull request instance by ID from regex match.
200 200 pull_request = PullRequest.get(match_dict['pr_id'])
201 201
202 202 # Only proceed if we got a pull request and if acl repo name from
203 203 # URL equals the target repo name of the pull request.
204 204 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
205 205
206 206 # Get file system path to shadow repository.
207 207 workspace_id = PullRequestModel()._workspace_id(pull_request)
208 208 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
209 209
210 210 # Store names for later usage.
211 211 self.vcs_repo_name = vcs_repo_name
212 212 self.acl_repo_name = acl_repo_name
213 213 self.is_shadow_repo = True
214 214
215 215 log.debug('Setting all VCS repository names: %s', {
216 216 'acl_repo_name': self.acl_repo_name,
217 217 'url_repo_name': self.url_repo_name,
218 218 'vcs_repo_name': self.vcs_repo_name,
219 219 })
220 220
221 221 @property
222 222 def scm_app(self):
223 223 custom_implementation = self.config['vcs.scm_app_implementation']
224 224 if custom_implementation == 'http':
225 225 log.debug('Using HTTP implementation of scm app.')
226 226 scm_app_impl = scm_app_http
227 227 else:
228 228 log.debug('Using custom implementation of scm_app: "{}"'.format(
229 229 custom_implementation))
230 230 scm_app_impl = importlib.import_module(custom_implementation)
231 231 return scm_app_impl
232 232
233 233 def _get_by_id(self, repo_name):
234 234 """
235 235 Gets a special pattern _<ID> from clone url and tries to replace it
236 236 with a repository_name for support of _<ID> non changeable urls
237 237 """
238 238
239 239 data = repo_name.split('/')
240 240 if len(data) >= 2:
241 241 from rhodecode.model.repo import RepoModel
242 242 by_id_match = RepoModel().get_repo_by_id(repo_name)
243 243 if by_id_match:
244 244 data[1] = by_id_match.repo_name
245 245
246 246 return safe_str('/'.join(data))
247 247
248 248 def _invalidate_cache(self, repo_name):
249 249 """
250 250 Set's cache for this repository for invalidation on next access
251 251
252 252 :param repo_name: full repo name, also a cache key
253 253 """
254 254 ScmModel().mark_for_invalidation(repo_name)
255 255
256 256 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
257 257 db_repo = Repository.get_by_repo_name(repo_name)
258 258 if not db_repo:
259 259 log.debug('Repository `%s` not found inside the database.',
260 260 repo_name)
261 261 return False
262 262
263 263 if db_repo.repo_type != scm_type:
264 264 log.warning(
265 265 'Repository `%s` have incorrect scm_type, expected %s got %s',
266 266 repo_name, db_repo.repo_type, scm_type)
267 267 return False
268 268
269 269 config = db_repo._config
270 270 config.set('extensions', 'largefiles', '')
271 271 return is_valid_repo(
272 272 repo_name, base_path,
273 273 explicit_scm=scm_type, expect_scm=scm_type, config=config)
274 274
275 275 def valid_and_active_user(self, user):
276 276 """
277 277 Checks if that user is not empty, and if it's actually object it checks
278 278 if he's active.
279 279
280 280 :param user: user object or None
281 281 :return: boolean
282 282 """
283 283 if user is None:
284 284 return False
285 285
286 286 elif user.active:
287 287 return True
288 288
289 289 return False
290 290
291 291 @property
292 292 def is_shadow_repo_dir(self):
293 293 return os.path.isdir(self.vcs_repo_name)
294 294
295 295 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
296 296 plugin_id='', plugin_cache_active=False, cache_ttl=0):
297 297 """
298 298 Checks permissions using action (push/pull) user and repository
299 299 name. If plugin_cache and ttl is set it will use the plugin which
300 300 authenticated the user to store the cached permissions result for N
301 301 amount of seconds as in cache_ttl
302 302
303 303 :param action: push or pull action
304 304 :param user: user instance
305 305 :param repo_name: repository name
306 306 """
307 307
308 308 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
309 309 plugin_id, plugin_cache_active, cache_ttl)
310 310
311 311 user_id = user.user_id
312 312 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
313 313 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
314 314
315 315 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
316 316 expiration_time=cache_ttl,
317 317 condition=plugin_cache_active)
318 318 def compute_perm_vcs(
319 319 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
320 320
321 321 log.debug('auth: calculating permission access now...')
322 322 # check IP
323 323 inherit = user.inherit_default_permissions
324 324 ip_allowed = AuthUser.check_ip_allowed(
325 325 user_id, ip_addr, inherit_from_default=inherit)
326 326 if ip_allowed:
327 327 log.info('Access for IP:%s allowed', ip_addr)
328 328 else:
329 329 return False
330 330
331 331 if action == 'push':
332 332 perms = ('repository.write', 'repository.admin')
333 333 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
334 334 return False
335 335
336 336 else:
337 337 # any other action need at least read permission
338 338 perms = (
339 339 'repository.read', 'repository.write', 'repository.admin')
340 340 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
341 341 return False
342 342
343 343 return True
344 344
345 345 start = time.time()
346 346 log.debug('Running plugin `%s` permissions check', plugin_id)
347 347
348 348 # for environ based auth, password can be empty, but then the validation is
349 349 # on the server that fills in the env data needed for authentication
350 350 perm_result = compute_perm_vcs(
351 351 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
352 352
353 353 auth_time = time.time() - start
354 354 log.debug('Permissions for plugin `%s` completed in %.4fs, '
355 355 'expiration time of fetched cache %.1fs.',
356 356 plugin_id, auth_time, cache_ttl)
357 357
358 358 return perm_result
359 359
360 360 def _get_http_scheme(self, environ):
361 361 try:
362 362 return environ['wsgi.url_scheme']
363 363 except Exception:
364 364 log.exception('Failed to read http scheme')
365 365 return 'http'
366 366
367 367 def _check_ssl(self, environ, start_response):
368 368 """
369 369 Checks the SSL check flag and returns False if SSL is not present
370 370 and required True otherwise
371 371 """
372 372 org_proto = environ['wsgi._org_proto']
373 373 # check if we have SSL required ! if not it's a bad request !
374 374 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
375 375 if require_ssl and org_proto == 'http':
376 376 log.debug(
377 377 'Bad request: detected protocol is `%s` and '
378 378 'SSL/HTTPS is required.', org_proto)
379 379 return False
380 380 return True
381 381
382 382 def _get_default_cache_ttl(self):
383 383 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
384 384 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
385 385 plugin_settings = plugin.get_settings()
386 386 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
387 387 plugin_settings) or (False, 0)
388 388 return plugin_cache_active, cache_ttl
389 389
390 390 def __call__(self, environ, start_response):
391 391 try:
392 392 return self._handle_request(environ, start_response)
393 393 except Exception:
394 394 log.exception("Exception while handling request")
395 395 appenlight.track_exception(environ)
396 396 return HTTPInternalServerError()(environ, start_response)
397 397 finally:
398 398 meta.Session.remove()
399 399
400 400 def _handle_request(self, environ, start_response):
401 401 if not self._check_ssl(environ, start_response):
402 402 reason = ('SSL required, while RhodeCode was unable '
403 403 'to detect this as SSL request')
404 404 log.debug('User not allowed to proceed, %s', reason)
405 405 return HTTPNotAcceptable(reason)(environ, start_response)
406 406
407 407 if not self.url_repo_name:
408 408 log.warning('Repository name is empty: %s', self.url_repo_name)
409 409 # failed to get repo name, we fail now
410 410 return HTTPNotFound()(environ, start_response)
411 411 log.debug('Extracted repo name is %s', self.url_repo_name)
412 412
413 413 ip_addr = get_ip_addr(environ)
414 414 user_agent = get_user_agent(environ)
415 415 username = None
416 416
417 417 # skip passing error to error controller
418 418 environ['pylons.status_code_redirect'] = True
419 419
420 420 # ======================================================================
421 421 # GET ACTION PULL or PUSH
422 422 # ======================================================================
423 423 action = self._get_action(environ)
424 424
425 425 # ======================================================================
426 426 # Check if this is a request to a shadow repository of a pull request.
427 427 # In this case only pull action is allowed.
428 428 # ======================================================================
429 429 if self.is_shadow_repo and action != 'pull':
430 430 reason = 'Only pull action is allowed for shadow repositories.'
431 431 log.debug('User not allowed to proceed, %s', reason)
432 432 return HTTPNotAcceptable(reason)(environ, start_response)
433 433
434 434 # Check if the shadow repo actually exists, in case someone refers
435 435 # to it, and it has been deleted because of successful merge.
436 436 if self.is_shadow_repo and not self.is_shadow_repo_dir:
437 437 log.debug(
438 438 'Shadow repo detected, and shadow repo dir `%s` is missing',
439 439 self.is_shadow_repo_dir)
440 440 return HTTPNotFound()(environ, start_response)
441 441
442 442 # ======================================================================
443 443 # CHECK ANONYMOUS PERMISSION
444 444 # ======================================================================
445 445 detect_force_push = False
446 446 check_branch_perms = False
447 447 if action in ['pull', 'push']:
448 448 user_obj = anonymous_user = User.get_default_user()
449 449 auth_user = user_obj.AuthUser()
450 450 username = anonymous_user.username
451 451 if anonymous_user.active:
452 452 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
453 453 # ONLY check permissions if the user is activated
454 454 anonymous_perm = self._check_permission(
455 455 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
456 456 plugin_id='anonymous_access',
457 457 plugin_cache_active=plugin_cache_active,
458 458 cache_ttl=cache_ttl,
459 459 )
460 460 else:
461 461 anonymous_perm = False
462 462
463 463 if not anonymous_user.active or not anonymous_perm:
464 464 if not anonymous_user.active:
465 465 log.debug('Anonymous access is disabled, running '
466 466 'authentication')
467 467
468 468 if not anonymous_perm:
469 469 log.debug('Not enough credentials to access this '
470 470 'repository as anonymous user')
471 471
472 472 username = None
473 473 # ==============================================================
474 474 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
475 475 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
476 476 # ==============================================================
477 477
478 478 # try to auth based on environ, container auth methods
479 479 log.debug('Running PRE-AUTH for container based authentication')
480 480 pre_auth = authenticate(
481 481 '', '', environ, VCS_TYPE, registry=self.registry,
482 482 acl_repo_name=self.acl_repo_name)
483 483 if pre_auth and pre_auth.get('username'):
484 484 username = pre_auth['username']
485 485 log.debug('PRE-AUTH got %s as username', username)
486 486 if pre_auth:
487 487 log.debug('PRE-AUTH successful from %s',
488 488 pre_auth.get('auth_data', {}).get('_plugin'))
489 489
490 490 # If not authenticated by the container, running basic auth
491 491 # before inject the calling repo_name for special scope checks
492 492 self.authenticate.acl_repo_name = self.acl_repo_name
493 493
494 494 plugin_cache_active, cache_ttl = False, 0
495 495 plugin = None
496 496 if not username:
497 497 self.authenticate.realm = self.authenticate.get_rc_realm()
498 498
499 499 try:
500 500 auth_result = self.authenticate(environ)
501 501 except (UserCreationError, NotAllowedToCreateUserError) as e:
502 502 log.error(e)
503 503 reason = safe_str(e)
504 504 return HTTPNotAcceptable(reason)(environ, start_response)
505 505
506 506 if isinstance(auth_result, dict):
507 507 AUTH_TYPE.update(environ, 'basic')
508 508 REMOTE_USER.update(environ, auth_result['username'])
509 509 username = auth_result['username']
510 510 plugin = auth_result.get('auth_data', {}).get('_plugin')
511 511 log.info(
512 512 'MAIN-AUTH successful for user `%s` from %s plugin',
513 513 username, plugin)
514 514
515 515 plugin_cache_active, cache_ttl = auth_result.get(
516 516 'auth_data', {}).get('_ttl_cache') or (False, 0)
517 517 else:
518 518 return auth_result.wsgi_application(environ, start_response)
519 519
520 520 # ==============================================================
521 521 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
522 522 # ==============================================================
523 523 user = User.get_by_username(username)
524 524 if not self.valid_and_active_user(user):
525 525 return HTTPForbidden()(environ, start_response)
526 526 username = user.username
527 527 user_id = user.user_id
528 528
529 529 # check user attributes for password change flag
530 530 user_obj = user
531 531 auth_user = user_obj.AuthUser()
532 532 if user_obj and user_obj.username != User.DEFAULT_USER and \
533 533 user_obj.user_data.get('force_password_change'):
534 534 reason = 'password change required'
535 535 log.debug('User not allowed to authenticate, %s', reason)
536 536 return HTTPNotAcceptable(reason)(environ, start_response)
537 537
538 538 # check permissions for this repository
539 539 perm = self._check_permission(
540 540 action, user, auth_user, self.acl_repo_name, ip_addr,
541 541 plugin, plugin_cache_active, cache_ttl)
542 542 if not perm:
543 543 return HTTPForbidden()(environ, start_response)
544 544 environ['rc_auth_user_id'] = user_id
545 545
546 546 if action == 'push':
547 547 perms = auth_user.get_branch_permissions(self.acl_repo_name)
548 548 if perms:
549 549 check_branch_perms = True
550 550 detect_force_push = True
551 551
552 552 # extras are injected into UI object and later available
553 553 # in hooks executed by RhodeCode
554 554 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
555 555
556 556 extras = vcs_operation_context(
557 557 environ, repo_name=self.acl_repo_name, username=username,
558 558 action=action, scm=self.SCM, check_locking=check_locking,
559 559 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
560 560 detect_force_push=detect_force_push
561 561 )
562 562
563 563 # ======================================================================
564 564 # REQUEST HANDLING
565 565 # ======================================================================
566 566 repo_path = os.path.join(
567 567 safe_str(self.base_path), safe_str(self.vcs_repo_name))
568 568 log.debug('Repository path is %s', repo_path)
569 569
570 570 fix_PATH()
571 571
572 572 log.info(
573 573 '%s action on %s repo "%s" by "%s" from %s %s',
574 574 action, self.SCM, safe_str(self.url_repo_name),
575 575 safe_str(username), ip_addr, user_agent)
576 576
577 577 return self._generate_vcs_response(
578 578 environ, start_response, repo_path, extras, action)
579 579
580 580 @initialize_generator
581 581 def _generate_vcs_response(
582 582 self, environ, start_response, repo_path, extras, action):
583 583 """
584 584 Returns a generator for the response content.
585 585
586 586 This method is implemented as a generator, so that it can trigger
587 587 the cache validation after all content sent back to the client. It
588 588 also handles the locking exceptions which will be triggered when
589 589 the first chunk is produced by the underlying WSGI application.
590 590 """
591 591 txn_id = ''
592 592 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
593 593 # case for SVN, we want to re-use the callback daemon port
594 594 # so we use the txn_id, for this we peek the body, and still save
595 595 # it as wsgi.input
596 596 data = environ['wsgi.input'].read()
597 597 environ['wsgi.input'] = StringIO(data)
598 598 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
599 599
600 600 callback_daemon, extras = self._prepare_callback_daemon(
601 601 extras, environ, action, txn_id=txn_id)
602 602 log.debug('HOOKS extras is %s', extras)
603 603
604 604 http_scheme = self._get_http_scheme(environ)
605 605
606 606 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
607 607 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
608 608 with callback_daemon:
609 609 app.rc_extras = extras
610 610
611 611 try:
612 612 response = app(environ, start_response)
613 613 finally:
614 614 # This statement works together with the decorator
615 615 # "initialize_generator" above. The decorator ensures that
616 616 # we hit the first yield statement before the generator is
617 617 # returned back to the WSGI server. This is needed to
618 618 # ensure that the call to "app" above triggers the
619 619 # needed callback to "start_response" before the
620 620 # generator is actually used.
621 621 yield "__init__"
622 622
623 623 # iter content
624 624 for chunk in response:
625 625 yield chunk
626 626
627 627 try:
628 628 # invalidate cache on push
629 629 if action == 'push':
630 630 self._invalidate_cache(self.url_repo_name)
631 631 finally:
632 632 meta.Session.remove()
633 633
634 634 def _get_repository_name(self, environ):
635 635 """Get repository name out of the environmnent
636 636
637 637 :param environ: WSGI environment
638 638 """
639 639 raise NotImplementedError()
640 640
641 641 def _get_action(self, environ):
642 642 """Map request commands into a pull or push command.
643 643
644 644 :param environ: WSGI environment
645 645 """
646 646 raise NotImplementedError()
647 647
648 648 def _create_wsgi_app(self, repo_path, repo_name, config):
649 649 """Return the WSGI app that will finally handle the request."""
650 650 raise NotImplementedError()
651 651
652 652 def _create_config(self, extras, repo_name, scheme='http'):
653 653 """Create a safe config representation."""
654 654 raise NotImplementedError()
655 655
656 656 def _should_use_callback_daemon(self, extras, environ, action):
657 if extras.get('is_shadow_repo'):
658 # we don't want to execute hooks, and callback daemon for shadow repos
659 return False
657 660 return True
658 661
659 662 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
660 663 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
661 664 if not self._should_use_callback_daemon(extras, environ, action):
662 665 # disable callback daemon for actions that don't require it
663 666 direct_calls = True
664 667
665 668 return prepare_callback_daemon(
666 669 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
667 670 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
668 671
669 672
670 673 def _should_check_locking(query_string):
671 674 # this is kind of hacky, but due to how mercurial handles client-server
672 675 # server see all operation on commit; bookmarks, phases and
673 676 # obsolescence marker in different transaction, we don't want to check
674 677 # locking on those
675 678 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now