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