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