vcs.py
312 lines
| 10.2 KiB
| text/x-python
|
PythonLexer
r1 | ||||
r5088 | # Copyright (C) 2010-2023 RhodeCode GmbH | |||
r1 | # | |||
# This program is free software: you can redistribute it and/or modify | ||||
# it under the terms of the GNU Affero General Public License, version 3 | ||||
# (only), as published by the Free Software Foundation. | ||||
# | ||||
# This program is distributed in the hope that it will be useful, | ||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||||
# GNU General Public License for more details. | ||||
# | ||||
# You should have received a copy of the GNU Affero General Public License | ||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. | ||||
# | ||||
# This program is dual-licensed. If you wish to learn more about the | ||||
# RhodeCode Enterprise Edition, including its added features, Support services, | ||||
# and proprietary license terms, please see https://rhodecode.com/licenses/ | ||||
import gzip | ||||
import shutil | ||||
import logging | ||||
import tempfile | ||||
r4919 | import urllib.parse | |||
r1 | ||||
r757 | from webob.exc import HTTPNotFound | |||
r1 | import rhodecode | |||
r5370 | from rhodecode.apps._base import ADMIN_PREFIX | |||
r5032 | from rhodecode.lib.middleware.utils import get_path_info | |||
r1 | from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled | |||
from rhodecode.lib.middleware.simplegit import SimpleGit, GIT_PROTO_PAT | ||||
from rhodecode.lib.middleware.simplehg import SimpleHg | ||||
from rhodecode.lib.middleware.simplesvn import SimpleSvn | ||||
r5229 | from rhodecode.lib.str_utils import safe_str | |||
r754 | from rhodecode.model.settings import VcsSettingsModel | |||
r1 | ||||
r5032 | ||||
r1 | log = logging.getLogger(__name__) | |||
r1297 | VCS_TYPE_KEY = '_rc_vcs_type' | |||
VCS_TYPE_SKIP = '_rc_vcs_skip' | ||||
r1 | ||||
def is_git(environ): | ||||
""" | ||||
Returns True if requests should be handled by GIT wsgi middleware | ||||
""" | ||||
r5032 | path_info = get_path_info(environ) | |||
is_git_path = GIT_PROTO_PAT.match(path_info) | ||||
r1 | log.debug( | |||
r5032 | 'request path: `%s` detected as GIT PROTOCOL %s', path_info, | |||
r1 | is_git_path is not None) | |||
return is_git_path | ||||
def is_hg(environ): | ||||
""" | ||||
Returns True if requests target is mercurial server - header | ||||
``HTTP_ACCEPT`` of such request would start with ``application/mercurial``. | ||||
""" | ||||
is_hg_path = False | ||||
http_accept = environ.get('HTTP_ACCEPT') | ||||
if http_accept and http_accept.startswith('application/mercurial'): | ||||
r4973 | query = urllib.parse.parse_qs(environ['QUERY_STRING']) | |||
r1 | if 'cmd' in query: | |||
is_hg_path = True | ||||
r5032 | path_info = get_path_info(environ) | |||
r1 | log.debug( | |||
r5032 | 'request path: `%s` detected as HG PROTOCOL %s', path_info, | |||
r1 | is_hg_path) | |||
return is_hg_path | ||||
def is_svn(environ): | ||||
""" | ||||
Returns True if requests target is Subversion server | ||||
""" | ||||
r2262 | ||||
r1 | http_dav = environ.get('HTTP_DAV', '') | |||
r437 | magic_path_segment = rhodecode.CONFIG.get( | |||
'rhodecode_subversion_magic_path', '/!svn') | ||||
r5032 | path_info = get_path_info(environ) | |||
r5215 | req_method = environ['REQUEST_METHOD'] | |||
r437 | is_svn_path = ( | |||
'subversion' in http_dav or | ||||
r5032 | magic_path_segment in path_info | |||
r5215 | or req_method in ['PROPFIND', 'PROPPATCH', 'HEAD'] | |||
r2262 | ) | |||
r1 | log.debug( | |||
r5032 | 'request path: `%s` detected as SVN PROTOCOL %s', path_info, | |||
r1 | is_svn_path) | |||
return is_svn_path | ||||
class GunzipMiddleware(object): | ||||
""" | ||||
WSGI middleware that unzips gzip-encoded requests before | ||||
passing on to the underlying application. | ||||
""" | ||||
def __init__(self, application): | ||||
self.app = application | ||||
def __call__(self, environ, start_response): | ||||
r5229 | accepts_encoding_header = safe_str(environ.get('HTTP_CONTENT_ENCODING', '')) | |||
r1 | ||||
r5229 | if 'gzip' in accepts_encoding_header: | |||
r1 | log.debug('gzip detected, now running gunzip wrapper') | |||
wsgi_input = environ['wsgi.input'] | ||||
if not hasattr(environ['wsgi.input'], 'seek'): | ||||
# The gzip implementation in the standard library of Python 2.x | ||||
# requires the '.seek()' and '.tell()' methods to be available | ||||
# on the input stream. Read the data into a temporary file to | ||||
# work around this limitation. | ||||
wsgi_input = tempfile.SpooledTemporaryFile(64 * 1024 * 1024) | ||||
shutil.copyfileobj(environ['wsgi.input'], wsgi_input) | ||||
wsgi_input.seek(0) | ||||
environ['wsgi.input'] = gzip.GzipFile(fileobj=wsgi_input, mode='r') | ||||
# since we "Ungzipped" the content we say now it's no longer gzip | ||||
# content encoding | ||||
del environ['HTTP_CONTENT_ENCODING'] | ||||
# content length has changes ? or i'm not sure | ||||
if 'CONTENT_LENGTH' in environ: | ||||
del environ['CONTENT_LENGTH'] | ||||
else: | ||||
log.debug('content not gzipped, gzipMiddleware passing ' | ||||
'request further') | ||||
return self.app(environ, start_response) | ||||
r1297 | def is_vcs_call(environ): | |||
if VCS_TYPE_KEY in environ: | ||||
raw_type = environ[VCS_TYPE_KEY] | ||||
return raw_type and raw_type != VCS_TYPE_SKIP | ||||
return False | ||||
def detect_vcs_request(environ, backends): | ||||
checks = { | ||||
'hg': (is_hg, SimpleHg), | ||||
'git': (is_git, SimpleGit), | ||||
'svn': (is_svn, SimpleSvn), | ||||
} | ||||
handler = None | ||||
r4475 | # List of path views first chunk we don't do any checks | |||
white_list = [ | ||||
r5082 | # favicon often requested by browsers | |||
'favicon.ico', | ||||
r5533 | # static files no detection | |||
'_static++', | ||||
# debug-toolbar | ||||
'_debug_toolbar++', | ||||
r4475 | # e.g /_file_store/download | |||
r5082 | '_file_store++', | |||
r5140 | # login | |||
r5533 | f"{ADMIN_PREFIX}/login", | |||
f"{ADMIN_PREFIX}/logout", | ||||
r5140 | ||||
r5370 | # 2fa | |||
f"{ADMIN_PREFIX}/check_2fa", | ||||
f"{ADMIN_PREFIX}/setup_2fa", | ||||
r5082 | # _admin/api is safe too | |||
r5370 | f'{ADMIN_PREFIX}/api', | |||
r5082 | ||||
# _admin/gist is safe too | ||||
r5370 | f'{ADMIN_PREFIX}/gists++', | |||
r5082 | ||||
# _admin/my_account is safe too | ||||
r5370 | f'{ADMIN_PREFIX}/my_account++', | |||
r4655 | ||||
r4736 | # skip ops ping, status | |||
r5370 | f'{ADMIN_PREFIX}/ops/ping', | |||
f'{ADMIN_PREFIX}/ops/status', | ||||
r4721 | ||||
r4655 | # full channelstream connect should be VCS skipped | |||
r5370 | f'{ADMIN_PREFIX}/channelstream/connect', | |||
r5131 | ||||
'++/repo_creating_check' | ||||
r4475 | ] | |||
r5533 | ||||
r5032 | path_info = get_path_info(environ) | |||
path_url = path_info.lstrip('/') | ||||
r5215 | req_method = environ.get('REQUEST_METHOD') | |||
r4655 | ||||
r5082 | for item in white_list: | |||
r5533 | item = item.lstrip('/') | |||
r5082 | if item.endswith('++') and path_url.startswith(item[:-2]): | |||
log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item) | ||||
return handler | ||||
r5131 | if item.startswith('++') and path_url.endswith(item[2:]): | |||
log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item) | ||||
return handler | ||||
r5082 | if item == path_url: | |||
log.debug('path `%s` in whitelist (match:%s), skipping...', path_url, item) | ||||
return handler | ||||
r4655 | ||||
r1297 | if VCS_TYPE_KEY in environ: | |||
raw_type = environ[VCS_TYPE_KEY] | ||||
if raw_type == VCS_TYPE_SKIP: | ||||
log.debug('got `skip` marker for vcs detection, skipping...') | ||||
return handler | ||||
_check, handler = checks.get(raw_type) or [None, None] | ||||
if handler: | ||||
log.debug('got handler:%s from environ', handler) | ||||
if not handler: | ||||
r5215 | log.debug('request start: checking if request for `%s:%s` is of VCS type in order: %s', | |||
req_method, path_url, backends) | ||||
r1297 | for vcs_type in backends: | |||
vcs_check, _handler = checks[vcs_type] | ||||
if vcs_check(environ): | ||||
log.debug('vcs handler found %s', _handler) | ||||
handler = _handler | ||||
break | ||||
return handler | ||||
r1 | class VCSMiddleware(object): | |||
r2351 | def __init__(self, app, registry, config, appenlight_client): | |||
r1 | self.application = app | |||
r2351 | self.registry = registry | |||
r1 | self.config = config | |||
self.appenlight_client = appenlight_client | ||||
r754 | self.use_gzip = True | |||
r757 | # order in which we check the middlewares, based on vcs.backends config | |||
self.check_middlewares = config['vcs.backends'] | ||||
r754 | ||||
def vcs_config(self, repo_name=None): | ||||
""" | ||||
returns serialized VcsSettings | ||||
""" | ||||
r2362 | try: | |||
return VcsSettingsModel( | ||||
repo=repo_name).get_ui_settings_as_config_obj() | ||||
except Exception: | ||||
pass | ||||
r754 | ||||
r757 | def wrap_in_gzip_if_enabled(self, app, config): | |||
r754 | if self.use_gzip: | |||
app = GunzipMiddleware(app) | ||||
return app | ||||
r1 | ||||
def _get_handler_app(self, environ): | ||||
app = None | ||||
r1297 | log.debug('VCSMiddleware: detecting vcs type.') | |||
handler = detect_vcs_request(environ, self.check_middlewares) | ||||
if handler: | ||||
r2351 | app = handler(self.config, self.registry) | |||
r1 | ||||
return app | ||||
def __call__(self, environ, start_response): | ||||
r757 | # check if we handle one of interesting protocols, optionally extract | |||
# specific vcsSettings and allow changes of how things are wrapped | ||||
r1 | vcs_handler = self._get_handler_app(environ) | |||
if vcs_handler: | ||||
r757 | # translate the _REPO_ID into real repo NAME for usage | |||
# in middleware | ||||
r5032 | ||||
path_info = get_path_info(environ) | ||||
environ['PATH_INFO'] = vcs_handler._get_by_id(path_info) | ||||
r757 | ||||
Martin Bornhold
|
r904 | # Set acl, url and vcs repo names. | ||
Martin Bornhold
|
r889 | vcs_handler.set_repo_names(environ) | ||
r887 | ||||
r2362 | # register repo config back to the handler | |||
vcs_conf = self.vcs_config(vcs_handler.acl_repo_name) | ||||
# maybe damaged/non existent settings. We still want to | ||||
# pass that point to validate on is_valid_and_existing_repo | ||||
# and return proper HTTP Code back to client | ||||
if vcs_conf: | ||||
vcs_handler.repo_vcs_config = vcs_conf | ||||
r757 | # check for type, presence in database and on filesystem | |||
if not vcs_handler.is_valid_and_existing_repo( | ||||
Martin Bornhold
|
r889 | vcs_handler.acl_repo_name, | ||
r2351 | vcs_handler.base_path, | |||
Martin Bornhold
|
r889 | vcs_handler.SCM): | ||
r757 | return HTTPNotFound()(environ, start_response) | |||
Martin Bornhold
|
r889 | environ['REPO_NAME'] = vcs_handler.url_repo_name | ||
r757 | ||||
Martin Bornhold
|
r889 | # Wrap handler in middlewares if they are enabled. | ||
r757 | vcs_handler = self.wrap_in_gzip_if_enabled( | |||
vcs_handler, self.config) | ||||
vcs_handler, _ = wrap_in_appenlight_if_enabled( | ||||
vcs_handler, self.config, self.appenlight_client) | ||||
Martin Bornhold
|
r889 | |||
r1 | return vcs_handler(environ, start_response) | |||
return self.application(environ, start_response) | ||||