##// END OF EJS Templates
feat(svn): improvements to handle SVN protocol 1.4 features...
super-admin -
r5215:e17d6d15 default
parent child Browse files
Show More
@@ -1,231 +1,239 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import base64
21 21 import logging
22 22 import urllib.request
23 23 import urllib.parse
24 24 import urllib.error
25 25 import urllib.parse
26 26
27 27 import requests
28 28 from pyramid.httpexceptions import HTTPNotAcceptable
29 29
30 30 from rhodecode.lib import rc_cache
31 31 from rhodecode.lib.middleware import simplevcs
32 32 from rhodecode.lib.middleware.utils import get_path_info
33 33 from rhodecode.lib.utils import is_valid_repo
34 34 from rhodecode.lib.str_utils import safe_str, safe_int, safe_bytes
35 35 from rhodecode.lib.type_utils import str2bool
36 36 from rhodecode.lib.ext_json import json
37 37 from rhodecode.lib.hooks_daemon import store_txn_id_data
38 38
39 39
40 40 log = logging.getLogger(__name__)
41 41
42 42
43 43 class SimpleSvnApp(object):
44 44 IGNORED_HEADERS = [
45 45 'connection', 'keep-alive', 'content-encoding',
46 46 'transfer-encoding', 'content-length']
47 47 rc_extras = {}
48 48
49 49 def __init__(self, config):
50 50 self.config = config
51 self.session = requests.Session()
51 52
52 53 def __call__(self, environ, start_response):
53 54 request_headers = self._get_request_headers(environ)
54 55 data_io = environ['wsgi.input']
55 56 req_method: str = environ['REQUEST_METHOD']
56 57 has_content_length = 'CONTENT_LENGTH' in environ
57 58
58 59 path_info = self._get_url(
59 60 self.config.get('subversion_http_server_url', ''), get_path_info(environ))
60 61 transfer_encoding = environ.get('HTTP_TRANSFER_ENCODING', '')
61 62 log.debug('Handling: %s method via `%s`', req_method, path_info)
62 63
63 64 # stream control flag, based on request and content type...
64 65 stream = False
65 66
66 67 if req_method in ['MKCOL'] or has_content_length:
67 68 data_processed = False
68 69 # read chunk to check if we have txn-with-props
69 70 initial_data: bytes = data_io.read(1024)
70 71 if initial_data.startswith(b'(create-txn-with-props'):
71 72 data_io = initial_data + data_io.read()
72 73 # store on-the-fly our rc_extra using svn revision properties
73 74 # those can be read later on in hooks executed so we have a way
74 75 # to pass in the data into svn hooks
75 76 rc_data = base64.urlsafe_b64encode(json.dumps(self.rc_extras))
76 77 rc_data_len = str(len(rc_data))
77 78 # header defines data length, and serialized data
78 79 skel = b' rc-scm-extras %b %b' % (safe_bytes(rc_data_len), safe_bytes(rc_data))
79 80 data_io = data_io[:-2] + skel + b'))'
80 81 data_processed = True
81 82
82 83 if not data_processed:
83 84 # NOTE(johbo): Avoid that we end up with sending the request in chunked
84 85 # transfer encoding (mainly on Gunicorn). If we know the content
85 86 # length, then we should transfer the payload in one request.
86 87 data_io = initial_data + data_io.read()
87 88
88 89 if req_method in ['GET', 'PUT'] or transfer_encoding == 'chunked':
89 90 # NOTE(marcink): when getting/uploading files, we want to STREAM content
90 91 # back to the client/proxy instead of buffering it here...
91 92 stream = True
92 93
93 94 stream = stream
94 95 log.debug('Calling SVN PROXY at `%s`, using method:%s. Stream: %s',
95 96 path_info, req_method, stream)
96 97
98 call_kwargs = dict(
99 data=data_io,
100 headers=request_headers,
101 stream=stream
102 )
103 if req_method in ['HEAD', 'DELETE']:
104 del call_kwargs['data']
105
97 106 try:
98 response = requests.request(
99 req_method, path_info,
100 data=data_io, headers=request_headers, stream=stream)
107 response = self.session.request(
108 req_method, path_info, **call_kwargs)
101 109 except requests.ConnectionError:
102 110 log.exception('ConnectionError occurred for endpoint %s', path_info)
103 111 raise
104 112
105 113 if response.status_code not in [200, 401]:
106 114 text = '\n{}'.format(safe_str(response.text)) if response.text else ''
107 115 if response.status_code >= 500:
108 116 log.error('Got SVN response:%s with text:`%s`', response, text)
109 117 else:
110 118 log.debug('Got SVN response:%s with text:`%s`', response, text)
111 119 else:
112 120 log.debug('got response code: %s', response.status_code)
113 121
114 122 response_headers = self._get_response_headers(response.headers)
115 123
116 124 if response.headers.get('SVN-Txn-name'):
117 125 svn_tx_id = response.headers.get('SVN-Txn-name')
118 126 txn_id = rc_cache.utils.compute_key_from_params(
119 127 self.config['repository'], svn_tx_id)
120 128 port = safe_int(self.rc_extras['hooks_uri'].split(':')[-1])
121 129 store_txn_id_data(txn_id, {'port': port})
122 130
123 131 start_response(f'{response.status_code} {response.reason}', response_headers)
124 132 return response.iter_content(chunk_size=1024)
125 133
126 134 def _get_url(self, svn_http_server, path):
127 135 svn_http_server_url = (svn_http_server or '').rstrip('/')
128 136 url_path = urllib.parse.urljoin(svn_http_server_url + '/', (path or '').lstrip('/'))
129 137 url_path = urllib.parse.quote(url_path, safe="/:=~+!$,;'")
130 138 return url_path
131 139
132 140 def _get_request_headers(self, environ):
133 141 headers = {}
134 142
135 143 for key in environ:
136 144 if not key.startswith('HTTP_'):
137 145 continue
138 146 new_key = key.split('_')
139 147 new_key = [k.capitalize() for k in new_key[1:]]
140 148 new_key = '-'.join(new_key)
141 149 headers[new_key] = environ[key]
142 150
143 151 if 'CONTENT_TYPE' in environ:
144 152 headers['Content-Type'] = environ['CONTENT_TYPE']
145 153
146 154 if 'CONTENT_LENGTH' in environ:
147 155 headers['Content-Length'] = environ['CONTENT_LENGTH']
148 156
149 157 return headers
150 158
151 159 def _get_response_headers(self, headers):
152 160 headers = [
153 161 (h, headers[h])
154 162 for h in headers
155 163 if h.lower() not in self.IGNORED_HEADERS
156 164 ]
157 165
158 166 return headers
159 167
160 168
161 169 class DisabledSimpleSvnApp(object):
162 170 def __init__(self, config):
163 171 self.config = config
164 172
165 173 def __call__(self, environ, start_response):
166 174 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
167 175 log.warning(reason)
168 176 return HTTPNotAcceptable(reason)(environ, start_response)
169 177
170 178
171 179 class SimpleSvn(simplevcs.SimpleVCS):
172 180
173 181 SCM = 'svn'
174 182 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
175 183 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
176 184
177 185 def _get_repository_name(self, environ):
178 186 """
179 187 Gets repository name out of PATH_INFO header
180 188
181 189 :param environ: environ where PATH_INFO is stored
182 190 """
183 191 path = get_path_info(environ).split('!')
184 192 repo_name = path[0].strip('/')
185 193
186 194 # SVN includes the whole path in it's requests, including
187 195 # subdirectories inside the repo. Therefore we have to search for
188 196 # the repo root directory.
189 197 if not is_valid_repo(
190 198 repo_name, self.base_path, explicit_scm=self.SCM):
191 199 current_path = ''
192 200 for component in repo_name.split('/'):
193 201 current_path += component
194 202 if is_valid_repo(
195 203 current_path, self.base_path, explicit_scm=self.SCM):
196 204 return current_path
197 205 current_path += '/'
198 206
199 207 return repo_name
200 208
201 209 def _get_action(self, environ):
202 210 return (
203 211 'pull'
204 212 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
205 213 else 'push')
206 214
207 215 def _should_use_callback_daemon(self, extras, environ, action):
208 216 # only MERGE command triggers hooks, so we don't want to start
209 217 # hooks server too many times. POST however starts the svn transaction
210 218 # so we also need to run the init of callback daemon of POST
211 219 if environ['REQUEST_METHOD'] in ['MERGE', 'POST']:
212 220 return True
213 221 return False
214 222
215 223 def _create_wsgi_app(self, repo_path, repo_name, config):
216 224 if self._is_svn_enabled():
217 225 return SimpleSvnApp(config)
218 226 # we don't have http proxy enabled return dummy request handler
219 227 return DisabledSimpleSvnApp(config)
220 228
221 229 def _is_svn_enabled(self):
222 230 conf = self.repo_vcs_config
223 231 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
224 232
225 233 def _create_config(self, extras, repo_name, scheme='http'):
226 234 conf = self.repo_vcs_config
227 235 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
228 236 server_url = server_url or self.DEFAULT_HTTP_SERVER
229 237
230 238 extras['subversion_http_server_url'] = server_url
231 239 return extras
@@ -1,700 +1,701 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 def extract_svn_txn_id(acl_repo_name, data):
66 def extract_svn_txn_id(acl_repo_name, data: bytes):
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 72 try:
72 73 root = etree.fromstring(data)
73 74 pat = re.compile(r'/txn/(?P<txn_id>.*)')
74 75 for el in root:
75 76 if el.tag == '{DAV:}source':
76 77 for sub_el in el:
77 78 if sub_el.tag == '{DAV:}href':
78 79 match = pat.search(sub_el.text)
79 80 if match:
80 81 svn_tx_id = match.groupdict()['txn_id']
81 82 txn_id = rc_cache.utils.compute_key_from_params(
82 83 acl_repo_name, svn_tx_id)
83 84 return txn_id
84 85 except Exception:
85 86 log.exception('Failed to extract txn_id')
86 87
87 88
88 89 def initialize_generator(factory):
89 90 """
90 91 Initializes the returned generator by draining its first element.
91 92
92 93 This can be used to give a generator an initializer, which is the code
93 94 up to the first yield statement. This decorator enforces that the first
94 95 produced element has the value ``"__init__"`` to make its special
95 96 purpose very explicit in the using code.
96 97 """
97 98
98 99 @wraps(factory)
99 100 def wrapper(*args, **kwargs):
100 101 gen = factory(*args, **kwargs)
101 102 try:
102 103 init = next(gen)
103 104 except StopIteration:
104 105 raise ValueError('Generator must yield at least one element.')
105 106 if init != "__init__":
106 107 raise ValueError('First yielded element must be "__init__".')
107 108 return gen
108 109 return wrapper
109 110
110 111
111 112 class SimpleVCS(object):
112 113 """Common functionality for SCM HTTP handlers."""
113 114
114 115 SCM = 'unknown'
115 116
116 117 acl_repo_name = None
117 118 url_repo_name = None
118 119 vcs_repo_name = None
119 120 rc_extras = {}
120 121
121 122 # We have to handle requests to shadow repositories different than requests
122 123 # to normal repositories. Therefore we have to distinguish them. To do this
123 124 # we use this regex which will match only on URLs pointing to shadow
124 125 # repositories.
125 126 shadow_repo_re = re.compile(
126 127 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
127 128 '(?P<target>{slug_pat})/' # target repo
128 129 'pull-request/(?P<pr_id>\\d+)/' # pull request
129 130 'repository$' # shadow repo
130 131 .format(slug_pat=SLUG_RE.pattern))
131 132
132 133 def __init__(self, config, registry):
133 134 self.registry = registry
134 135 self.config = config
135 136 # re-populated by specialized middleware
136 137 self.repo_vcs_config = base.Config()
137 138
138 139 rc_settings = SettingsModel().get_all_settings(cache=True, from_request=False)
139 140 realm = rc_settings.get('rhodecode_realm') or 'RhodeCode AUTH'
140 141
141 142 # authenticate this VCS request using authfunc
142 143 auth_ret_code_detection = \
143 144 str2bool(self.config.get('auth_ret_code_detection', False))
144 145 self.authenticate = BasicAuth(
145 146 '', authenticate, registry, config.get('auth_ret_code'),
146 147 auth_ret_code_detection, rc_realm=realm)
147 148 self.ip_addr = '0.0.0.0'
148 149
149 150 @LazyProperty
150 151 def global_vcs_config(self):
151 152 try:
152 153 return VcsSettingsModel().get_ui_settings_as_config_obj()
153 154 except Exception:
154 155 return base.Config()
155 156
156 157 @property
157 158 def base_path(self):
158 159 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
159 160
160 161 if not settings_path:
161 162 settings_path = self.global_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
162 163
163 164 if not settings_path:
164 165 # try, maybe we passed in explicitly as config option
165 166 settings_path = self.config.get('base_path')
166 167
167 168 if not settings_path:
168 169 raise ValueError('FATAL: base_path is empty')
169 170 return settings_path
170 171
171 172 def set_repo_names(self, environ):
172 173 """
173 174 This will populate the attributes acl_repo_name, url_repo_name,
174 175 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 176 shadow) repositories all names are equal. In case of requests to a
176 177 shadow repository the acl-name points to the target repo of the pull
177 178 request and the vcs-name points to the shadow repo file system path.
178 179 The url-name is always the URL used by the vcs client program.
179 180
180 181 Example in case of a shadow repo:
181 182 acl_repo_name = RepoGroup/MyRepo
182 183 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 184 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 185 """
185 186 # First we set the repo name from URL for all attributes. This is the
186 187 # default if handling normal (non shadow) repo requests.
187 188 self.url_repo_name = self._get_repository_name(environ)
188 189 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 190 self.is_shadow_repo = False
190 191
191 192 # Check if this is a request to a shadow repository.
192 193 match = self.shadow_repo_re.match(self.url_repo_name)
193 194 if match:
194 195 match_dict = match.groupdict()
195 196
196 197 # Build acl repo name from regex match.
197 198 acl_repo_name = safe_str('{groups}{target}'.format(
198 199 groups=match_dict['groups'] or '',
199 200 target=match_dict['target']))
200 201
201 202 # Retrieve pull request instance by ID from regex match.
202 203 pull_request = PullRequest.get(match_dict['pr_id'])
203 204
204 205 # Only proceed if we got a pull request and if acl repo name from
205 206 # URL equals the target repo name of the pull request.
206 207 if pull_request and (acl_repo_name == pull_request.target_repo.repo_name):
207 208
208 209 # Get file system path to shadow repository.
209 210 workspace_id = PullRequestModel()._workspace_id(pull_request)
210 211 vcs_repo_name = pull_request.target_repo.get_shadow_repository_path(workspace_id)
211 212
212 213 # Store names for later usage.
213 214 self.vcs_repo_name = vcs_repo_name
214 215 self.acl_repo_name = acl_repo_name
215 216 self.is_shadow_repo = True
216 217
217 218 log.debug('Setting all VCS repository names: %s', {
218 219 'acl_repo_name': self.acl_repo_name,
219 220 'url_repo_name': self.url_repo_name,
220 221 'vcs_repo_name': self.vcs_repo_name,
221 222 })
222 223
223 224 @property
224 225 def scm_app(self):
225 226 custom_implementation = self.config['vcs.scm_app_implementation']
226 227 if custom_implementation == 'http':
227 228 log.debug('Using HTTP implementation of scm app.')
228 229 scm_app_impl = scm_app_http
229 230 else:
230 231 log.debug('Using custom implementation of scm_app: "{}"'.format(
231 232 custom_implementation))
232 233 scm_app_impl = importlib.import_module(custom_implementation)
233 234 return scm_app_impl
234 235
235 236 def _get_by_id(self, repo_name):
236 237 """
237 238 Gets a special pattern _<ID> from clone url and tries to replace it
238 239 with a repository_name for support of _<ID> non changeable urls
239 240 """
240 241
241 242 data = repo_name.split('/')
242 243 if len(data) >= 2:
243 244 from rhodecode.model.repo import RepoModel
244 245 by_id_match = RepoModel().get_repo_by_id(repo_name)
245 246 if by_id_match:
246 247 data[1] = by_id_match.repo_name
247 248
248 249 # Because PEP-3333-WSGI uses bytes-tunneled-in-latin-1 as PATH_INFO
249 250 # and we use this data
250 251 maybe_new_path = '/'.join(data)
251 252 return safe_bytes(maybe_new_path).decode('latin1')
252 253
253 254 def _invalidate_cache(self, repo_name):
254 255 """
255 256 Set's cache for this repository for invalidation on next access
256 257
257 258 :param repo_name: full repo name, also a cache key
258 259 """
259 260 ScmModel().mark_for_invalidation(repo_name)
260 261
261 262 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 263 db_repo = Repository.get_by_repo_name(repo_name)
263 264 if not db_repo:
264 265 log.debug('Repository `%s` not found inside the database.',
265 266 repo_name)
266 267 return False
267 268
268 269 if db_repo.repo_type != scm_type:
269 270 log.warning(
270 271 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 272 repo_name, db_repo.repo_type, scm_type)
272 273 return False
273 274
274 275 config = db_repo._config
275 276 config.set('extensions', 'largefiles', '')
276 277 return is_valid_repo(
277 278 repo_name, base_path,
278 279 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279 280
280 281 def valid_and_active_user(self, user):
281 282 """
282 283 Checks if that user is not empty, and if it's actually object it checks
283 284 if he's active.
284 285
285 286 :param user: user object or None
286 287 :return: boolean
287 288 """
288 289 if user is None:
289 290 return False
290 291
291 292 elif user.active:
292 293 return True
293 294
294 295 return False
295 296
296 297 @property
297 298 def is_shadow_repo_dir(self):
298 299 return os.path.isdir(self.vcs_repo_name)
299 300
300 301 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
301 302 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 303 """
303 304 Checks permissions using action (push/pull) user and repository
304 305 name. If plugin_cache and ttl is set it will use the plugin which
305 306 authenticated the user to store the cached permissions result for N
306 307 amount of seconds as in cache_ttl
307 308
308 309 :param action: push or pull action
309 310 :param user: user instance
310 311 :param repo_name: repository name
311 312 """
312 313
313 314 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 315 plugin_id, plugin_cache_active, cache_ttl)
315 316
316 317 user_id = user.user_id
317 318 cache_namespace_uid = f'cache_user_auth.{rc_cache.PERMISSIONS_CACHE_VER}.{user_id}'
318 319 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319 320
320 321 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 322 expiration_time=cache_ttl,
322 323 condition=plugin_cache_active)
323 324 def compute_perm_vcs(
324 325 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325 326
326 327 log.debug('auth: calculating permission access now...')
327 328 # check IP
328 329 inherit = user.inherit_default_permissions
329 330 ip_allowed = AuthUser.check_ip_allowed(
330 331 user_id, ip_addr, inherit_from_default=inherit)
331 332 if ip_allowed:
332 333 log.info('Access for IP:%s allowed', ip_addr)
333 334 else:
334 335 return False
335 336
336 337 if action == 'push':
337 338 perms = ('repository.write', 'repository.admin')
338 339 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
339 340 return False
340 341
341 342 else:
342 343 # any other action need at least read permission
343 344 perms = (
344 345 'repository.read', 'repository.write', 'repository.admin')
345 346 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
346 347 return False
347 348
348 349 return True
349 350
350 351 start = time.time()
351 352 log.debug('Running plugin `%s` permissions check', plugin_id)
352 353
353 354 # for environ based auth, password can be empty, but then the validation is
354 355 # on the server that fills in the env data needed for authentication
355 356 perm_result = compute_perm_vcs(
356 357 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357 358
358 359 auth_time = time.time() - start
359 360 log.debug('Permissions for plugin `%s` completed in %.4fs, '
360 361 'expiration time of fetched cache %.1fs.',
361 362 plugin_id, auth_time, cache_ttl)
362 363
363 364 return perm_result
364 365
365 366 def _get_http_scheme(self, environ):
366 367 try:
367 368 return environ['wsgi.url_scheme']
368 369 except Exception:
369 370 log.exception('Failed to read http scheme')
370 371 return 'http'
371 372
372 373 def _check_ssl(self, environ, start_response):
373 374 """
374 375 Checks the SSL check flag and returns False if SSL is not present
375 376 and required True otherwise
376 377 """
377 378 org_proto = environ['wsgi._org_proto']
378 379 # check if we have SSL required ! if not it's a bad request !
379 380 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
380 381 if require_ssl and org_proto == 'http':
381 382 log.debug(
382 383 'Bad request: detected protocol is `%s` and '
383 384 'SSL/HTTPS is required.', org_proto)
384 385 return False
385 386 return True
386 387
387 388 def _get_default_cache_ttl(self):
388 389 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
389 390 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
390 391 plugin_settings = plugin.get_settings()
391 392 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
392 393 plugin_settings) or (False, 0)
393 394 return plugin_cache_active, cache_ttl
394 395
395 396 def __call__(self, environ, start_response):
396 397 try:
397 398 return self._handle_request(environ, start_response)
398 399 except Exception:
399 400 log.exception("Exception while handling request")
400 401 appenlight.track_exception(environ)
401 402 return HTTPInternalServerError()(environ, start_response)
402 403 finally:
403 404 meta.Session.remove()
404 405
405 406 def _handle_request(self, environ, start_response):
406 407 if not self._check_ssl(environ, start_response):
407 408 reason = ('SSL required, while RhodeCode was unable '
408 409 'to detect this as SSL request')
409 410 log.debug('User not allowed to proceed, %s', reason)
410 411 return HTTPNotAcceptable(reason)(environ, start_response)
411 412
412 413 if not self.url_repo_name:
413 414 log.warning('Repository name is empty: %s', self.url_repo_name)
414 415 # failed to get repo name, we fail now
415 416 return HTTPNotFound()(environ, start_response)
416 417 log.debug('Extracted repo name is %s', self.url_repo_name)
417 418
418 419 ip_addr = get_ip_addr(environ)
419 420 user_agent = get_user_agent(environ)
420 421 username = None
421 422
422 423 # skip passing error to error controller
423 424 environ['pylons.status_code_redirect'] = True
424 425
425 426 # ======================================================================
426 427 # GET ACTION PULL or PUSH
427 428 # ======================================================================
428 429 action = self._get_action(environ)
429 430
430 431 # ======================================================================
431 432 # Check if this is a request to a shadow repository of a pull request.
432 433 # In this case only pull action is allowed.
433 434 # ======================================================================
434 435 if self.is_shadow_repo and action != 'pull':
435 436 reason = 'Only pull action is allowed for shadow repositories.'
436 437 log.debug('User not allowed to proceed, %s', reason)
437 438 return HTTPNotAcceptable(reason)(environ, start_response)
438 439
439 440 # Check if the shadow repo actually exists, in case someone refers
440 441 # to it, and it has been deleted because of successful merge.
441 442 if self.is_shadow_repo and not self.is_shadow_repo_dir:
442 443 log.debug(
443 444 'Shadow repo detected, and shadow repo dir `%s` is missing',
444 445 self.is_shadow_repo_dir)
445 446 return HTTPNotFound()(environ, start_response)
446 447
447 448 # ======================================================================
448 449 # CHECK ANONYMOUS PERMISSION
449 450 # ======================================================================
450 451 detect_force_push = False
451 452 check_branch_perms = False
452 453 if action in ['pull', 'push']:
453 454 user_obj = anonymous_user = User.get_default_user()
454 455 auth_user = user_obj.AuthUser()
455 456 username = anonymous_user.username
456 457 if anonymous_user.active:
457 458 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
458 459 # ONLY check permissions if the user is activated
459 460 anonymous_perm = self._check_permission(
460 461 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
461 462 plugin_id='anonymous_access',
462 463 plugin_cache_active=plugin_cache_active,
463 464 cache_ttl=cache_ttl,
464 465 )
465 466 else:
466 467 anonymous_perm = False
467 468
468 469 if not anonymous_user.active or not anonymous_perm:
469 470 if not anonymous_user.active:
470 471 log.debug('Anonymous access is disabled, running '
471 472 'authentication')
472 473
473 474 if not anonymous_perm:
474 475 log.debug('Not enough credentials to access repo: `%s` '
475 476 'repository as anonymous user', self.acl_repo_name)
476 477
477 478
478 479 username = None
479 480 # ==============================================================
480 481 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
481 482 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
482 483 # ==============================================================
483 484
484 485 # try to auth based on environ, container auth methods
485 486 log.debug('Running PRE-AUTH for container|headers based authentication')
486 487
487 488 # headers auth, by just reading special headers and bypass the auth with user/passwd
488 489 pre_auth = authenticate(
489 490 '', '', environ, VCS_TYPE, registry=self.registry,
490 491 acl_repo_name=self.acl_repo_name)
491 492
492 493 if pre_auth and pre_auth.get('username'):
493 494 username = pre_auth['username']
494 495 log.debug('PRE-AUTH got `%s` as username', username)
495 496 if pre_auth:
496 497 log.debug('PRE-AUTH successful from %s',
497 498 pre_auth.get('auth_data', {}).get('_plugin'))
498 499
499 500 # If not authenticated by the container, running basic auth
500 501 # before inject the calling repo_name for special scope checks
501 502 self.authenticate.acl_repo_name = self.acl_repo_name
502 503
503 504 plugin_cache_active, cache_ttl = False, 0
504 505 plugin = None
505 506
506 507 # regular auth chain
507 508 if not username:
508 509 self.authenticate.realm = self.authenticate.get_rc_realm()
509 510
510 511 try:
511 512 auth_result = self.authenticate(environ)
512 513 except (UserCreationError, NotAllowedToCreateUserError) as e:
513 514 log.error(e)
514 515 reason = safe_str(e)
515 516 return HTTPNotAcceptable(reason)(environ, start_response)
516 517
517 518 if isinstance(auth_result, dict):
518 519 AUTH_TYPE.update(environ, 'basic')
519 520 REMOTE_USER.update(environ, auth_result['username'])
520 521 username = auth_result['username']
521 522 plugin = auth_result.get('auth_data', {}).get('_plugin')
522 523 log.info(
523 524 'MAIN-AUTH successful for user `%s` from %s plugin',
524 525 username, plugin)
525 526
526 527 plugin_cache_active, cache_ttl = auth_result.get(
527 528 'auth_data', {}).get('_ttl_cache') or (False, 0)
528 529 else:
529 530 return auth_result.wsgi_application(environ, start_response)
530 531
531 532 # ==============================================================
532 533 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
533 534 # ==============================================================
534 535 user = User.get_by_username(username)
535 536 if not self.valid_and_active_user(user):
536 537 return HTTPForbidden()(environ, start_response)
537 538 username = user.username
538 539 user_id = user.user_id
539 540
540 541 # check user attributes for password change flag
541 542 user_obj = user
542 543 auth_user = user_obj.AuthUser()
543 544 if user_obj and user_obj.username != User.DEFAULT_USER and \
544 545 user_obj.user_data.get('force_password_change'):
545 546 reason = 'password change required'
546 547 log.debug('User not allowed to authenticate, %s', reason)
547 548 return HTTPNotAcceptable(reason)(environ, start_response)
548 549
549 550 # check permissions for this repository
550 551 perm = self._check_permission(
551 552 action, user, auth_user, self.acl_repo_name, ip_addr,
552 553 plugin, plugin_cache_active, cache_ttl)
553 554 if not perm:
554 555 return HTTPForbidden()(environ, start_response)
555 556 environ['rc_auth_user_id'] = str(user_id)
556 557
557 558 if action == 'push':
558 559 perms = auth_user.get_branch_permissions(self.acl_repo_name)
559 560 if perms:
560 561 check_branch_perms = True
561 562 detect_force_push = True
562 563
563 564 # extras are injected into UI object and later available
564 565 # in hooks executed by RhodeCode
565 566 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
566 567
567 568 extras = vcs_operation_context(
568 569 environ, repo_name=self.acl_repo_name, username=username,
569 570 action=action, scm=self.SCM, check_locking=check_locking,
570 571 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
571 572 detect_force_push=detect_force_push
572 573 )
573 574
574 575 # ======================================================================
575 576 # REQUEST HANDLING
576 577 # ======================================================================
577 578 repo_path = os.path.join(
578 579 safe_str(self.base_path), safe_str(self.vcs_repo_name))
579 580 log.debug('Repository path is %s', repo_path)
580 581
581 582 fix_PATH()
582 583
583 584 log.info(
584 585 '%s action on %s repo "%s" by "%s" from %s %s',
585 586 action, self.SCM, safe_str(self.url_repo_name),
586 587 safe_str(username), ip_addr, user_agent)
587 588
588 589 return self._generate_vcs_response(
589 590 environ, start_response, repo_path, extras, action)
590 591
591 592 @initialize_generator
592 593 def _generate_vcs_response(
593 594 self, environ, start_response, repo_path, extras, action):
594 595 """
595 596 Returns a generator for the response content.
596 597
597 598 This method is implemented as a generator, so that it can trigger
598 599 the cache validation after all content sent back to the client. It
599 600 also handles the locking exceptions which will be triggered when
600 601 the first chunk is produced by the underlying WSGI application.
601 602 """
602 603
603 604 txn_id = ''
604 605 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
605 606 # case for SVN, we want to re-use the callback daemon port
606 607 # so we use the txn_id, for this we peek the body, and still save
607 608 # it as wsgi.input
608 609
609 610 stream = environ['wsgi.input']
610 611
611 612 if isinstance(stream, io.BytesIO):
612 data: str = safe_str(stream.getvalue())
613 data: bytes = stream.getvalue()
613 614 elif hasattr(stream, 'buf'): # most likely gunicorn.http.body.Body
614 data: str = safe_str(stream.buf.getvalue())
615 data: bytes = stream.buf.getvalue()
615 616 else:
616 617 # fallback to the crudest way, copy the iterator
617 data = safe_str(stream.read())
618 environ['wsgi.input'] = io.BytesIO(safe_bytes(data))
618 data = safe_bytes(stream.read())
619 environ['wsgi.input'] = io.BytesIO(data)
619 620
620 621 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
621 622
622 623 callback_daemon, extras = self._prepare_callback_daemon(
623 624 extras, environ, action, txn_id=txn_id)
624 625 log.debug('HOOKS extras is %s', extras)
625 626
626 627 http_scheme = self._get_http_scheme(environ)
627 628
628 629 config = self._create_config(extras, self.acl_repo_name, scheme=http_scheme)
629 630 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
630 631 with callback_daemon:
631 632 app.rc_extras = extras
632 633
633 634 try:
634 635 response = app(environ, start_response)
635 636 finally:
636 637 # This statement works together with the decorator
637 638 # "initialize_generator" above. The decorator ensures that
638 639 # we hit the first yield statement before the generator is
639 640 # returned back to the WSGI server. This is needed to
640 641 # ensure that the call to "app" above triggers the
641 642 # needed callback to "start_response" before the
642 643 # generator is actually used.
643 644 yield "__init__"
644 645
645 646 # iter content
646 647 for chunk in response:
647 648 yield chunk
648 649
649 650 try:
650 651 # invalidate cache on push
651 652 if action == 'push':
652 653 self._invalidate_cache(self.url_repo_name)
653 654 finally:
654 655 meta.Session.remove()
655 656
656 657 def _get_repository_name(self, environ):
657 658 """Get repository name out of the environmnent
658 659
659 660 :param environ: WSGI environment
660 661 """
661 662 raise NotImplementedError()
662 663
663 664 def _get_action(self, environ):
664 665 """Map request commands into a pull or push command.
665 666
666 667 :param environ: WSGI environment
667 668 """
668 669 raise NotImplementedError()
669 670
670 671 def _create_wsgi_app(self, repo_path, repo_name, config):
671 672 """Return the WSGI app that will finally handle the request."""
672 673 raise NotImplementedError()
673 674
674 675 def _create_config(self, extras, repo_name, scheme='http'):
675 676 """Create a safe config representation."""
676 677 raise NotImplementedError()
677 678
678 679 def _should_use_callback_daemon(self, extras, environ, action):
679 680 if extras.get('is_shadow_repo'):
680 681 # we don't want to execute hooks, and callback daemon for shadow repos
681 682 return False
682 683 return True
683 684
684 685 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
685 686 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
686 687 if not self._should_use_callback_daemon(extras, environ, action):
687 688 # disable callback daemon for actions that don't require it
688 689 direct_calls = True
689 690
690 691 return prepare_callback_daemon(
691 692 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
692 693 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
693 694
694 695
695 696 def _should_check_locking(query_string):
696 697 # this is kind of hacky, but due to how mercurial handles client-server
697 698 # server see all operation on commit; bookmarks, phases and
698 699 # obsolescence marker in different transaction, we don't want to check
699 700 # locking on those
700 701 return query_string not in ['cmd=listkeys']
@@ -1,298 +1,302 b''
1 1
2 2 # Copyright (C) 2010-2023 RhodeCode GmbH
3 3 #
4 4 # This program is free software: you can redistribute it and/or modify
5 5 # it under the terms of the GNU Affero General Public License, version 3
6 6 # (only), as published by the Free Software Foundation.
7 7 #
8 8 # This program is distributed in the hope that it will be useful,
9 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 11 # GNU General Public License for more details.
12 12 #
13 13 # You should have received a copy of the GNU Affero General Public License
14 14 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 15 #
16 16 # This program is dual-licensed. If you wish to learn more about the
17 17 # RhodeCode Enterprise Edition, including its added features, Support services,
18 18 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 19
20 20 import gzip
21 21 import shutil
22 22 import logging
23 23 import tempfile
24 24 import urllib.parse
25 25
26 26 from webob.exc import HTTPNotFound
27 27
28 28 import rhodecode
29 29 from rhodecode.lib.middleware.utils import get_path_info
30 30 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
31 31 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
32 32 from rhodecode.lib.middleware.simplehg import SimpleHg
33 33 from rhodecode.lib.middleware.simplesvn import SimpleSvn
34 34 from rhodecode.model.settings import VcsSettingsModel
35 35
36 36
37 37 log = logging.getLogger(__name__)
38 38
39 39 VCS_TYPE_KEY = '_rc_vcs_type'
40 40 VCS_TYPE_SKIP = '_rc_vcs_skip'
41 41
42 42
43 43 def is_git(environ):
44 44 """
45 45 Returns True if requests should be handled by GIT wsgi middleware
46 46 """
47 47 path_info = get_path_info(environ)
48 48 is_git_path = GIT_PROTO_PAT.match(path_info)
49 49 log.debug(
50 50 'request path: `%s` detected as GIT PROTOCOL %s', path_info,
51 51 is_git_path is not None)
52 52
53 53 return is_git_path
54 54
55 55
56 56 def is_hg(environ):
57 57 """
58 58 Returns True if requests target is mercurial server - header
59 59 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
60 60 """
61 61 is_hg_path = False
62 62
63 63 http_accept = environ.get('HTTP_ACCEPT')
64 64
65 65 if http_accept and http_accept.startswith('application/mercurial'):
66 66 query = urllib.parse.parse_qs(environ['QUERY_STRING'])
67 67 if 'cmd' in query:
68 68 is_hg_path = True
69 69
70 70 path_info = get_path_info(environ)
71 71 log.debug(
72 72 'request path: `%s` detected as HG PROTOCOL %s', path_info,
73 73 is_hg_path)
74 74
75 75 return is_hg_path
76 76
77 77
78 78 def is_svn(environ):
79 79 """
80 80 Returns True if requests target is Subversion server
81 81 """
82 82
83 83 http_dav = environ.get('HTTP_DAV', '')
84 84 magic_path_segment = rhodecode.CONFIG.get(
85 85 'rhodecode_subversion_magic_path', '/!svn')
86 86 path_info = get_path_info(environ)
87 req_method = environ['REQUEST_METHOD']
88
87 89 is_svn_path = (
88 90 'subversion' in http_dav or
89 91 magic_path_segment in path_info
90 or environ['REQUEST_METHOD'] in ['PROPFIND', 'PROPPATCH']
92 or req_method in ['PROPFIND', 'PROPPATCH', 'HEAD']
91 93 )
92 94 log.debug(
93 95 'request path: `%s` detected as SVN PROTOCOL %s', path_info,
94 96 is_svn_path)
95 97
96 98 return is_svn_path
97 99
98 100
99 101 class GunzipMiddleware(object):
100 102 """
101 103 WSGI middleware that unzips gzip-encoded requests before
102 104 passing on to the underlying application.
103 105 """
104 106
105 107 def __init__(self, application):
106 108 self.app = application
107 109
108 110 def __call__(self, environ, start_response):
109 111 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
110 112
111 113 if b'gzip' in accepts_encoding_header:
112 114 log.debug('gzip detected, now running gunzip wrapper')
113 115 wsgi_input = environ['wsgi.input']
114 116
115 117 if not hasattr(environ['wsgi.input'], 'seek'):
116 118 # The gzip implementation in the standard library of Python 2.x
117 119 # requires the '.seek()' and '.tell()' methods to be available
118 120 # on the input stream. Read the data into a temporary file to
119 121 # work around this limitation.
120 122
121 123 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
122 124 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
123 125 wsgi_input.seek(0)
124 126
125 127 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
126 128 # since we "Ungzipped" the content we say now it's no longer gzip
127 129 # content encoding
128 130 del environ['HTTP_CONTENT_ENCODING']
129 131
130 132 # content length has changes ? or i'm not sure
131 133 if 'CONTENT_LENGTH' in environ:
132 134 del environ['CONTENT_LENGTH']
133 135 else:
134 136 log.debug('content not gzipped, gzipMiddleware passing '
135 137 'request further')
136 138 return self.app(environ, start_response)
137 139
138 140
139 141 def is_vcs_call(environ):
140 142 if VCS_TYPE_KEY in environ:
141 143 raw_type = environ[VCS_TYPE_KEY]
142 144 return raw_type and raw_type != VCS_TYPE_SKIP
143 145 return False
144 146
145 147
146 148 def detect_vcs_request(environ, backends):
147 149 checks = {
148 150 'hg': (is_hg, SimpleHg),
149 151 'git': (is_git, SimpleGit),
150 152 'svn': (is_svn, SimpleSvn),
151 153 }
152 154 handler = None
153 155 # List of path views first chunk we don't do any checks
154 156 white_list = [
155 157 # favicon often requested by browsers
156 158 'favicon.ico',
157 159
158 160 # e.g /_file_store/download
159 161 '_file_store++',
160 162
161 163 # login
162 164 "_admin/login",
163 165
164 166 # _admin/api is safe too
165 167 '_admin/api',
166 168
167 169 # _admin/gist is safe too
168 170 '_admin/gists++',
169 171
170 172 # _admin/my_account is safe too
171 173 '_admin/my_account++',
172 174
173 175 # static files no detection
174 176 '_static++',
175 177
176 178 # debug-toolbar
177 179 '_debug_toolbar++',
178 180
179 181 # skip ops ping, status
180 182 '_admin/ops/ping',
181 183 '_admin/ops/status',
182 184
183 185 # full channelstream connect should be VCS skipped
184 186 '_admin/channelstream/connect',
185 187
186 188 '++/repo_creating_check'
187 189 ]
188 190 path_info = get_path_info(environ)
189 191 path_url = path_info.lstrip('/')
192 req_method = environ.get('REQUEST_METHOD')
190 193
191 194 for item in white_list:
192 195 if item.endswith('++') and path_url.startswith(item[:-2]):
193 196 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
194 197 return handler
195 198 if item.startswith('++') and path_url.endswith(item[2:]):
196 199 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
197 200 return handler
198 201 if item == path_url:
199 202 log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item)
200 203 return handler
201 204
202 205 if VCS_TYPE_KEY in environ:
203 206 raw_type = environ[VCS_TYPE_KEY]
204 207 if raw_type == VCS_TYPE_SKIP:
205 208 log.debug('got `skip` marker for vcs detection, skipping...')
206 209 return handler
207 210
208 211 _check, handler = checks.get(raw_type) or [None, None]
209 212 if handler:
210 213 log.debug('got handler:%s from environ', handler)
211 214
212 215 if not handler:
213 log.debug('request start: checking if request for `%s` is of VCS type in order: %s', path_url, backends)
216 log.debug('request start: checking if request for `%s:%s` is of VCS type in order: %s',
217 req_method, path_url, backends)
214 218 for vcs_type in backends:
215 219 vcs_check, _handler = checks[vcs_type]
216 220 if vcs_check(environ):
217 221 log.debug('vcs handler found %s', _handler)
218 222 handler = _handler
219 223 break
220 224
221 225 return handler
222 226
223 227
224 228 class VCSMiddleware(object):
225 229
226 230 def __init__(self, app, registry, config, appenlight_client):
227 231 self.application = app
228 232 self.registry = registry
229 233 self.config = config
230 234 self.appenlight_client = appenlight_client
231 235 self.use_gzip = True
232 236 # order in which we check the middlewares, based on vcs.backends config
233 237 self.check_middlewares = config['vcs.backends']
234 238
235 239 def vcs_config(self, repo_name=None):
236 240 """
237 241 returns serialized VcsSettings
238 242 """
239 243 try:
240 244 return VcsSettingsModel(
241 245 repo=repo_name).get_ui_settings_as_config_obj()
242 246 except Exception:
243 247 pass
244 248
245 249 def wrap_in_gzip_if_enabled(self, app, config):
246 250 if self.use_gzip:
247 251 app = GunzipMiddleware(app)
248 252 return app
249 253
250 254 def _get_handler_app(self, environ):
251 255 app = None
252 256 log.debug('VCSMiddleware: detecting vcs type.')
253 257 handler = detect_vcs_request(environ, self.check_middlewares)
254 258 if handler:
255 259 app = handler(self.config, self.registry)
256 260
257 261 return app
258 262
259 263 def __call__(self, environ, start_response):
260 264 # check if we handle one of interesting protocols, optionally extract
261 265 # specific vcsSettings and allow changes of how things are wrapped
262 266 vcs_handler = self._get_handler_app(environ)
263 267 if vcs_handler:
264 268 # translate the _REPO_ID into real repo NAME for usage
265 269 # in middleware
266 270
267 271 path_info = get_path_info(environ)
268 272 environ['PATH_INFO'] = vcs_handler._get_by_id(path_info)
269 273
270 274 # Set acl, url and vcs repo names.
271 275 vcs_handler.set_repo_names(environ)
272 276
273 277 # register repo config back to the handler
274 278 vcs_conf = self.vcs_config(vcs_handler.acl_repo_name)
275 279 # maybe damaged/non existent settings. We still want to
276 280 # pass that point to validate on is_valid_and_existing_repo
277 281 # and return proper HTTP Code back to client
278 282 if vcs_conf:
279 283 vcs_handler.repo_vcs_config = vcs_conf
280 284
281 285 # check for type, presence in database and on filesystem
282 286 if not vcs_handler.is_valid_and_existing_repo(
283 287 vcs_handler.acl_repo_name,
284 288 vcs_handler.base_path,
285 289 vcs_handler.SCM):
286 290 return HTTPNotFound()(environ, start_response)
287 291
288 292 environ['REPO_NAME'] = vcs_handler.url_repo_name
289 293
290 294 # Wrap handler in middlewares if they are enabled.
291 295 vcs_handler = self.wrap_in_gzip_if_enabled(
292 296 vcs_handler, self.config)
293 297 vcs_handler, _ = wrap_in_appenlight_if_enabled(
294 298 vcs_handler, self.config, self.appenlight_client)
295 299
296 300 return vcs_handler(environ, start_response)
297 301
298 302 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now