##// END OF EJS Templates
ssh: fix problems with backend and ssh clones.
marcink -
r2595:c8a919b2 stable
parent child Browse files
Show More
@@ -1,228 +1,227 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2018 RhodeCode GmbH
3 # Copyright (C) 2016-2018 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import os
21 import os
22 import re
22 import re
23 import sys
23 import sys
24 import logging
24 import logging
25 import signal
25 import signal
26 import tempfile
26 import tempfile
27 from subprocess import Popen, PIPE
27 from subprocess import Popen, PIPE
28 import urlparse
28 import urlparse
29
29
30 from .base import VcsServer
30 from .base import VcsServer
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34
34
35 class SubversionTunnelWrapper(object):
35 class SubversionTunnelWrapper(object):
36 process = None
36 process = None
37
37
38 def __init__(self, server):
38 def __init__(self, server):
39 self.server = server
39 self.server = server
40 self.timeout = 30
40 self.timeout = 30
41 self.stdin = sys.stdin
41 self.stdin = sys.stdin
42 self.stdout = sys.stdout
42 self.stdout = sys.stdout
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
43 self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
44 self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
45
45
46 self.read_only = True # flag that we set to make the hooks readonly
46 self.read_only = True # flag that we set to make the hooks readonly
47
47
48 def create_svn_config(self):
48 def create_svn_config(self):
49 content = (
49 content = (
50 '[general]\n'
50 '[general]\n'
51 'hooks-env = {}\n').format(self.hooks_env_path)
51 'hooks-env = {}\n').format(self.hooks_env_path)
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
52 with os.fdopen(self.svn_conf_fd, 'w') as config_file:
53 config_file.write(content)
53 config_file.write(content)
54
54
55 def create_hooks_env(self):
55 def create_hooks_env(self):
56 content = (
56 content = (
57 '[default]\n'
57 '[default]\n'
58 'LANG = en_US.UTF-8\n')
58 'LANG = en_US.UTF-8\n')
59 if self.read_only:
59 if self.read_only:
60 content += 'SSH_READ_ONLY = 1\n'
60 content += 'SSH_READ_ONLY = 1\n'
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
61 with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
62 hooks_env_file.write(content)
62 hooks_env_file.write(content)
63
63
64 def remove_configs(self):
64 def remove_configs(self):
65 os.remove(self.svn_conf_path)
65 os.remove(self.svn_conf_path)
66 os.remove(self.hooks_env_path)
66 os.remove(self.hooks_env_path)
67
67
68 def command(self):
68 def command(self):
69 root = self.server.get_root_store()
69 root = self.server.get_root_store()
70 command = [
70 command = [
71 self.server.svn_path, '-t',
71 self.server.svn_path, '-t',
72 '--config-file', self.svn_conf_path,
72 '--config-file', self.svn_conf_path,
73 '-r', root]
73 '-r', root]
74 log.debug("Final CMD: %s", command)
74 log.debug("Final CMD: %s", ' '.join(command))
75 return command
75 return command
76
76
77 def start(self):
77 def start(self):
78 command = self.command()
78 command = self.command()
79 self.process = Popen(command, stdin=PIPE)
79 self.process = Popen(' '.join(command), stdin=PIPE, shell=True)
80
80
81 def sync(self):
81 def sync(self):
82 while self.process.poll() is None:
82 while self.process.poll() is None:
83 next_byte = self.stdin.read(1)
83 next_byte = self.stdin.read(1)
84 if not next_byte:
84 if not next_byte:
85 break
85 break
86 self.process.stdin.write(next_byte)
86 self.process.stdin.write(next_byte)
87 self.remove_configs()
87 self.remove_configs()
88
88
89 @property
89 @property
90 def return_code(self):
90 def return_code(self):
91 return self.process.returncode
91 return self.process.returncode
92
92
93 def get_first_client_response(self):
93 def get_first_client_response(self):
94 signal.signal(signal.SIGALRM, self.interrupt)
94 signal.signal(signal.SIGALRM, self.interrupt)
95 signal.alarm(self.timeout)
95 signal.alarm(self.timeout)
96 first_response = self._read_first_client_response()
96 first_response = self._read_first_client_response()
97 signal.alarm(0)
97 signal.alarm(0)
98 return (
98 return (
99 self._parse_first_client_response(first_response)
99 self._parse_first_client_response(first_response)
100 if first_response else None)
100 if first_response else None)
101
101
102 def patch_first_client_response(self, response, **kwargs):
102 def patch_first_client_response(self, response, **kwargs):
103 self.create_hooks_env()
103 self.create_hooks_env()
104 data = response.copy()
104 data = response.copy()
105 data.update(kwargs)
105 data.update(kwargs)
106 data['url'] = self._svn_string(data['url'])
106 data['url'] = self._svn_string(data['url'])
107 data['ra_client'] = self._svn_string(data['ra_client'])
107 data['ra_client'] = self._svn_string(data['ra_client'])
108 data['client'] = data['client'] or ''
108 data['client'] = data['client'] or ''
109 buffer_ = (
109 buffer_ = (
110 "( {version} ( {capabilities} ) {url}{ra_client}"
110 "( {version} ( {capabilities} ) {url}{ra_client}"
111 "( {client}) ) ".format(**data))
111 "( {client}) ) ".format(**data))
112 self.process.stdin.write(buffer_)
112 self.process.stdin.write(buffer_)
113
113
114 def fail(self, message):
114 def fail(self, message):
115 print(
115 print(
116 "( failure ( ( 210005 {message} 0: 0 ) ) )".format(
116 "( failure ( ( 210005 {message} 0: 0 ) ) )".format(
117 message=self._svn_string(message)))
117 message=self._svn_string(message)))
118 self.remove_configs()
118 self.remove_configs()
119 self.process.kill()
119 self.process.kill()
120
120
121 def interrupt(self, signum, frame):
121 def interrupt(self, signum, frame):
122 self.fail("Exited by timeout")
122 self.fail("Exited by timeout")
123
123
124 def _svn_string(self, str_):
124 def _svn_string(self, str_):
125 if not str_:
125 if not str_:
126 return ''
126 return ''
127 return '{length}:{string} '.format(length=len(str_), string=str_)
127 return '{length}:{string} '.format(length=len(str_), string=str_)
128
128
129 def _read_first_client_response(self):
129 def _read_first_client_response(self):
130 buffer_ = ""
130 buffer_ = ""
131 brackets_stack = []
131 brackets_stack = []
132 while True:
132 while True:
133 next_byte = self.stdin.read(1)
133 next_byte = self.stdin.read(1)
134 buffer_ += next_byte
134 buffer_ += next_byte
135 if next_byte == "(":
135 if next_byte == "(":
136 brackets_stack.append(next_byte)
136 brackets_stack.append(next_byte)
137 elif next_byte == ")":
137 elif next_byte == ")":
138 brackets_stack.pop()
138 brackets_stack.pop()
139 elif next_byte == " " and not brackets_stack:
139 elif next_byte == " " and not brackets_stack:
140 break
140 break
141 return buffer_
141 return buffer_
142
142
143 def _parse_first_client_response(self, buffer_):
143 def _parse_first_client_response(self, buffer_):
144 """
144 """
145 According to the Subversion RA protocol, the first request
145 According to the Subversion RA protocol, the first request
146 should look like:
146 should look like:
147
147
148 ( version:number ( cap:word ... ) url:string ? ra-client:string
148 ( version:number ( cap:word ... ) url:string ? ra-client:string
149 ( ? client:string ) )
149 ( ? client:string ) )
150
150
151 Please check https://svn.apache.org/repos/asf/subversion/trunk/
151 Please check https://svn.apache.org/repos/asf/subversion/trunk/
152 subversion/libsvn_ra_svn/protocol
152 subversion/libsvn_ra_svn/protocol
153 """
153 """
154 version_re = r'(?P<version>\d+)'
154 version_re = r'(?P<version>\d+)'
155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
155 capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)'
156 url_re = r'\d+\:(?P<url>[\W\w]+)'
156 url_re = r'\d+\:(?P<url>[\W\w]+)'
157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
157 ra_client_re = r'(\d+\:(?P<ra_client>[\W\w]+)\s)'
158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
158 client_re = r'(\d+\:(?P<client>[\W\w]+)\s)*'
159 regex = re.compile(
159 regex = re.compile(
160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
160 r'^\(\s{version}\s{capabilities}\s{url}\s{ra_client}'
161 r'\(\s{client}\)\s\)\s*$'.format(
161 r'\(\s{client}\)\s\)\s*$'.format(
162 version=version_re, capabilities=capabilities_re,
162 version=version_re, capabilities=capabilities_re,
163 url=url_re, ra_client=ra_client_re, client=client_re))
163 url=url_re, ra_client=ra_client_re, client=client_re))
164 matcher = regex.match(buffer_)
164 matcher = regex.match(buffer_)
165 return matcher.groupdict() if matcher else None
165 return matcher.groupdict() if matcher else None
166
166
167 def run(self, extras):
167 def run(self, extras):
168 action = 'pull'
168 action = 'pull'
169 self.create_svn_config()
169 self.create_svn_config()
170 self.start()
170 self.start()
171
171
172 first_response = self.get_first_client_response()
172 first_response = self.get_first_client_response()
173 if not first_response:
173 if not first_response:
174 self.fail("Repository name cannot be extracted")
174 self.fail("Repository name cannot be extracted")
175 return 1
176
175
177 url_parts = urlparse.urlparse(first_response['url'])
176 url_parts = urlparse.urlparse(first_response['url'])
178 self.server.repo_name = url_parts.path.strip('/')
177 self.server.repo_name = url_parts.path.strip('/')
179
178
180 exit_code = self.server._check_permissions(action)
179 exit_code = self.server._check_permissions(action)
181 if exit_code:
180 if exit_code:
182 return exit_code
181 return exit_code
183
182
184 # set the readonly flag to False if we have proper permissions
183 # set the readonly flag to False if we have proper permissions
185 if self.server.has_write_perm():
184 if self.server.has_write_perm():
186 self.read_only = False
185 self.read_only = False
187 self.server.update_environment(action=action, extras=extras)
186 self.server.update_environment(action=action, extras=extras)
188
187
189 self.patch_first_client_response(first_response)
188 self.patch_first_client_response(first_response)
190 self.sync()
189 self.sync()
191 return self.return_code
190 return self.return_code
192
191
193
192
194 class SubversionServer(VcsServer):
193 class SubversionServer(VcsServer):
195 backend = 'svn'
194 backend = 'svn'
196
195
197 def __init__(self, store, ini_path, repo_name,
196 def __init__(self, store, ini_path, repo_name,
198 user, user_permissions, config, env):
197 user, user_permissions, config, env):
199 super(SubversionServer, self)\
198 super(SubversionServer, self)\
200 .__init__(user, user_permissions, config, env)
199 .__init__(user, user_permissions, config, env)
201 self.store = store
200 self.store = store
202 self.ini_path = ini_path
201 self.ini_path = ini_path
203 # this is set in .run() from input stream
202 # this is set in .run() from input stream
204 self.repo_name = repo_name
203 self.repo_name = repo_name
205 self._path = self.svn_path = config.get(
204 self._path = self.svn_path = config.get(
206 'app:main', 'ssh.executable.svn')
205 'app:main', 'ssh.executable.svn')
207
206
208 self.tunnel = SubversionTunnelWrapper(server=self)
207 self.tunnel = SubversionTunnelWrapper(server=self)
209
208
210 def _handle_tunnel(self, extras):
209 def _handle_tunnel(self, extras):
211
210
212 # pre-auth
211 # pre-auth
213 action = 'pull'
212 action = 'pull'
214 # Special case for SVN, we extract repo name at later stage
213 # Special case for SVN, we extract repo name at later stage
215 # exit_code = self._check_permissions(action)
214 # exit_code = self._check_permissions(action)
216 # if exit_code:
215 # if exit_code:
217 # return exit_code, False
216 # return exit_code, False
218
217
219 req = self.env['request']
218 req = self.env['request']
220 server_url = req.host_url + req.script_name
219 server_url = req.host_url + req.script_name
221 extras['server_url'] = server_url
220 extras['server_url'] = server_url
222
221
223 log.debug('Using %s binaries from path %s', self.backend, self._path)
222 log.debug('Using %s binaries from path %s', self.backend, self._path)
224 exit_code = self.tunnel.run(extras)
223 exit_code = self.tunnel.run(extras)
225
224
226 return exit_code, action == "push"
225 return exit_code, action == "push"
227
226
228
227
General Comments 0
You need to be logged in to leave comments. Login now