##// END OF EJS Templates
wireproto: turn client capabilities into sets, sorted on the wire...
Joerg Sonnenberger -
r37429:3e168871 default
parent child Browse files
Show More
@@ -1,502 +1,502 b''
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import io
12 import io
13 import os
13 import os
14 import socket
14 import socket
15 import struct
15 import struct
16 import tempfile
16 import tempfile
17
17
18 from .i18n import _
18 from .i18n import _
19 from . import (
19 from . import (
20 bundle2,
20 bundle2,
21 error,
21 error,
22 httpconnection,
22 httpconnection,
23 pycompat,
23 pycompat,
24 statichttprepo,
24 statichttprepo,
25 url as urlmod,
25 url as urlmod,
26 util,
26 util,
27 wireproto,
27 wireproto,
28 )
28 )
29
29
30 httplib = util.httplib
30 httplib = util.httplib
31 urlerr = util.urlerr
31 urlerr = util.urlerr
32 urlreq = util.urlreq
32 urlreq = util.urlreq
33
33
34 def encodevalueinheaders(value, header, limit):
34 def encodevalueinheaders(value, header, limit):
35 """Encode a string value into multiple HTTP headers.
35 """Encode a string value into multiple HTTP headers.
36
36
37 ``value`` will be encoded into 1 or more HTTP headers with the names
37 ``value`` will be encoded into 1 or more HTTP headers with the names
38 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
38 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
39 name + value will be at most ``limit`` bytes long.
39 name + value will be at most ``limit`` bytes long.
40
40
41 Returns an iterable of 2-tuples consisting of header names and
41 Returns an iterable of 2-tuples consisting of header names and
42 values as native strings.
42 values as native strings.
43 """
43 """
44 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
44 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
45 # not bytes. This function always takes bytes in as arguments.
45 # not bytes. This function always takes bytes in as arguments.
46 fmt = pycompat.strurl(header) + r'-%s'
46 fmt = pycompat.strurl(header) + r'-%s'
47 # Note: it is *NOT* a bug that the last bit here is a bytestring
47 # Note: it is *NOT* a bug that the last bit here is a bytestring
48 # and not a unicode: we're just getting the encoded length anyway,
48 # and not a unicode: we're just getting the encoded length anyway,
49 # and using an r-string to make it portable between Python 2 and 3
49 # and using an r-string to make it portable between Python 2 and 3
50 # doesn't work because then the \r is a literal backslash-r
50 # doesn't work because then the \r is a literal backslash-r
51 # instead of a carriage return.
51 # instead of a carriage return.
52 valuelen = limit - len(fmt % r'000') - len(': \r\n')
52 valuelen = limit - len(fmt % r'000') - len(': \r\n')
53 result = []
53 result = []
54
54
55 n = 0
55 n = 0
56 for i in xrange(0, len(value), valuelen):
56 for i in xrange(0, len(value), valuelen):
57 n += 1
57 n += 1
58 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
58 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
59
59
60 return result
60 return result
61
61
62 def _wraphttpresponse(resp):
62 def _wraphttpresponse(resp):
63 """Wrap an HTTPResponse with common error handlers.
63 """Wrap an HTTPResponse with common error handlers.
64
64
65 This ensures that any I/O from any consumer raises the appropriate
65 This ensures that any I/O from any consumer raises the appropriate
66 error and messaging.
66 error and messaging.
67 """
67 """
68 origread = resp.read
68 origread = resp.read
69
69
70 class readerproxy(resp.__class__):
70 class readerproxy(resp.__class__):
71 def read(self, size=None):
71 def read(self, size=None):
72 try:
72 try:
73 return origread(size)
73 return origread(size)
74 except httplib.IncompleteRead as e:
74 except httplib.IncompleteRead as e:
75 # e.expected is an integer if length known or None otherwise.
75 # e.expected is an integer if length known or None otherwise.
76 if e.expected:
76 if e.expected:
77 msg = _('HTTP request error (incomplete response; '
77 msg = _('HTTP request error (incomplete response; '
78 'expected %d bytes got %d)') % (e.expected,
78 'expected %d bytes got %d)') % (e.expected,
79 len(e.partial))
79 len(e.partial))
80 else:
80 else:
81 msg = _('HTTP request error (incomplete response)')
81 msg = _('HTTP request error (incomplete response)')
82
82
83 raise error.PeerTransportError(
83 raise error.PeerTransportError(
84 msg,
84 msg,
85 hint=_('this may be an intermittent network failure; '
85 hint=_('this may be an intermittent network failure; '
86 'if the error persists, consider contacting the '
86 'if the error persists, consider contacting the '
87 'network or server operator'))
87 'network or server operator'))
88 except httplib.HTTPException as e:
88 except httplib.HTTPException as e:
89 raise error.PeerTransportError(
89 raise error.PeerTransportError(
90 _('HTTP request error (%s)') % e,
90 _('HTTP request error (%s)') % e,
91 hint=_('this may be an intermittent network failure; '
91 hint=_('this may be an intermittent network failure; '
92 'if the error persists, consider contacting the '
92 'if the error persists, consider contacting the '
93 'network or server operator'))
93 'network or server operator'))
94
94
95 resp.__class__ = readerproxy
95 resp.__class__ = readerproxy
96
96
97 class _multifile(object):
97 class _multifile(object):
98 def __init__(self, *fileobjs):
98 def __init__(self, *fileobjs):
99 for f in fileobjs:
99 for f in fileobjs:
100 if not util.safehasattr(f, 'length'):
100 if not util.safehasattr(f, 'length'):
101 raise ValueError(
101 raise ValueError(
102 '_multifile only supports file objects that '
102 '_multifile only supports file objects that '
103 'have a length but this one does not:', type(f), f)
103 'have a length but this one does not:', type(f), f)
104 self._fileobjs = fileobjs
104 self._fileobjs = fileobjs
105 self._index = 0
105 self._index = 0
106
106
107 @property
107 @property
108 def length(self):
108 def length(self):
109 return sum(f.length for f in self._fileobjs)
109 return sum(f.length for f in self._fileobjs)
110
110
111 def read(self, amt=None):
111 def read(self, amt=None):
112 if amt <= 0:
112 if amt <= 0:
113 return ''.join(f.read() for f in self._fileobjs)
113 return ''.join(f.read() for f in self._fileobjs)
114 parts = []
114 parts = []
115 while amt and self._index < len(self._fileobjs):
115 while amt and self._index < len(self._fileobjs):
116 parts.append(self._fileobjs[self._index].read(amt))
116 parts.append(self._fileobjs[self._index].read(amt))
117 got = len(parts[-1])
117 got = len(parts[-1])
118 if got < amt:
118 if got < amt:
119 self._index += 1
119 self._index += 1
120 amt -= got
120 amt -= got
121 return ''.join(parts)
121 return ''.join(parts)
122
122
123 def seek(self, offset, whence=os.SEEK_SET):
123 def seek(self, offset, whence=os.SEEK_SET):
124 if whence != os.SEEK_SET:
124 if whence != os.SEEK_SET:
125 raise NotImplementedError(
125 raise NotImplementedError(
126 '_multifile does not support anything other'
126 '_multifile does not support anything other'
127 ' than os.SEEK_SET for whence on seek()')
127 ' than os.SEEK_SET for whence on seek()')
128 if offset != 0:
128 if offset != 0:
129 raise NotImplementedError(
129 raise NotImplementedError(
130 '_multifile only supports seeking to start, but that '
130 '_multifile only supports seeking to start, but that '
131 'could be fixed if you need it')
131 'could be fixed if you need it')
132 for f in self._fileobjs:
132 for f in self._fileobjs:
133 f.seek(0)
133 f.seek(0)
134 self._index = 0
134 self._index = 0
135
135
136 class httppeer(wireproto.wirepeer):
136 class httppeer(wireproto.wirepeer):
137 def __init__(self, ui, path, url, opener):
137 def __init__(self, ui, path, url, opener):
138 self.ui = ui
138 self.ui = ui
139 self._path = path
139 self._path = path
140 self._url = url
140 self._url = url
141 self._caps = None
141 self._caps = None
142 self._urlopener = opener
142 self._urlopener = opener
143 # This is an its own attribute to facilitate extensions overriding
143 # This is an its own attribute to facilitate extensions overriding
144 # the default type.
144 # the default type.
145 self._requestbuilder = urlreq.request
145 self._requestbuilder = urlreq.request
146
146
147 def __del__(self):
147 def __del__(self):
148 for h in self._urlopener.handlers:
148 for h in self._urlopener.handlers:
149 h.close()
149 h.close()
150 getattr(h, "close_all", lambda: None)()
150 getattr(h, "close_all", lambda: None)()
151
151
152 def _openurl(self, req):
152 def _openurl(self, req):
153 if (self.ui.debugflag
153 if (self.ui.debugflag
154 and self.ui.configbool('devel', 'debug.peer-request')):
154 and self.ui.configbool('devel', 'debug.peer-request')):
155 dbg = self.ui.debug
155 dbg = self.ui.debug
156 line = 'devel-peer-request: %s\n'
156 line = 'devel-peer-request: %s\n'
157 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
157 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
158 hgargssize = None
158 hgargssize = None
159
159
160 for header, value in sorted(req.header_items()):
160 for header, value in sorted(req.header_items()):
161 if header.startswith('X-hgarg-'):
161 if header.startswith('X-hgarg-'):
162 if hgargssize is None:
162 if hgargssize is None:
163 hgargssize = 0
163 hgargssize = 0
164 hgargssize += len(value)
164 hgargssize += len(value)
165 else:
165 else:
166 dbg(line % ' %s %s' % (header, value))
166 dbg(line % ' %s %s' % (header, value))
167
167
168 if hgargssize is not None:
168 if hgargssize is not None:
169 dbg(line % ' %d bytes of commands arguments in headers'
169 dbg(line % ' %d bytes of commands arguments in headers'
170 % hgargssize)
170 % hgargssize)
171
171
172 if req.has_data():
172 if req.has_data():
173 data = req.get_data()
173 data = req.get_data()
174 length = getattr(data, 'length', None)
174 length = getattr(data, 'length', None)
175 if length is None:
175 if length is None:
176 length = len(data)
176 length = len(data)
177 dbg(line % ' %d bytes of data' % length)
177 dbg(line % ' %d bytes of data' % length)
178
178
179 start = util.timer()
179 start = util.timer()
180
180
181 ret = self._urlopener.open(req)
181 ret = self._urlopener.open(req)
182 if self.ui.configbool('devel', 'debug.peer-request'):
182 if self.ui.configbool('devel', 'debug.peer-request'):
183 dbg(line % ' finished in %.4f seconds (%s)'
183 dbg(line % ' finished in %.4f seconds (%s)'
184 % (util.timer() - start, ret.code))
184 % (util.timer() - start, ret.code))
185 return ret
185 return ret
186
186
187 # Begin of ipeerconnection interface.
187 # Begin of ipeerconnection interface.
188
188
189 def url(self):
189 def url(self):
190 return self._path
190 return self._path
191
191
192 def local(self):
192 def local(self):
193 return None
193 return None
194
194
195 def peer(self):
195 def peer(self):
196 return self
196 return self
197
197
198 def canpush(self):
198 def canpush(self):
199 return True
199 return True
200
200
201 def close(self):
201 def close(self):
202 pass
202 pass
203
203
204 # End of ipeerconnection interface.
204 # End of ipeerconnection interface.
205
205
206 # Begin of ipeercommands interface.
206 # Begin of ipeercommands interface.
207
207
208 def capabilities(self):
208 def capabilities(self):
209 # self._fetchcaps() should have been called as part of peer
209 # self._fetchcaps() should have been called as part of peer
210 # handshake. So self._caps should always be set.
210 # handshake. So self._caps should always be set.
211 assert self._caps is not None
211 assert self._caps is not None
212 return self._caps
212 return self._caps
213
213
214 # End of ipeercommands interface.
214 # End of ipeercommands interface.
215
215
216 # look up capabilities only when needed
216 # look up capabilities only when needed
217
217
218 def _fetchcaps(self):
218 def _fetchcaps(self):
219 self._caps = set(self._call('capabilities').split())
219 self._caps = set(self._call('capabilities').split())
220
220
221 def _callstream(self, cmd, _compressible=False, **args):
221 def _callstream(self, cmd, _compressible=False, **args):
222 args = pycompat.byteskwargs(args)
222 args = pycompat.byteskwargs(args)
223 if cmd == 'pushkey':
223 if cmd == 'pushkey':
224 args['data'] = ''
224 args['data'] = ''
225 data = args.pop('data', None)
225 data = args.pop('data', None)
226 headers = args.pop('headers', {})
226 headers = args.pop('headers', {})
227
227
228 self.ui.debug("sending %s command\n" % cmd)
228 self.ui.debug("sending %s command\n" % cmd)
229 q = [('cmd', cmd)]
229 q = [('cmd', cmd)]
230 headersize = 0
230 headersize = 0
231 varyheaders = []
231 varyheaders = []
232 # Important: don't use self.capable() here or else you end up
232 # Important: don't use self.capable() here or else you end up
233 # with infinite recursion when trying to look up capabilities
233 # with infinite recursion when trying to look up capabilities
234 # for the first time.
234 # for the first time.
235 postargsok = self._caps is not None and 'httppostargs' in self._caps
235 postargsok = self._caps is not None and 'httppostargs' in self._caps
236
236
237 # Send arguments via POST.
237 # Send arguments via POST.
238 if postargsok and args:
238 if postargsok and args:
239 strargs = urlreq.urlencode(sorted(args.items()))
239 strargs = urlreq.urlencode(sorted(args.items()))
240 if not data:
240 if not data:
241 data = strargs
241 data = strargs
242 else:
242 else:
243 if isinstance(data, bytes):
243 if isinstance(data, bytes):
244 i = io.BytesIO(data)
244 i = io.BytesIO(data)
245 i.length = len(data)
245 i.length = len(data)
246 data = i
246 data = i
247 argsio = io.BytesIO(strargs)
247 argsio = io.BytesIO(strargs)
248 argsio.length = len(strargs)
248 argsio.length = len(strargs)
249 data = _multifile(argsio, data)
249 data = _multifile(argsio, data)
250 headers[r'X-HgArgs-Post'] = len(strargs)
250 headers[r'X-HgArgs-Post'] = len(strargs)
251 elif args:
251 elif args:
252 # Calling self.capable() can infinite loop if we are calling
252 # Calling self.capable() can infinite loop if we are calling
253 # "capabilities". But that command should never accept wire
253 # "capabilities". But that command should never accept wire
254 # protocol arguments. So this should never happen.
254 # protocol arguments. So this should never happen.
255 assert cmd != 'capabilities'
255 assert cmd != 'capabilities'
256 httpheader = self.capable('httpheader')
256 httpheader = self.capable('httpheader')
257 if httpheader:
257 if httpheader:
258 headersize = int(httpheader.split(',', 1)[0])
258 headersize = int(httpheader.split(',', 1)[0])
259
259
260 # Send arguments via HTTP headers.
260 # Send arguments via HTTP headers.
261 if headersize > 0:
261 if headersize > 0:
262 # The headers can typically carry more data than the URL.
262 # The headers can typically carry more data than the URL.
263 encargs = urlreq.urlencode(sorted(args.items()))
263 encargs = urlreq.urlencode(sorted(args.items()))
264 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
264 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
265 headersize):
265 headersize):
266 headers[header] = value
266 headers[header] = value
267 varyheaders.append(header)
267 varyheaders.append(header)
268 # Send arguments via query string (Mercurial <1.9).
268 # Send arguments via query string (Mercurial <1.9).
269 else:
269 else:
270 q += sorted(args.items())
270 q += sorted(args.items())
271
271
272 qs = '?%s' % urlreq.urlencode(q)
272 qs = '?%s' % urlreq.urlencode(q)
273 cu = "%s%s" % (self._url, qs)
273 cu = "%s%s" % (self._url, qs)
274 size = 0
274 size = 0
275 if util.safehasattr(data, 'length'):
275 if util.safehasattr(data, 'length'):
276 size = data.length
276 size = data.length
277 elif data is not None:
277 elif data is not None:
278 size = len(data)
278 size = len(data)
279 if data is not None and r'Content-Type' not in headers:
279 if data is not None and r'Content-Type' not in headers:
280 headers[r'Content-Type'] = r'application/mercurial-0.1'
280 headers[r'Content-Type'] = r'application/mercurial-0.1'
281
281
282 # Tell the server we accept application/mercurial-0.2 and multiple
282 # Tell the server we accept application/mercurial-0.2 and multiple
283 # compression formats if the server is capable of emitting those
283 # compression formats if the server is capable of emitting those
284 # payloads.
284 # payloads.
285 protoparams = []
285 protoparams = set()
286
286
287 mediatypes = set()
287 mediatypes = set()
288 if self._caps is not None:
288 if self._caps is not None:
289 mt = self.capable('httpmediatype')
289 mt = self.capable('httpmediatype')
290 if mt:
290 if mt:
291 protoparams.append('0.1')
291 protoparams.add('0.1')
292 mediatypes = set(mt.split(','))
292 mediatypes = set(mt.split(','))
293
293
294 if '0.2tx' in mediatypes:
294 if '0.2tx' in mediatypes:
295 protoparams.append('0.2')
295 protoparams.add('0.2')
296
296
297 if '0.2tx' in mediatypes and self.capable('compression'):
297 if '0.2tx' in mediatypes and self.capable('compression'):
298 # We /could/ compare supported compression formats and prune
298 # We /could/ compare supported compression formats and prune
299 # non-mutually supported or error if nothing is mutually supported.
299 # non-mutually supported or error if nothing is mutually supported.
300 # For now, send the full list to the server and have it error.
300 # For now, send the full list to the server and have it error.
301 comps = [e.wireprotosupport().name for e in
301 comps = [e.wireprotosupport().name for e in
302 util.compengines.supportedwireengines(util.CLIENTROLE)]
302 util.compengines.supportedwireengines(util.CLIENTROLE)]
303 protoparams.append('comp=%s' % ','.join(comps))
303 protoparams.add('comp=%s' % ','.join(comps))
304
304
305 if protoparams:
305 if protoparams:
306 protoheaders = encodevalueinheaders(' '.join(protoparams),
306 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
307 'X-HgProto',
307 'X-HgProto',
308 headersize or 1024)
308 headersize or 1024)
309 for header, value in protoheaders:
309 for header, value in protoheaders:
310 headers[header] = value
310 headers[header] = value
311 varyheaders.append(header)
311 varyheaders.append(header)
312
312
313 if varyheaders:
313 if varyheaders:
314 headers[r'Vary'] = r','.join(varyheaders)
314 headers[r'Vary'] = r','.join(varyheaders)
315
315
316 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
316 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
317
317
318 if data is not None:
318 if data is not None:
319 self.ui.debug("sending %d bytes\n" % size)
319 self.ui.debug("sending %d bytes\n" % size)
320 req.add_unredirected_header(r'Content-Length', r'%d' % size)
320 req.add_unredirected_header(r'Content-Length', r'%d' % size)
321 try:
321 try:
322 resp = self._openurl(req)
322 resp = self._openurl(req)
323 except urlerr.httperror as inst:
323 except urlerr.httperror as inst:
324 if inst.code == 401:
324 if inst.code == 401:
325 raise error.Abort(_('authorization failed'))
325 raise error.Abort(_('authorization failed'))
326 raise
326 raise
327 except httplib.HTTPException as inst:
327 except httplib.HTTPException as inst:
328 self.ui.debug('http error while sending %s command\n' % cmd)
328 self.ui.debug('http error while sending %s command\n' % cmd)
329 self.ui.traceback()
329 self.ui.traceback()
330 raise IOError(None, inst)
330 raise IOError(None, inst)
331
331
332 # Insert error handlers for common I/O failures.
332 # Insert error handlers for common I/O failures.
333 _wraphttpresponse(resp)
333 _wraphttpresponse(resp)
334
334
335 # record the url we got redirected to
335 # record the url we got redirected to
336 resp_url = pycompat.bytesurl(resp.geturl())
336 resp_url = pycompat.bytesurl(resp.geturl())
337 if resp_url.endswith(qs):
337 if resp_url.endswith(qs):
338 resp_url = resp_url[:-len(qs)]
338 resp_url = resp_url[:-len(qs)]
339 if self._url.rstrip('/') != resp_url.rstrip('/'):
339 if self._url.rstrip('/') != resp_url.rstrip('/'):
340 if not self.ui.quiet:
340 if not self.ui.quiet:
341 self.ui.warn(_('real URL is %s\n') % resp_url)
341 self.ui.warn(_('real URL is %s\n') % resp_url)
342 self._url = resp_url
342 self._url = resp_url
343 try:
343 try:
344 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
344 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
345 except AttributeError:
345 except AttributeError:
346 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
346 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
347
347
348 safeurl = util.hidepassword(self._url)
348 safeurl = util.hidepassword(self._url)
349 if proto.startswith('application/hg-error'):
349 if proto.startswith('application/hg-error'):
350 raise error.OutOfBandError(resp.read())
350 raise error.OutOfBandError(resp.read())
351 # accept old "text/plain" and "application/hg-changegroup" for now
351 # accept old "text/plain" and "application/hg-changegroup" for now
352 if not (proto.startswith('application/mercurial-') or
352 if not (proto.startswith('application/mercurial-') or
353 (proto.startswith('text/plain')
353 (proto.startswith('text/plain')
354 and not resp.headers.get('content-length')) or
354 and not resp.headers.get('content-length')) or
355 proto.startswith('application/hg-changegroup')):
355 proto.startswith('application/hg-changegroup')):
356 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
356 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
357 raise error.RepoError(
357 raise error.RepoError(
358 _("'%s' does not appear to be an hg repository:\n"
358 _("'%s' does not appear to be an hg repository:\n"
359 "---%%<--- (%s)\n%s\n---%%<---\n")
359 "---%%<--- (%s)\n%s\n---%%<---\n")
360 % (safeurl, proto or 'no content-type', resp.read(1024)))
360 % (safeurl, proto or 'no content-type', resp.read(1024)))
361
361
362 if proto.startswith('application/mercurial-'):
362 if proto.startswith('application/mercurial-'):
363 try:
363 try:
364 version = proto.split('-', 1)[1]
364 version = proto.split('-', 1)[1]
365 version_info = tuple([int(n) for n in version.split('.')])
365 version_info = tuple([int(n) for n in version.split('.')])
366 except ValueError:
366 except ValueError:
367 raise error.RepoError(_("'%s' sent a broken Content-Type "
367 raise error.RepoError(_("'%s' sent a broken Content-Type "
368 "header (%s)") % (safeurl, proto))
368 "header (%s)") % (safeurl, proto))
369
369
370 # TODO consider switching to a decompression reader that uses
370 # TODO consider switching to a decompression reader that uses
371 # generators.
371 # generators.
372 if version_info == (0, 1):
372 if version_info == (0, 1):
373 if _compressible:
373 if _compressible:
374 return util.compengines['zlib'].decompressorreader(resp)
374 return util.compengines['zlib'].decompressorreader(resp)
375 return resp
375 return resp
376 elif version_info == (0, 2):
376 elif version_info == (0, 2):
377 # application/mercurial-0.2 always identifies the compression
377 # application/mercurial-0.2 always identifies the compression
378 # engine in the payload header.
378 # engine in the payload header.
379 elen = struct.unpack('B', resp.read(1))[0]
379 elen = struct.unpack('B', resp.read(1))[0]
380 ename = resp.read(elen)
380 ename = resp.read(elen)
381 engine = util.compengines.forwiretype(ename)
381 engine = util.compengines.forwiretype(ename)
382 return engine.decompressorreader(resp)
382 return engine.decompressorreader(resp)
383 else:
383 else:
384 raise error.RepoError(_("'%s' uses newer protocol %s") %
384 raise error.RepoError(_("'%s' uses newer protocol %s") %
385 (safeurl, version))
385 (safeurl, version))
386
386
387 if _compressible:
387 if _compressible:
388 return util.compengines['zlib'].decompressorreader(resp)
388 return util.compengines['zlib'].decompressorreader(resp)
389
389
390 return resp
390 return resp
391
391
392 def _call(self, cmd, **args):
392 def _call(self, cmd, **args):
393 fp = self._callstream(cmd, **args)
393 fp = self._callstream(cmd, **args)
394 try:
394 try:
395 return fp.read()
395 return fp.read()
396 finally:
396 finally:
397 # if using keepalive, allow connection to be reused
397 # if using keepalive, allow connection to be reused
398 fp.close()
398 fp.close()
399
399
400 def _callpush(self, cmd, cg, **args):
400 def _callpush(self, cmd, cg, **args):
401 # have to stream bundle to a temp file because we do not have
401 # have to stream bundle to a temp file because we do not have
402 # http 1.1 chunked transfer.
402 # http 1.1 chunked transfer.
403
403
404 types = self.capable('unbundle')
404 types = self.capable('unbundle')
405 try:
405 try:
406 types = types.split(',')
406 types = types.split(',')
407 except AttributeError:
407 except AttributeError:
408 # servers older than d1b16a746db6 will send 'unbundle' as a
408 # servers older than d1b16a746db6 will send 'unbundle' as a
409 # boolean capability. They only support headerless/uncompressed
409 # boolean capability. They only support headerless/uncompressed
410 # bundles.
410 # bundles.
411 types = [""]
411 types = [""]
412 for x in types:
412 for x in types:
413 if x in bundle2.bundletypes:
413 if x in bundle2.bundletypes:
414 type = x
414 type = x
415 break
415 break
416
416
417 tempname = bundle2.writebundle(self.ui, cg, None, type)
417 tempname = bundle2.writebundle(self.ui, cg, None, type)
418 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
418 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
419 headers = {r'Content-Type': r'application/mercurial-0.1'}
419 headers = {r'Content-Type': r'application/mercurial-0.1'}
420
420
421 try:
421 try:
422 r = self._call(cmd, data=fp, headers=headers, **args)
422 r = self._call(cmd, data=fp, headers=headers, **args)
423 vals = r.split('\n', 1)
423 vals = r.split('\n', 1)
424 if len(vals) < 2:
424 if len(vals) < 2:
425 raise error.ResponseError(_("unexpected response:"), r)
425 raise error.ResponseError(_("unexpected response:"), r)
426 return vals
426 return vals
427 except urlerr.httperror:
427 except urlerr.httperror:
428 # Catch and re-raise these so we don't try and treat them
428 # Catch and re-raise these so we don't try and treat them
429 # like generic socket errors. They lack any values in
429 # like generic socket errors. They lack any values in
430 # .args on Python 3 which breaks our socket.error block.
430 # .args on Python 3 which breaks our socket.error block.
431 raise
431 raise
432 except socket.error as err:
432 except socket.error as err:
433 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
433 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
434 raise error.Abort(_('push failed: %s') % err.args[1])
434 raise error.Abort(_('push failed: %s') % err.args[1])
435 raise error.Abort(err.args[1])
435 raise error.Abort(err.args[1])
436 finally:
436 finally:
437 fp.close()
437 fp.close()
438 os.unlink(tempname)
438 os.unlink(tempname)
439
439
440 def _calltwowaystream(self, cmd, fp, **args):
440 def _calltwowaystream(self, cmd, fp, **args):
441 fh = None
441 fh = None
442 fp_ = None
442 fp_ = None
443 filename = None
443 filename = None
444 try:
444 try:
445 # dump bundle to disk
445 # dump bundle to disk
446 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
446 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
447 fh = os.fdopen(fd, r"wb")
447 fh = os.fdopen(fd, r"wb")
448 d = fp.read(4096)
448 d = fp.read(4096)
449 while d:
449 while d:
450 fh.write(d)
450 fh.write(d)
451 d = fp.read(4096)
451 d = fp.read(4096)
452 fh.close()
452 fh.close()
453 # start http push
453 # start http push
454 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
454 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
455 headers = {r'Content-Type': r'application/mercurial-0.1'}
455 headers = {r'Content-Type': r'application/mercurial-0.1'}
456 return self._callstream(cmd, data=fp_, headers=headers, **args)
456 return self._callstream(cmd, data=fp_, headers=headers, **args)
457 finally:
457 finally:
458 if fp_ is not None:
458 if fp_ is not None:
459 fp_.close()
459 fp_.close()
460 if fh is not None:
460 if fh is not None:
461 fh.close()
461 fh.close()
462 os.unlink(filename)
462 os.unlink(filename)
463
463
464 def _callcompressable(self, cmd, **args):
464 def _callcompressable(self, cmd, **args):
465 return self._callstream(cmd, _compressible=True, **args)
465 return self._callstream(cmd, _compressible=True, **args)
466
466
467 def _abort(self, exception):
467 def _abort(self, exception):
468 raise exception
468 raise exception
469
469
470 def makepeer(ui, path):
470 def makepeer(ui, path):
471 u = util.url(path)
471 u = util.url(path)
472 if u.query or u.fragment:
472 if u.query or u.fragment:
473 raise error.Abort(_('unsupported URL component: "%s"') %
473 raise error.Abort(_('unsupported URL component: "%s"') %
474 (u.query or u.fragment))
474 (u.query or u.fragment))
475
475
476 # urllib cannot handle URLs with embedded user or passwd.
476 # urllib cannot handle URLs with embedded user or passwd.
477 url, authinfo = u.authinfo()
477 url, authinfo = u.authinfo()
478 ui.debug('using %s\n' % url)
478 ui.debug('using %s\n' % url)
479
479
480 opener = urlmod.opener(ui, authinfo)
480 opener = urlmod.opener(ui, authinfo)
481
481
482 return httppeer(ui, path, url, opener)
482 return httppeer(ui, path, url, opener)
483
483
484 def instance(ui, path, create):
484 def instance(ui, path, create):
485 if create:
485 if create:
486 raise error.Abort(_('cannot create new http repository'))
486 raise error.Abort(_('cannot create new http repository'))
487 try:
487 try:
488 if path.startswith('https:') and not urlmod.has_https:
488 if path.startswith('https:') and not urlmod.has_https:
489 raise error.Abort(_('Python support for SSL and HTTPS '
489 raise error.Abort(_('Python support for SSL and HTTPS '
490 'is not installed'))
490 'is not installed'))
491
491
492 inst = makepeer(ui, path)
492 inst = makepeer(ui, path)
493 inst._fetchcaps()
493 inst._fetchcaps()
494
494
495 return inst
495 return inst
496 except error.RepoError as httpexception:
496 except error.RepoError as httpexception:
497 try:
497 try:
498 r = statichttprepo.instance(ui, "static-" + path, create)
498 r = statichttprepo.instance(ui, "static-" + path, create)
499 ui.note(_('(falling back to static-http)\n'))
499 ui.note(_('(falling back to static-http)\n'))
500 return r
500 return r
501 except error.RepoError:
501 except error.RepoError:
502 raise httpexception # use the original http RepoError instead
502 raise httpexception # use the original http RepoError instead
@@ -1,634 +1,635 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 import uuid
11 import uuid
12
12
13 from .i18n import _
13 from .i18n import _
14 from . import (
14 from . import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 wireproto,
18 wireproto,
19 wireprotoserver,
19 wireprotoserver,
20 wireprototypes,
20 wireprototypes,
21 )
21 )
22 from .utils import (
22 from .utils import (
23 procutil,
23 procutil,
24 )
24 )
25
25
26 def _serverquote(s):
26 def _serverquote(s):
27 """quote a string for the remote shell ... which we assume is sh"""
27 """quote a string for the remote shell ... which we assume is sh"""
28 if not s:
28 if not s:
29 return s
29 return s
30 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
30 if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
31 return s
31 return s
32 return "'%s'" % s.replace("'", "'\\''")
32 return "'%s'" % s.replace("'", "'\\''")
33
33
34 def _forwardoutput(ui, pipe):
34 def _forwardoutput(ui, pipe):
35 """display all data currently available on pipe as remote output.
35 """display all data currently available on pipe as remote output.
36
36
37 This is non blocking."""
37 This is non blocking."""
38 if pipe:
38 if pipe:
39 s = procutil.readpipe(pipe)
39 s = procutil.readpipe(pipe)
40 if s:
40 if s:
41 for l in s.splitlines():
41 for l in s.splitlines():
42 ui.status(_("remote: "), l, '\n')
42 ui.status(_("remote: "), l, '\n')
43
43
44 class doublepipe(object):
44 class doublepipe(object):
45 """Operate a side-channel pipe in addition of a main one
45 """Operate a side-channel pipe in addition of a main one
46
46
47 The side-channel pipe contains server output to be forwarded to the user
47 The side-channel pipe contains server output to be forwarded to the user
48 input. The double pipe will behave as the "main" pipe, but will ensure the
48 input. The double pipe will behave as the "main" pipe, but will ensure the
49 content of the "side" pipe is properly processed while we wait for blocking
49 content of the "side" pipe is properly processed while we wait for blocking
50 call on the "main" pipe.
50 call on the "main" pipe.
51
51
52 If large amounts of data are read from "main", the forward will cease after
52 If large amounts of data are read from "main", the forward will cease after
53 the first bytes start to appear. This simplifies the implementation
53 the first bytes start to appear. This simplifies the implementation
54 without affecting actual output of sshpeer too much as we rarely issue
54 without affecting actual output of sshpeer too much as we rarely issue
55 large read for data not yet emitted by the server.
55 large read for data not yet emitted by the server.
56
56
57 The main pipe is expected to be a 'bufferedinputpipe' from the util module
57 The main pipe is expected to be a 'bufferedinputpipe' from the util module
58 that handle all the os specific bits. This class lives in this module
58 that handle all the os specific bits. This class lives in this module
59 because it focus on behavior specific to the ssh protocol."""
59 because it focus on behavior specific to the ssh protocol."""
60
60
61 def __init__(self, ui, main, side):
61 def __init__(self, ui, main, side):
62 self._ui = ui
62 self._ui = ui
63 self._main = main
63 self._main = main
64 self._side = side
64 self._side = side
65
65
66 def _wait(self):
66 def _wait(self):
67 """wait until some data are available on main or side
67 """wait until some data are available on main or side
68
68
69 return a pair of boolean (ismainready, issideready)
69 return a pair of boolean (ismainready, issideready)
70
70
71 (This will only wait for data if the setup is supported by `util.poll`)
71 (This will only wait for data if the setup is supported by `util.poll`)
72 """
72 """
73 if (isinstance(self._main, util.bufferedinputpipe) and
73 if (isinstance(self._main, util.bufferedinputpipe) and
74 self._main.hasbuffer):
74 self._main.hasbuffer):
75 # Main has data. Assume side is worth poking at.
75 # Main has data. Assume side is worth poking at.
76 return True, True
76 return True, True
77
77
78 fds = [self._main.fileno(), self._side.fileno()]
78 fds = [self._main.fileno(), self._side.fileno()]
79 try:
79 try:
80 act = util.poll(fds)
80 act = util.poll(fds)
81 except NotImplementedError:
81 except NotImplementedError:
82 # non supported yet case, assume all have data.
82 # non supported yet case, assume all have data.
83 act = fds
83 act = fds
84 return (self._main.fileno() in act, self._side.fileno() in act)
84 return (self._main.fileno() in act, self._side.fileno() in act)
85
85
86 def write(self, data):
86 def write(self, data):
87 return self._call('write', data)
87 return self._call('write', data)
88
88
89 def read(self, size):
89 def read(self, size):
90 r = self._call('read', size)
90 r = self._call('read', size)
91 if size != 0 and not r:
91 if size != 0 and not r:
92 # We've observed a condition that indicates the
92 # We've observed a condition that indicates the
93 # stdout closed unexpectedly. Check stderr one
93 # stdout closed unexpectedly. Check stderr one
94 # more time and snag anything that's there before
94 # more time and snag anything that's there before
95 # letting anyone know the main part of the pipe
95 # letting anyone know the main part of the pipe
96 # closed prematurely.
96 # closed prematurely.
97 _forwardoutput(self._ui, self._side)
97 _forwardoutput(self._ui, self._side)
98 return r
98 return r
99
99
100 def readline(self):
100 def readline(self):
101 return self._call('readline')
101 return self._call('readline')
102
102
103 def _call(self, methname, data=None):
103 def _call(self, methname, data=None):
104 """call <methname> on "main", forward output of "side" while blocking
104 """call <methname> on "main", forward output of "side" while blocking
105 """
105 """
106 # data can be '' or 0
106 # data can be '' or 0
107 if (data is not None and not data) or self._main.closed:
107 if (data is not None and not data) or self._main.closed:
108 _forwardoutput(self._ui, self._side)
108 _forwardoutput(self._ui, self._side)
109 return ''
109 return ''
110 while True:
110 while True:
111 mainready, sideready = self._wait()
111 mainready, sideready = self._wait()
112 if sideready:
112 if sideready:
113 _forwardoutput(self._ui, self._side)
113 _forwardoutput(self._ui, self._side)
114 if mainready:
114 if mainready:
115 meth = getattr(self._main, methname)
115 meth = getattr(self._main, methname)
116 if data is None:
116 if data is None:
117 return meth()
117 return meth()
118 else:
118 else:
119 return meth(data)
119 return meth(data)
120
120
121 def close(self):
121 def close(self):
122 return self._main.close()
122 return self._main.close()
123
123
124 def flush(self):
124 def flush(self):
125 return self._main.flush()
125 return self._main.flush()
126
126
127 def _cleanuppipes(ui, pipei, pipeo, pipee):
127 def _cleanuppipes(ui, pipei, pipeo, pipee):
128 """Clean up pipes used by an SSH connection."""
128 """Clean up pipes used by an SSH connection."""
129 if pipeo:
129 if pipeo:
130 pipeo.close()
130 pipeo.close()
131 if pipei:
131 if pipei:
132 pipei.close()
132 pipei.close()
133
133
134 if pipee:
134 if pipee:
135 # Try to read from the err descriptor until EOF.
135 # Try to read from the err descriptor until EOF.
136 try:
136 try:
137 for l in pipee:
137 for l in pipee:
138 ui.status(_('remote: '), l)
138 ui.status(_('remote: '), l)
139 except (IOError, ValueError):
139 except (IOError, ValueError):
140 pass
140 pass
141
141
142 pipee.close()
142 pipee.close()
143
143
144 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
144 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
145 """Create an SSH connection to a server.
145 """Create an SSH connection to a server.
146
146
147 Returns a tuple of (process, stdin, stdout, stderr) for the
147 Returns a tuple of (process, stdin, stdout, stderr) for the
148 spawned process.
148 spawned process.
149 """
149 """
150 cmd = '%s %s %s' % (
150 cmd = '%s %s %s' % (
151 sshcmd,
151 sshcmd,
152 args,
152 args,
153 procutil.shellquote('%s -R %s serve --stdio' % (
153 procutil.shellquote('%s -R %s serve --stdio' % (
154 _serverquote(remotecmd), _serverquote(path))))
154 _serverquote(remotecmd), _serverquote(path))))
155
155
156 ui.debug('running %s\n' % cmd)
156 ui.debug('running %s\n' % cmd)
157 cmd = procutil.quotecommand(cmd)
157 cmd = procutil.quotecommand(cmd)
158
158
159 # no buffer allow the use of 'select'
159 # no buffer allow the use of 'select'
160 # feel free to remove buffering and select usage when we ultimately
160 # feel free to remove buffering and select usage when we ultimately
161 # move to threading.
161 # move to threading.
162 stdin, stdout, stderr, proc = procutil.popen4(cmd, bufsize=0, env=sshenv)
162 stdin, stdout, stderr, proc = procutil.popen4(cmd, bufsize=0, env=sshenv)
163
163
164 return proc, stdin, stdout, stderr
164 return proc, stdin, stdout, stderr
165
165
166 def _clientcapabilities():
166 def _clientcapabilities():
167 """Return list of capabilities of this client.
167 """Return list of capabilities of this client.
168
168
169 Returns a list of capabilities that are supported by this client.
169 Returns a list of capabilities that are supported by this client.
170 """
170 """
171 protoparams = []
171 protoparams = set()
172 comps = [e.wireprotosupport().name for e in
172 comps = [e.wireprotosupport().name for e in
173 util.compengines.supportedwireengines(util.CLIENTROLE)]
173 util.compengines.supportedwireengines(util.CLIENTROLE)]
174 protoparams.append('comp=%s' % ','.join(comps))
174 protoparams.add('comp=%s' % ','.join(comps))
175 return protoparams
175 return protoparams
176
176
177 def _performhandshake(ui, stdin, stdout, stderr):
177 def _performhandshake(ui, stdin, stdout, stderr):
178 def badresponse():
178 def badresponse():
179 # Flush any output on stderr.
179 # Flush any output on stderr.
180 _forwardoutput(ui, stderr)
180 _forwardoutput(ui, stderr)
181
181
182 msg = _('no suitable response from remote hg')
182 msg = _('no suitable response from remote hg')
183 hint = ui.config('ui', 'ssherrorhint')
183 hint = ui.config('ui', 'ssherrorhint')
184 raise error.RepoError(msg, hint=hint)
184 raise error.RepoError(msg, hint=hint)
185
185
186 # The handshake consists of sending wire protocol commands in reverse
186 # The handshake consists of sending wire protocol commands in reverse
187 # order of protocol implementation and then sniffing for a response
187 # order of protocol implementation and then sniffing for a response
188 # to one of them.
188 # to one of them.
189 #
189 #
190 # Those commands (from oldest to newest) are:
190 # Those commands (from oldest to newest) are:
191 #
191 #
192 # ``between``
192 # ``between``
193 # Asks for the set of revisions between a pair of revisions. Command
193 # Asks for the set of revisions between a pair of revisions. Command
194 # present in all Mercurial server implementations.
194 # present in all Mercurial server implementations.
195 #
195 #
196 # ``hello``
196 # ``hello``
197 # Instructs the server to advertise its capabilities. Introduced in
197 # Instructs the server to advertise its capabilities. Introduced in
198 # Mercurial 0.9.1.
198 # Mercurial 0.9.1.
199 #
199 #
200 # ``upgrade``
200 # ``upgrade``
201 # Requests upgrade from default transport protocol version 1 to
201 # Requests upgrade from default transport protocol version 1 to
202 # a newer version. Introduced in Mercurial 4.6 as an experimental
202 # a newer version. Introduced in Mercurial 4.6 as an experimental
203 # feature.
203 # feature.
204 #
204 #
205 # The ``between`` command is issued with a request for the null
205 # The ``between`` command is issued with a request for the null
206 # range. If the remote is a Mercurial server, this request will
206 # range. If the remote is a Mercurial server, this request will
207 # generate a specific response: ``1\n\n``. This represents the
207 # generate a specific response: ``1\n\n``. This represents the
208 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
208 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
209 # in the output stream and know this is the response to ``between``
209 # in the output stream and know this is the response to ``between``
210 # and we're at the end of our handshake reply.
210 # and we're at the end of our handshake reply.
211 #
211 #
212 # The response to the ``hello`` command will be a line with the
212 # The response to the ``hello`` command will be a line with the
213 # length of the value returned by that command followed by that
213 # length of the value returned by that command followed by that
214 # value. If the server doesn't support ``hello`` (which should be
214 # value. If the server doesn't support ``hello`` (which should be
215 # rare), that line will be ``0\n``. Otherwise, the value will contain
215 # rare), that line will be ``0\n``. Otherwise, the value will contain
216 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
216 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
217 # the capabilities of the server.
217 # the capabilities of the server.
218 #
218 #
219 # The ``upgrade`` command isn't really a command in the traditional
219 # The ``upgrade`` command isn't really a command in the traditional
220 # sense of version 1 of the transport because it isn't using the
220 # sense of version 1 of the transport because it isn't using the
221 # proper mechanism for formatting insteads: instead, it just encodes
221 # proper mechanism for formatting insteads: instead, it just encodes
222 # arguments on the line, delimited by spaces.
222 # arguments on the line, delimited by spaces.
223 #
223 #
224 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
224 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
225 # If the server doesn't support protocol upgrades, it will reply to
225 # If the server doesn't support protocol upgrades, it will reply to
226 # this line with ``0\n``. Otherwise, it emits an
226 # this line with ``0\n``. Otherwise, it emits an
227 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
227 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
228 # Content immediately following this line describes additional
228 # Content immediately following this line describes additional
229 # protocol and server state.
229 # protocol and server state.
230 #
230 #
231 # In addition to the responses to our command requests, the server
231 # In addition to the responses to our command requests, the server
232 # may emit "banner" output on stdout. SSH servers are allowed to
232 # may emit "banner" output on stdout. SSH servers are allowed to
233 # print messages to stdout on login. Issuing commands on connection
233 # print messages to stdout on login. Issuing commands on connection
234 # allows us to flush this banner output from the server by scanning
234 # allows us to flush this banner output from the server by scanning
235 # for output to our well-known ``between`` command. Of course, if
235 # for output to our well-known ``between`` command. Of course, if
236 # the banner contains ``1\n\n``, this will throw off our detection.
236 # the banner contains ``1\n\n``, this will throw off our detection.
237
237
238 requestlog = ui.configbool('devel', 'debug.peer-request')
238 requestlog = ui.configbool('devel', 'debug.peer-request')
239
239
240 # Generate a random token to help identify responses to version 2
240 # Generate a random token to help identify responses to version 2
241 # upgrade request.
241 # upgrade request.
242 token = pycompat.sysbytes(str(uuid.uuid4()))
242 token = pycompat.sysbytes(str(uuid.uuid4()))
243 upgradecaps = [
243 upgradecaps = [
244 ('proto', wireprotoserver.SSHV2),
244 ('proto', wireprotoserver.SSHV2),
245 ]
245 ]
246 upgradecaps = util.urlreq.urlencode(upgradecaps)
246 upgradecaps = util.urlreq.urlencode(upgradecaps)
247
247
248 try:
248 try:
249 pairsarg = '%s-%s' % ('0' * 40, '0' * 40)
249 pairsarg = '%s-%s' % ('0' * 40, '0' * 40)
250 handshake = [
250 handshake = [
251 'hello\n',
251 'hello\n',
252 'between\n',
252 'between\n',
253 'pairs %d\n' % len(pairsarg),
253 'pairs %d\n' % len(pairsarg),
254 pairsarg,
254 pairsarg,
255 ]
255 ]
256
256
257 # Request upgrade to version 2 if configured.
257 # Request upgrade to version 2 if configured.
258 if ui.configbool('experimental', 'sshpeer.advertise-v2'):
258 if ui.configbool('experimental', 'sshpeer.advertise-v2'):
259 ui.debug('sending upgrade request: %s %s\n' % (token, upgradecaps))
259 ui.debug('sending upgrade request: %s %s\n' % (token, upgradecaps))
260 handshake.insert(0, 'upgrade %s %s\n' % (token, upgradecaps))
260 handshake.insert(0, 'upgrade %s %s\n' % (token, upgradecaps))
261
261
262 if requestlog:
262 if requestlog:
263 ui.debug('devel-peer-request: hello\n')
263 ui.debug('devel-peer-request: hello\n')
264 ui.debug('sending hello command\n')
264 ui.debug('sending hello command\n')
265 if requestlog:
265 if requestlog:
266 ui.debug('devel-peer-request: between\n')
266 ui.debug('devel-peer-request: between\n')
267 ui.debug('devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
267 ui.debug('devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
268 ui.debug('sending between command\n')
268 ui.debug('sending between command\n')
269
269
270 stdin.write(''.join(handshake))
270 stdin.write(''.join(handshake))
271 stdin.flush()
271 stdin.flush()
272 except IOError:
272 except IOError:
273 badresponse()
273 badresponse()
274
274
275 # Assume version 1 of wire protocol by default.
275 # Assume version 1 of wire protocol by default.
276 protoname = wireprototypes.SSHV1
276 protoname = wireprototypes.SSHV1
277 reupgraded = re.compile(b'^upgraded %s (.*)$' % re.escape(token))
277 reupgraded = re.compile(b'^upgraded %s (.*)$' % re.escape(token))
278
278
279 lines = ['', 'dummy']
279 lines = ['', 'dummy']
280 max_noise = 500
280 max_noise = 500
281 while lines[-1] and max_noise:
281 while lines[-1] and max_noise:
282 try:
282 try:
283 l = stdout.readline()
283 l = stdout.readline()
284 _forwardoutput(ui, stderr)
284 _forwardoutput(ui, stderr)
285
285
286 # Look for reply to protocol upgrade request. It has a token
286 # Look for reply to protocol upgrade request. It has a token
287 # in it, so there should be no false positives.
287 # in it, so there should be no false positives.
288 m = reupgraded.match(l)
288 m = reupgraded.match(l)
289 if m:
289 if m:
290 protoname = m.group(1)
290 protoname = m.group(1)
291 ui.debug('protocol upgraded to %s\n' % protoname)
291 ui.debug('protocol upgraded to %s\n' % protoname)
292 # If an upgrade was handled, the ``hello`` and ``between``
292 # If an upgrade was handled, the ``hello`` and ``between``
293 # requests are ignored. The next output belongs to the
293 # requests are ignored. The next output belongs to the
294 # protocol, so stop scanning lines.
294 # protocol, so stop scanning lines.
295 break
295 break
296
296
297 # Otherwise it could be a banner, ``0\n`` response if server
297 # Otherwise it could be a banner, ``0\n`` response if server
298 # doesn't support upgrade.
298 # doesn't support upgrade.
299
299
300 if lines[-1] == '1\n' and l == '\n':
300 if lines[-1] == '1\n' and l == '\n':
301 break
301 break
302 if l:
302 if l:
303 ui.debug('remote: ', l)
303 ui.debug('remote: ', l)
304 lines.append(l)
304 lines.append(l)
305 max_noise -= 1
305 max_noise -= 1
306 except IOError:
306 except IOError:
307 badresponse()
307 badresponse()
308 else:
308 else:
309 badresponse()
309 badresponse()
310
310
311 caps = set()
311 caps = set()
312
312
313 # For version 1, we should see a ``capabilities`` line in response to the
313 # For version 1, we should see a ``capabilities`` line in response to the
314 # ``hello`` command.
314 # ``hello`` command.
315 if protoname == wireprototypes.SSHV1:
315 if protoname == wireprototypes.SSHV1:
316 for l in reversed(lines):
316 for l in reversed(lines):
317 # Look for response to ``hello`` command. Scan from the back so
317 # Look for response to ``hello`` command. Scan from the back so
318 # we don't misinterpret banner output as the command reply.
318 # we don't misinterpret banner output as the command reply.
319 if l.startswith('capabilities:'):
319 if l.startswith('capabilities:'):
320 caps.update(l[:-1].split(':')[1].split())
320 caps.update(l[:-1].split(':')[1].split())
321 break
321 break
322 elif protoname == wireprotoserver.SSHV2:
322 elif protoname == wireprotoserver.SSHV2:
323 # We see a line with number of bytes to follow and then a value
323 # We see a line with number of bytes to follow and then a value
324 # looking like ``capabilities: *``.
324 # looking like ``capabilities: *``.
325 line = stdout.readline()
325 line = stdout.readline()
326 try:
326 try:
327 valuelen = int(line)
327 valuelen = int(line)
328 except ValueError:
328 except ValueError:
329 badresponse()
329 badresponse()
330
330
331 capsline = stdout.read(valuelen)
331 capsline = stdout.read(valuelen)
332 if not capsline.startswith('capabilities: '):
332 if not capsline.startswith('capabilities: '):
333 badresponse()
333 badresponse()
334
334
335 ui.debug('remote: %s\n' % capsline)
335 ui.debug('remote: %s\n' % capsline)
336
336
337 caps.update(capsline.split(':')[1].split())
337 caps.update(capsline.split(':')[1].split())
338 # Trailing newline.
338 # Trailing newline.
339 stdout.read(1)
339 stdout.read(1)
340
340
341 # Error if we couldn't find capabilities, this means:
341 # Error if we couldn't find capabilities, this means:
342 #
342 #
343 # 1. Remote isn't a Mercurial server
343 # 1. Remote isn't a Mercurial server
344 # 2. Remote is a <0.9.1 Mercurial server
344 # 2. Remote is a <0.9.1 Mercurial server
345 # 3. Remote is a future Mercurial server that dropped ``hello``
345 # 3. Remote is a future Mercurial server that dropped ``hello``
346 # and other attempted handshake mechanisms.
346 # and other attempted handshake mechanisms.
347 if not caps:
347 if not caps:
348 badresponse()
348 badresponse()
349
349
350 # Flush any output on stderr before proceeding.
350 # Flush any output on stderr before proceeding.
351 _forwardoutput(ui, stderr)
351 _forwardoutput(ui, stderr)
352
352
353 return protoname, caps
353 return protoname, caps
354
354
355 class sshv1peer(wireproto.wirepeer):
355 class sshv1peer(wireproto.wirepeer):
356 def __init__(self, ui, url, proc, stdin, stdout, stderr, caps,
356 def __init__(self, ui, url, proc, stdin, stdout, stderr, caps,
357 autoreadstderr=True):
357 autoreadstderr=True):
358 """Create a peer from an existing SSH connection.
358 """Create a peer from an existing SSH connection.
359
359
360 ``proc`` is a handle on the underlying SSH process.
360 ``proc`` is a handle on the underlying SSH process.
361 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
361 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
362 pipes for that process.
362 pipes for that process.
363 ``caps`` is a set of capabilities supported by the remote.
363 ``caps`` is a set of capabilities supported by the remote.
364 ``autoreadstderr`` denotes whether to automatically read from
364 ``autoreadstderr`` denotes whether to automatically read from
365 stderr and to forward its output.
365 stderr and to forward its output.
366 """
366 """
367 self._url = url
367 self._url = url
368 self.ui = ui
368 self.ui = ui
369 # self._subprocess is unused. Keeping a handle on the process
369 # self._subprocess is unused. Keeping a handle on the process
370 # holds a reference and prevents it from being garbage collected.
370 # holds a reference and prevents it from being garbage collected.
371 self._subprocess = proc
371 self._subprocess = proc
372
372
373 # And we hook up our "doublepipe" wrapper to allow querying
373 # And we hook up our "doublepipe" wrapper to allow querying
374 # stderr any time we perform I/O.
374 # stderr any time we perform I/O.
375 if autoreadstderr:
375 if autoreadstderr:
376 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
376 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
377 stdin = doublepipe(ui, stdin, stderr)
377 stdin = doublepipe(ui, stdin, stderr)
378
378
379 self._pipeo = stdin
379 self._pipeo = stdin
380 self._pipei = stdout
380 self._pipei = stdout
381 self._pipee = stderr
381 self._pipee = stderr
382 self._caps = caps
382 self._caps = caps
383 self._autoreadstderr = autoreadstderr
383 self._autoreadstderr = autoreadstderr
384
384
385 # Commands that have a "framed" response where the first line of the
385 # Commands that have a "framed" response where the first line of the
386 # response contains the length of that response.
386 # response contains the length of that response.
387 _FRAMED_COMMANDS = {
387 _FRAMED_COMMANDS = {
388 'batch',
388 'batch',
389 }
389 }
390
390
391 # Begin of ipeerconnection interface.
391 # Begin of ipeerconnection interface.
392
392
393 def url(self):
393 def url(self):
394 return self._url
394 return self._url
395
395
396 def local(self):
396 def local(self):
397 return None
397 return None
398
398
399 def peer(self):
399 def peer(self):
400 return self
400 return self
401
401
402 def canpush(self):
402 def canpush(self):
403 return True
403 return True
404
404
405 def close(self):
405 def close(self):
406 pass
406 pass
407
407
408 # End of ipeerconnection interface.
408 # End of ipeerconnection interface.
409
409
410 # Begin of ipeercommands interface.
410 # Begin of ipeercommands interface.
411
411
412 def capabilities(self):
412 def capabilities(self):
413 return self._caps
413 return self._caps
414
414
415 # End of ipeercommands interface.
415 # End of ipeercommands interface.
416
416
417 def _readerr(self):
417 def _readerr(self):
418 _forwardoutput(self.ui, self._pipee)
418 _forwardoutput(self.ui, self._pipee)
419
419
420 def _abort(self, exception):
420 def _abort(self, exception):
421 self._cleanup()
421 self._cleanup()
422 raise exception
422 raise exception
423
423
424 def _cleanup(self):
424 def _cleanup(self):
425 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
425 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
426
426
427 __del__ = _cleanup
427 __del__ = _cleanup
428
428
429 def _sendrequest(self, cmd, args, framed=False):
429 def _sendrequest(self, cmd, args, framed=False):
430 if (self.ui.debugflag
430 if (self.ui.debugflag
431 and self.ui.configbool('devel', 'debug.peer-request')):
431 and self.ui.configbool('devel', 'debug.peer-request')):
432 dbg = self.ui.debug
432 dbg = self.ui.debug
433 line = 'devel-peer-request: %s\n'
433 line = 'devel-peer-request: %s\n'
434 dbg(line % cmd)
434 dbg(line % cmd)
435 for key, value in sorted(args.items()):
435 for key, value in sorted(args.items()):
436 if not isinstance(value, dict):
436 if not isinstance(value, dict):
437 dbg(line % ' %s: %d bytes' % (key, len(value)))
437 dbg(line % ' %s: %d bytes' % (key, len(value)))
438 else:
438 else:
439 for dk, dv in sorted(value.items()):
439 for dk, dv in sorted(value.items()):
440 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
440 dbg(line % ' %s-%s: %d' % (key, dk, len(dv)))
441 self.ui.debug("sending %s command\n" % cmd)
441 self.ui.debug("sending %s command\n" % cmd)
442 self._pipeo.write("%s\n" % cmd)
442 self._pipeo.write("%s\n" % cmd)
443 _func, names = wireproto.commands[cmd]
443 _func, names = wireproto.commands[cmd]
444 keys = names.split()
444 keys = names.split()
445 wireargs = {}
445 wireargs = {}
446 for k in keys:
446 for k in keys:
447 if k == '*':
447 if k == '*':
448 wireargs['*'] = args
448 wireargs['*'] = args
449 break
449 break
450 else:
450 else:
451 wireargs[k] = args[k]
451 wireargs[k] = args[k]
452 del args[k]
452 del args[k]
453 for k, v in sorted(wireargs.iteritems()):
453 for k, v in sorted(wireargs.iteritems()):
454 self._pipeo.write("%s %d\n" % (k, len(v)))
454 self._pipeo.write("%s %d\n" % (k, len(v)))
455 if isinstance(v, dict):
455 if isinstance(v, dict):
456 for dk, dv in v.iteritems():
456 for dk, dv in v.iteritems():
457 self._pipeo.write("%s %d\n" % (dk, len(dv)))
457 self._pipeo.write("%s %d\n" % (dk, len(dv)))
458 self._pipeo.write(dv)
458 self._pipeo.write(dv)
459 else:
459 else:
460 self._pipeo.write(v)
460 self._pipeo.write(v)
461 self._pipeo.flush()
461 self._pipeo.flush()
462
462
463 # We know exactly how many bytes are in the response. So return a proxy
463 # We know exactly how many bytes are in the response. So return a proxy
464 # around the raw output stream that allows reading exactly this many
464 # around the raw output stream that allows reading exactly this many
465 # bytes. Callers then can read() without fear of overrunning the
465 # bytes. Callers then can read() without fear of overrunning the
466 # response.
466 # response.
467 if framed:
467 if framed:
468 amount = self._getamount()
468 amount = self._getamount()
469 return util.cappedreader(self._pipei, amount)
469 return util.cappedreader(self._pipei, amount)
470
470
471 return self._pipei
471 return self._pipei
472
472
473 def _callstream(self, cmd, **args):
473 def _callstream(self, cmd, **args):
474 args = pycompat.byteskwargs(args)
474 args = pycompat.byteskwargs(args)
475 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
475 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
476
476
477 def _callcompressable(self, cmd, **args):
477 def _callcompressable(self, cmd, **args):
478 args = pycompat.byteskwargs(args)
478 args = pycompat.byteskwargs(args)
479 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
479 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
480
480
481 def _call(self, cmd, **args):
481 def _call(self, cmd, **args):
482 args = pycompat.byteskwargs(args)
482 args = pycompat.byteskwargs(args)
483 return self._sendrequest(cmd, args, framed=True).read()
483 return self._sendrequest(cmd, args, framed=True).read()
484
484
485 def _callpush(self, cmd, fp, **args):
485 def _callpush(self, cmd, fp, **args):
486 # The server responds with an empty frame if the client should
486 # The server responds with an empty frame if the client should
487 # continue submitting the payload.
487 # continue submitting the payload.
488 r = self._call(cmd, **args)
488 r = self._call(cmd, **args)
489 if r:
489 if r:
490 return '', r
490 return '', r
491
491
492 # The payload consists of frames with content followed by an empty
492 # The payload consists of frames with content followed by an empty
493 # frame.
493 # frame.
494 for d in iter(lambda: fp.read(4096), ''):
494 for d in iter(lambda: fp.read(4096), ''):
495 self._writeframed(d)
495 self._writeframed(d)
496 self._writeframed("", flush=True)
496 self._writeframed("", flush=True)
497
497
498 # In case of success, there is an empty frame and a frame containing
498 # In case of success, there is an empty frame and a frame containing
499 # the integer result (as a string).
499 # the integer result (as a string).
500 # In case of error, there is a non-empty frame containing the error.
500 # In case of error, there is a non-empty frame containing the error.
501 r = self._readframed()
501 r = self._readframed()
502 if r:
502 if r:
503 return '', r
503 return '', r
504 return self._readframed(), ''
504 return self._readframed(), ''
505
505
506 def _calltwowaystream(self, cmd, fp, **args):
506 def _calltwowaystream(self, cmd, fp, **args):
507 # The server responds with an empty frame if the client should
507 # The server responds with an empty frame if the client should
508 # continue submitting the payload.
508 # continue submitting the payload.
509 r = self._call(cmd, **args)
509 r = self._call(cmd, **args)
510 if r:
510 if r:
511 # XXX needs to be made better
511 # XXX needs to be made better
512 raise error.Abort(_('unexpected remote reply: %s') % r)
512 raise error.Abort(_('unexpected remote reply: %s') % r)
513
513
514 # The payload consists of frames with content followed by an empty
514 # The payload consists of frames with content followed by an empty
515 # frame.
515 # frame.
516 for d in iter(lambda: fp.read(4096), ''):
516 for d in iter(lambda: fp.read(4096), ''):
517 self._writeframed(d)
517 self._writeframed(d)
518 self._writeframed("", flush=True)
518 self._writeframed("", flush=True)
519
519
520 return self._pipei
520 return self._pipei
521
521
522 def _getamount(self):
522 def _getamount(self):
523 l = self._pipei.readline()
523 l = self._pipei.readline()
524 if l == '\n':
524 if l == '\n':
525 if self._autoreadstderr:
525 if self._autoreadstderr:
526 self._readerr()
526 self._readerr()
527 msg = _('check previous remote output')
527 msg = _('check previous remote output')
528 self._abort(error.OutOfBandError(hint=msg))
528 self._abort(error.OutOfBandError(hint=msg))
529 if self._autoreadstderr:
529 if self._autoreadstderr:
530 self._readerr()
530 self._readerr()
531 try:
531 try:
532 return int(l)
532 return int(l)
533 except ValueError:
533 except ValueError:
534 self._abort(error.ResponseError(_("unexpected response:"), l))
534 self._abort(error.ResponseError(_("unexpected response:"), l))
535
535
536 def _readframed(self):
536 def _readframed(self):
537 size = self._getamount()
537 size = self._getamount()
538 if not size:
538 if not size:
539 return b''
539 return b''
540
540
541 return self._pipei.read(size)
541 return self._pipei.read(size)
542
542
543 def _writeframed(self, data, flush=False):
543 def _writeframed(self, data, flush=False):
544 self._pipeo.write("%d\n" % len(data))
544 self._pipeo.write("%d\n" % len(data))
545 if data:
545 if data:
546 self._pipeo.write(data)
546 self._pipeo.write(data)
547 if flush:
547 if flush:
548 self._pipeo.flush()
548 self._pipeo.flush()
549 if self._autoreadstderr:
549 if self._autoreadstderr:
550 self._readerr()
550 self._readerr()
551
551
552 class sshv2peer(sshv1peer):
552 class sshv2peer(sshv1peer):
553 """A peer that speakers version 2 of the transport protocol."""
553 """A peer that speakers version 2 of the transport protocol."""
554 # Currently version 2 is identical to version 1 post handshake.
554 # Currently version 2 is identical to version 1 post handshake.
555 # And handshake is performed before the peer is instantiated. So
555 # And handshake is performed before the peer is instantiated. So
556 # we need no custom code.
556 # we need no custom code.
557
557
558 def makepeer(ui, path, proc, stdin, stdout, stderr, autoreadstderr=True):
558 def makepeer(ui, path, proc, stdin, stdout, stderr, autoreadstderr=True):
559 """Make a peer instance from existing pipes.
559 """Make a peer instance from existing pipes.
560
560
561 ``path`` and ``proc`` are stored on the eventual peer instance and may
561 ``path`` and ``proc`` are stored on the eventual peer instance and may
562 not be used for anything meaningful.
562 not be used for anything meaningful.
563
563
564 ``stdin``, ``stdout``, and ``stderr`` are the pipes connected to the
564 ``stdin``, ``stdout``, and ``stderr`` are the pipes connected to the
565 SSH server's stdio handles.
565 SSH server's stdio handles.
566
566
567 This function is factored out to allow creating peers that don't
567 This function is factored out to allow creating peers that don't
568 actually spawn a new process. It is useful for starting SSH protocol
568 actually spawn a new process. It is useful for starting SSH protocol
569 servers and clients via non-standard means, which can be useful for
569 servers and clients via non-standard means, which can be useful for
570 testing.
570 testing.
571 """
571 """
572 try:
572 try:
573 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
573 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
574 except Exception:
574 except Exception:
575 _cleanuppipes(ui, stdout, stdin, stderr)
575 _cleanuppipes(ui, stdout, stdin, stderr)
576 raise
576 raise
577
577
578 if protoname == wireprototypes.SSHV1:
578 if protoname == wireprototypes.SSHV1:
579 return sshv1peer(ui, path, proc, stdin, stdout, stderr, caps,
579 return sshv1peer(ui, path, proc, stdin, stdout, stderr, caps,
580 autoreadstderr=autoreadstderr)
580 autoreadstderr=autoreadstderr)
581 elif protoname == wireprototypes.SSHV2:
581 elif protoname == wireprototypes.SSHV2:
582 return sshv2peer(ui, path, proc, stdin, stdout, stderr, caps,
582 return sshv2peer(ui, path, proc, stdin, stdout, stderr, caps,
583 autoreadstderr=autoreadstderr)
583 autoreadstderr=autoreadstderr)
584 else:
584 else:
585 _cleanuppipes(ui, stdout, stdin, stderr)
585 _cleanuppipes(ui, stdout, stdin, stderr)
586 raise error.RepoError(_('unknown version of SSH protocol: %s') %
586 raise error.RepoError(_('unknown version of SSH protocol: %s') %
587 protoname)
587 protoname)
588
588
589 def instance(ui, path, create):
589 def instance(ui, path, create):
590 """Create an SSH peer.
590 """Create an SSH peer.
591
591
592 The returned object conforms to the ``wireproto.wirepeer`` interface.
592 The returned object conforms to the ``wireproto.wirepeer`` interface.
593 """
593 """
594 u = util.url(path, parsequery=False, parsefragment=False)
594 u = util.url(path, parsequery=False, parsefragment=False)
595 if u.scheme != 'ssh' or not u.host or u.path is None:
595 if u.scheme != 'ssh' or not u.host or u.path is None:
596 raise error.RepoError(_("couldn't parse location %s") % path)
596 raise error.RepoError(_("couldn't parse location %s") % path)
597
597
598 util.checksafessh(path)
598 util.checksafessh(path)
599
599
600 if u.passwd is not None:
600 if u.passwd is not None:
601 raise error.RepoError(_('password in URL not supported'))
601 raise error.RepoError(_('password in URL not supported'))
602
602
603 sshcmd = ui.config('ui', 'ssh')
603 sshcmd = ui.config('ui', 'ssh')
604 remotecmd = ui.config('ui', 'remotecmd')
604 remotecmd = ui.config('ui', 'remotecmd')
605 sshaddenv = dict(ui.configitems('sshenv'))
605 sshaddenv = dict(ui.configitems('sshenv'))
606 sshenv = procutil.shellenviron(sshaddenv)
606 sshenv = procutil.shellenviron(sshaddenv)
607 remotepath = u.path or '.'
607 remotepath = u.path or '.'
608
608
609 args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
609 args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
610
610
611 if create:
611 if create:
612 cmd = '%s %s %s' % (sshcmd, args,
612 cmd = '%s %s %s' % (sshcmd, args,
613 procutil.shellquote('%s init %s' %
613 procutil.shellquote('%s init %s' %
614 (_serverquote(remotecmd), _serverquote(remotepath))))
614 (_serverquote(remotecmd), _serverquote(remotepath))))
615 ui.debug('running %s\n' % cmd)
615 ui.debug('running %s\n' % cmd)
616 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
616 res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
617 if res != 0:
617 if res != 0:
618 raise error.RepoError(_('could not create remote repo'))
618 raise error.RepoError(_('could not create remote repo'))
619
619
620 proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
620 proc, stdin, stdout, stderr = _makeconnection(ui, sshcmd, args, remotecmd,
621 remotepath, sshenv)
621 remotepath, sshenv)
622
622
623 peer = makepeer(ui, path, proc, stdin, stdout, stderr)
623 peer = makepeer(ui, path, proc, stdin, stdout, stderr)
624
624
625 # Finally, if supported by the server, notify it about our own
625 # Finally, if supported by the server, notify it about our own
626 # capabilities.
626 # capabilities.
627 if 'protocaps' in peer.capabilities():
627 if 'protocaps' in peer.capabilities():
628 try:
628 try:
629 peer._call("protocaps", caps=' '.join(_clientcapabilities()))
629 peer._call("protocaps",
630 caps=' '.join(sorted(_clientcapabilities())))
630 except IOError:
631 except IOError:
631 peer._cleanup()
632 peer._cleanup()
632 raise error.RepoError(_('capability exchange failed'))
633 raise error.RepoError(_('capability exchange failed'))
633
634
634 return peer
635 return peer
General Comments 0
You need to be logged in to leave comments. Login now