##// END OF EJS Templates
sshpeer: fix path when handling invalid url exception...
Felipe Resende -
r52403:a2f1d97e stable
parent child Browse files
Show More
@@ -1,709 +1,709 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 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005, 2006 Olivia Mackall <olivia@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
8
9 import re
9 import re
10 import uuid
10 import uuid
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 wireprototypes,
17 wireprototypes,
18 wireprotov1peer,
18 wireprotov1peer,
19 wireprotov1server,
19 wireprotov1server,
20 )
20 )
21 from .utils import (
21 from .utils import (
22 procutil,
22 procutil,
23 stringutil,
23 stringutil,
24 urlutil,
24 urlutil,
25 )
25 )
26
26
27
27
28 def _serverquote(s):
28 def _serverquote(s):
29 """quote a string for the remote shell ... which we assume is sh"""
29 """quote a string for the remote shell ... which we assume is sh"""
30 if not s:
30 if not s:
31 return s
31 return s
32 if re.match(b'[a-zA-Z0-9@%_+=:,./-]*$', s):
32 if re.match(b'[a-zA-Z0-9@%_+=:,./-]*$', s):
33 return s
33 return s
34 return b"'%s'" % s.replace(b"'", b"'\\''")
34 return b"'%s'" % s.replace(b"'", b"'\\''")
35
35
36
36
37 def _forwardoutput(ui, pipe, warn=False):
37 def _forwardoutput(ui, pipe, warn=False):
38 """display all data currently available on pipe as remote output.
38 """display all data currently available on pipe as remote output.
39
39
40 This is non blocking."""
40 This is non blocking."""
41 if pipe and not pipe.closed:
41 if pipe and not pipe.closed:
42 s = procutil.readpipe(pipe)
42 s = procutil.readpipe(pipe)
43 if s:
43 if s:
44 display = ui.warn if warn else ui.status
44 display = ui.warn if warn else ui.status
45 for l in s.splitlines():
45 for l in s.splitlines():
46 display(_(b"remote: "), l, b'\n')
46 display(_(b"remote: "), l, b'\n')
47
47
48
48
49 class doublepipe:
49 class doublepipe:
50 """Operate a side-channel pipe in addition of a main one
50 """Operate a side-channel pipe in addition of a main one
51
51
52 The side-channel pipe contains server output to be forwarded to the user
52 The side-channel pipe contains server output to be forwarded to the user
53 input. The double pipe will behave as the "main" pipe, but will ensure the
53 input. The double pipe will behave as the "main" pipe, but will ensure the
54 content of the "side" pipe is properly processed while we wait for blocking
54 content of the "side" pipe is properly processed while we wait for blocking
55 call on the "main" pipe.
55 call on the "main" pipe.
56
56
57 If large amounts of data are read from "main", the forward will cease after
57 If large amounts of data are read from "main", the forward will cease after
58 the first bytes start to appear. This simplifies the implementation
58 the first bytes start to appear. This simplifies the implementation
59 without affecting actual output of sshpeer too much as we rarely issue
59 without affecting actual output of sshpeer too much as we rarely issue
60 large read for data not yet emitted by the server.
60 large read for data not yet emitted by the server.
61
61
62 The main pipe is expected to be a 'bufferedinputpipe' from the util module
62 The main pipe is expected to be a 'bufferedinputpipe' from the util module
63 that handle all the os specific bits. This class lives in this module
63 that handle all the os specific bits. This class lives in this module
64 because it focus on behavior specific to the ssh protocol."""
64 because it focus on behavior specific to the ssh protocol."""
65
65
66 def __init__(self, ui, main, side):
66 def __init__(self, ui, main, side):
67 self._ui = ui
67 self._ui = ui
68 self._main = main
68 self._main = main
69 self._side = side
69 self._side = side
70
70
71 def _wait(self):
71 def _wait(self):
72 """wait until some data are available on main or side
72 """wait until some data are available on main or side
73
73
74 return a pair of boolean (ismainready, issideready)
74 return a pair of boolean (ismainready, issideready)
75
75
76 (This will only wait for data if the setup is supported by `util.poll`)
76 (This will only wait for data if the setup is supported by `util.poll`)
77 """
77 """
78 if (
78 if (
79 isinstance(self._main, util.bufferedinputpipe)
79 isinstance(self._main, util.bufferedinputpipe)
80 and self._main.hasbuffer
80 and self._main.hasbuffer
81 ):
81 ):
82 # Main has data. Assume side is worth poking at.
82 # Main has data. Assume side is worth poking at.
83 return True, True
83 return True, True
84
84
85 fds = [self._main.fileno(), self._side.fileno()]
85 fds = [self._main.fileno(), self._side.fileno()]
86 try:
86 try:
87 act = util.poll(fds)
87 act = util.poll(fds)
88 except NotImplementedError:
88 except NotImplementedError:
89 # non supported yet case, assume all have data.
89 # non supported yet case, assume all have data.
90 act = fds
90 act = fds
91 return (self._main.fileno() in act, self._side.fileno() in act)
91 return (self._main.fileno() in act, self._side.fileno() in act)
92
92
93 def write(self, data):
93 def write(self, data):
94 return self._call(b'write', data)
94 return self._call(b'write', data)
95
95
96 def read(self, size):
96 def read(self, size):
97 r = self._call(b'read', size)
97 r = self._call(b'read', size)
98 if size != 0 and not r:
98 if size != 0 and not r:
99 # We've observed a condition that indicates the
99 # We've observed a condition that indicates the
100 # stdout closed unexpectedly. Check stderr one
100 # stdout closed unexpectedly. Check stderr one
101 # more time and snag anything that's there before
101 # more time and snag anything that's there before
102 # letting anyone know the main part of the pipe
102 # letting anyone know the main part of the pipe
103 # closed prematurely.
103 # closed prematurely.
104 _forwardoutput(self._ui, self._side)
104 _forwardoutput(self._ui, self._side)
105 return r
105 return r
106
106
107 def unbufferedread(self, size):
107 def unbufferedread(self, size):
108 r = self._call(b'unbufferedread', size)
108 r = self._call(b'unbufferedread', size)
109 if size != 0 and not r:
109 if size != 0 and not r:
110 # We've observed a condition that indicates the
110 # We've observed a condition that indicates the
111 # stdout closed unexpectedly. Check stderr one
111 # stdout closed unexpectedly. Check stderr one
112 # more time and snag anything that's there before
112 # more time and snag anything that's there before
113 # letting anyone know the main part of the pipe
113 # letting anyone know the main part of the pipe
114 # closed prematurely.
114 # closed prematurely.
115 _forwardoutput(self._ui, self._side)
115 _forwardoutput(self._ui, self._side)
116 return r
116 return r
117
117
118 def readline(self):
118 def readline(self):
119 return self._call(b'readline')
119 return self._call(b'readline')
120
120
121 def _call(self, methname, data=None):
121 def _call(self, methname, data=None):
122 """call <methname> on "main", forward output of "side" while blocking"""
122 """call <methname> on "main", forward output of "side" while blocking"""
123 # data can be '' or 0
123 # data can be '' or 0
124 if (data is not None and not data) or self._main.closed:
124 if (data is not None and not data) or self._main.closed:
125 _forwardoutput(self._ui, self._side)
125 _forwardoutput(self._ui, self._side)
126 return b''
126 return b''
127 while True:
127 while True:
128 mainready, sideready = self._wait()
128 mainready, sideready = self._wait()
129 if sideready:
129 if sideready:
130 _forwardoutput(self._ui, self._side)
130 _forwardoutput(self._ui, self._side)
131 if mainready:
131 if mainready:
132 meth = getattr(self._main, pycompat.sysstr(methname))
132 meth = getattr(self._main, pycompat.sysstr(methname))
133 if data is None:
133 if data is None:
134 return meth()
134 return meth()
135 else:
135 else:
136 return meth(data)
136 return meth(data)
137
137
138 def close(self):
138 def close(self):
139 return self._main.close()
139 return self._main.close()
140
140
141 @property
141 @property
142 def closed(self):
142 def closed(self):
143 return self._main.closed
143 return self._main.closed
144
144
145 def flush(self):
145 def flush(self):
146 return self._main.flush()
146 return self._main.flush()
147
147
148
148
149 def _cleanuppipes(ui, pipei, pipeo, pipee, warn):
149 def _cleanuppipes(ui, pipei, pipeo, pipee, warn):
150 """Clean up pipes used by an SSH connection."""
150 """Clean up pipes used by an SSH connection."""
151 didsomething = False
151 didsomething = False
152 if pipeo and not pipeo.closed:
152 if pipeo and not pipeo.closed:
153 didsomething = True
153 didsomething = True
154 pipeo.close()
154 pipeo.close()
155 if pipei and not pipei.closed:
155 if pipei and not pipei.closed:
156 didsomething = True
156 didsomething = True
157 pipei.close()
157 pipei.close()
158
158
159 if pipee and not pipee.closed:
159 if pipee and not pipee.closed:
160 didsomething = True
160 didsomething = True
161 # Try to read from the err descriptor until EOF.
161 # Try to read from the err descriptor until EOF.
162 try:
162 try:
163 for l in pipee:
163 for l in pipee:
164 ui.status(_(b'remote: '), l)
164 ui.status(_(b'remote: '), l)
165 except (IOError, ValueError):
165 except (IOError, ValueError):
166 pass
166 pass
167
167
168 pipee.close()
168 pipee.close()
169
169
170 if didsomething and warn is not None:
170 if didsomething and warn is not None:
171 # Encourage explicit close of sshpeers. Closing via __del__ is
171 # Encourage explicit close of sshpeers. Closing via __del__ is
172 # not very predictable when exceptions are thrown, which has led
172 # not very predictable when exceptions are thrown, which has led
173 # to deadlocks due to a peer get gc'ed in a fork
173 # to deadlocks due to a peer get gc'ed in a fork
174 # We add our own stack trace, because the stacktrace when called
174 # We add our own stack trace, because the stacktrace when called
175 # from __del__ is useless.
175 # from __del__ is useless.
176 ui.develwarn(b'missing close on SSH connection created at:\n%s' % warn)
176 ui.develwarn(b'missing close on SSH connection created at:\n%s' % warn)
177
177
178
178
179 def _makeconnection(
179 def _makeconnection(
180 ui, sshcmd, args, remotecmd, path, sshenv=None, remotehidden=False
180 ui, sshcmd, args, remotecmd, path, sshenv=None, remotehidden=False
181 ):
181 ):
182 """Create an SSH connection to a server.
182 """Create an SSH connection to a server.
183
183
184 Returns a tuple of (process, stdin, stdout, stderr) for the
184 Returns a tuple of (process, stdin, stdout, stderr) for the
185 spawned process.
185 spawned process.
186 """
186 """
187 cmd = b'%s %s %s' % (
187 cmd = b'%s %s %s' % (
188 sshcmd,
188 sshcmd,
189 args,
189 args,
190 procutil.shellquote(
190 procutil.shellquote(
191 b'%s -R %s serve --stdio%s'
191 b'%s -R %s serve --stdio%s'
192 % (
192 % (
193 _serverquote(remotecmd),
193 _serverquote(remotecmd),
194 _serverquote(path),
194 _serverquote(path),
195 b' --hidden' if remotehidden else b'',
195 b' --hidden' if remotehidden else b'',
196 )
196 )
197 ),
197 ),
198 )
198 )
199
199
200 ui.debug(b'running %s\n' % cmd)
200 ui.debug(b'running %s\n' % cmd)
201
201
202 # no buffer allow the use of 'select'
202 # no buffer allow the use of 'select'
203 # feel free to remove buffering and select usage when we ultimately
203 # feel free to remove buffering and select usage when we ultimately
204 # move to threading.
204 # move to threading.
205 stdin, stdout, stderr, proc = procutil.popen4(cmd, bufsize=0, env=sshenv)
205 stdin, stdout, stderr, proc = procutil.popen4(cmd, bufsize=0, env=sshenv)
206
206
207 return proc, stdin, stdout, stderr
207 return proc, stdin, stdout, stderr
208
208
209
209
210 def _clientcapabilities():
210 def _clientcapabilities():
211 """Return list of capabilities of this client.
211 """Return list of capabilities of this client.
212
212
213 Returns a list of capabilities that are supported by this client.
213 Returns a list of capabilities that are supported by this client.
214 """
214 """
215 protoparams = {b'partial-pull'}
215 protoparams = {b'partial-pull'}
216 comps = [
216 comps = [
217 e.wireprotosupport().name
217 e.wireprotosupport().name
218 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
218 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
219 ]
219 ]
220 protoparams.add(b'comp=%s' % b','.join(comps))
220 protoparams.add(b'comp=%s' % b','.join(comps))
221 return protoparams
221 return protoparams
222
222
223
223
224 def _performhandshake(ui, stdin, stdout, stderr):
224 def _performhandshake(ui, stdin, stdout, stderr):
225 def badresponse():
225 def badresponse():
226 # Flush any output on stderr. In general, the stderr contains errors
226 # Flush any output on stderr. In general, the stderr contains errors
227 # from the remote (ssh errors, some hg errors), and status indications
227 # from the remote (ssh errors, some hg errors), and status indications
228 # (like "adding changes"), with no current way to tell them apart.
228 # (like "adding changes"), with no current way to tell them apart.
229 # Here we failed so early that it's almost certainly only errors, so
229 # Here we failed so early that it's almost certainly only errors, so
230 # use warn=True so -q doesn't hide them.
230 # use warn=True so -q doesn't hide them.
231 _forwardoutput(ui, stderr, warn=True)
231 _forwardoutput(ui, stderr, warn=True)
232
232
233 msg = _(b'no suitable response from remote hg')
233 msg = _(b'no suitable response from remote hg')
234 hint = ui.config(b'ui', b'ssherrorhint')
234 hint = ui.config(b'ui', b'ssherrorhint')
235 raise error.RepoError(msg, hint=hint)
235 raise error.RepoError(msg, hint=hint)
236
236
237 # The handshake consists of sending wire protocol commands in reverse
237 # The handshake consists of sending wire protocol commands in reverse
238 # order of protocol implementation and then sniffing for a response
238 # order of protocol implementation and then sniffing for a response
239 # to one of them.
239 # to one of them.
240 #
240 #
241 # Those commands (from oldest to newest) are:
241 # Those commands (from oldest to newest) are:
242 #
242 #
243 # ``between``
243 # ``between``
244 # Asks for the set of revisions between a pair of revisions. Command
244 # Asks for the set of revisions between a pair of revisions. Command
245 # present in all Mercurial server implementations.
245 # present in all Mercurial server implementations.
246 #
246 #
247 # ``hello``
247 # ``hello``
248 # Instructs the server to advertise its capabilities. Introduced in
248 # Instructs the server to advertise its capabilities. Introduced in
249 # Mercurial 0.9.1.
249 # Mercurial 0.9.1.
250 #
250 #
251 # ``upgrade``
251 # ``upgrade``
252 # Requests upgrade from default transport protocol version 1 to
252 # Requests upgrade from default transport protocol version 1 to
253 # a newer version. Introduced in Mercurial 4.6 as an experimental
253 # a newer version. Introduced in Mercurial 4.6 as an experimental
254 # feature.
254 # feature.
255 #
255 #
256 # The ``between`` command is issued with a request for the null
256 # The ``between`` command is issued with a request for the null
257 # range. If the remote is a Mercurial server, this request will
257 # range. If the remote is a Mercurial server, this request will
258 # generate a specific response: ``1\n\n``. This represents the
258 # generate a specific response: ``1\n\n``. This represents the
259 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
259 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
260 # in the output stream and know this is the response to ``between``
260 # in the output stream and know this is the response to ``between``
261 # and we're at the end of our handshake reply.
261 # and we're at the end of our handshake reply.
262 #
262 #
263 # The response to the ``hello`` command will be a line with the
263 # The response to the ``hello`` command will be a line with the
264 # length of the value returned by that command followed by that
264 # length of the value returned by that command followed by that
265 # value. If the server doesn't support ``hello`` (which should be
265 # value. If the server doesn't support ``hello`` (which should be
266 # rare), that line will be ``0\n``. Otherwise, the value will contain
266 # rare), that line will be ``0\n``. Otherwise, the value will contain
267 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
267 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
268 # the capabilities of the server.
268 # the capabilities of the server.
269 #
269 #
270 # The ``upgrade`` command isn't really a command in the traditional
270 # The ``upgrade`` command isn't really a command in the traditional
271 # sense of version 1 of the transport because it isn't using the
271 # sense of version 1 of the transport because it isn't using the
272 # proper mechanism for formatting insteads: instead, it just encodes
272 # proper mechanism for formatting insteads: instead, it just encodes
273 # arguments on the line, delimited by spaces.
273 # arguments on the line, delimited by spaces.
274 #
274 #
275 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
275 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
276 # If the server doesn't support protocol upgrades, it will reply to
276 # If the server doesn't support protocol upgrades, it will reply to
277 # this line with ``0\n``. Otherwise, it emits an
277 # this line with ``0\n``. Otherwise, it emits an
278 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
278 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
279 # Content immediately following this line describes additional
279 # Content immediately following this line describes additional
280 # protocol and server state.
280 # protocol and server state.
281 #
281 #
282 # In addition to the responses to our command requests, the server
282 # In addition to the responses to our command requests, the server
283 # may emit "banner" output on stdout. SSH servers are allowed to
283 # may emit "banner" output on stdout. SSH servers are allowed to
284 # print messages to stdout on login. Issuing commands on connection
284 # print messages to stdout on login. Issuing commands on connection
285 # allows us to flush this banner output from the server by scanning
285 # allows us to flush this banner output from the server by scanning
286 # for output to our well-known ``between`` command. Of course, if
286 # for output to our well-known ``between`` command. Of course, if
287 # the banner contains ``1\n\n``, this will throw off our detection.
287 # the banner contains ``1\n\n``, this will throw off our detection.
288
288
289 requestlog = ui.configbool(b'devel', b'debug.peer-request')
289 requestlog = ui.configbool(b'devel', b'debug.peer-request')
290
290
291 # Generate a random token to help identify responses to version 2
291 # Generate a random token to help identify responses to version 2
292 # upgrade request.
292 # upgrade request.
293 token = pycompat.sysbytes(str(uuid.uuid4()))
293 token = pycompat.sysbytes(str(uuid.uuid4()))
294
294
295 try:
295 try:
296 pairsarg = b'%s-%s' % (b'0' * 40, b'0' * 40)
296 pairsarg = b'%s-%s' % (b'0' * 40, b'0' * 40)
297 handshake = [
297 handshake = [
298 b'hello\n',
298 b'hello\n',
299 b'between\n',
299 b'between\n',
300 b'pairs %d\n' % len(pairsarg),
300 b'pairs %d\n' % len(pairsarg),
301 pairsarg,
301 pairsarg,
302 ]
302 ]
303
303
304 if requestlog:
304 if requestlog:
305 ui.debug(b'devel-peer-request: hello+between\n')
305 ui.debug(b'devel-peer-request: hello+between\n')
306 ui.debug(b'devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
306 ui.debug(b'devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
307 ui.debug(b'sending hello command\n')
307 ui.debug(b'sending hello command\n')
308 ui.debug(b'sending between command\n')
308 ui.debug(b'sending between command\n')
309
309
310 stdin.write(b''.join(handshake))
310 stdin.write(b''.join(handshake))
311 stdin.flush()
311 stdin.flush()
312 except IOError:
312 except IOError:
313 badresponse()
313 badresponse()
314
314
315 # Assume version 1 of wire protocol by default.
315 # Assume version 1 of wire protocol by default.
316 protoname = wireprototypes.SSHV1
316 protoname = wireprototypes.SSHV1
317 reupgraded = re.compile(b'^upgraded %s (.*)$' % stringutil.reescape(token))
317 reupgraded = re.compile(b'^upgraded %s (.*)$' % stringutil.reescape(token))
318
318
319 lines = [b'', b'dummy']
319 lines = [b'', b'dummy']
320 max_noise = 500
320 max_noise = 500
321 while lines[-1] and max_noise:
321 while lines[-1] and max_noise:
322 try:
322 try:
323 l = stdout.readline()
323 l = stdout.readline()
324 _forwardoutput(ui, stderr, warn=True)
324 _forwardoutput(ui, stderr, warn=True)
325
325
326 # Look for reply to protocol upgrade request. It has a token
326 # Look for reply to protocol upgrade request. It has a token
327 # in it, so there should be no false positives.
327 # in it, so there should be no false positives.
328 m = reupgraded.match(l)
328 m = reupgraded.match(l)
329 if m:
329 if m:
330 protoname = m.group(1)
330 protoname = m.group(1)
331 ui.debug(b'protocol upgraded to %s\n' % protoname)
331 ui.debug(b'protocol upgraded to %s\n' % protoname)
332 # If an upgrade was handled, the ``hello`` and ``between``
332 # If an upgrade was handled, the ``hello`` and ``between``
333 # requests are ignored. The next output belongs to the
333 # requests are ignored. The next output belongs to the
334 # protocol, so stop scanning lines.
334 # protocol, so stop scanning lines.
335 break
335 break
336
336
337 # Otherwise it could be a banner, ``0\n`` response if server
337 # Otherwise it could be a banner, ``0\n`` response if server
338 # doesn't support upgrade.
338 # doesn't support upgrade.
339
339
340 if lines[-1] == b'1\n' and l == b'\n':
340 if lines[-1] == b'1\n' and l == b'\n':
341 break
341 break
342 if l:
342 if l:
343 ui.debug(b'remote: ', l)
343 ui.debug(b'remote: ', l)
344 lines.append(l)
344 lines.append(l)
345 max_noise -= 1
345 max_noise -= 1
346 except IOError:
346 except IOError:
347 badresponse()
347 badresponse()
348 else:
348 else:
349 badresponse()
349 badresponse()
350
350
351 caps = set()
351 caps = set()
352
352
353 # For version 1, we should see a ``capabilities`` line in response to the
353 # For version 1, we should see a ``capabilities`` line in response to the
354 # ``hello`` command.
354 # ``hello`` command.
355 if protoname == wireprototypes.SSHV1:
355 if protoname == wireprototypes.SSHV1:
356 for l in reversed(lines):
356 for l in reversed(lines):
357 # Look for response to ``hello`` command. Scan from the back so
357 # Look for response to ``hello`` command. Scan from the back so
358 # we don't misinterpret banner output as the command reply.
358 # we don't misinterpret banner output as the command reply.
359 if l.startswith(b'capabilities:'):
359 if l.startswith(b'capabilities:'):
360 caps.update(l[:-1].split(b':')[1].split())
360 caps.update(l[:-1].split(b':')[1].split())
361 break
361 break
362
362
363 # Error if we couldn't find capabilities, this means:
363 # Error if we couldn't find capabilities, this means:
364 #
364 #
365 # 1. Remote isn't a Mercurial server
365 # 1. Remote isn't a Mercurial server
366 # 2. Remote is a <0.9.1 Mercurial server
366 # 2. Remote is a <0.9.1 Mercurial server
367 # 3. Remote is a future Mercurial server that dropped ``hello``
367 # 3. Remote is a future Mercurial server that dropped ``hello``
368 # and other attempted handshake mechanisms.
368 # and other attempted handshake mechanisms.
369 if not caps:
369 if not caps:
370 badresponse()
370 badresponse()
371
371
372 # Flush any output on stderr before proceeding.
372 # Flush any output on stderr before proceeding.
373 _forwardoutput(ui, stderr, warn=True)
373 _forwardoutput(ui, stderr, warn=True)
374
374
375 return protoname, caps
375 return protoname, caps
376
376
377
377
378 class sshv1peer(wireprotov1peer.wirepeer):
378 class sshv1peer(wireprotov1peer.wirepeer):
379 def __init__(
379 def __init__(
380 self,
380 self,
381 ui,
381 ui,
382 path,
382 path,
383 proc,
383 proc,
384 stdin,
384 stdin,
385 stdout,
385 stdout,
386 stderr,
386 stderr,
387 caps,
387 caps,
388 autoreadstderr=True,
388 autoreadstderr=True,
389 remotehidden=False,
389 remotehidden=False,
390 ):
390 ):
391 """Create a peer from an existing SSH connection.
391 """Create a peer from an existing SSH connection.
392
392
393 ``proc`` is a handle on the underlying SSH process.
393 ``proc`` is a handle on the underlying SSH process.
394 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
394 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
395 pipes for that process.
395 pipes for that process.
396 ``caps`` is a set of capabilities supported by the remote.
396 ``caps`` is a set of capabilities supported by the remote.
397 ``autoreadstderr`` denotes whether to automatically read from
397 ``autoreadstderr`` denotes whether to automatically read from
398 stderr and to forward its output.
398 stderr and to forward its output.
399 """
399 """
400 super().__init__(ui, path=path, remotehidden=remotehidden)
400 super().__init__(ui, path=path, remotehidden=remotehidden)
401 # self._subprocess is unused. Keeping a handle on the process
401 # self._subprocess is unused. Keeping a handle on the process
402 # holds a reference and prevents it from being garbage collected.
402 # holds a reference and prevents it from being garbage collected.
403 self._subprocess = proc
403 self._subprocess = proc
404
404
405 # And we hook up our "doublepipe" wrapper to allow querying
405 # And we hook up our "doublepipe" wrapper to allow querying
406 # stderr any time we perform I/O.
406 # stderr any time we perform I/O.
407 if autoreadstderr:
407 if autoreadstderr:
408 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
408 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
409 stdin = doublepipe(ui, stdin, stderr)
409 stdin = doublepipe(ui, stdin, stderr)
410
410
411 self._pipeo = stdin
411 self._pipeo = stdin
412 self._pipei = stdout
412 self._pipei = stdout
413 self._pipee = stderr
413 self._pipee = stderr
414 self._caps = caps
414 self._caps = caps
415 self._autoreadstderr = autoreadstderr
415 self._autoreadstderr = autoreadstderr
416 self._initstack = b''.join(util.getstackframes(1))
416 self._initstack = b''.join(util.getstackframes(1))
417 self._remotehidden = remotehidden
417 self._remotehidden = remotehidden
418
418
419 # Commands that have a "framed" response where the first line of the
419 # Commands that have a "framed" response where the first line of the
420 # response contains the length of that response.
420 # response contains the length of that response.
421 _FRAMED_COMMANDS = {
421 _FRAMED_COMMANDS = {
422 b'batch',
422 b'batch',
423 }
423 }
424
424
425 # Begin of ipeerconnection interface.
425 # Begin of ipeerconnection interface.
426
426
427 def url(self):
427 def url(self):
428 return self.path.loc
428 return self.path.loc
429
429
430 def local(self):
430 def local(self):
431 return None
431 return None
432
432
433 def canpush(self):
433 def canpush(self):
434 return True
434 return True
435
435
436 def close(self):
436 def close(self):
437 self._cleanup()
437 self._cleanup()
438
438
439 # End of ipeerconnection interface.
439 # End of ipeerconnection interface.
440
440
441 # Begin of ipeercommands interface.
441 # Begin of ipeercommands interface.
442
442
443 def capabilities(self):
443 def capabilities(self):
444 return self._caps
444 return self._caps
445
445
446 # End of ipeercommands interface.
446 # End of ipeercommands interface.
447
447
448 def _readerr(self):
448 def _readerr(self):
449 _forwardoutput(self.ui, self._pipee)
449 _forwardoutput(self.ui, self._pipee)
450
450
451 def _abort(self, exception):
451 def _abort(self, exception):
452 self._cleanup()
452 self._cleanup()
453 raise exception
453 raise exception
454
454
455 def _cleanup(self, warn=None):
455 def _cleanup(self, warn=None):
456 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee, warn=warn)
456 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee, warn=warn)
457
457
458 def __del__(self):
458 def __del__(self):
459 self._cleanup(warn=self._initstack)
459 self._cleanup(warn=self._initstack)
460
460
461 def _sendrequest(self, cmd, args, framed=False):
461 def _sendrequest(self, cmd, args, framed=False):
462 if self.ui.debugflag and self.ui.configbool(
462 if self.ui.debugflag and self.ui.configbool(
463 b'devel', b'debug.peer-request'
463 b'devel', b'debug.peer-request'
464 ):
464 ):
465 dbg = self.ui.debug
465 dbg = self.ui.debug
466 line = b'devel-peer-request: %s\n'
466 line = b'devel-peer-request: %s\n'
467 dbg(line % cmd)
467 dbg(line % cmd)
468 for key, value in sorted(args.items()):
468 for key, value in sorted(args.items()):
469 if not isinstance(value, dict):
469 if not isinstance(value, dict):
470 dbg(line % b' %s: %d bytes' % (key, len(value)))
470 dbg(line % b' %s: %d bytes' % (key, len(value)))
471 else:
471 else:
472 for dk, dv in sorted(value.items()):
472 for dk, dv in sorted(value.items()):
473 dbg(line % b' %s-%s: %d' % (key, dk, len(dv)))
473 dbg(line % b' %s-%s: %d' % (key, dk, len(dv)))
474 self.ui.debug(b"sending %s command\n" % cmd)
474 self.ui.debug(b"sending %s command\n" % cmd)
475 self._pipeo.write(b"%s\n" % cmd)
475 self._pipeo.write(b"%s\n" % cmd)
476 _func, names = wireprotov1server.commands[cmd]
476 _func, names = wireprotov1server.commands[cmd]
477 keys = names.split()
477 keys = names.split()
478 wireargs = {}
478 wireargs = {}
479 for k in keys:
479 for k in keys:
480 if k == b'*':
480 if k == b'*':
481 wireargs[b'*'] = args
481 wireargs[b'*'] = args
482 break
482 break
483 else:
483 else:
484 wireargs[k] = args[k]
484 wireargs[k] = args[k]
485 del args[k]
485 del args[k]
486 for k, v in sorted(wireargs.items()):
486 for k, v in sorted(wireargs.items()):
487 self._pipeo.write(b"%s %d\n" % (k, len(v)))
487 self._pipeo.write(b"%s %d\n" % (k, len(v)))
488 if isinstance(v, dict):
488 if isinstance(v, dict):
489 for dk, dv in v.items():
489 for dk, dv in v.items():
490 self._pipeo.write(b"%s %d\n" % (dk, len(dv)))
490 self._pipeo.write(b"%s %d\n" % (dk, len(dv)))
491 self._pipeo.write(dv)
491 self._pipeo.write(dv)
492 else:
492 else:
493 self._pipeo.write(v)
493 self._pipeo.write(v)
494 self._pipeo.flush()
494 self._pipeo.flush()
495
495
496 # We know exactly how many bytes are in the response. So return a proxy
496 # We know exactly how many bytes are in the response. So return a proxy
497 # around the raw output stream that allows reading exactly this many
497 # around the raw output stream that allows reading exactly this many
498 # bytes. Callers then can read() without fear of overrunning the
498 # bytes. Callers then can read() without fear of overrunning the
499 # response.
499 # response.
500 if framed:
500 if framed:
501 amount = self._getamount()
501 amount = self._getamount()
502 return util.cappedreader(self._pipei, amount)
502 return util.cappedreader(self._pipei, amount)
503
503
504 return self._pipei
504 return self._pipei
505
505
506 def _callstream(self, cmd, **args):
506 def _callstream(self, cmd, **args):
507 args = pycompat.byteskwargs(args)
507 args = pycompat.byteskwargs(args)
508 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
508 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
509
509
510 def _callcompressable(self, cmd, **args):
510 def _callcompressable(self, cmd, **args):
511 args = pycompat.byteskwargs(args)
511 args = pycompat.byteskwargs(args)
512 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
512 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
513
513
514 def _call(self, cmd, **args):
514 def _call(self, cmd, **args):
515 args = pycompat.byteskwargs(args)
515 args = pycompat.byteskwargs(args)
516 return self._sendrequest(cmd, args, framed=True).read()
516 return self._sendrequest(cmd, args, framed=True).read()
517
517
518 def _callpush(self, cmd, fp, **args):
518 def _callpush(self, cmd, fp, **args):
519 # The server responds with an empty frame if the client should
519 # The server responds with an empty frame if the client should
520 # continue submitting the payload.
520 # continue submitting the payload.
521 r = self._call(cmd, **args)
521 r = self._call(cmd, **args)
522 if r:
522 if r:
523 return b'', r
523 return b'', r
524
524
525 # The payload consists of frames with content followed by an empty
525 # The payload consists of frames with content followed by an empty
526 # frame.
526 # frame.
527 for d in iter(lambda: fp.read(4096), b''):
527 for d in iter(lambda: fp.read(4096), b''):
528 self._writeframed(d)
528 self._writeframed(d)
529 self._writeframed(b"", flush=True)
529 self._writeframed(b"", flush=True)
530
530
531 # In case of success, there is an empty frame and a frame containing
531 # In case of success, there is an empty frame and a frame containing
532 # the integer result (as a string).
532 # the integer result (as a string).
533 # In case of error, there is a non-empty frame containing the error.
533 # In case of error, there is a non-empty frame containing the error.
534 r = self._readframed()
534 r = self._readframed()
535 if r:
535 if r:
536 return b'', r
536 return b'', r
537 return self._readframed(), b''
537 return self._readframed(), b''
538
538
539 def _calltwowaystream(self, cmd, fp, **args):
539 def _calltwowaystream(self, cmd, fp, **args):
540 # The server responds with an empty frame if the client should
540 # The server responds with an empty frame if the client should
541 # continue submitting the payload.
541 # continue submitting the payload.
542 r = self._call(cmd, **args)
542 r = self._call(cmd, **args)
543 if r:
543 if r:
544 # XXX needs to be made better
544 # XXX needs to be made better
545 raise error.Abort(_(b'unexpected remote reply: %s') % r)
545 raise error.Abort(_(b'unexpected remote reply: %s') % r)
546
546
547 # The payload consists of frames with content followed by an empty
547 # The payload consists of frames with content followed by an empty
548 # frame.
548 # frame.
549 for d in iter(lambda: fp.read(4096), b''):
549 for d in iter(lambda: fp.read(4096), b''):
550 self._writeframed(d)
550 self._writeframed(d)
551 self._writeframed(b"", flush=True)
551 self._writeframed(b"", flush=True)
552
552
553 return self._pipei
553 return self._pipei
554
554
555 def _getamount(self):
555 def _getamount(self):
556 l = self._pipei.readline()
556 l = self._pipei.readline()
557 if l == b'\n':
557 if l == b'\n':
558 if self._autoreadstderr:
558 if self._autoreadstderr:
559 self._readerr()
559 self._readerr()
560 msg = _(b'check previous remote output')
560 msg = _(b'check previous remote output')
561 self._abort(error.OutOfBandError(hint=msg))
561 self._abort(error.OutOfBandError(hint=msg))
562 if self._autoreadstderr:
562 if self._autoreadstderr:
563 self._readerr()
563 self._readerr()
564 try:
564 try:
565 return int(l)
565 return int(l)
566 except ValueError:
566 except ValueError:
567 self._abort(error.ResponseError(_(b"unexpected response:"), l))
567 self._abort(error.ResponseError(_(b"unexpected response:"), l))
568
568
569 def _readframed(self):
569 def _readframed(self):
570 size = self._getamount()
570 size = self._getamount()
571 if not size:
571 if not size:
572 return b''
572 return b''
573
573
574 return self._pipei.read(size)
574 return self._pipei.read(size)
575
575
576 def _writeframed(self, data, flush=False):
576 def _writeframed(self, data, flush=False):
577 self._pipeo.write(b"%d\n" % len(data))
577 self._pipeo.write(b"%d\n" % len(data))
578 if data:
578 if data:
579 self._pipeo.write(data)
579 self._pipeo.write(data)
580 if flush:
580 if flush:
581 self._pipeo.flush()
581 self._pipeo.flush()
582 if self._autoreadstderr:
582 if self._autoreadstderr:
583 self._readerr()
583 self._readerr()
584
584
585
585
586 def _make_peer(
586 def _make_peer(
587 ui,
587 ui,
588 path,
588 path,
589 proc,
589 proc,
590 stdin,
590 stdin,
591 stdout,
591 stdout,
592 stderr,
592 stderr,
593 autoreadstderr=True,
593 autoreadstderr=True,
594 remotehidden=False,
594 remotehidden=False,
595 ):
595 ):
596 """Make a peer instance from existing pipes.
596 """Make a peer instance from existing pipes.
597
597
598 ``path`` and ``proc`` are stored on the eventual peer instance and may
598 ``path`` and ``proc`` are stored on the eventual peer instance and may
599 not be used for anything meaningful.
599 not be used for anything meaningful.
600
600
601 ``stdin``, ``stdout``, and ``stderr`` are the pipes connected to the
601 ``stdin``, ``stdout``, and ``stderr`` are the pipes connected to the
602 SSH server's stdio handles.
602 SSH server's stdio handles.
603
603
604 This function is factored out to allow creating peers that don't
604 This function is factored out to allow creating peers that don't
605 actually spawn a new process. It is useful for starting SSH protocol
605 actually spawn a new process. It is useful for starting SSH protocol
606 servers and clients via non-standard means, which can be useful for
606 servers and clients via non-standard means, which can be useful for
607 testing.
607 testing.
608 """
608 """
609 try:
609 try:
610 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
610 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
611 except Exception:
611 except Exception:
612 _cleanuppipes(ui, stdout, stdin, stderr, warn=None)
612 _cleanuppipes(ui, stdout, stdin, stderr, warn=None)
613 raise
613 raise
614
614
615 if protoname == wireprototypes.SSHV1:
615 if protoname == wireprototypes.SSHV1:
616 return sshv1peer(
616 return sshv1peer(
617 ui,
617 ui,
618 path,
618 path,
619 proc,
619 proc,
620 stdin,
620 stdin,
621 stdout,
621 stdout,
622 stderr,
622 stderr,
623 caps,
623 caps,
624 autoreadstderr=autoreadstderr,
624 autoreadstderr=autoreadstderr,
625 remotehidden=remotehidden,
625 remotehidden=remotehidden,
626 )
626 )
627 else:
627 else:
628 _cleanuppipes(ui, stdout, stdin, stderr, warn=None)
628 _cleanuppipes(ui, stdout, stdin, stderr, warn=None)
629 raise error.RepoError(
629 raise error.RepoError(
630 _(b'unknown version of SSH protocol: %s') % protoname
630 _(b'unknown version of SSH protocol: %s') % protoname
631 )
631 )
632
632
633
633
634 def make_peer(
634 def make_peer(
635 ui, path, create, intents=None, createopts=None, remotehidden=False
635 ui, path, create, intents=None, createopts=None, remotehidden=False
636 ):
636 ):
637 """Create an SSH peer.
637 """Create an SSH peer.
638
638
639 The returned object conforms to the ``wireprotov1peer.wirepeer`` interface.
639 The returned object conforms to the ``wireprotov1peer.wirepeer`` interface.
640 """
640 """
641 u = urlutil.url(path.loc, parsequery=False, parsefragment=False)
641 u = urlutil.url(path.loc, parsequery=False, parsefragment=False)
642 if u.scheme != b'ssh' or not u.host or u.path is None:
642 if u.scheme != b'ssh' or not u.host or u.path is None:
643 raise error.RepoError(_(b"couldn't parse location %s") % path)
643 raise error.RepoError(_(b"couldn't parse location %s") % path.loc)
644
644
645 urlutil.checksafessh(path.loc)
645 urlutil.checksafessh(path.loc)
646
646
647 if u.passwd is not None:
647 if u.passwd is not None:
648 raise error.RepoError(_(b'password in URL not supported'))
648 raise error.RepoError(_(b'password in URL not supported'))
649
649
650 sshcmd = ui.config(b'ui', b'ssh')
650 sshcmd = ui.config(b'ui', b'ssh')
651 remotecmd = ui.config(b'ui', b'remotecmd')
651 remotecmd = ui.config(b'ui', b'remotecmd')
652 sshaddenv = dict(ui.configitems(b'sshenv'))
652 sshaddenv = dict(ui.configitems(b'sshenv'))
653 sshenv = procutil.shellenviron(sshaddenv)
653 sshenv = procutil.shellenviron(sshaddenv)
654 remotepath = u.path or b'.'
654 remotepath = u.path or b'.'
655
655
656 args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
656 args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
657
657
658 if create:
658 if create:
659 # We /could/ do this, but only if the remote init command knows how to
659 # We /could/ do this, but only if the remote init command knows how to
660 # handle them. We don't yet make any assumptions about that. And without
660 # handle them. We don't yet make any assumptions about that. And without
661 # querying the remote, there's no way of knowing if the remote even
661 # querying the remote, there's no way of knowing if the remote even
662 # supports said requested feature.
662 # supports said requested feature.
663 if createopts:
663 if createopts:
664 raise error.RepoError(
664 raise error.RepoError(
665 _(
665 _(
666 b'cannot create remote SSH repositories '
666 b'cannot create remote SSH repositories '
667 b'with extra options'
667 b'with extra options'
668 )
668 )
669 )
669 )
670
670
671 cmd = b'%s %s %s' % (
671 cmd = b'%s %s %s' % (
672 sshcmd,
672 sshcmd,
673 args,
673 args,
674 procutil.shellquote(
674 procutil.shellquote(
675 b'%s init %s'
675 b'%s init %s'
676 % (_serverquote(remotecmd), _serverquote(remotepath))
676 % (_serverquote(remotecmd), _serverquote(remotepath))
677 ),
677 ),
678 )
678 )
679 ui.debug(b'running %s\n' % cmd)
679 ui.debug(b'running %s\n' % cmd)
680 res = ui.system(cmd, blockedtag=b'sshpeer', environ=sshenv)
680 res = ui.system(cmd, blockedtag=b'sshpeer', environ=sshenv)
681 if res != 0:
681 if res != 0:
682 raise error.RepoError(_(b'could not create remote repo'))
682 raise error.RepoError(_(b'could not create remote repo'))
683
683
684 proc, stdin, stdout, stderr = _makeconnection(
684 proc, stdin, stdout, stderr = _makeconnection(
685 ui,
685 ui,
686 sshcmd,
686 sshcmd,
687 args,
687 args,
688 remotecmd,
688 remotecmd,
689 remotepath,
689 remotepath,
690 sshenv,
690 sshenv,
691 remotehidden=remotehidden,
691 remotehidden=remotehidden,
692 )
692 )
693
693
694 peer = _make_peer(
694 peer = _make_peer(
695 ui, path, proc, stdin, stdout, stderr, remotehidden=remotehidden
695 ui, path, proc, stdin, stdout, stderr, remotehidden=remotehidden
696 )
696 )
697
697
698 # Finally, if supported by the server, notify it about our own
698 # Finally, if supported by the server, notify it about our own
699 # capabilities.
699 # capabilities.
700 if b'protocaps' in peer.capabilities():
700 if b'protocaps' in peer.capabilities():
701 try:
701 try:
702 peer._call(
702 peer._call(
703 b"protocaps", caps=b' '.join(sorted(_clientcapabilities()))
703 b"protocaps", caps=b' '.join(sorted(_clientcapabilities()))
704 )
704 )
705 except IOError:
705 except IOError:
706 peer._cleanup()
706 peer._cleanup()
707 raise error.RepoError(_(b'capability exchange failed'))
707 raise error.RepoError(_(b'capability exchange failed'))
708
708
709 return peer
709 return peer
General Comments 0
You need to be logged in to leave comments. Login now