From f172aa41445cbbaaf666de9985961814af225e75 Mon Sep 17 00:00:00 2001 From: RhodeCode Admin Date: Fri, 28 Nov 2025 11:32:10 +0100 Subject: [PATCH] svn: fixed issue with custom svn client user agents --- .../apps/ssh_support/lib/backends/svn.py | 18 ++++--- .../apps/ssh_support/tests/test_server_svn.py | 54 ++++++++++++++++++- 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/rhodecode/apps/ssh_support/lib/backends/svn.py b/rhodecode/apps/ssh_support/lib/backends/svn.py index 2f6d7ac5..5c24be24 100644 --- a/rhodecode/apps/ssh_support/lib/backends/svn.py +++ b/rhodecode/apps/ssh_support/lib/backends/svn.py @@ -110,15 +110,19 @@ class SubversionTunnelWrapper(object): signal.alarm(0) return self._parse_first_client_response(first_response) if first_response else None - def patch_first_client_response(self, response, **kwargs): + def _prepare_first_client_response(self, response: dict, **kwargs) -> bytes: 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"]) + client = b" " + self._svn_bytes(response["client"]) or b"" - buffer_ = b"( %b ( %b ) %b%b( %b) ) " % (version, capabilities, url, ra_client, client) + buffer_ = b"( %b ( %b ) %b%b(%b) )" % (version, capabilities, url, ra_client, client) + return buffer_ + + def patch_first_client_response(self, response, **kwargs): + buffer_ = self._prepare_first_client_response(response, **kwargs) self.process.stdin.write(buffer_) self.process.stdin.flush() @@ -162,8 +166,7 @@ class SubversionTunnelWrapper(object): According to the Subversion RA protocol, the first request should look like: - ( version:number ( cap:word ... ) url:string ? ra-client:string - ( ? client:string ) ) + ( version:number ( cap:word ... ) url:string ? ra-client:string ( ? client:string ) ) Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol """ @@ -173,15 +176,14 @@ class SubversionTunnelWrapper(object): ra_client_re = rb"(\d+\:(?P[\W\w]+)\s)" client_re = rb"(\d+\:(?P[\W\w]+)\s)*" regex = re.compile( - rb"^\(\s%b\s%b\s%b\s%b" - rb"\(\s%b\)\s\)\s*$" % (version_re, capabilities_re, url_re, ra_client_re, client_re) + rb"^\(\s%b\s%b\s%b\s%b\(\s%b\)\s\)\s*$" % (version_re, capabilities_re, url_re, ra_client_re, client_re) ) matcher = regex.match(buffer_) return matcher.groupdict() if matcher else None def _match_repo_name(self, url): """ - Given an server url, try to match it against ALL known repository names. + Given a 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. diff --git a/rhodecode/apps/ssh_support/tests/test_server_svn.py b/rhodecode/apps/ssh_support/tests/test_server_svn.py index d926299f..10b1d0dd 100644 --- a/rhodecode/apps/ssh_support/tests/test_server_svn.py +++ b/rhodecode/apps/ssh_support/tests/test_server_svn.py @@ -21,7 +21,7 @@ import os import mock import pytest -from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer +from rhodecode.apps.ssh_support.lib.backends.svn import SubversionServer, SubversionTunnelWrapper from rhodecode.apps.ssh_support.tests.conftest import plain_dummy_env, plain_dummy_user @@ -224,3 +224,55 @@ class TestSubversionServer(object): exit_code = server.run(tunnel_extras={"config": server.ini_path}) assert exit_code == (1, False) + + @pytest.mark.parametrize( + "first_resp, expected_match", + [ + ( + b"( 2 ( edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops ) 44:svn+ssh://rc@code.example.com/TestRepo/trunk 34:SVN/1.14.5 (x64-microsoft-windows) ( 24:TortoiseSVN-1.14.9.29743 ) )", + { + "version": b"2", + "capabilities": b"edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops", + "url": b"svn+ssh://rc@code.example.com/TestRepo/trunk", + "ra_client": b"SVN/1.14.5 (x64-microsoft-windows)", + "client": b"TortoiseSVN-1.14.9.29743", + }, + ), + ( + b"( 2 ( edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops ) 44:svn+ssh://rc@code.example.com/TestRepo/trunk 34:SVN/1.14.5 (x64-microsoft-windows) ( ) )", + { + "version": b"2", + "capabilities": b"edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops", + "url": b"svn+ssh://rc@code.example.com/TestRepo/trunk", + "ra_client": b"SVN/1.14.5 (x64-microsoft-windows)", + "client": None, + }, + ), + ], + ) + def test__parse_first_client_response(self, svn_server, first_resp, expected_match): + server = svn_server.create() + tunnel = SubversionTunnelWrapper(server) + parsed = tunnel._parse_first_client_response(first_resp) + assert parsed == expected_match + + @pytest.mark.parametrize( + "first_resp, expected_match", + [ + ( + b"( 2 ( edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops ) 44:svn+ssh://rc@code.example.com/TestRepo/trunk 34:SVN/1.14.5 (x64-microsoft-windows) ( 24:TortoiseSVN-1.14.9.29743 ) )", + None, + ), + ( + b"( 2 ( edit-pipeline svndiff1 accepts-svndiff2 absent-entries depth mergeinfo log-revprops ) 44:svn+ssh://rc@code.example.com/TestRepo/trunk 34:SVN/1.14.5 (x64-microsoft-windows) ( ) )", + None, + ), + ], + ) + def test__parse_and_prepare_first_client_response(self, svn_server, first_resp, expected_match): + server = svn_server.create() + tunnel = SubversionTunnelWrapper(server) + parsed = tunnel._parse_first_client_response(first_resp) + first_response_prepared = tunnel._prepare_first_client_response(parsed) + + assert first_response_prepared == first_resp