##// END OF EJS Templates
logging: reduce info logs
marcink -
r3876:f9e8a72d default
parent child Browse files
Show More
@@ -1,678 +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 \
205 205 (acl_repo_name == pull_request.target_repo.repo_name):
206 206 repo_id = pull_request.target_repo.repo_id
207 207 # Get file system path to shadow repository.
208 208 workspace_id = PullRequestModel()._workspace_id(pull_request)
209 209 target_vcs = pull_request.target_repo.scm_instance()
210 210 vcs_repo_name = target_vcs._get_shadow_repository_path(
211 211 repo_id, workspace_id)
212 212
213 213 # Store names for later usage.
214 214 self.vcs_repo_name = vcs_repo_name
215 215 self.acl_repo_name = acl_repo_name
216 216 self.is_shadow_repo = True
217 217
218 218 log.debug('Setting all VCS repository names: %s', {
219 219 'acl_repo_name': self.acl_repo_name,
220 220 'url_repo_name': self.url_repo_name,
221 221 'vcs_repo_name': self.vcs_repo_name,
222 222 })
223 223
224 224 @property
225 225 def scm_app(self):
226 226 custom_implementation = self.config['vcs.scm_app_implementation']
227 227 if custom_implementation == 'http':
228 log.info('Using HTTP implementation of scm app.')
228 log.debug('Using HTTP implementation of scm app.')
229 229 scm_app_impl = scm_app_http
230 230 else:
231 log.info('Using custom implementation of scm_app: "{}"'.format(
231 log.debug('Using custom implementation of scm_app: "{}"'.format(
232 232 custom_implementation))
233 233 scm_app_impl = importlib.import_module(custom_implementation)
234 234 return scm_app_impl
235 235
236 236 def _get_by_id(self, repo_name):
237 237 """
238 238 Gets a special pattern _<ID> from clone url and tries to replace it
239 239 with a repository_name for support of _<ID> non changeable urls
240 240 """
241 241
242 242 data = repo_name.split('/')
243 243 if len(data) >= 2:
244 244 from rhodecode.model.repo import RepoModel
245 245 by_id_match = RepoModel().get_repo_by_id(repo_name)
246 246 if by_id_match:
247 247 data[1] = by_id_match.repo_name
248 248
249 249 return safe_str('/'.join(data))
250 250
251 251 def _invalidate_cache(self, repo_name):
252 252 """
253 253 Set's cache for this repository for invalidation on next access
254 254
255 255 :param repo_name: full repo name, also a cache key
256 256 """
257 257 ScmModel().mark_for_invalidation(repo_name)
258 258
259 259 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
260 260 db_repo = Repository.get_by_repo_name(repo_name)
261 261 if not db_repo:
262 262 log.debug('Repository `%s` not found inside the database.',
263 263 repo_name)
264 264 return False
265 265
266 266 if db_repo.repo_type != scm_type:
267 267 log.warning(
268 268 'Repository `%s` have incorrect scm_type, expected %s got %s',
269 269 repo_name, db_repo.repo_type, scm_type)
270 270 return False
271 271
272 272 config = db_repo._config
273 273 config.set('extensions', 'largefiles', '')
274 274 return is_valid_repo(
275 275 repo_name, base_path,
276 276 explicit_scm=scm_type, expect_scm=scm_type, config=config)
277 277
278 278 def valid_and_active_user(self, user):
279 279 """
280 280 Checks if that user is not empty, and if it's actually object it checks
281 281 if he's active.
282 282
283 283 :param user: user object or None
284 284 :return: boolean
285 285 """
286 286 if user is None:
287 287 return False
288 288
289 289 elif user.active:
290 290 return True
291 291
292 292 return False
293 293
294 294 @property
295 295 def is_shadow_repo_dir(self):
296 296 return os.path.isdir(self.vcs_repo_name)
297 297
298 298 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
299 299 plugin_id='', plugin_cache_active=False, cache_ttl=0):
300 300 """
301 301 Checks permissions using action (push/pull) user and repository
302 302 name. If plugin_cache and ttl is set it will use the plugin which
303 303 authenticated the user to store the cached permissions result for N
304 304 amount of seconds as in cache_ttl
305 305
306 306 :param action: push or pull action
307 307 :param user: user instance
308 308 :param repo_name: repository name
309 309 """
310 310
311 311 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
312 312 plugin_id, plugin_cache_active, cache_ttl)
313 313
314 314 user_id = user.user_id
315 315 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
316 316 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
317 317
318 318 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
319 319 expiration_time=cache_ttl,
320 320 condition=plugin_cache_active)
321 321 def compute_perm_vcs(
322 322 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
323 323
324 324 log.debug('auth: calculating permission access now...')
325 325 # check IP
326 326 inherit = user.inherit_default_permissions
327 327 ip_allowed = AuthUser.check_ip_allowed(
328 328 user_id, ip_addr, inherit_from_default=inherit)
329 329 if ip_allowed:
330 330 log.info('Access for IP:%s allowed', ip_addr)
331 331 else:
332 332 return False
333 333
334 334 if action == 'push':
335 335 perms = ('repository.write', 'repository.admin')
336 336 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
337 337 return False
338 338
339 339 else:
340 340 # any other action need at least read permission
341 341 perms = (
342 342 'repository.read', 'repository.write', 'repository.admin')
343 343 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
344 344 return False
345 345
346 346 return True
347 347
348 348 start = time.time()
349 349 log.debug('Running plugin `%s` permissions check', plugin_id)
350 350
351 351 # for environ based auth, password can be empty, but then the validation is
352 352 # on the server that fills in the env data needed for authentication
353 353 perm_result = compute_perm_vcs(
354 354 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
355 355
356 356 auth_time = time.time() - start
357 357 log.debug('Permissions for plugin `%s` completed in %.4fs, '
358 358 'expiration time of fetched cache %.1fs.',
359 359 plugin_id, auth_time, cache_ttl)
360 360
361 361 return perm_result
362 362
363 363 def _get_http_scheme(self, environ):
364 364 try:
365 365 return environ['wsgi.url_scheme']
366 366 except Exception:
367 367 log.exception('Failed to read http scheme')
368 368 return 'http'
369 369
370 370 def _check_ssl(self, environ, start_response):
371 371 """
372 372 Checks the SSL check flag and returns False if SSL is not present
373 373 and required True otherwise
374 374 """
375 375 org_proto = environ['wsgi._org_proto']
376 376 # check if we have SSL required ! if not it's a bad request !
377 377 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
378 378 if require_ssl and org_proto == 'http':
379 379 log.debug(
380 380 'Bad request: detected protocol is `%s` and '
381 381 'SSL/HTTPS is required.', org_proto)
382 382 return False
383 383 return True
384 384
385 385 def _get_default_cache_ttl(self):
386 386 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
387 387 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
388 388 plugin_settings = plugin.get_settings()
389 389 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
390 390 plugin_settings) or (False, 0)
391 391 return plugin_cache_active, cache_ttl
392 392
393 393 def __call__(self, environ, start_response):
394 394 try:
395 395 return self._handle_request(environ, start_response)
396 396 except Exception:
397 397 log.exception("Exception while handling request")
398 398 appenlight.track_exception(environ)
399 399 return HTTPInternalServerError()(environ, start_response)
400 400 finally:
401 401 meta.Session.remove()
402 402
403 403 def _handle_request(self, environ, start_response):
404 404 if not self._check_ssl(environ, start_response):
405 405 reason = ('SSL required, while RhodeCode was unable '
406 406 'to detect this as SSL request')
407 407 log.debug('User not allowed to proceed, %s', reason)
408 408 return HTTPNotAcceptable(reason)(environ, start_response)
409 409
410 410 if not self.url_repo_name:
411 411 log.warning('Repository name is empty: %s', self.url_repo_name)
412 412 # failed to get repo name, we fail now
413 413 return HTTPNotFound()(environ, start_response)
414 414 log.debug('Extracted repo name is %s', self.url_repo_name)
415 415
416 416 ip_addr = get_ip_addr(environ)
417 417 user_agent = get_user_agent(environ)
418 418 username = None
419 419
420 420 # skip passing error to error controller
421 421 environ['pylons.status_code_redirect'] = True
422 422
423 423 # ======================================================================
424 424 # GET ACTION PULL or PUSH
425 425 # ======================================================================
426 426 action = self._get_action(environ)
427 427
428 428 # ======================================================================
429 429 # Check if this is a request to a shadow repository of a pull request.
430 430 # In this case only pull action is allowed.
431 431 # ======================================================================
432 432 if self.is_shadow_repo and action != 'pull':
433 433 reason = 'Only pull action is allowed for shadow repositories.'
434 434 log.debug('User not allowed to proceed, %s', reason)
435 435 return HTTPNotAcceptable(reason)(environ, start_response)
436 436
437 437 # Check if the shadow repo actually exists, in case someone refers
438 438 # to it, and it has been deleted because of successful merge.
439 439 if self.is_shadow_repo and not self.is_shadow_repo_dir:
440 440 log.debug(
441 441 'Shadow repo detected, and shadow repo dir `%s` is missing',
442 442 self.is_shadow_repo_dir)
443 443 return HTTPNotFound()(environ, start_response)
444 444
445 445 # ======================================================================
446 446 # CHECK ANONYMOUS PERMISSION
447 447 # ======================================================================
448 448 detect_force_push = False
449 449 check_branch_perms = False
450 450 if action in ['pull', 'push']:
451 451 user_obj = anonymous_user = User.get_default_user()
452 452 auth_user = user_obj.AuthUser()
453 453 username = anonymous_user.username
454 454 if anonymous_user.active:
455 455 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
456 456 # ONLY check permissions if the user is activated
457 457 anonymous_perm = self._check_permission(
458 458 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
459 459 plugin_id='anonymous_access',
460 460 plugin_cache_active=plugin_cache_active,
461 461 cache_ttl=cache_ttl,
462 462 )
463 463 else:
464 464 anonymous_perm = False
465 465
466 466 if not anonymous_user.active or not anonymous_perm:
467 467 if not anonymous_user.active:
468 468 log.debug('Anonymous access is disabled, running '
469 469 'authentication')
470 470
471 471 if not anonymous_perm:
472 472 log.debug('Not enough credentials to access this '
473 473 'repository as anonymous user')
474 474
475 475 username = None
476 476 # ==============================================================
477 477 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
478 478 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
479 479 # ==============================================================
480 480
481 481 # try to auth based on environ, container auth methods
482 482 log.debug('Running PRE-AUTH for container based authentication')
483 483 pre_auth = authenticate(
484 484 '', '', environ, VCS_TYPE, registry=self.registry,
485 485 acl_repo_name=self.acl_repo_name)
486 486 if pre_auth and pre_auth.get('username'):
487 487 username = pre_auth['username']
488 488 log.debug('PRE-AUTH got %s as username', username)
489 489 if pre_auth:
490 490 log.debug('PRE-AUTH successful from %s',
491 491 pre_auth.get('auth_data', {}).get('_plugin'))
492 492
493 493 # If not authenticated by the container, running basic auth
494 494 # before inject the calling repo_name for special scope checks
495 495 self.authenticate.acl_repo_name = self.acl_repo_name
496 496
497 497 plugin_cache_active, cache_ttl = False, 0
498 498 plugin = None
499 499 if not username:
500 500 self.authenticate.realm = self.authenticate.get_rc_realm()
501 501
502 502 try:
503 503 auth_result = self.authenticate(environ)
504 504 except (UserCreationError, NotAllowedToCreateUserError) as e:
505 505 log.error(e)
506 506 reason = safe_str(e)
507 507 return HTTPNotAcceptable(reason)(environ, start_response)
508 508
509 509 if isinstance(auth_result, dict):
510 510 AUTH_TYPE.update(environ, 'basic')
511 511 REMOTE_USER.update(environ, auth_result['username'])
512 512 username = auth_result['username']
513 513 plugin = auth_result.get('auth_data', {}).get('_plugin')
514 514 log.info(
515 515 'MAIN-AUTH successful for user `%s` from %s plugin',
516 516 username, plugin)
517 517
518 518 plugin_cache_active, cache_ttl = auth_result.get(
519 519 'auth_data', {}).get('_ttl_cache') or (False, 0)
520 520 else:
521 521 return auth_result.wsgi_application(environ, start_response)
522 522
523 523 # ==============================================================
524 524 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
525 525 # ==============================================================
526 526 user = User.get_by_username(username)
527 527 if not self.valid_and_active_user(user):
528 528 return HTTPForbidden()(environ, start_response)
529 529 username = user.username
530 530 user_id = user.user_id
531 531
532 532 # check user attributes for password change flag
533 533 user_obj = user
534 534 auth_user = user_obj.AuthUser()
535 535 if user_obj and user_obj.username != User.DEFAULT_USER and \
536 536 user_obj.user_data.get('force_password_change'):
537 537 reason = 'password change required'
538 538 log.debug('User not allowed to authenticate, %s', reason)
539 539 return HTTPNotAcceptable(reason)(environ, start_response)
540 540
541 541 # check permissions for this repository
542 542 perm = self._check_permission(
543 543 action, user, auth_user, self.acl_repo_name, ip_addr,
544 544 plugin, plugin_cache_active, cache_ttl)
545 545 if not perm:
546 546 return HTTPForbidden()(environ, start_response)
547 547 environ['rc_auth_user_id'] = user_id
548 548
549 549 if action == 'push':
550 550 perms = auth_user.get_branch_permissions(self.acl_repo_name)
551 551 if perms:
552 552 check_branch_perms = True
553 553 detect_force_push = True
554 554
555 555 # extras are injected into UI object and later available
556 556 # in hooks executed by RhodeCode
557 557 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
558 558
559 559 extras = vcs_operation_context(
560 560 environ, repo_name=self.acl_repo_name, username=username,
561 561 action=action, scm=self.SCM, check_locking=check_locking,
562 562 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
563 563 detect_force_push=detect_force_push
564 564 )
565 565
566 566 # ======================================================================
567 567 # REQUEST HANDLING
568 568 # ======================================================================
569 569 repo_path = os.path.join(
570 570 safe_str(self.base_path), safe_str(self.vcs_repo_name))
571 571 log.debug('Repository path is %s', repo_path)
572 572
573 573 fix_PATH()
574 574
575 575 log.info(
576 576 '%s action on %s repo "%s" by "%s" from %s %s',
577 577 action, self.SCM, safe_str(self.url_repo_name),
578 578 safe_str(username), ip_addr, user_agent)
579 579
580 580 return self._generate_vcs_response(
581 581 environ, start_response, repo_path, extras, action)
582 582
583 583 @initialize_generator
584 584 def _generate_vcs_response(
585 585 self, environ, start_response, repo_path, extras, action):
586 586 """
587 587 Returns a generator for the response content.
588 588
589 589 This method is implemented as a generator, so that it can trigger
590 590 the cache validation after all content sent back to the client. It
591 591 also handles the locking exceptions which will be triggered when
592 592 the first chunk is produced by the underlying WSGI application.
593 593 """
594 594 txn_id = ''
595 595 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
596 596 # case for SVN, we want to re-use the callback daemon port
597 597 # so we use the txn_id, for this we peek the body, and still save
598 598 # it as wsgi.input
599 599 data = environ['wsgi.input'].read()
600 600 environ['wsgi.input'] = StringIO(data)
601 601 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
602 602
603 603 callback_daemon, extras = self._prepare_callback_daemon(
604 604 extras, environ, action, txn_id=txn_id)
605 605 log.debug('HOOKS extras is %s', extras)
606 606
607 607 http_scheme = self._get_http_scheme(environ)
608 608
609 609 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
610 610 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
611 611 with callback_daemon:
612 612 app.rc_extras = extras
613 613
614 614 try:
615 615 response = app(environ, start_response)
616 616 finally:
617 617 # This statement works together with the decorator
618 618 # "initialize_generator" above. The decorator ensures that
619 619 # we hit the first yield statement before the generator is
620 620 # returned back to the WSGI server. This is needed to
621 621 # ensure that the call to "app" above triggers the
622 622 # needed callback to "start_response" before the
623 623 # generator is actually used.
624 624 yield "__init__"
625 625
626 626 # iter content
627 627 for chunk in response:
628 628 yield chunk
629 629
630 630 try:
631 631 # invalidate cache on push
632 632 if action == 'push':
633 633 self._invalidate_cache(self.url_repo_name)
634 634 finally:
635 635 meta.Session.remove()
636 636
637 637 def _get_repository_name(self, environ):
638 638 """Get repository name out of the environmnent
639 639
640 640 :param environ: WSGI environment
641 641 """
642 642 raise NotImplementedError()
643 643
644 644 def _get_action(self, environ):
645 645 """Map request commands into a pull or push command.
646 646
647 647 :param environ: WSGI environment
648 648 """
649 649 raise NotImplementedError()
650 650
651 651 def _create_wsgi_app(self, repo_path, repo_name, config):
652 652 """Return the WSGI app that will finally handle the request."""
653 653 raise NotImplementedError()
654 654
655 655 def _create_config(self, extras, repo_name, scheme='http'):
656 656 """Create a safe config representation."""
657 657 raise NotImplementedError()
658 658
659 659 def _should_use_callback_daemon(self, extras, environ, action):
660 660 return True
661 661
662 662 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
663 663 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
664 664 if not self._should_use_callback_daemon(extras, environ, action):
665 665 # disable callback daemon for actions that don't require it
666 666 direct_calls = True
667 667
668 668 return prepare_callback_daemon(
669 669 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
670 670 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
671 671
672 672
673 673 def _should_check_locking(query_string):
674 674 # this is kind of hacky, but due to how mercurial handles client-server
675 675 # server see all operation on commit; bookmarks, phases and
676 676 # obsolescence marker in different transaction, we don't want to check
677 677 # locking on those
678 678 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now