##// END OF EJS Templates
feat(configs): deprecared old hooks protocol and ssh wrapper....
feat(configs): deprecared old hooks protocol and ssh wrapper. New defaults are now set on v2 keys, so previous installation are automatically set to new keys. Fallback mode is still available.

File last commit:

r5336:7c832518 default
r5496:cab50adf default
Show More
svn.py
269 lines | 8.9 KiB | text/x-python | PythonLexer
copyrights: updated for 2023
r5088 # Copyright (C) 2016-2023 RhodeCode GmbH
ssh-support: enabled full handling of all backends via SSH....
r2187 #
# 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 os
import re
import sys
import logging
import signal
import tempfile
from subprocess import Popen, PIPE
python3: fixed urlparse import
r4919 import urllib.parse
ssh-support: enabled full handling of all backends via SSH....
r2187
fix(svn-ssh): fixed svn ssh wrapper
r5330 from rhodecode_tools.lib.utils import safe_str
feat(svn-config): updated with the latest changes.
r5329 from .base import SshVcsServer
ssh-support: enabled full handling of all backends via SSH....
r2187
log = logging.getLogger(__name__)
class SubversionTunnelWrapper(object):
process = None
def __init__(self, server):
self.server = server
self.timeout = 30
self.stdin = sys.stdin
self.stdout = sys.stdout
self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
self.read_only = True # flag that we set to make the hooks readonly
def create_svn_config(self):
content = (
'[general]\n'
'hooks-env = {}\n').format(self.hooks_env_path)
with os.fdopen(self.svn_conf_fd, 'w') as config_file:
config_file.write(content)
def create_hooks_env(self):
content = (
'[default]\n'
'LANG = en_US.UTF-8\n')
if self.read_only:
content += 'SSH_READ_ONLY = 1\n'
with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
hooks_env_file.write(content)
def remove_configs(self):
os.remove(self.svn_conf_path)
os.remove(self.hooks_env_path)
def command(self):
root = self.server.get_root_store()
dan
svn: explicitly specify tunnel-user to properly map rhodecode username on svn commit via SSH backend...
r4286 username = self.server.user.username
ssh-support: enabled full handling of all backends via SSH....
r2187 command = [
self.server.svn_path, '-t',
'--config-file', self.svn_conf_path,
dan
svn: explicitly specify tunnel-user to properly map rhodecode username on svn commit via SSH backend...
r4286 '--tunnel-user', username,
ssh-support: enabled full handling of all backends via SSH....
r2187 '-r', root]
ssh: fix problems with backend and ssh clones.
r2595 log.debug("Final CMD: %s", ' '.join(command))
ssh-support: enabled full handling of all backends via SSH....
r2187 return command
def start(self):
command = self.command()
ssh: fix problems with backend and ssh clones.
r2595 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
ssh-support: enabled full handling of all backends via SSH....
r2187
def sync(self):
while self.process.poll() is None:
fix(svn-ssh): fixed svn ssh wrapper
r5330 next_byte = self.stdin.buffer.read(1)
ssh-support: enabled full handling of all backends via SSH....
r2187 if not next_byte:
break
self.process.stdin.write(next_byte)
self.remove_configs()
@property
def return_code(self):
return self.process.returncode
def get_first_client_response(self):
signal.signal(signal.SIGALRM, self.interrupt)
signal.alarm(self.timeout)
first_response = self._read_first_client_response()
signal.alarm(0)
dan
svn: fixed case of wrong extracted repository name for SSH backend. In cases...
r4281 return (self._parse_first_client_response(first_response)
if first_response else None)
ssh-support: enabled full handling of all backends via SSH....
r2187
def patch_first_client_response(self, response, **kwargs):
self.create_hooks_env()
fix(svn): more binary protocol svn ssh fixes
r5333
version = response['version']
capabilities = response['capabilities']
client = response['client'] or b''
url = self._svn_bytes(response['url'])
ra_client = self._svn_bytes(response['ra_client'])
buffer_ = b"( %b ( %b ) %b%b( %b) ) " % (
version,
capabilities,
url,
ra_client,
client
)
ssh-support: enabled full handling of all backends via SSH....
r2187 self.process.stdin.write(buffer_)
def fail(self, message):
fix(svn): more binary protocol svn ssh fixes
r5333 fail_msg = b"( failure ( ( 210005 %b 0: 0 ) ) )" % self._svn_bytes(message)
sys.stdout.buffer.write(fail_msg)
sys.stdout.flush()
ssh-support: enabled full handling of all backends via SSH....
r2187 self.remove_configs()
self.process.kill()
svn: fixed tests for svn backend.
r2603 return 1
ssh-support: enabled full handling of all backends via SSH....
r2187
def interrupt(self, signum, frame):
self.fail("Exited by timeout")
fix(svn): more binary protocol svn ssh fixes
r5333 def _svn_bytes(self, bytes_: bytes) -> bytes:
if not bytes_:
return b''
return f'{len(bytes_)}:'.encode() + bytes_ + b' '
ssh-support: enabled full handling of all backends via SSH....
r2187
def _read_first_client_response(self):
fix(svn-ssh): fixed svn ssh wrapper
r5330 buffer_ = b""
ssh-support: enabled full handling of all backends via SSH....
r2187 brackets_stack = []
while True:
fix(svn-ssh): fixed svn ssh wrapper
r5330 next_byte = self.stdin.buffer.read(1)
ssh-support: enabled full handling of all backends via SSH....
r2187 buffer_ += next_byte
fix(svn-ssh): fixed svn ssh wrapper
r5330 if next_byte == b"(":
ssh-support: enabled full handling of all backends via SSH....
r2187 brackets_stack.append(next_byte)
fix(svn-ssh): fixed svn ssh wrapper
r5330 elif next_byte == b")":
ssh-support: enabled full handling of all backends via SSH....
r2187 brackets_stack.pop()
fix(svn-ssh): fixed svn ssh wrapper
r5330 elif next_byte == b" " and not brackets_stack:
ssh-support: enabled full handling of all backends via SSH....
r2187 break
dan
svn: fixed case of wrong extracted repository name for SSH backend. In cases...
r4281
ssh-support: enabled full handling of all backends via SSH....
r2187 return buffer_
fix(svn-ssh): fixed svn ssh wrapper
r5330 def _parse_first_client_response(self, buffer_: bytes):
ssh-support: enabled full handling of all backends via SSH....
r2187 """
According to the Subversion RA protocol, the first request
should look like:
( version:number ( cap:word ... ) url:string ? ra-client:string
( ? client:string ) )
dan
svn: fixed case of wrong extracted repository name for SSH backend. In cases...
r4281 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
ssh-support: enabled full handling of all backends via SSH....
r2187 """
fix(svn-ssh): fixed svn ssh wrapper
r5330 version_re = br'(?P<version>\d+)'
capabilities_re = br'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
url_re = br'\d+\:(?P<url>[\W\w]+)'
ra_client_re = br'(\d+\:(?P<ra_client>[\W\w]+)\s)'
client_re = br'(\d+\:(?P<client>[\W\w]+)\s)*'
ssh-support: enabled full handling of all backends via SSH....
r2187 regex = re.compile(
fix(svn-ssh): fixed svn ssh wrapper
r5330 br'^\(\s%b\s%b\s%b\s%b'
br'\(\s%b\)\s\)\s*$' % (
version_re,
capabilities_re,
url_re,
ra_client_re,
client_re)
)
ssh-support: enabled full handling of all backends via SSH....
r2187 matcher = regex.match(buffer_)
dan
svn: fixed case of wrong extracted repository name for SSH backend. In cases...
r4281
ssh-support: enabled full handling of all backends via SSH....
r2187 return matcher.groupdict() if matcher else None
dan
svn: fixed case of wrong extracted repository name for SSH backend. In cases...
r4281 def _match_repo_name(self, url):
"""
Given an server url, try to match it against ALL known repository names.
This handles a tricky SVN case for SSH and subdir commits.
E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
result in the url with this subdir added.
"""
# case 1 direct match, we don't do any "heavy" lookups
if url in self.server.user_permissions:
return url
log.debug('Extracting repository name from subdir path %s', url)
# case 2 we check all permissions, and match closes possible case...
# NOTE(dan): In this case we only know that url has a subdir parts, it's safe
# to assume that it will have the repo name as prefix, we ensure the prefix
# for similar repositories isn't matched by adding a /
# e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
for repo_name in self.server.user_permissions:
repo_name_prefix = repo_name + '/'
if url.startswith(repo_name_prefix):
log.debug('Found prefix %s match, returning proper repository name',
repo_name_prefix)
return repo_name
return
ssh-support: enabled full handling of all backends via SSH....
r2187 def run(self, extras):
action = 'pull'
self.create_svn_config()
self.start()
first_response = self.get_first_client_response()
if not first_response:
fix(tests): fixed svn tests
r5336 return self.fail(b"Repository name cannot be extracted")
ssh-support: enabled full handling of all backends via SSH....
r2187
python3: fixed urllib usage
r4950 url_parts = urllib.parse.urlparse(first_response['url'])
dan
svn: fixed case of wrong extracted repository name for SSH backend. In cases...
r4281
fix(svn-ssh): fixed svn ssh wrapper
r5330 self.server.repo_name = self._match_repo_name(safe_str(url_parts.path).strip('/'))
ssh-support: enabled full handling of all backends via SSH....
r2187
exit_code = self.server._check_permissions(action)
if exit_code:
return exit_code
# set the readonly flag to False if we have proper permissions
if self.server.has_write_perm():
self.read_only = False
self.server.update_environment(action=action, extras=extras)
self.patch_first_client_response(first_response)
self.sync()
return self.return_code
feat(svn-config): updated with the latest changes.
r5329 class SubversionServer(SshVcsServer):
ssh-support: enabled full handling of all backends via SSH....
r2187 backend = 'svn'
statsd/audit-logs: cleanup push/pull user agent code....
r4858 repo_user_agent = 'svn'
ssh-support: enabled full handling of all backends via SSH....
r2187
feat(svn-config): updated with the latest changes.
r5329 def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env):
super().__init__(user, user_permissions, settings, env)
ssh-support: enabled full handling of all backends via SSH....
r2187 self.store = store
self.ini_path = ini_path
dan
svn: fixed case of wrong extracted repository name for SSH backend. In cases...
r4281 # NOTE(dan): repo_name at this point is empty,
# this is set later in .run() based from parsed input stream
ssh-support: enabled full handling of all backends via SSH....
r2187 self.repo_name = repo_name
feat(svn-config): updated with the latest changes.
r5329 self._path = self.svn_path = settings['ssh.executable.svn']
ssh-support: enabled full handling of all backends via SSH....
r2187
self.tunnel = SubversionTunnelWrapper(server=self)
def _handle_tunnel(self, extras):
# pre-auth
action = 'pull'
# Special case for SVN, we extract repo name at later stage
# exit_code = self._check_permissions(action)
# if exit_code:
# return exit_code, False
feat(svn-config): moved svn related config keys to *.ini file. Fixes: RCCE-60
r5328 req = self.env.get('request')
if req:
server_url = req.host_url + req.script_name
extras['server_url'] = server_url
ssh-support: enabled full handling of all backends via SSH....
r2187
log.debug('Using %s binaries from path %s', self.backend, self._path)
exit_code = self.tunnel.run(extras)
return exit_code, action == "push"