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