##// END OF EJS Templates
fix(ssh-backend): fix accidentally added rhodecode_tools import
super-admin -
r5603:9026c170 default
parent child Browse files
Show More
@@ -1,269 +1,269 b''
1 # Copyright (C) 2016-2023 RhodeCode GmbH
1 # Copyright (C) 2016-2023 RhodeCode GmbH
2 #
2 #
3 # This program is free software: you can redistribute it and/or modify
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
5 # (only), as published by the Free Software Foundation.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU Affero General Public License
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
14 #
15 # This program is dual-licensed. If you wish to learn more about the
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 import os
19 import os
20 import re
20 import re
21 import sys
21 import sys
22 import logging
22 import logging
23 import signal
23 import signal
24 import tempfile
24 import tempfile
25 from subprocess import Popen, PIPE
25 from subprocess import Popen, PIPE
26 import urllib.parse
26 import urllib.parse
27
27
28 from rhodecode_tools.lib.utils import safe_str
28 from rhodecode.lib.str_utils import safe_str
29 from .base import SshVcsServer
29 from .base import SshVcsServer
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 class SubversionTunnelWrapper(object):
34 class SubversionTunnelWrapper(object):
35 process = None
35 process = None
36
36
37 def __init__(self, server):
37 def __init__(self, server):
38 self.server = server
38 self.server = server
39 self.timeout = 30
39 self.timeout = 30
40 self.stdin = sys.stdin
40 self.stdin = sys.stdin
41 self.stdout = sys.stdout
41 self.stdout = sys.stdout
42 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
42 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
43 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
43 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
44
44
45 self.read_only = True # flag that we set to make the hooks readonly
45 self.read_only = True # flag that we set to make the hooks readonly
46
46
47 def create_svn_config(self):
47 def create_svn_config(self):
48 content = (
48 content = (
49 '[general]\n'
49 '[general]\n'
50 'hooks-env = {}\n').format(self.hooks_env_path)
50 'hooks-env = {}\n').format(self.hooks_env_path)
51 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
51 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
52 config_file.write(content)
52 config_file.write(content)
53
53
54 def create_hooks_env(self):
54 def create_hooks_env(self):
55 content = (
55 content = (
56 '[default]\n'
56 '[default]\n'
57 'LANG = en_US.UTF-8\n')
57 'LANG = en_US.UTF-8\n')
58 if self.read_only:
58 if self.read_only:
59 content += 'SSH_READ_ONLY = 1\n'
59 content += 'SSH_READ_ONLY = 1\n'
60 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
60 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
61 hooks_env_file.write(content)
61 hooks_env_file.write(content)
62
62
63 def remove_configs(self):
63 def remove_configs(self):
64 os.remove(self.svn_conf_path)
64 os.remove(self.svn_conf_path)
65 os.remove(self.hooks_env_path)
65 os.remove(self.hooks_env_path)
66
66
67 def command(self):
67 def command(self):
68 root = self.server.get_root_store()
68 root = self.server.get_root_store()
69 username = self.server.user.username
69 username = self.server.user.username
70
70
71 command = [
71 command = [
72 self.server.svn_path, '-t',
72 self.server.svn_path, '-t',
73 '--config-file', self.svn_conf_path,
73 '--config-file', self.svn_conf_path,
74 '--tunnel-user', username,
74 '--tunnel-user', username,
75 '-r', root]
75 '-r', root]
76 log.debug("Final CMD: %s", ' '.join(command))
76 log.debug("Final CMD: %s", ' '.join(command))
77 return command
77 return command
78
78
79 def start(self):
79 def start(self):
80 command = self.command()
80 command = self.command()
81 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
81 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
82
82
83 def sync(self):
83 def sync(self):
84 while self.process.poll() is None:
84 while self.process.poll() is None:
85 next_byte = self.stdin.buffer.read(1)
85 next_byte = self.stdin.buffer.read(1)
86 if not next_byte:
86 if not next_byte:
87 break
87 break
88 self.process.stdin.write(next_byte)
88 self.process.stdin.write(next_byte)
89 self.remove_configs()
89 self.remove_configs()
90
90
91 @property
91 @property
92 def return_code(self):
92 def return_code(self):
93 return self.process.returncode
93 return self.process.returncode
94
94
95 def get_first_client_response(self):
95 def get_first_client_response(self):
96 signal.signal(signal.SIGALRM, self.interrupt)
96 signal.signal(signal.SIGALRM, self.interrupt)
97 signal.alarm(self.timeout)
97 signal.alarm(self.timeout)
98 first_response = self._read_first_client_response()
98 first_response = self._read_first_client_response()
99 signal.alarm(0)
99 signal.alarm(0)
100 return (self._parse_first_client_response(first_response)
100 return (self._parse_first_client_response(first_response)
101 if first_response else None)
101 if first_response else None)
102
102
103 def patch_first_client_response(self, response, **kwargs):
103 def patch_first_client_response(self, response, **kwargs):
104 self.create_hooks_env()
104 self.create_hooks_env()
105
105
106 version = response['version']
106 version = response['version']
107 capabilities = response['capabilities']
107 capabilities = response['capabilities']
108 client = response['client'] or b''
108 client = response['client'] or b''
109
109
110 url = self._svn_bytes(response['url'])
110 url = self._svn_bytes(response['url'])
111 ra_client = self._svn_bytes(response['ra_client'])
111 ra_client = self._svn_bytes(response['ra_client'])
112
112
113 buffer_ = b"( %b ( %b ) %b%b( %b) ) " % (
113 buffer_ = b"( %b ( %b ) %b%b( %b) ) " % (
114 version,
114 version,
115 capabilities,
115 capabilities,
116 url,
116 url,
117 ra_client,
117 ra_client,
118 client
118 client
119 )
119 )
120 self.process.stdin.write(buffer_)
120 self.process.stdin.write(buffer_)
121
121
122 def fail(self, message):
122 def fail(self, message):
123 fail_msg = b"( failure ( ( 210005 %b 0: 0 ) ) )" % self._svn_bytes(message)
123 fail_msg = b"( failure ( ( 210005 %b 0: 0 ) ) )" % self._svn_bytes(message)
124 sys.stdout.buffer.write(fail_msg)
124 sys.stdout.buffer.write(fail_msg)
125 sys.stdout.flush()
125 sys.stdout.flush()
126 self.remove_configs()
126 self.remove_configs()
127 self.process.kill()
127 self.process.kill()
128 return 1
128 return 1
129
129
130 def interrupt(self, signum, frame):
130 def interrupt(self, signum, frame):
131 self.fail("Exited by timeout")
131 self.fail("Exited by timeout")
132
132
133 def _svn_bytes(self, bytes_: bytes) -> bytes:
133 def _svn_bytes(self, bytes_: bytes) -> bytes:
134 if not bytes_:
134 if not bytes_:
135 return b''
135 return b''
136
136
137 return f'{len(bytes_)}:'.encode() + bytes_ + b' '
137 return f'{len(bytes_)}:'.encode() + bytes_ + b' '
138
138
139 def _read_first_client_response(self):
139 def _read_first_client_response(self):
140 buffer_ = b""
140 buffer_ = b""
141 brackets_stack = []
141 brackets_stack = []
142 while True:
142 while True:
143 next_byte = self.stdin.buffer.read(1)
143 next_byte = self.stdin.buffer.read(1)
144 buffer_ += next_byte
144 buffer_ += next_byte
145 if next_byte == b"(":
145 if next_byte == b"(":
146 brackets_stack.append(next_byte)
146 brackets_stack.append(next_byte)
147 elif next_byte == b")":
147 elif next_byte == b")":
148 brackets_stack.pop()
148 brackets_stack.pop()
149 elif next_byte == b" " and not brackets_stack:
149 elif next_byte == b" " and not brackets_stack:
150 break
150 break
151
151
152 return buffer_
152 return buffer_
153
153
154 def _parse_first_client_response(self, buffer_: bytes):
154 def _parse_first_client_response(self, buffer_: bytes):
155 """
155 """
156 According to the Subversion RA protocol, the first request
156 According to the Subversion RA protocol, the first request
157 should look like:
157 should look like:
158
158
159 ( version:number ( cap:word ... ) url:string ? ra-client:string
159 ( version:number ( cap:word ... ) url:string ? ra-client:string
160 ( ? client:string ) )
160 ( ? client:string ) )
161
161
162 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
162 Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol
163 """
163 """
164 version_re = br'(?P<version>\d+)'
164 version_re = br'(?P<version>\d+)'
165 capabilities_re = br'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
165 capabilities_re = br'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
166 url_re = br'\d+\:(?P<url>[\W\w]+)'
166 url_re = br'\d+\:(?P<url>[\W\w]+)'
167 ra_client_re = br'(\d+\:(?P<ra_client>[\W\w]+)\s)'
167 ra_client_re = br'(\d+\:(?P<ra_client>[\W\w]+)\s)'
168 client_re = br'(\d+\:(?P<client>[\W\w]+)\s)*'
168 client_re = br'(\d+\:(?P<client>[\W\w]+)\s)*'
169 regex = re.compile(
169 regex = re.compile(
170 br'^\(\s%b\s%b\s%b\s%b'
170 br'^\(\s%b\s%b\s%b\s%b'
171 br'\(\s%b\)\s\)\s*$' % (
171 br'\(\s%b\)\s\)\s*$' % (
172 version_re,
172 version_re,
173 capabilities_re,
173 capabilities_re,
174 url_re,
174 url_re,
175 ra_client_re,
175 ra_client_re,
176 client_re)
176 client_re)
177 )
177 )
178 matcher = regex.match(buffer_)
178 matcher = regex.match(buffer_)
179
179
180 return matcher.groupdict() if matcher else None
180 return matcher.groupdict() if matcher else None
181
181
182 def _match_repo_name(self, url):
182 def _match_repo_name(self, url):
183 """
183 """
184 Given an server url, try to match it against ALL known repository names.
184 Given an server url, try to match it against ALL known repository names.
185 This handles a tricky SVN case for SSH and subdir commits.
185 This handles a tricky SVN case for SSH and subdir commits.
186 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
186 E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would
187 result in the url with this subdir added.
187 result in the url with this subdir added.
188 """
188 """
189 # case 1 direct match, we don't do any "heavy" lookups
189 # case 1 direct match, we don't do any "heavy" lookups
190 if url in self.server.user_permissions:
190 if url in self.server.user_permissions:
191 return url
191 return url
192
192
193 log.debug('Extracting repository name from subdir path %s', url)
193 log.debug('Extracting repository name from subdir path %s', url)
194 # case 2 we check all permissions, and match closes possible case...
194 # case 2 we check all permissions, and match closes possible case...
195 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
195 # NOTE(dan): In this case we only know that url has a subdir parts, it's safe
196 # to assume that it will have the repo name as prefix, we ensure the prefix
196 # to assume that it will have the repo name as prefix, we ensure the prefix
197 # for similar repositories isn't matched by adding a /
197 # for similar repositories isn't matched by adding a /
198 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
198 # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct.
199 for repo_name in self.server.user_permissions:
199 for repo_name in self.server.user_permissions:
200 repo_name_prefix = repo_name + '/'
200 repo_name_prefix = repo_name + '/'
201 if url.startswith(repo_name_prefix):
201 if url.startswith(repo_name_prefix):
202 log.debug('Found prefix %s match, returning proper repository name',
202 log.debug('Found prefix %s match, returning proper repository name',
203 repo_name_prefix)
203 repo_name_prefix)
204 return repo_name
204 return repo_name
205
205
206 return
206 return
207
207
208 def run(self, extras):
208 def run(self, extras):
209 action = 'pull'
209 action = 'pull'
210 self.create_svn_config()
210 self.create_svn_config()
211 self.start()
211 self.start()
212
212
213 first_response = self.get_first_client_response()
213 first_response = self.get_first_client_response()
214 if not first_response:
214 if not first_response:
215 return self.fail(b"Repository name cannot be extracted")
215 return self.fail(b"Repository name cannot be extracted")
216
216
217 url_parts = urllib.parse.urlparse(first_response['url'])
217 url_parts = urllib.parse.urlparse(first_response['url'])
218
218
219 self.server.repo_name = self._match_repo_name(safe_str(url_parts.path).strip('/'))
219 self.server.repo_name = self._match_repo_name(safe_str(url_parts.path).strip('/'))
220
220
221 exit_code = self.server._check_permissions(action)
221 exit_code = self.server._check_permissions(action)
222 if exit_code:
222 if exit_code:
223 return exit_code
223 return exit_code
224
224
225 # set the readonly flag to False if we have proper permissions
225 # set the readonly flag to False if we have proper permissions
226 if self.server.has_write_perm():
226 if self.server.has_write_perm():
227 self.read_only = False
227 self.read_only = False
228 self.server.update_environment(action=action, extras=extras)
228 self.server.update_environment(action=action, extras=extras)
229
229
230 self.patch_first_client_response(first_response)
230 self.patch_first_client_response(first_response)
231 self.sync()
231 self.sync()
232 return self.return_code
232 return self.return_code
233
233
234
234
235 class SubversionServer(SshVcsServer):
235 class SubversionServer(SshVcsServer):
236 backend = 'svn'
236 backend = 'svn'
237 repo_user_agent = 'svn'
237 repo_user_agent = 'svn'
238
238
239 def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env):
239 def __init__(self, store, ini_path, repo_name, user, user_permissions, settings, env):
240 super().__init__(user, user_permissions, settings, env)
240 super().__init__(user, user_permissions, settings, env)
241 self.store = store
241 self.store = store
242 self.ini_path = ini_path
242 self.ini_path = ini_path
243 # NOTE(dan): repo_name at this point is empty,
243 # NOTE(dan): repo_name at this point is empty,
244 # this is set later in .run() based from parsed input stream
244 # this is set later in .run() based from parsed input stream
245 self.repo_name = repo_name
245 self.repo_name = repo_name
246 self._path = self.svn_path = settings['ssh.executable.svn']
246 self._path = self.svn_path = settings['ssh.executable.svn']
247
247
248 self.tunnel = SubversionTunnelWrapper(server=self)
248 self.tunnel = SubversionTunnelWrapper(server=self)
249
249
250 def _handle_tunnel(self, extras):
250 def _handle_tunnel(self, extras):
251
251
252 # pre-auth
252 # pre-auth
253 action = 'pull'
253 action = 'pull'
254 # Special case for SVN, we extract repo name at later stage
254 # Special case for SVN, we extract repo name at later stage
255 # exit_code = self._check_permissions(action)
255 # exit_code = self._check_permissions(action)
256 # if exit_code:
256 # if exit_code:
257 # return exit_code, False
257 # return exit_code, False
258
258
259 req = self.env.get('request')
259 req = self.env.get('request')
260 if req:
260 if req:
261 server_url = req.host_url + req.script_name
261 server_url = req.host_url + req.script_name
262 extras['server_url'] = server_url
262 extras['server_url'] = server_url
263
263
264 log.debug('Using %s binaries from path %s', self.backend, self._path)
264 log.debug('Using %s binaries from path %s', self.backend, self._path)
265 exit_code = self.tunnel.run(extras)
265 exit_code = self.tunnel.run(extras)
266
266
267 return exit_code, action == "push"
267 return exit_code, action == "push"
268
268
269
269
General Comments 0
You need to be logged in to leave comments. Login now