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