##// END OF EJS Templates
sshpeer: check for safe ssh url (SEC)...
Sean Farley -
r33725:d7a1c4c1 stable
parent child Browse files
Show More
@@ -1,369 +1,371 b''
1 # sshpeer.py - ssh repository proxy class for mercurial
1 # sshpeer.py - ssh repository proxy class for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import re
10 import re
11
11
12 from .i18n import _
12 from .i18n import _
13 from . import (
13 from . import (
14 error,
14 error,
15 pycompat,
15 pycompat,
16 util,
16 util,
17 wireproto,
17 wireproto,
18 )
18 )
19
19
20 class remotelock(object):
20 class remotelock(object):
21 def __init__(self, repo):
21 def __init__(self, repo):
22 self.repo = repo
22 self.repo = repo
23 def release(self):
23 def release(self):
24 self.repo.unlock()
24 self.repo.unlock()
25 self.repo = None
25 self.repo = None
26 def __enter__(self):
26 def __enter__(self):
27 return self
27 return self
28 def __exit__(self, exc_type, exc_val, exc_tb):
28 def __exit__(self, exc_type, exc_val, exc_tb):
29 if self.repo:
29 if self.repo:
30 self.release()
30 self.release()
31 def __del__(self):
31 def __del__(self):
32 if self.repo:
32 if self.repo:
33 self.release()
33 self.release()
34
34
35 def _serverquote(s):
35 def _serverquote(s):
36 if not s:
36 if not s:
37 return s
37 return s
38 '''quote a string for the remote shell ... which we assume is sh'''
38 '''quote a string for the remote shell ... which we assume is sh'''
39 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
39 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
40 return s
40 return s
41 return "'%s'" % s.replace("'", "'\\''")
41 return "'%s'" % s.replace("'", "'\\''")
42
42
43 def _forwardoutput(ui, pipe):
43 def _forwardoutput(ui, pipe):
44 """display all data currently available on pipe as remote output.
44 """display all data currently available on pipe as remote output.
45
45
46 This is non blocking."""
46 This is non blocking."""
47 s = util.readpipe(pipe)
47 s = util.readpipe(pipe)
48 if s:
48 if s:
49 for l in s.splitlines():
49 for l in s.splitlines():
50 ui.status(_("remote: "), l, '\n')
50 ui.status(_("remote: "), l, '\n')
51
51
52 class doublepipe(object):
52 class doublepipe(object):
53 """Operate a side-channel pipe in addition of a main one
53 """Operate a side-channel pipe in addition of a main one
54
54
55 The side-channel pipe contains server output to be forwarded to the user
55 The side-channel pipe contains server output to be forwarded to the user
56 input. The double pipe will behave as the "main" pipe, but will ensure the
56 input. The double pipe will behave as the "main" pipe, but will ensure the
57 content of the "side" pipe is properly processed while we wait for blocking
57 content of the "side" pipe is properly processed while we wait for blocking
58 call on the "main" pipe.
58 call on the "main" pipe.
59
59
60 If large amounts of data are read from "main", the forward will cease after
60 If large amounts of data are read from "main", the forward will cease after
61 the first bytes start to appear. This simplifies the implementation
61 the first bytes start to appear. This simplifies the implementation
62 without affecting actual output of sshpeer too much as we rarely issue
62 without affecting actual output of sshpeer too much as we rarely issue
63 large read for data not yet emitted by the server.
63 large read for data not yet emitted by the server.
64
64
65 The main pipe is expected to be a 'bufferedinputpipe' from the util module
65 The main pipe is expected to be a 'bufferedinputpipe' from the util module
66 that handle all the os specific bits. This class lives in this module
66 that handle all the os specific bits. This class lives in this module
67 because it focus on behavior specific to the ssh protocol."""
67 because it focus on behavior specific to the ssh protocol."""
68
68
69 def __init__(self, ui, main, side):
69 def __init__(self, ui, main, side):
70 self._ui = ui
70 self._ui = ui
71 self._main = main
71 self._main = main
72 self._side = side
72 self._side = side
73
73
74 def _wait(self):
74 def _wait(self):
75 """wait until some data are available on main or side
75 """wait until some data are available on main or side
76
76
77 return a pair of boolean (ismainready, issideready)
77 return a pair of boolean (ismainready, issideready)
78
78
79 (This will only wait for data if the setup is supported by `util.poll`)
79 (This will only wait for data if the setup is supported by `util.poll`)
80 """
80 """
81 if getattr(self._main, 'hasbuffer', False): # getattr for classic pipe
81 if getattr(self._main, 'hasbuffer', False): # getattr for classic pipe
82 return (True, True) # main has data, assume side is worth poking at.
82 return (True, True) # main has data, assume side is worth poking at.
83 fds = [self._main.fileno(), self._side.fileno()]
83 fds = [self._main.fileno(), self._side.fileno()]
84 try:
84 try:
85 act = util.poll(fds)
85 act = util.poll(fds)
86 except NotImplementedError:
86 except NotImplementedError:
87 # non supported yet case, assume all have data.
87 # non supported yet case, assume all have data.
88 act = fds
88 act = fds
89 return (self._main.fileno() in act, self._side.fileno() in act)
89 return (self._main.fileno() in act, self._side.fileno() in act)
90
90
91 def write(self, data):
91 def write(self, data):
92 return self._call('write', data)
92 return self._call('write', data)
93
93
94 def read(self, size):
94 def read(self, size):
95 r = self._call('read', size)
95 r = self._call('read', size)
96 if size != 0 and not r:
96 if size != 0 and not r:
97 # We've observed a condition that indicates the
97 # We've observed a condition that indicates the
98 # stdout closed unexpectedly. Check stderr one
98 # stdout closed unexpectedly. Check stderr one
99 # more time and snag anything that's there before
99 # more time and snag anything that's there before
100 # letting anyone know the main part of the pipe
100 # letting anyone know the main part of the pipe
101 # closed prematurely.
101 # closed prematurely.
102 _forwardoutput(self._ui, self._side)
102 _forwardoutput(self._ui, self._side)
103 return r
103 return r
104
104
105 def readline(self):
105 def readline(self):
106 return self._call('readline')
106 return self._call('readline')
107
107
108 def _call(self, methname, data=None):
108 def _call(self, methname, data=None):
109 """call <methname> on "main", forward output of "side" while blocking
109 """call <methname> on "main", forward output of "side" while blocking
110 """
110 """
111 # data can be '' or 0
111 # data can be '' or 0
112 if (data is not None and not data) or self._main.closed:
112 if (data is not None and not data) or self._main.closed:
113 _forwardoutput(self._ui, self._side)
113 _forwardoutput(self._ui, self._side)
114 return ''
114 return ''
115 while True:
115 while True:
116 mainready, sideready = self._wait()
116 mainready, sideready = self._wait()
117 if sideready:
117 if sideready:
118 _forwardoutput(self._ui, self._side)
118 _forwardoutput(self._ui, self._side)
119 if mainready:
119 if mainready:
120 meth = getattr(self._main, methname)
120 meth = getattr(self._main, methname)
121 if data is None:
121 if data is None:
122 return meth()
122 return meth()
123 else:
123 else:
124 return meth(data)
124 return meth(data)
125
125
126 def close(self):
126 def close(self):
127 return self._main.close()
127 return self._main.close()
128
128
129 def flush(self):
129 def flush(self):
130 return self._main.flush()
130 return self._main.flush()
131
131
132 class sshpeer(wireproto.wirepeer):
132 class sshpeer(wireproto.wirepeer):
133 def __init__(self, ui, path, create=False):
133 def __init__(self, ui, path, create=False):
134 self._url = path
134 self._url = path
135 self.ui = ui
135 self.ui = ui
136 self.pipeo = self.pipei = self.pipee = None
136 self.pipeo = self.pipei = self.pipee = None
137
137
138 u = util.url(path, parsequery=False, parsefragment=False)
138 u = util.url(path, parsequery=False, parsefragment=False)
139 if u.scheme != 'ssh' or not u.host or u.path is None:
139 if u.scheme != 'ssh' or not u.host or u.path is None:
140 self._abort(error.RepoError(_("couldn't parse location %s") % path))
140 self._abort(error.RepoError(_("couldn't parse location %s") % path))
141
141
142 util.checksafessh(path)
143
142 self.user = u.user
144 self.user = u.user
143 if u.passwd is not None:
145 if u.passwd is not None:
144 self._abort(error.RepoError(_("password in URL not supported")))
146 self._abort(error.RepoError(_("password in URL not supported")))
145 self.host = u.host
147 self.host = u.host
146 self.port = u.port
148 self.port = u.port
147 self.path = u.path or "."
149 self.path = u.path or "."
148
150
149 sshcmd = self.ui.config("ui", "ssh")
151 sshcmd = self.ui.config("ui", "ssh")
150 remotecmd = self.ui.config("ui", "remotecmd")
152 remotecmd = self.ui.config("ui", "remotecmd")
151
153
152 args = util.sshargs(sshcmd,
154 args = util.sshargs(sshcmd,
153 _serverquote(self.host),
155 _serverquote(self.host),
154 _serverquote(self.user),
156 _serverquote(self.user),
155 _serverquote(self.port))
157 _serverquote(self.port))
156
158
157 if create:
159 if create:
158 cmd = '%s %s %s' % (sshcmd, args,
160 cmd = '%s %s %s' % (sshcmd, args,
159 util.shellquote("%s init %s" %
161 util.shellquote("%s init %s" %
160 (_serverquote(remotecmd), _serverquote(self.path))))
162 (_serverquote(remotecmd), _serverquote(self.path))))
161 ui.debug('running %s\n' % cmd)
163 ui.debug('running %s\n' % cmd)
162 res = ui.system(cmd, blockedtag='sshpeer')
164 res = ui.system(cmd, blockedtag='sshpeer')
163 if res != 0:
165 if res != 0:
164 self._abort(error.RepoError(_("could not create remote repo")))
166 self._abort(error.RepoError(_("could not create remote repo")))
165
167
166 self._validaterepo(sshcmd, args, remotecmd)
168 self._validaterepo(sshcmd, args, remotecmd)
167
169
168 def url(self):
170 def url(self):
169 return self._url
171 return self._url
170
172
171 def _validaterepo(self, sshcmd, args, remotecmd):
173 def _validaterepo(self, sshcmd, args, remotecmd):
172 # cleanup up previous run
174 # cleanup up previous run
173 self.cleanup()
175 self.cleanup()
174
176
175 cmd = '%s %s %s' % (sshcmd, args,
177 cmd = '%s %s %s' % (sshcmd, args,
176 util.shellquote("%s -R %s serve --stdio" %
178 util.shellquote("%s -R %s serve --stdio" %
177 (_serverquote(remotecmd), _serverquote(self.path))))
179 (_serverquote(remotecmd), _serverquote(self.path))))
178 self.ui.debug('running %s\n' % cmd)
180 self.ui.debug('running %s\n' % cmd)
179 cmd = util.quotecommand(cmd)
181 cmd = util.quotecommand(cmd)
180
182
181 # while self.subprocess isn't used, having it allows the subprocess to
183 # while self.subprocess isn't used, having it allows the subprocess to
182 # to clean up correctly later
184 # to clean up correctly later
183 #
185 #
184 # no buffer allow the use of 'select'
186 # no buffer allow the use of 'select'
185 # feel free to remove buffering and select usage when we ultimately
187 # feel free to remove buffering and select usage when we ultimately
186 # move to threading.
188 # move to threading.
187 sub = util.popen4(cmd, bufsize=0)
189 sub = util.popen4(cmd, bufsize=0)
188 self.pipeo, self.pipei, self.pipee, self.subprocess = sub
190 self.pipeo, self.pipei, self.pipee, self.subprocess = sub
189
191
190 self.pipei = util.bufferedinputpipe(self.pipei)
192 self.pipei = util.bufferedinputpipe(self.pipei)
191 self.pipei = doublepipe(self.ui, self.pipei, self.pipee)
193 self.pipei = doublepipe(self.ui, self.pipei, self.pipee)
192 self.pipeo = doublepipe(self.ui, self.pipeo, self.pipee)
194 self.pipeo = doublepipe(self.ui, self.pipeo, self.pipee)
193
195
194 # skip any noise generated by remote shell
196 # skip any noise generated by remote shell
195 self._callstream("hello")
197 self._callstream("hello")
196 r = self._callstream("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
198 r = self._callstream("between", pairs=("%s-%s" % ("0"*40, "0"*40)))
197 lines = ["", "dummy"]
199 lines = ["", "dummy"]
198 max_noise = 500
200 max_noise = 500
199 while lines[-1] and max_noise:
201 while lines[-1] and max_noise:
200 l = r.readline()
202 l = r.readline()
201 self.readerr()
203 self.readerr()
202 if lines[-1] == "1\n" and l == "\n":
204 if lines[-1] == "1\n" and l == "\n":
203 break
205 break
204 if l:
206 if l:
205 self.ui.debug("remote: ", l)
207 self.ui.debug("remote: ", l)
206 lines.append(l)
208 lines.append(l)
207 max_noise -= 1
209 max_noise -= 1
208 else:
210 else:
209 self._abort(error.RepoError(_('no suitable response from '
211 self._abort(error.RepoError(_('no suitable response from '
210 'remote hg')))
212 'remote hg')))
211
213
212 self._caps = set()
214 self._caps = set()
213 for l in reversed(lines):
215 for l in reversed(lines):
214 if l.startswith("capabilities:"):
216 if l.startswith("capabilities:"):
215 self._caps.update(l[:-1].split(":")[1].split())
217 self._caps.update(l[:-1].split(":")[1].split())
216 break
218 break
217
219
218 def _capabilities(self):
220 def _capabilities(self):
219 return self._caps
221 return self._caps
220
222
221 def readerr(self):
223 def readerr(self):
222 _forwardoutput(self.ui, self.pipee)
224 _forwardoutput(self.ui, self.pipee)
223
225
224 def _abort(self, exception):
226 def _abort(self, exception):
225 self.cleanup()
227 self.cleanup()
226 raise exception
228 raise exception
227
229
228 def cleanup(self):
230 def cleanup(self):
229 if self.pipeo is None:
231 if self.pipeo is None:
230 return
232 return
231 self.pipeo.close()
233 self.pipeo.close()
232 self.pipei.close()
234 self.pipei.close()
233 try:
235 try:
234 # read the error descriptor until EOF
236 # read the error descriptor until EOF
235 for l in self.pipee:
237 for l in self.pipee:
236 self.ui.status(_("remote: "), l)
238 self.ui.status(_("remote: "), l)
237 except (IOError, ValueError):
239 except (IOError, ValueError):
238 pass
240 pass
239 self.pipee.close()
241 self.pipee.close()
240
242
241 __del__ = cleanup
243 __del__ = cleanup
242
244
243 def _submitbatch(self, req):
245 def _submitbatch(self, req):
244 rsp = self._callstream("batch", cmds=wireproto.encodebatchcmds(req))
246 rsp = self._callstream("batch", cmds=wireproto.encodebatchcmds(req))
245 available = self._getamount()
247 available = self._getamount()
246 # TODO this response parsing is probably suboptimal for large
248 # TODO this response parsing is probably suboptimal for large
247 # batches with large responses.
249 # batches with large responses.
248 toread = min(available, 1024)
250 toread = min(available, 1024)
249 work = rsp.read(toread)
251 work = rsp.read(toread)
250 available -= toread
252 available -= toread
251 chunk = work
253 chunk = work
252 while chunk:
254 while chunk:
253 while ';' in work:
255 while ';' in work:
254 one, work = work.split(';', 1)
256 one, work = work.split(';', 1)
255 yield wireproto.unescapearg(one)
257 yield wireproto.unescapearg(one)
256 toread = min(available, 1024)
258 toread = min(available, 1024)
257 chunk = rsp.read(toread)
259 chunk = rsp.read(toread)
258 available -= toread
260 available -= toread
259 work += chunk
261 work += chunk
260 yield wireproto.unescapearg(work)
262 yield wireproto.unescapearg(work)
261
263
262 def _callstream(self, cmd, **args):
264 def _callstream(self, cmd, **args):
263 args = pycompat.byteskwargs(args)
265 args = pycompat.byteskwargs(args)
264 self.ui.debug("sending %s command\n" % cmd)
266 self.ui.debug("sending %s command\n" % cmd)
265 self.pipeo.write("%s\n" % cmd)
267 self.pipeo.write("%s\n" % cmd)
266 _func, names = wireproto.commands[cmd]
268 _func, names = wireproto.commands[cmd]
267 keys = names.split()
269 keys = names.split()
268 wireargs = {}
270 wireargs = {}
269 for k in keys:
271 for k in keys:
270 if k == '*':
272 if k == '*':
271 wireargs['*'] = args
273 wireargs['*'] = args
272 break
274 break
273 else:
275 else:
274 wireargs[k] = args[k]
276 wireargs[k] = args[k]
275 del args[k]
277 del args[k]
276 for k, v in sorted(wireargs.iteritems()):
278 for k, v in sorted(wireargs.iteritems()):
277 self.pipeo.write("%s %d\n" % (k, len(v)))
279 self.pipeo.write("%s %d\n" % (k, len(v)))
278 if isinstance(v, dict):
280 if isinstance(v, dict):
279 for dk, dv in v.iteritems():
281 for dk, dv in v.iteritems():
280 self.pipeo.write("%s %d\n" % (dk, len(dv)))
282 self.pipeo.write("%s %d\n" % (dk, len(dv)))
281 self.pipeo.write(dv)
283 self.pipeo.write(dv)
282 else:
284 else:
283 self.pipeo.write(v)
285 self.pipeo.write(v)
284 self.pipeo.flush()
286 self.pipeo.flush()
285
287
286 return self.pipei
288 return self.pipei
287
289
288 def _callcompressable(self, cmd, **args):
290 def _callcompressable(self, cmd, **args):
289 return self._callstream(cmd, **args)
291 return self._callstream(cmd, **args)
290
292
291 def _call(self, cmd, **args):
293 def _call(self, cmd, **args):
292 self._callstream(cmd, **args)
294 self._callstream(cmd, **args)
293 return self._recv()
295 return self._recv()
294
296
295 def _callpush(self, cmd, fp, **args):
297 def _callpush(self, cmd, fp, **args):
296 r = self._call(cmd, **args)
298 r = self._call(cmd, **args)
297 if r:
299 if r:
298 return '', r
300 return '', r
299 for d in iter(lambda: fp.read(4096), ''):
301 for d in iter(lambda: fp.read(4096), ''):
300 self._send(d)
302 self._send(d)
301 self._send("", flush=True)
303 self._send("", flush=True)
302 r = self._recv()
304 r = self._recv()
303 if r:
305 if r:
304 return '', r
306 return '', r
305 return self._recv(), ''
307 return self._recv(), ''
306
308
307 def _calltwowaystream(self, cmd, fp, **args):
309 def _calltwowaystream(self, cmd, fp, **args):
308 r = self._call(cmd, **args)
310 r = self._call(cmd, **args)
309 if r:
311 if r:
310 # XXX needs to be made better
312 # XXX needs to be made better
311 raise error.Abort(_('unexpected remote reply: %s') % r)
313 raise error.Abort(_('unexpected remote reply: %s') % r)
312 for d in iter(lambda: fp.read(4096), ''):
314 for d in iter(lambda: fp.read(4096), ''):
313 self._send(d)
315 self._send(d)
314 self._send("", flush=True)
316 self._send("", flush=True)
315 return self.pipei
317 return self.pipei
316
318
317 def _getamount(self):
319 def _getamount(self):
318 l = self.pipei.readline()
320 l = self.pipei.readline()
319 if l == '\n':
321 if l == '\n':
320 self.readerr()
322 self.readerr()
321 msg = _('check previous remote output')
323 msg = _('check previous remote output')
322 self._abort(error.OutOfBandError(hint=msg))
324 self._abort(error.OutOfBandError(hint=msg))
323 self.readerr()
325 self.readerr()
324 try:
326 try:
325 return int(l)
327 return int(l)
326 except ValueError:
328 except ValueError:
327 self._abort(error.ResponseError(_("unexpected response:"), l))
329 self._abort(error.ResponseError(_("unexpected response:"), l))
328
330
329 def _recv(self):
331 def _recv(self):
330 return self.pipei.read(self._getamount())
332 return self.pipei.read(self._getamount())
331
333
332 def _send(self, data, flush=False):
334 def _send(self, data, flush=False):
333 self.pipeo.write("%d\n" % len(data))
335 self.pipeo.write("%d\n" % len(data))
334 if data:
336 if data:
335 self.pipeo.write(data)
337 self.pipeo.write(data)
336 if flush:
338 if flush:
337 self.pipeo.flush()
339 self.pipeo.flush()
338 self.readerr()
340 self.readerr()
339
341
340 def lock(self):
342 def lock(self):
341 self._call("lock")
343 self._call("lock")
342 return remotelock(self)
344 return remotelock(self)
343
345
344 def unlock(self):
346 def unlock(self):
345 self._call("unlock")
347 self._call("unlock")
346
348
347 def addchangegroup(self, cg, source, url, lock=None):
349 def addchangegroup(self, cg, source, url, lock=None):
348 '''Send a changegroup to the remote server. Return an integer
350 '''Send a changegroup to the remote server. Return an integer
349 similar to unbundle(). DEPRECATED, since it requires locking the
351 similar to unbundle(). DEPRECATED, since it requires locking the
350 remote.'''
352 remote.'''
351 d = self._call("addchangegroup")
353 d = self._call("addchangegroup")
352 if d:
354 if d:
353 self._abort(error.RepoError(_("push refused: %s") % d))
355 self._abort(error.RepoError(_("push refused: %s") % d))
354 for d in iter(lambda: cg.read(4096), ''):
356 for d in iter(lambda: cg.read(4096), ''):
355 self.pipeo.write(d)
357 self.pipeo.write(d)
356 self.readerr()
358 self.readerr()
357
359
358 self.pipeo.flush()
360 self.pipeo.flush()
359
361
360 self.readerr()
362 self.readerr()
361 r = self._recv()
363 r = self._recv()
362 if not r:
364 if not r:
363 return 1
365 return 1
364 try:
366 try:
365 return int(r)
367 return int(r)
366 except ValueError:
368 except ValueError:
367 self._abort(error.ResponseError(_("unexpected response:"), r))
369 self._abort(error.ResponseError(_("unexpected response:"), r))
368
370
369 instance = sshpeer
371 instance = sshpeer
General Comments 0
You need to be logged in to leave comments. Login now