##// END OF EJS Templates
vcs: add translation of _REPO_ID into real repo name into more generic...
marcink -
r755:75333e72 default
parent child Browse files
Show More
@@ -1,452 +1,450 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2016 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 logging
28 28 import importlib
29 29 from functools import wraps
30 30
31 31 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
32 32 from webob.exc import (
33 33 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
34 34
35 35 import rhodecode
36 36 from rhodecode.authentication.base import authenticate, VCS_TYPE
37 37 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
38 38 from rhodecode.lib.base import BasicAuth, get_ip_addr, vcs_operation_context
39 39 from rhodecode.lib.exceptions import (
40 40 HTTPLockedRC, HTTPRequirementError, UserCreationError,
41 41 NotAllowedToCreateUserError)
42 42 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
43 43 from rhodecode.lib.middleware import appenlight
44 44 from rhodecode.lib.middleware.utils import scm_app
45 45 from rhodecode.lib.utils import (
46 46 is_valid_repo, get_rhodecode_realm, get_rhodecode_base_path)
47 47 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool
48 48 from rhodecode.lib.vcs.conf import settings as vcs_settings
49 49 from rhodecode.lib.vcs.backends import base
50 50 from rhodecode.model import meta
51 51 from rhodecode.model.db import User, Repository
52 52 from rhodecode.model.scm import ScmModel
53 53 from rhodecode.model.settings import SettingsModel
54 54
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 def initialize_generator(factory):
60 60 """
61 61 Initializes the returned generator by draining its first element.
62 62
63 63 This can be used to give a generator an initializer, which is the code
64 64 up to the first yield statement. This decorator enforces that the first
65 65 produced element has the value ``"__init__"`` to make its special
66 66 purpose very explicit in the using code.
67 67 """
68 68
69 69 @wraps(factory)
70 70 def wrapper(*args, **kwargs):
71 71 gen = factory(*args, **kwargs)
72 72 try:
73 73 init = gen.next()
74 74 except StopIteration:
75 75 raise ValueError('Generator must yield at least one element.')
76 76 if init != "__init__":
77 77 raise ValueError('First yielded element must be "__init__".')
78 78 return gen
79 79 return wrapper
80 80
81 81
82 82 class SimpleVCS(object):
83 83 """Common functionality for SCM HTTP handlers."""
84 84
85 85 SCM = 'unknown'
86 86
87 87 def __init__(self, application, config, registry):
88 88 self.registry = registry
89 89 self.application = application
90 90 self.config = config
91 91 # re-populated by specialized middlewares
92 92 self.repo_vcs_config = base.Config()
93 93 # base path of repo locations
94 94 self.basepath = get_rhodecode_base_path()
95 95 # authenticate this VCS request using authfunc
96 96 auth_ret_code_detection = \
97 97 str2bool(self.config.get('auth_ret_code_detection', False))
98 98 self.authenticate = BasicAuth(
99 99 '', authenticate, registry, config.get('auth_ret_code'),
100 100 auth_ret_code_detection)
101 101 self.ip_addr = '0.0.0.0'
102 102
103 103 @property
104 104 def scm_app(self):
105 105 custom_implementation = self.config.get('vcs.scm_app_implementation')
106 106 if custom_implementation and custom_implementation != 'pyro4':
107 107 log.info(
108 108 "Using custom implementation of scm_app: %s",
109 109 custom_implementation)
110 110 scm_app_impl = importlib.import_module(custom_implementation)
111 111 else:
112 112 scm_app_impl = scm_app
113 113 return scm_app_impl
114 114
115 115 def _get_by_id(self, repo_name):
116 116 """
117 117 Gets a special pattern _<ID> from clone url and tries to replace it
118 118 with a repository_name for support of _<ID> non changable urls
119 119
120 120 :param repo_name:
121 121 """
122 122
123 123 data = repo_name.split('/')
124 124 if len(data) >= 2:
125 125 from rhodecode.model.repo import RepoModel
126 126 by_id_match = RepoModel().get_repo_by_id(repo_name)
127 127 if by_id_match:
128 128 data[1] = by_id_match.repo_name
129 129
130 130 return safe_str('/'.join(data))
131 131
132 132 def _invalidate_cache(self, repo_name):
133 133 """
134 134 Set's cache for this repository for invalidation on next access
135 135
136 136 :param repo_name: full repo name, also a cache key
137 137 """
138 138 ScmModel().mark_for_invalidation(repo_name)
139 139
140 140 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
141 141 db_repo = Repository.get_by_repo_name(repo_name)
142 142 if not db_repo:
143 143 log.debug('Repository `%s` not found inside the database.',
144 144 repo_name)
145 145 return False
146 146
147 147 if db_repo.repo_type != scm_type:
148 148 log.warning(
149 149 'Repository `%s` have incorrect scm_type, expected %s got %s',
150 150 repo_name, db_repo.repo_type, scm_type)
151 151 return False
152 152
153 153 return is_valid_repo(repo_name, base_path, expect_scm=scm_type)
154 154
155 155 def valid_and_active_user(self, user):
156 156 """
157 157 Checks if that user is not empty, and if it's actually object it checks
158 158 if he's active.
159 159
160 160 :param user: user object or None
161 161 :return: boolean
162 162 """
163 163 if user is None:
164 164 return False
165 165
166 166 elif user.active:
167 167 return True
168 168
169 169 return False
170 170
171 171 def _check_permission(self, action, user, repo_name, ip_addr=None):
172 172 """
173 173 Checks permissions using action (push/pull) user and repository
174 174 name
175 175
176 176 :param action: push or pull action
177 177 :param user: user instance
178 178 :param repo_name: repository name
179 179 """
180 180 # check IP
181 181 inherit = user.inherit_default_permissions
182 182 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
183 183 inherit_from_default=inherit)
184 184 if ip_allowed:
185 185 log.info('Access for IP:%s allowed', ip_addr)
186 186 else:
187 187 return False
188 188
189 189 if action == 'push':
190 190 if not HasPermissionAnyMiddleware('repository.write',
191 191 'repository.admin')(user,
192 192 repo_name):
193 193 return False
194 194
195 195 else:
196 196 # any other action need at least read permission
197 197 if not HasPermissionAnyMiddleware('repository.read',
198 198 'repository.write',
199 199 'repository.admin')(user,
200 200 repo_name):
201 201 return False
202 202
203 203 return True
204 204
205 205 def _check_ssl(self, environ, start_response):
206 206 """
207 207 Checks the SSL check flag and returns False if SSL is not present
208 208 and required True otherwise
209 209 """
210 210 org_proto = environ['wsgi._org_proto']
211 211 # check if we have SSL required ! if not it's a bad request !
212 212 require_ssl = str2bool(
213 213 SettingsModel().get_ui_by_key('push_ssl').ui_value)
214 214 if require_ssl and org_proto == 'http':
215 215 log.debug('proto is %s and SSL is required BAD REQUEST !',
216 216 org_proto)
217 217 return False
218 218 return True
219 219
220 220 def __call__(self, environ, start_response):
221 221 try:
222 222 return self._handle_request(environ, start_response)
223 223 except Exception:
224 224 log.exception("Exception while handling request")
225 225 appenlight.track_exception(environ)
226 226 return HTTPInternalServerError()(environ, start_response)
227 227 finally:
228 228 meta.Session.remove()
229 229
230 230 def _handle_request(self, environ, start_response):
231 231
232 232 if not self._check_ssl(environ, start_response):
233 233 reason = ('SSL required, while RhodeCode was unable '
234 234 'to detect this as SSL request')
235 235 log.debug('User not allowed to proceed, %s', reason)
236 236 return HTTPNotAcceptable(reason)(environ, start_response)
237 237
238 238 ip_addr = get_ip_addr(environ)
239 239 username = None
240 240
241 241 # skip passing error to error controller
242 242 environ['pylons.status_code_redirect'] = True
243 243
244 244 # ======================================================================
245 # EXTRACT REPOSITORY NAME FROM ENV
245 # EXTRACT REPOSITORY NAME FROM ENV SET IN `class VCSMiddleware`
246 246 # ======================================================================
247 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
248 repo_name = self._get_repository_name(environ)
249 environ['REPO_NAME'] = repo_name
247 repo_name = environ['REPO_NAME']
250 248 log.debug('Extracted repo name is %s', repo_name)
251 249
252 250 # check for type, presence in database and on filesystem
253 251 if not self.is_valid_and_existing_repo(
254 252 repo_name, self.basepath, self.SCM):
255 253 return HTTPNotFound()(environ, start_response)
256 254
257 255 # ======================================================================
258 256 # GET ACTION PULL or PUSH
259 257 # ======================================================================
260 258 action = self._get_action(environ)
261 259
262 260 # ======================================================================
263 261 # CHECK ANONYMOUS PERMISSION
264 262 # ======================================================================
265 263 if action in ['pull', 'push']:
266 264 anonymous_user = User.get_default_user()
267 265 username = anonymous_user.username
268 266 if anonymous_user.active:
269 267 # ONLY check permissions if the user is activated
270 268 anonymous_perm = self._check_permission(
271 269 action, anonymous_user, repo_name, ip_addr)
272 270 else:
273 271 anonymous_perm = False
274 272
275 273 if not anonymous_user.active or not anonymous_perm:
276 274 if not anonymous_user.active:
277 275 log.debug('Anonymous access is disabled, running '
278 276 'authentication')
279 277
280 278 if not anonymous_perm:
281 279 log.debug('Not enough credentials to access this '
282 280 'repository as anonymous user')
283 281
284 282 username = None
285 283 # ==============================================================
286 284 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
287 285 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
288 286 # ==============================================================
289 287
290 288 # try to auth based on environ, container auth methods
291 289 log.debug('Running PRE-AUTH for container based authentication')
292 290 pre_auth = authenticate(
293 291 '', '', environ, VCS_TYPE, registry=self.registry)
294 292 if pre_auth and pre_auth.get('username'):
295 293 username = pre_auth['username']
296 294 log.debug('PRE-AUTH got %s as username', username)
297 295
298 296 # If not authenticated by the container, running basic auth
299 297 if not username:
300 298 self.authenticate.realm = get_rhodecode_realm()
301 299
302 300 try:
303 301 result = self.authenticate(environ)
304 302 except (UserCreationError, NotAllowedToCreateUserError) as e:
305 303 log.error(e)
306 304 reason = safe_str(e)
307 305 return HTTPNotAcceptable(reason)(environ, start_response)
308 306
309 307 if isinstance(result, str):
310 308 AUTH_TYPE.update(environ, 'basic')
311 309 REMOTE_USER.update(environ, result)
312 310 username = result
313 311 else:
314 312 return result.wsgi_application(environ, start_response)
315 313
316 314 # ==============================================================
317 315 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
318 316 # ==============================================================
319 317 user = User.get_by_username(username)
320 318 if not self.valid_and_active_user(user):
321 319 return HTTPForbidden()(environ, start_response)
322 320 username = user.username
323 321 user.update_lastactivity()
324 322 meta.Session().commit()
325 323
326 324 # check user attributes for password change flag
327 325 user_obj = user
328 326 if user_obj and user_obj.username != User.DEFAULT_USER and \
329 327 user_obj.user_data.get('force_password_change'):
330 328 reason = 'password change required'
331 329 log.debug('User not allowed to authenticate, %s', reason)
332 330 return HTTPNotAcceptable(reason)(environ, start_response)
333 331
334 332 # check permissions for this repository
335 333 perm = self._check_permission(action, user, repo_name, ip_addr)
336 334 if not perm:
337 335 return HTTPForbidden()(environ, start_response)
338 336
339 337 # extras are injected into UI object and later available
340 338 # in hooks executed by rhodecode
341 339 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
342 340 extras = vcs_operation_context(
343 341 environ, repo_name=repo_name, username=username,
344 342 action=action, scm=self.SCM,
345 343 check_locking=check_locking)
346 344
347 345 # ======================================================================
348 346 # REQUEST HANDLING
349 347 # ======================================================================
350 348 str_repo_name = safe_str(repo_name)
351 349 repo_path = os.path.join(safe_str(self.basepath), str_repo_name)
352 350 log.debug('Repository path is %s', repo_path)
353 351
354 352 fix_PATH()
355 353
356 354 log.info(
357 355 '%s action on %s repo "%s" by "%s" from %s',
358 356 action, self.SCM, str_repo_name, safe_str(username), ip_addr)
359 357
360 358 return self._generate_vcs_response(
361 359 environ, start_response, repo_path, repo_name, extras, action)
362 360
363 361 @initialize_generator
364 362 def _generate_vcs_response(
365 363 self, environ, start_response, repo_path, repo_name, extras,
366 364 action):
367 365 """
368 366 Returns a generator for the response content.
369 367
370 368 This method is implemented as a generator, so that it can trigger
371 369 the cache validation after all content sent back to the client. It
372 370 also handles the locking exceptions which will be triggered when
373 371 the first chunk is produced by the underlying WSGI application.
374 372 """
375 373 callback_daemon, extras = self._prepare_callback_daemon(extras)
376 374 config = self._create_config(extras, repo_name)
377 375 log.debug('HOOKS extras is %s', extras)
378 376 app = self._create_wsgi_app(repo_path, repo_name, config)
379 377
380 378 try:
381 379 with callback_daemon:
382 380 try:
383 381 response = app(environ, start_response)
384 382 finally:
385 383 # This statement works together with the decorator
386 384 # "initialize_generator" above. The decorator ensures that
387 385 # we hit the first yield statement before the generator is
388 386 # returned back to the WSGI server. This is needed to
389 387 # ensure that the call to "app" above triggers the
390 388 # needed callback to "start_response" before the
391 389 # generator is actually used.
392 390 yield "__init__"
393 391
394 392 for chunk in response:
395 393 yield chunk
396 394 except Exception as exc:
397 395 # TODO: johbo: Improve "translating" back the exception.
398 396 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
399 397 exc = HTTPLockedRC(*exc.args)
400 398 _code = rhodecode.CONFIG.get('lock_ret_code')
401 399 log.debug('Repository LOCKED ret code %s!', (_code,))
402 400 elif getattr(exc, '_vcs_kind', None) == 'requirement':
403 401 log.debug(
404 402 'Repository requires features unknown to this Mercurial')
405 403 exc = HTTPRequirementError(*exc.args)
406 404 else:
407 405 raise
408 406
409 407 for chunk in exc(environ, start_response):
410 408 yield chunk
411 409 finally:
412 410 # invalidate cache on push
413 411 try:
414 412 if action == 'push':
415 413 self._invalidate_cache(repo_name)
416 414 finally:
417 415 meta.Session.remove()
418 416
419 417 def _get_repository_name(self, environ):
420 418 """Get repository name out of the environmnent
421 419
422 420 :param environ: WSGI environment
423 421 """
424 422 raise NotImplementedError()
425 423
426 424 def _get_action(self, environ):
427 425 """Map request commands into a pull or push command.
428 426
429 427 :param environ: WSGI environment
430 428 """
431 429 raise NotImplementedError()
432 430
433 431 def _create_wsgi_app(self, repo_path, repo_name, config):
434 432 """Return the WSGI app that will finally handle the request."""
435 433 raise NotImplementedError()
436 434
437 435 def _create_config(self, extras, repo_name):
438 436 """Create a Pyro safe config representation."""
439 437 raise NotImplementedError()
440 438
441 439 def _prepare_callback_daemon(self, extras):
442 440 return prepare_callback_daemon(
443 441 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
444 442 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
445 443
446 444
447 445 def _should_check_locking(query_string):
448 446 # this is kind of hacky, but due to how mercurial handles client-server
449 447 # server see all operation on commit; bookmarks, phases and
450 448 # obsolescence marker in different transaction, we don't want to check
451 449 # locking on those
452 450 return query_string not in ['cmd=listkeys']
@@ -1,178 +1,183 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 import gzip
22 22 import shutil
23 23 import logging
24 24 import tempfile
25 25 import urlparse
26 26
27 27 import rhodecode
28 28 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
29 29 from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT
30 30 from rhodecode.lib.middleware.simplehg import SimpleHg
31 31 from rhodecode.lib.middleware.simplesvn import SimpleSvn
32 32 from rhodecode.lib.vcs.backends import base
33 33 from rhodecode.model.settings import VcsSettingsModel
34 34
35 35 log = logging.getLogger(__name__)
36 36
37 37
38 38 def is_git(environ):
39 39 """
40 40 Returns True if requests should be handled by GIT wsgi middleware
41 41 """
42 42 is_git_path = GIT_PROTO_PAT.match(environ['PATH_INFO'])
43 43 log.debug(
44 44 'request path: `%s` detected as GIT PROTOCOL %s', environ['PATH_INFO'],
45 45 is_git_path is not None)
46 46
47 47 return is_git_path
48 48
49 49
50 50 def is_hg(environ):
51 51 """
52 52 Returns True if requests target is mercurial server - header
53 53 ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
54 54 """
55 55 is_hg_path = False
56 56
57 57 http_accept = environ.get('HTTP_ACCEPT')
58 58
59 59 if http_accept and http_accept.startswith('application/mercurial'):
60 60 query = urlparse.parse_qs(environ['QUERY_STRING'])
61 61 if 'cmd' in query:
62 62 is_hg_path = True
63 63
64 64 log.debug(
65 65 'request path: `%s` detected as HG PROTOCOL %s', environ['PATH_INFO'],
66 66 is_hg_path)
67 67
68 68 return is_hg_path
69 69
70 70
71 71 def is_svn(environ):
72 72 """
73 73 Returns True if requests target is Subversion server
74 74 """
75 75 http_dav = environ.get('HTTP_DAV', '')
76 76 magic_path_segment = rhodecode.CONFIG.get(
77 77 'rhodecode_subversion_magic_path', '/!svn')
78 78 is_svn_path = (
79 79 'subversion' in http_dav or
80 80 magic_path_segment in environ['PATH_INFO'])
81 81 log.debug(
82 82 'request path: `%s` detected as SVN PROTOCOL %s', environ['PATH_INFO'],
83 83 is_svn_path)
84 84
85 85 return is_svn_path
86 86
87 87
88 88 class GunzipMiddleware(object):
89 89 """
90 90 WSGI middleware that unzips gzip-encoded requests before
91 91 passing on to the underlying application.
92 92 """
93 93
94 94 def __init__(self, application):
95 95 self.app = application
96 96
97 97 def __call__(self, environ, start_response):
98 98 accepts_encoding_header = environ.get('HTTP_CONTENT_ENCODING', b'')
99 99
100 100 if b'gzip' in accepts_encoding_header:
101 101 log.debug('gzip detected, now running gunzip wrapper')
102 102 wsgi_input = environ['wsgi.input']
103 103
104 104 if not hasattr(environ['wsgi.input'], 'seek'):
105 105 # The gzip implementation in the standard library of Python 2.x
106 106 # requires the '.seek()' and '.tell()' methods to be available
107 107 # on the input stream. Read the data into a temporary file to
108 108 # work around this limitation.
109 109
110 110 wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024)
111 111 shutil.copyfileobj(environ['wsgi.input'], wsgi_input)
112 112 wsgi_input.seek(0)
113 113
114 114 environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r')
115 115 # since we "Ungzipped" the content we say now it's no longer gzip
116 116 # content encoding
117 117 del environ['HTTP_CONTENT_ENCODING']
118 118
119 119 # content length has changes ? or i'm not sure
120 120 if 'CONTENT_LENGTH' in environ:
121 121 del environ['CONTENT_LENGTH']
122 122 else:
123 123 log.debug('content not gzipped, gzipMiddleware passing '
124 124 'request further')
125 125 return self.app(environ, start_response)
126 126
127 127
128 128 class VCSMiddleware(object):
129 129
130 130 def __init__(self, app, config, appenlight_client, registry):
131 131 self.application = app
132 132 self.config = config
133 133 self.appenlight_client = appenlight_client
134 134 self.registry = registry
135 135 self.repo_vcs_config = base.Config()
136 136 self.use_gzip = True
137 137
138 138 def vcs_config(self, repo_name=None):
139 139 """
140 140 returns serialized VcsSettings
141 141 """
142 142 return VcsSettingsModel(repo=repo_name).get_ui_settings_as_config_obj()
143 143
144 144 def wrap_in_gzip_if_enabled(self, app):
145 145 if self.use_gzip:
146 146 app = GunzipMiddleware(app)
147 147 return app
148 148
149 149 def _get_handler_app(self, environ):
150 150 app = None
151 151
152 152 if is_hg(environ):
153 153 app = SimpleHg(self.application, self.config, self.registry)
154 154
155 155 if is_git(environ):
156 156 app = SimpleGit(self.application, self.config, self.registry)
157 157
158 158 if is_svn(environ):
159 159 app = SimpleSvn(self.application, self.config, self.registry)
160 160
161 161 if app:
162 # translate the _REPO_ID into real repo NAME for usage
163 # in midddleware
164 environ['PATH_INFO'] = app._get_by_id(environ['PATH_INFO'])
162 165 repo_name = app._get_repository_name(environ)
166 environ['REPO_NAME'] = repo_name
167
163 168 self.repo_vcs_config = self.vcs_config(repo_name)
164 169 app.repo_vcs_config = self.repo_vcs_config
165 170
166 171 app = self.wrap_in_gzip_if_enabled(app)
167 172 app, _ = wrap_in_appenlight_if_enabled(
168 173 app, self.config, self.appenlight_client)
169 174
170 175 return app
171 176
172 177 def __call__(self, environ, start_response):
173 178 # check if we handle one of interesting protocols ?
174 179 vcs_handler = self._get_handler_app(environ)
175 180 if vcs_handler:
176 181 return vcs_handler(environ, start_response)
177 182
178 183 return self.application(environ, start_response)
General Comments 0
You need to be logged in to leave comments. Login now