##// END OF EJS Templates
httppeer: use our CBOR decoder...
Gregory Szorc -
r39476:cdb56f29 default
parent child Browse files
Show More
@@ -1,1003 +1,1001 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 weakref
16 import weakref
17
17
18 from .i18n import _
18 from .i18n import _
19 from .thirdparty import (
20 cbor,
21 )
22 from . import (
19 from . import (
23 bundle2,
20 bundle2,
24 error,
21 error,
25 httpconnection,
22 httpconnection,
26 pycompat,
23 pycompat,
27 repository,
24 repository,
28 statichttprepo,
25 statichttprepo,
29 url as urlmod,
26 url as urlmod,
30 util,
27 util,
31 wireprotoframing,
28 wireprotoframing,
32 wireprototypes,
29 wireprototypes,
33 wireprotov1peer,
30 wireprotov1peer,
34 wireprotov2peer,
31 wireprotov2peer,
35 wireprotov2server,
32 wireprotov2server,
36 )
33 )
37 from .utils import (
34 from .utils import (
35 cborutil,
38 interfaceutil,
36 interfaceutil,
39 stringutil,
37 stringutil,
40 )
38 )
41
39
42 httplib = util.httplib
40 httplib = util.httplib
43 urlerr = util.urlerr
41 urlerr = util.urlerr
44 urlreq = util.urlreq
42 urlreq = util.urlreq
45
43
46 def encodevalueinheaders(value, header, limit):
44 def encodevalueinheaders(value, header, limit):
47 """Encode a string value into multiple HTTP headers.
45 """Encode a string value into multiple HTTP headers.
48
46
49 ``value`` will be encoded into 1 or more HTTP headers with the names
47 ``value`` will be encoded into 1 or more HTTP headers with the names
50 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
48 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
51 name + value will be at most ``limit`` bytes long.
49 name + value will be at most ``limit`` bytes long.
52
50
53 Returns an iterable of 2-tuples consisting of header names and
51 Returns an iterable of 2-tuples consisting of header names and
54 values as native strings.
52 values as native strings.
55 """
53 """
56 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
54 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
57 # not bytes. This function always takes bytes in as arguments.
55 # not bytes. This function always takes bytes in as arguments.
58 fmt = pycompat.strurl(header) + r'-%s'
56 fmt = pycompat.strurl(header) + r'-%s'
59 # Note: it is *NOT* a bug that the last bit here is a bytestring
57 # Note: it is *NOT* a bug that the last bit here is a bytestring
60 # and not a unicode: we're just getting the encoded length anyway,
58 # and not a unicode: we're just getting the encoded length anyway,
61 # and using an r-string to make it portable between Python 2 and 3
59 # and using an r-string to make it portable between Python 2 and 3
62 # doesn't work because then the \r is a literal backslash-r
60 # doesn't work because then the \r is a literal backslash-r
63 # instead of a carriage return.
61 # instead of a carriage return.
64 valuelen = limit - len(fmt % r'000') - len(': \r\n')
62 valuelen = limit - len(fmt % r'000') - len(': \r\n')
65 result = []
63 result = []
66
64
67 n = 0
65 n = 0
68 for i in pycompat.xrange(0, len(value), valuelen):
66 for i in pycompat.xrange(0, len(value), valuelen):
69 n += 1
67 n += 1
70 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
68 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
71
69
72 return result
70 return result
73
71
74 def _wraphttpresponse(resp):
72 def _wraphttpresponse(resp):
75 """Wrap an HTTPResponse with common error handlers.
73 """Wrap an HTTPResponse with common error handlers.
76
74
77 This ensures that any I/O from any consumer raises the appropriate
75 This ensures that any I/O from any consumer raises the appropriate
78 error and messaging.
76 error and messaging.
79 """
77 """
80 origread = resp.read
78 origread = resp.read
81
79
82 class readerproxy(resp.__class__):
80 class readerproxy(resp.__class__):
83 def read(self, size=None):
81 def read(self, size=None):
84 try:
82 try:
85 return origread(size)
83 return origread(size)
86 except httplib.IncompleteRead as e:
84 except httplib.IncompleteRead as e:
87 # e.expected is an integer if length known or None otherwise.
85 # e.expected is an integer if length known or None otherwise.
88 if e.expected:
86 if e.expected:
89 msg = _('HTTP request error (incomplete response; '
87 msg = _('HTTP request error (incomplete response; '
90 'expected %d bytes got %d)') % (e.expected,
88 'expected %d bytes got %d)') % (e.expected,
91 len(e.partial))
89 len(e.partial))
92 else:
90 else:
93 msg = _('HTTP request error (incomplete response)')
91 msg = _('HTTP request error (incomplete response)')
94
92
95 raise error.PeerTransportError(
93 raise error.PeerTransportError(
96 msg,
94 msg,
97 hint=_('this may be an intermittent network failure; '
95 hint=_('this may be an intermittent network failure; '
98 'if the error persists, consider contacting the '
96 'if the error persists, consider contacting the '
99 'network or server operator'))
97 'network or server operator'))
100 except httplib.HTTPException as e:
98 except httplib.HTTPException as e:
101 raise error.PeerTransportError(
99 raise error.PeerTransportError(
102 _('HTTP request error (%s)') % e,
100 _('HTTP request error (%s)') % e,
103 hint=_('this may be an intermittent network failure; '
101 hint=_('this may be an intermittent network failure; '
104 'if the error persists, consider contacting the '
102 'if the error persists, consider contacting the '
105 'network or server operator'))
103 'network or server operator'))
106
104
107 resp.__class__ = readerproxy
105 resp.__class__ = readerproxy
108
106
109 class _multifile(object):
107 class _multifile(object):
110 def __init__(self, *fileobjs):
108 def __init__(self, *fileobjs):
111 for f in fileobjs:
109 for f in fileobjs:
112 if not util.safehasattr(f, 'length'):
110 if not util.safehasattr(f, 'length'):
113 raise ValueError(
111 raise ValueError(
114 '_multifile only supports file objects that '
112 '_multifile only supports file objects that '
115 'have a length but this one does not:', type(f), f)
113 'have a length but this one does not:', type(f), f)
116 self._fileobjs = fileobjs
114 self._fileobjs = fileobjs
117 self._index = 0
115 self._index = 0
118
116
119 @property
117 @property
120 def length(self):
118 def length(self):
121 return sum(f.length for f in self._fileobjs)
119 return sum(f.length for f in self._fileobjs)
122
120
123 def read(self, amt=None):
121 def read(self, amt=None):
124 if amt <= 0:
122 if amt <= 0:
125 return ''.join(f.read() for f in self._fileobjs)
123 return ''.join(f.read() for f in self._fileobjs)
126 parts = []
124 parts = []
127 while amt and self._index < len(self._fileobjs):
125 while amt and self._index < len(self._fileobjs):
128 parts.append(self._fileobjs[self._index].read(amt))
126 parts.append(self._fileobjs[self._index].read(amt))
129 got = len(parts[-1])
127 got = len(parts[-1])
130 if got < amt:
128 if got < amt:
131 self._index += 1
129 self._index += 1
132 amt -= got
130 amt -= got
133 return ''.join(parts)
131 return ''.join(parts)
134
132
135 def seek(self, offset, whence=os.SEEK_SET):
133 def seek(self, offset, whence=os.SEEK_SET):
136 if whence != os.SEEK_SET:
134 if whence != os.SEEK_SET:
137 raise NotImplementedError(
135 raise NotImplementedError(
138 '_multifile does not support anything other'
136 '_multifile does not support anything other'
139 ' than os.SEEK_SET for whence on seek()')
137 ' than os.SEEK_SET for whence on seek()')
140 if offset != 0:
138 if offset != 0:
141 raise NotImplementedError(
139 raise NotImplementedError(
142 '_multifile only supports seeking to start, but that '
140 '_multifile only supports seeking to start, but that '
143 'could be fixed if you need it')
141 'could be fixed if you need it')
144 for f in self._fileobjs:
142 for f in self._fileobjs:
145 f.seek(0)
143 f.seek(0)
146 self._index = 0
144 self._index = 0
147
145
148 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
146 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
149 repobaseurl, cmd, args):
147 repobaseurl, cmd, args):
150 """Make an HTTP request to run a command for a version 1 client.
148 """Make an HTTP request to run a command for a version 1 client.
151
149
152 ``caps`` is a set of known server capabilities. The value may be
150 ``caps`` is a set of known server capabilities. The value may be
153 None if capabilities are not yet known.
151 None if capabilities are not yet known.
154
152
155 ``capablefn`` is a function to evaluate a capability.
153 ``capablefn`` is a function to evaluate a capability.
156
154
157 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
155 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
158 raw data to pass to it.
156 raw data to pass to it.
159 """
157 """
160 if cmd == 'pushkey':
158 if cmd == 'pushkey':
161 args['data'] = ''
159 args['data'] = ''
162 data = args.pop('data', None)
160 data = args.pop('data', None)
163 headers = args.pop('headers', {})
161 headers = args.pop('headers', {})
164
162
165 ui.debug("sending %s command\n" % cmd)
163 ui.debug("sending %s command\n" % cmd)
166 q = [('cmd', cmd)]
164 q = [('cmd', cmd)]
167 headersize = 0
165 headersize = 0
168 # Important: don't use self.capable() here or else you end up
166 # Important: don't use self.capable() here or else you end up
169 # with infinite recursion when trying to look up capabilities
167 # with infinite recursion when trying to look up capabilities
170 # for the first time.
168 # for the first time.
171 postargsok = caps is not None and 'httppostargs' in caps
169 postargsok = caps is not None and 'httppostargs' in caps
172
170
173 # Send arguments via POST.
171 # Send arguments via POST.
174 if postargsok and args:
172 if postargsok and args:
175 strargs = urlreq.urlencode(sorted(args.items()))
173 strargs = urlreq.urlencode(sorted(args.items()))
176 if not data:
174 if not data:
177 data = strargs
175 data = strargs
178 else:
176 else:
179 if isinstance(data, bytes):
177 if isinstance(data, bytes):
180 i = io.BytesIO(data)
178 i = io.BytesIO(data)
181 i.length = len(data)
179 i.length = len(data)
182 data = i
180 data = i
183 argsio = io.BytesIO(strargs)
181 argsio = io.BytesIO(strargs)
184 argsio.length = len(strargs)
182 argsio.length = len(strargs)
185 data = _multifile(argsio, data)
183 data = _multifile(argsio, data)
186 headers[r'X-HgArgs-Post'] = len(strargs)
184 headers[r'X-HgArgs-Post'] = len(strargs)
187 elif args:
185 elif args:
188 # Calling self.capable() can infinite loop if we are calling
186 # Calling self.capable() can infinite loop if we are calling
189 # "capabilities". But that command should never accept wire
187 # "capabilities". But that command should never accept wire
190 # protocol arguments. So this should never happen.
188 # protocol arguments. So this should never happen.
191 assert cmd != 'capabilities'
189 assert cmd != 'capabilities'
192 httpheader = capablefn('httpheader')
190 httpheader = capablefn('httpheader')
193 if httpheader:
191 if httpheader:
194 headersize = int(httpheader.split(',', 1)[0])
192 headersize = int(httpheader.split(',', 1)[0])
195
193
196 # Send arguments via HTTP headers.
194 # Send arguments via HTTP headers.
197 if headersize > 0:
195 if headersize > 0:
198 # The headers can typically carry more data than the URL.
196 # The headers can typically carry more data than the URL.
199 encargs = urlreq.urlencode(sorted(args.items()))
197 encargs = urlreq.urlencode(sorted(args.items()))
200 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
198 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
201 headersize):
199 headersize):
202 headers[header] = value
200 headers[header] = value
203 # Send arguments via query string (Mercurial <1.9).
201 # Send arguments via query string (Mercurial <1.9).
204 else:
202 else:
205 q += sorted(args.items())
203 q += sorted(args.items())
206
204
207 qs = '?%s' % urlreq.urlencode(q)
205 qs = '?%s' % urlreq.urlencode(q)
208 cu = "%s%s" % (repobaseurl, qs)
206 cu = "%s%s" % (repobaseurl, qs)
209 size = 0
207 size = 0
210 if util.safehasattr(data, 'length'):
208 if util.safehasattr(data, 'length'):
211 size = data.length
209 size = data.length
212 elif data is not None:
210 elif data is not None:
213 size = len(data)
211 size = len(data)
214 if data is not None and r'Content-Type' not in headers:
212 if data is not None and r'Content-Type' not in headers:
215 headers[r'Content-Type'] = r'application/mercurial-0.1'
213 headers[r'Content-Type'] = r'application/mercurial-0.1'
216
214
217 # Tell the server we accept application/mercurial-0.2 and multiple
215 # Tell the server we accept application/mercurial-0.2 and multiple
218 # compression formats if the server is capable of emitting those
216 # compression formats if the server is capable of emitting those
219 # payloads.
217 # payloads.
220 # Note: Keep this set empty by default, as client advertisement of
218 # Note: Keep this set empty by default, as client advertisement of
221 # protocol parameters should only occur after the handshake.
219 # protocol parameters should only occur after the handshake.
222 protoparams = set()
220 protoparams = set()
223
221
224 mediatypes = set()
222 mediatypes = set()
225 if caps is not None:
223 if caps is not None:
226 mt = capablefn('httpmediatype')
224 mt = capablefn('httpmediatype')
227 if mt:
225 if mt:
228 protoparams.add('0.1')
226 protoparams.add('0.1')
229 mediatypes = set(mt.split(','))
227 mediatypes = set(mt.split(','))
230
228
231 protoparams.add('partial-pull')
229 protoparams.add('partial-pull')
232
230
233 if '0.2tx' in mediatypes:
231 if '0.2tx' in mediatypes:
234 protoparams.add('0.2')
232 protoparams.add('0.2')
235
233
236 if '0.2tx' in mediatypes and capablefn('compression'):
234 if '0.2tx' in mediatypes and capablefn('compression'):
237 # We /could/ compare supported compression formats and prune
235 # We /could/ compare supported compression formats and prune
238 # non-mutually supported or error if nothing is mutually supported.
236 # non-mutually supported or error if nothing is mutually supported.
239 # For now, send the full list to the server and have it error.
237 # For now, send the full list to the server and have it error.
240 comps = [e.wireprotosupport().name for e in
238 comps = [e.wireprotosupport().name for e in
241 util.compengines.supportedwireengines(util.CLIENTROLE)]
239 util.compengines.supportedwireengines(util.CLIENTROLE)]
242 protoparams.add('comp=%s' % ','.join(comps))
240 protoparams.add('comp=%s' % ','.join(comps))
243
241
244 if protoparams:
242 if protoparams:
245 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
243 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
246 'X-HgProto',
244 'X-HgProto',
247 headersize or 1024)
245 headersize or 1024)
248 for header, value in protoheaders:
246 for header, value in protoheaders:
249 headers[header] = value
247 headers[header] = value
250
248
251 varyheaders = []
249 varyheaders = []
252 for header in headers:
250 for header in headers:
253 if header.lower().startswith(r'x-hg'):
251 if header.lower().startswith(r'x-hg'):
254 varyheaders.append(header)
252 varyheaders.append(header)
255
253
256 if varyheaders:
254 if varyheaders:
257 headers[r'Vary'] = r','.join(sorted(varyheaders))
255 headers[r'Vary'] = r','.join(sorted(varyheaders))
258
256
259 req = requestbuilder(pycompat.strurl(cu), data, headers)
257 req = requestbuilder(pycompat.strurl(cu), data, headers)
260
258
261 if data is not None:
259 if data is not None:
262 ui.debug("sending %d bytes\n" % size)
260 ui.debug("sending %d bytes\n" % size)
263 req.add_unredirected_header(r'Content-Length', r'%d' % size)
261 req.add_unredirected_header(r'Content-Length', r'%d' % size)
264
262
265 return req, cu, qs
263 return req, cu, qs
266
264
267 def _reqdata(req):
265 def _reqdata(req):
268 """Get request data, if any. If no data, returns None."""
266 """Get request data, if any. If no data, returns None."""
269 if pycompat.ispy3:
267 if pycompat.ispy3:
270 return req.data
268 return req.data
271 if not req.has_data():
269 if not req.has_data():
272 return None
270 return None
273 return req.get_data()
271 return req.get_data()
274
272
275 def sendrequest(ui, opener, req):
273 def sendrequest(ui, opener, req):
276 """Send a prepared HTTP request.
274 """Send a prepared HTTP request.
277
275
278 Returns the response object.
276 Returns the response object.
279 """
277 """
280 dbg = ui.debug
278 dbg = ui.debug
281 if (ui.debugflag
279 if (ui.debugflag
282 and ui.configbool('devel', 'debug.peer-request')):
280 and ui.configbool('devel', 'debug.peer-request')):
283 line = 'devel-peer-request: %s\n'
281 line = 'devel-peer-request: %s\n'
284 dbg(line % '%s %s' % (pycompat.bytesurl(req.get_method()),
282 dbg(line % '%s %s' % (pycompat.bytesurl(req.get_method()),
285 pycompat.bytesurl(req.get_full_url())))
283 pycompat.bytesurl(req.get_full_url())))
286 hgargssize = None
284 hgargssize = None
287
285
288 for header, value in sorted(req.header_items()):
286 for header, value in sorted(req.header_items()):
289 header = pycompat.bytesurl(header)
287 header = pycompat.bytesurl(header)
290 value = pycompat.bytesurl(value)
288 value = pycompat.bytesurl(value)
291 if header.startswith('X-hgarg-'):
289 if header.startswith('X-hgarg-'):
292 if hgargssize is None:
290 if hgargssize is None:
293 hgargssize = 0
291 hgargssize = 0
294 hgargssize += len(value)
292 hgargssize += len(value)
295 else:
293 else:
296 dbg(line % ' %s %s' % (header, value))
294 dbg(line % ' %s %s' % (header, value))
297
295
298 if hgargssize is not None:
296 if hgargssize is not None:
299 dbg(line % ' %d bytes of commands arguments in headers'
297 dbg(line % ' %d bytes of commands arguments in headers'
300 % hgargssize)
298 % hgargssize)
301 data = _reqdata(req)
299 data = _reqdata(req)
302 if data is not None:
300 if data is not None:
303 length = getattr(data, 'length', None)
301 length = getattr(data, 'length', None)
304 if length is None:
302 if length is None:
305 length = len(data)
303 length = len(data)
306 dbg(line % ' %d bytes of data' % length)
304 dbg(line % ' %d bytes of data' % length)
307
305
308 start = util.timer()
306 start = util.timer()
309
307
310 res = None
308 res = None
311 try:
309 try:
312 res = opener.open(req)
310 res = opener.open(req)
313 except urlerr.httperror as inst:
311 except urlerr.httperror as inst:
314 if inst.code == 401:
312 if inst.code == 401:
315 raise error.Abort(_('authorization failed'))
313 raise error.Abort(_('authorization failed'))
316 raise
314 raise
317 except httplib.HTTPException as inst:
315 except httplib.HTTPException as inst:
318 ui.debug('http error requesting %s\n' %
316 ui.debug('http error requesting %s\n' %
319 util.hidepassword(req.get_full_url()))
317 util.hidepassword(req.get_full_url()))
320 ui.traceback()
318 ui.traceback()
321 raise IOError(None, inst)
319 raise IOError(None, inst)
322 finally:
320 finally:
323 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
321 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
324 code = res.code if res else -1
322 code = res.code if res else -1
325 dbg(line % ' finished in %.4f seconds (%d)'
323 dbg(line % ' finished in %.4f seconds (%d)'
326 % (util.timer() - start, code))
324 % (util.timer() - start, code))
327
325
328 # Insert error handlers for common I/O failures.
326 # Insert error handlers for common I/O failures.
329 _wraphttpresponse(res)
327 _wraphttpresponse(res)
330
328
331 return res
329 return res
332
330
333 class RedirectedRepoError(error.RepoError):
331 class RedirectedRepoError(error.RepoError):
334 def __init__(self, msg, respurl):
332 def __init__(self, msg, respurl):
335 super(RedirectedRepoError, self).__init__(msg)
333 super(RedirectedRepoError, self).__init__(msg)
336 self.respurl = respurl
334 self.respurl = respurl
337
335
338 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
336 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
339 allowcbor=False):
337 allowcbor=False):
340 # record the url we got redirected to
338 # record the url we got redirected to
341 redirected = False
339 redirected = False
342 respurl = pycompat.bytesurl(resp.geturl())
340 respurl = pycompat.bytesurl(resp.geturl())
343 if respurl.endswith(qs):
341 if respurl.endswith(qs):
344 respurl = respurl[:-len(qs)]
342 respurl = respurl[:-len(qs)]
345 qsdropped = False
343 qsdropped = False
346 else:
344 else:
347 qsdropped = True
345 qsdropped = True
348
346
349 if baseurl.rstrip('/') != respurl.rstrip('/'):
347 if baseurl.rstrip('/') != respurl.rstrip('/'):
350 redirected = True
348 redirected = True
351 if not ui.quiet:
349 if not ui.quiet:
352 ui.warn(_('real URL is %s\n') % respurl)
350 ui.warn(_('real URL is %s\n') % respurl)
353
351
354 try:
352 try:
355 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
353 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
356 except AttributeError:
354 except AttributeError:
357 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
355 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
358
356
359 safeurl = util.hidepassword(baseurl)
357 safeurl = util.hidepassword(baseurl)
360 if proto.startswith('application/hg-error'):
358 if proto.startswith('application/hg-error'):
361 raise error.OutOfBandError(resp.read())
359 raise error.OutOfBandError(resp.read())
362
360
363 # Pre 1.0 versions of Mercurial used text/plain and
361 # Pre 1.0 versions of Mercurial used text/plain and
364 # application/hg-changegroup. We don't support such old servers.
362 # application/hg-changegroup. We don't support such old servers.
365 if not proto.startswith('application/mercurial-'):
363 if not proto.startswith('application/mercurial-'):
366 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
364 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
367 msg = _("'%s' does not appear to be an hg repository:\n"
365 msg = _("'%s' does not appear to be an hg repository:\n"
368 "---%%<--- (%s)\n%s\n---%%<---\n") % (
366 "---%%<--- (%s)\n%s\n---%%<---\n") % (
369 safeurl, proto or 'no content-type', resp.read(1024))
367 safeurl, proto or 'no content-type', resp.read(1024))
370
368
371 # Some servers may strip the query string from the redirect. We
369 # Some servers may strip the query string from the redirect. We
372 # raise a special error type so callers can react to this specially.
370 # raise a special error type so callers can react to this specially.
373 if redirected and qsdropped:
371 if redirected and qsdropped:
374 raise RedirectedRepoError(msg, respurl)
372 raise RedirectedRepoError(msg, respurl)
375 else:
373 else:
376 raise error.RepoError(msg)
374 raise error.RepoError(msg)
377
375
378 try:
376 try:
379 subtype = proto.split('-', 1)[1]
377 subtype = proto.split('-', 1)[1]
380
378
381 # Unless we end up supporting CBOR in the legacy wire protocol,
379 # Unless we end up supporting CBOR in the legacy wire protocol,
382 # this should ONLY be encountered for the initial capabilities
380 # this should ONLY be encountered for the initial capabilities
383 # request during handshake.
381 # request during handshake.
384 if subtype == 'cbor':
382 if subtype == 'cbor':
385 if allowcbor:
383 if allowcbor:
386 return respurl, proto, resp
384 return respurl, proto, resp
387 else:
385 else:
388 raise error.RepoError(_('unexpected CBOR response from '
386 raise error.RepoError(_('unexpected CBOR response from '
389 'server'))
387 'server'))
390
388
391 version_info = tuple([int(n) for n in subtype.split('.')])
389 version_info = tuple([int(n) for n in subtype.split('.')])
392 except ValueError:
390 except ValueError:
393 raise error.RepoError(_("'%s' sent a broken Content-Type "
391 raise error.RepoError(_("'%s' sent a broken Content-Type "
394 "header (%s)") % (safeurl, proto))
392 "header (%s)") % (safeurl, proto))
395
393
396 # TODO consider switching to a decompression reader that uses
394 # TODO consider switching to a decompression reader that uses
397 # generators.
395 # generators.
398 if version_info == (0, 1):
396 if version_info == (0, 1):
399 if compressible:
397 if compressible:
400 resp = util.compengines['zlib'].decompressorreader(resp)
398 resp = util.compengines['zlib'].decompressorreader(resp)
401
399
402 elif version_info == (0, 2):
400 elif version_info == (0, 2):
403 # application/mercurial-0.2 always identifies the compression
401 # application/mercurial-0.2 always identifies the compression
404 # engine in the payload header.
402 # engine in the payload header.
405 elen = struct.unpack('B', resp.read(1))[0]
403 elen = struct.unpack('B', resp.read(1))[0]
406 ename = resp.read(elen)
404 ename = resp.read(elen)
407 engine = util.compengines.forwiretype(ename)
405 engine = util.compengines.forwiretype(ename)
408
406
409 resp = engine.decompressorreader(resp)
407 resp = engine.decompressorreader(resp)
410 else:
408 else:
411 raise error.RepoError(_("'%s' uses newer protocol %s") %
409 raise error.RepoError(_("'%s' uses newer protocol %s") %
412 (safeurl, subtype))
410 (safeurl, subtype))
413
411
414 return respurl, proto, resp
412 return respurl, proto, resp
415
413
416 class httppeer(wireprotov1peer.wirepeer):
414 class httppeer(wireprotov1peer.wirepeer):
417 def __init__(self, ui, path, url, opener, requestbuilder, caps):
415 def __init__(self, ui, path, url, opener, requestbuilder, caps):
418 self.ui = ui
416 self.ui = ui
419 self._path = path
417 self._path = path
420 self._url = url
418 self._url = url
421 self._caps = caps
419 self._caps = caps
422 self._urlopener = opener
420 self._urlopener = opener
423 self._requestbuilder = requestbuilder
421 self._requestbuilder = requestbuilder
424
422
425 def __del__(self):
423 def __del__(self):
426 for h in self._urlopener.handlers:
424 for h in self._urlopener.handlers:
427 h.close()
425 h.close()
428 getattr(h, "close_all", lambda: None)()
426 getattr(h, "close_all", lambda: None)()
429
427
430 # Begin of ipeerconnection interface.
428 # Begin of ipeerconnection interface.
431
429
432 def url(self):
430 def url(self):
433 return self._path
431 return self._path
434
432
435 def local(self):
433 def local(self):
436 return None
434 return None
437
435
438 def peer(self):
436 def peer(self):
439 return self
437 return self
440
438
441 def canpush(self):
439 def canpush(self):
442 return True
440 return True
443
441
444 def close(self):
442 def close(self):
445 pass
443 pass
446
444
447 # End of ipeerconnection interface.
445 # End of ipeerconnection interface.
448
446
449 # Begin of ipeercommands interface.
447 # Begin of ipeercommands interface.
450
448
451 def capabilities(self):
449 def capabilities(self):
452 return self._caps
450 return self._caps
453
451
454 # End of ipeercommands interface.
452 # End of ipeercommands interface.
455
453
456 def _callstream(self, cmd, _compressible=False, **args):
454 def _callstream(self, cmd, _compressible=False, **args):
457 args = pycompat.byteskwargs(args)
455 args = pycompat.byteskwargs(args)
458
456
459 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
457 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
460 self._caps, self.capable,
458 self._caps, self.capable,
461 self._url, cmd, args)
459 self._url, cmd, args)
462
460
463 resp = sendrequest(self.ui, self._urlopener, req)
461 resp = sendrequest(self.ui, self._urlopener, req)
464
462
465 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
463 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
466 resp, _compressible)
464 resp, _compressible)
467
465
468 return resp
466 return resp
469
467
470 def _call(self, cmd, **args):
468 def _call(self, cmd, **args):
471 fp = self._callstream(cmd, **args)
469 fp = self._callstream(cmd, **args)
472 try:
470 try:
473 return fp.read()
471 return fp.read()
474 finally:
472 finally:
475 # if using keepalive, allow connection to be reused
473 # if using keepalive, allow connection to be reused
476 fp.close()
474 fp.close()
477
475
478 def _callpush(self, cmd, cg, **args):
476 def _callpush(self, cmd, cg, **args):
479 # have to stream bundle to a temp file because we do not have
477 # have to stream bundle to a temp file because we do not have
480 # http 1.1 chunked transfer.
478 # http 1.1 chunked transfer.
481
479
482 types = self.capable('unbundle')
480 types = self.capable('unbundle')
483 try:
481 try:
484 types = types.split(',')
482 types = types.split(',')
485 except AttributeError:
483 except AttributeError:
486 # servers older than d1b16a746db6 will send 'unbundle' as a
484 # servers older than d1b16a746db6 will send 'unbundle' as a
487 # boolean capability. They only support headerless/uncompressed
485 # boolean capability. They only support headerless/uncompressed
488 # bundles.
486 # bundles.
489 types = [""]
487 types = [""]
490 for x in types:
488 for x in types:
491 if x in bundle2.bundletypes:
489 if x in bundle2.bundletypes:
492 type = x
490 type = x
493 break
491 break
494
492
495 tempname = bundle2.writebundle(self.ui, cg, None, type)
493 tempname = bundle2.writebundle(self.ui, cg, None, type)
496 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
494 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
497 headers = {r'Content-Type': r'application/mercurial-0.1'}
495 headers = {r'Content-Type': r'application/mercurial-0.1'}
498
496
499 try:
497 try:
500 r = self._call(cmd, data=fp, headers=headers, **args)
498 r = self._call(cmd, data=fp, headers=headers, **args)
501 vals = r.split('\n', 1)
499 vals = r.split('\n', 1)
502 if len(vals) < 2:
500 if len(vals) < 2:
503 raise error.ResponseError(_("unexpected response:"), r)
501 raise error.ResponseError(_("unexpected response:"), r)
504 return vals
502 return vals
505 except urlerr.httperror:
503 except urlerr.httperror:
506 # Catch and re-raise these so we don't try and treat them
504 # Catch and re-raise these so we don't try and treat them
507 # like generic socket errors. They lack any values in
505 # like generic socket errors. They lack any values in
508 # .args on Python 3 which breaks our socket.error block.
506 # .args on Python 3 which breaks our socket.error block.
509 raise
507 raise
510 except socket.error as err:
508 except socket.error as err:
511 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
509 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
512 raise error.Abort(_('push failed: %s') % err.args[1])
510 raise error.Abort(_('push failed: %s') % err.args[1])
513 raise error.Abort(err.args[1])
511 raise error.Abort(err.args[1])
514 finally:
512 finally:
515 fp.close()
513 fp.close()
516 os.unlink(tempname)
514 os.unlink(tempname)
517
515
518 def _calltwowaystream(self, cmd, fp, **args):
516 def _calltwowaystream(self, cmd, fp, **args):
519 fh = None
517 fh = None
520 fp_ = None
518 fp_ = None
521 filename = None
519 filename = None
522 try:
520 try:
523 # dump bundle to disk
521 # dump bundle to disk
524 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
522 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
525 fh = os.fdopen(fd, r"wb")
523 fh = os.fdopen(fd, r"wb")
526 d = fp.read(4096)
524 d = fp.read(4096)
527 while d:
525 while d:
528 fh.write(d)
526 fh.write(d)
529 d = fp.read(4096)
527 d = fp.read(4096)
530 fh.close()
528 fh.close()
531 # start http push
529 # start http push
532 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
530 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
533 headers = {r'Content-Type': r'application/mercurial-0.1'}
531 headers = {r'Content-Type': r'application/mercurial-0.1'}
534 return self._callstream(cmd, data=fp_, headers=headers, **args)
532 return self._callstream(cmd, data=fp_, headers=headers, **args)
535 finally:
533 finally:
536 if fp_ is not None:
534 if fp_ is not None:
537 fp_.close()
535 fp_.close()
538 if fh is not None:
536 if fh is not None:
539 fh.close()
537 fh.close()
540 os.unlink(filename)
538 os.unlink(filename)
541
539
542 def _callcompressable(self, cmd, **args):
540 def _callcompressable(self, cmd, **args):
543 return self._callstream(cmd, _compressible=True, **args)
541 return self._callstream(cmd, _compressible=True, **args)
544
542
545 def _abort(self, exception):
543 def _abort(self, exception):
546 raise exception
544 raise exception
547
545
548 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests):
546 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests):
549 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
547 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
550 buffersends=True)
548 buffersends=True)
551
549
552 handler = wireprotov2peer.clienthandler(ui, reactor)
550 handler = wireprotov2peer.clienthandler(ui, reactor)
553
551
554 url = '%s/%s' % (apiurl, permission)
552 url = '%s/%s' % (apiurl, permission)
555
553
556 if len(requests) > 1:
554 if len(requests) > 1:
557 url += '/multirequest'
555 url += '/multirequest'
558 else:
556 else:
559 url += '/%s' % requests[0][0]
557 url += '/%s' % requests[0][0]
560
558
561 ui.debug('sending %d commands\n' % len(requests))
559 ui.debug('sending %d commands\n' % len(requests))
562 for command, args, f in requests:
560 for command, args, f in requests:
563 ui.debug('sending command %s: %s\n' % (
561 ui.debug('sending command %s: %s\n' % (
564 command, stringutil.pprint(args, indent=2)))
562 command, stringutil.pprint(args, indent=2)))
565 assert not list(handler.callcommand(command, args, f))
563 assert not list(handler.callcommand(command, args, f))
566
564
567 # TODO stream this.
565 # TODO stream this.
568 body = b''.join(map(bytes, handler.flushcommands()))
566 body = b''.join(map(bytes, handler.flushcommands()))
569
567
570 # TODO modify user-agent to reflect v2
568 # TODO modify user-agent to reflect v2
571 headers = {
569 headers = {
572 r'Accept': wireprotov2server.FRAMINGTYPE,
570 r'Accept': wireprotov2server.FRAMINGTYPE,
573 r'Content-Type': wireprotov2server.FRAMINGTYPE,
571 r'Content-Type': wireprotov2server.FRAMINGTYPE,
574 }
572 }
575
573
576 req = requestbuilder(pycompat.strurl(url), body, headers)
574 req = requestbuilder(pycompat.strurl(url), body, headers)
577 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
575 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
578
576
579 try:
577 try:
580 res = opener.open(req)
578 res = opener.open(req)
581 except urlerr.httperror as e:
579 except urlerr.httperror as e:
582 if e.code == 401:
580 if e.code == 401:
583 raise error.Abort(_('authorization failed'))
581 raise error.Abort(_('authorization failed'))
584
582
585 raise
583 raise
586 except httplib.HTTPException as e:
584 except httplib.HTTPException as e:
587 ui.traceback()
585 ui.traceback()
588 raise IOError(None, e)
586 raise IOError(None, e)
589
587
590 return handler, res
588 return handler, res
591
589
592 class queuedcommandfuture(pycompat.futures.Future):
590 class queuedcommandfuture(pycompat.futures.Future):
593 """Wraps result() on command futures to trigger submission on call."""
591 """Wraps result() on command futures to trigger submission on call."""
594
592
595 def result(self, timeout=None):
593 def result(self, timeout=None):
596 if self.done():
594 if self.done():
597 return pycompat.futures.Future.result(self, timeout)
595 return pycompat.futures.Future.result(self, timeout)
598
596
599 self._peerexecutor.sendcommands()
597 self._peerexecutor.sendcommands()
600
598
601 # sendcommands() will restore the original __class__ and self.result
599 # sendcommands() will restore the original __class__ and self.result
602 # will resolve to Future.result.
600 # will resolve to Future.result.
603 return self.result(timeout)
601 return self.result(timeout)
604
602
605 @interfaceutil.implementer(repository.ipeercommandexecutor)
603 @interfaceutil.implementer(repository.ipeercommandexecutor)
606 class httpv2executor(object):
604 class httpv2executor(object):
607 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor):
605 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor):
608 self._ui = ui
606 self._ui = ui
609 self._opener = opener
607 self._opener = opener
610 self._requestbuilder = requestbuilder
608 self._requestbuilder = requestbuilder
611 self._apiurl = apiurl
609 self._apiurl = apiurl
612 self._descriptor = descriptor
610 self._descriptor = descriptor
613 self._sent = False
611 self._sent = False
614 self._closed = False
612 self._closed = False
615 self._neededpermissions = set()
613 self._neededpermissions = set()
616 self._calls = []
614 self._calls = []
617 self._futures = weakref.WeakSet()
615 self._futures = weakref.WeakSet()
618 self._responseexecutor = None
616 self._responseexecutor = None
619 self._responsef = None
617 self._responsef = None
620
618
621 def __enter__(self):
619 def __enter__(self):
622 return self
620 return self
623
621
624 def __exit__(self, exctype, excvalue, exctb):
622 def __exit__(self, exctype, excvalue, exctb):
625 self.close()
623 self.close()
626
624
627 def callcommand(self, command, args):
625 def callcommand(self, command, args):
628 if self._sent:
626 if self._sent:
629 raise error.ProgrammingError('callcommand() cannot be used after '
627 raise error.ProgrammingError('callcommand() cannot be used after '
630 'commands are sent')
628 'commands are sent')
631
629
632 if self._closed:
630 if self._closed:
633 raise error.ProgrammingError('callcommand() cannot be used after '
631 raise error.ProgrammingError('callcommand() cannot be used after '
634 'close()')
632 'close()')
635
633
636 # The service advertises which commands are available. So if we attempt
634 # The service advertises which commands are available. So if we attempt
637 # to call an unknown command or pass an unknown argument, we can screen
635 # to call an unknown command or pass an unknown argument, we can screen
638 # for this.
636 # for this.
639 if command not in self._descriptor['commands']:
637 if command not in self._descriptor['commands']:
640 raise error.ProgrammingError(
638 raise error.ProgrammingError(
641 'wire protocol command %s is not available' % command)
639 'wire protocol command %s is not available' % command)
642
640
643 cmdinfo = self._descriptor['commands'][command]
641 cmdinfo = self._descriptor['commands'][command]
644 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
642 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
645
643
646 if unknownargs:
644 if unknownargs:
647 raise error.ProgrammingError(
645 raise error.ProgrammingError(
648 'wire protocol command %s does not accept argument: %s' % (
646 'wire protocol command %s does not accept argument: %s' % (
649 command, ', '.join(sorted(unknownargs))))
647 command, ', '.join(sorted(unknownargs))))
650
648
651 self._neededpermissions |= set(cmdinfo['permissions'])
649 self._neededpermissions |= set(cmdinfo['permissions'])
652
650
653 # TODO we /could/ also validate types here, since the API descriptor
651 # TODO we /could/ also validate types here, since the API descriptor
654 # includes types...
652 # includes types...
655
653
656 f = pycompat.futures.Future()
654 f = pycompat.futures.Future()
657
655
658 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
656 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
659 # could deadlock.
657 # could deadlock.
660 f.__class__ = queuedcommandfuture
658 f.__class__ = queuedcommandfuture
661 f._peerexecutor = self
659 f._peerexecutor = self
662
660
663 self._futures.add(f)
661 self._futures.add(f)
664 self._calls.append((command, args, f))
662 self._calls.append((command, args, f))
665
663
666 return f
664 return f
667
665
668 def sendcommands(self):
666 def sendcommands(self):
669 if self._sent:
667 if self._sent:
670 return
668 return
671
669
672 if not self._calls:
670 if not self._calls:
673 return
671 return
674
672
675 self._sent = True
673 self._sent = True
676
674
677 # Unhack any future types so caller sees a clean type and so we
675 # Unhack any future types so caller sees a clean type and so we
678 # break reference cycle.
676 # break reference cycle.
679 for f in self._futures:
677 for f in self._futures:
680 if isinstance(f, queuedcommandfuture):
678 if isinstance(f, queuedcommandfuture):
681 f.__class__ = pycompat.futures.Future
679 f.__class__ = pycompat.futures.Future
682 f._peerexecutor = None
680 f._peerexecutor = None
683
681
684 # Mark the future as running and filter out cancelled futures.
682 # Mark the future as running and filter out cancelled futures.
685 calls = [(command, args, f)
683 calls = [(command, args, f)
686 for command, args, f in self._calls
684 for command, args, f in self._calls
687 if f.set_running_or_notify_cancel()]
685 if f.set_running_or_notify_cancel()]
688
686
689 # Clear out references, prevent improper object usage.
687 # Clear out references, prevent improper object usage.
690 self._calls = None
688 self._calls = None
691
689
692 if not calls:
690 if not calls:
693 return
691 return
694
692
695 permissions = set(self._neededpermissions)
693 permissions = set(self._neededpermissions)
696
694
697 if 'push' in permissions and 'pull' in permissions:
695 if 'push' in permissions and 'pull' in permissions:
698 permissions.remove('pull')
696 permissions.remove('pull')
699
697
700 if len(permissions) > 1:
698 if len(permissions) > 1:
701 raise error.RepoError(_('cannot make request requiring multiple '
699 raise error.RepoError(_('cannot make request requiring multiple '
702 'permissions: %s') %
700 'permissions: %s') %
703 _(', ').join(sorted(permissions)))
701 _(', ').join(sorted(permissions)))
704
702
705 permission = {
703 permission = {
706 'push': 'rw',
704 'push': 'rw',
707 'pull': 'ro',
705 'pull': 'ro',
708 }[permissions.pop()]
706 }[permissions.pop()]
709
707
710 handler, resp = sendv2request(
708 handler, resp = sendv2request(
711 self._ui, self._opener, self._requestbuilder, self._apiurl,
709 self._ui, self._opener, self._requestbuilder, self._apiurl,
712 permission, calls)
710 permission, calls)
713
711
714 # TODO we probably want to validate the HTTP code, media type, etc.
712 # TODO we probably want to validate the HTTP code, media type, etc.
715
713
716 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
714 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
717 self._responsef = self._responseexecutor.submit(self._handleresponse,
715 self._responsef = self._responseexecutor.submit(self._handleresponse,
718 handler, resp)
716 handler, resp)
719
717
720 def close(self):
718 def close(self):
721 if self._closed:
719 if self._closed:
722 return
720 return
723
721
724 self.sendcommands()
722 self.sendcommands()
725
723
726 self._closed = True
724 self._closed = True
727
725
728 if not self._responsef:
726 if not self._responsef:
729 return
727 return
730
728
731 # TODO ^C here may not result in immediate program termination.
729 # TODO ^C here may not result in immediate program termination.
732
730
733 try:
731 try:
734 self._responsef.result()
732 self._responsef.result()
735 finally:
733 finally:
736 self._responseexecutor.shutdown(wait=True)
734 self._responseexecutor.shutdown(wait=True)
737 self._responsef = None
735 self._responsef = None
738 self._responseexecutor = None
736 self._responseexecutor = None
739
737
740 # If any of our futures are still in progress, mark them as
738 # If any of our futures are still in progress, mark them as
741 # errored, otherwise a result() could wait indefinitely.
739 # errored, otherwise a result() could wait indefinitely.
742 for f in self._futures:
740 for f in self._futures:
743 if not f.done():
741 if not f.done():
744 f.set_exception(error.ResponseError(
742 f.set_exception(error.ResponseError(
745 _('unfulfilled command response')))
743 _('unfulfilled command response')))
746
744
747 self._futures = None
745 self._futures = None
748
746
749 def _handleresponse(self, handler, resp):
747 def _handleresponse(self, handler, resp):
750 # Called in a thread to read the response.
748 # Called in a thread to read the response.
751
749
752 while handler.readframe(resp):
750 while handler.readframe(resp):
753 pass
751 pass
754
752
755 # TODO implement interface for version 2 peers
753 # TODO implement interface for version 2 peers
756 @interfaceutil.implementer(repository.ipeerconnection,
754 @interfaceutil.implementer(repository.ipeerconnection,
757 repository.ipeercapabilities,
755 repository.ipeercapabilities,
758 repository.ipeerrequests)
756 repository.ipeerrequests)
759 class httpv2peer(object):
757 class httpv2peer(object):
760 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
758 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
761 apidescriptor):
759 apidescriptor):
762 self.ui = ui
760 self.ui = ui
763
761
764 if repourl.endswith('/'):
762 if repourl.endswith('/'):
765 repourl = repourl[:-1]
763 repourl = repourl[:-1]
766
764
767 self._url = repourl
765 self._url = repourl
768 self._apipath = apipath
766 self._apipath = apipath
769 self._apiurl = '%s/%s' % (repourl, apipath)
767 self._apiurl = '%s/%s' % (repourl, apipath)
770 self._opener = opener
768 self._opener = opener
771 self._requestbuilder = requestbuilder
769 self._requestbuilder = requestbuilder
772 self._descriptor = apidescriptor
770 self._descriptor = apidescriptor
773
771
774 # Start of ipeerconnection.
772 # Start of ipeerconnection.
775
773
776 def url(self):
774 def url(self):
777 return self._url
775 return self._url
778
776
779 def local(self):
777 def local(self):
780 return None
778 return None
781
779
782 def peer(self):
780 def peer(self):
783 return self
781 return self
784
782
785 def canpush(self):
783 def canpush(self):
786 # TODO change once implemented.
784 # TODO change once implemented.
787 return False
785 return False
788
786
789 def close(self):
787 def close(self):
790 pass
788 pass
791
789
792 # End of ipeerconnection.
790 # End of ipeerconnection.
793
791
794 # Start of ipeercapabilities.
792 # Start of ipeercapabilities.
795
793
796 def capable(self, name):
794 def capable(self, name):
797 # The capabilities used internally historically map to capabilities
795 # The capabilities used internally historically map to capabilities
798 # advertised from the "capabilities" wire protocol command. However,
796 # advertised from the "capabilities" wire protocol command. However,
799 # version 2 of that command works differently.
797 # version 2 of that command works differently.
800
798
801 # Maps to commands that are available.
799 # Maps to commands that are available.
802 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
800 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
803 return True
801 return True
804
802
805 # Other concepts.
803 # Other concepts.
806 if name in ('bundle2',):
804 if name in ('bundle2',):
807 return True
805 return True
808
806
809 return False
807 return False
810
808
811 def requirecap(self, name, purpose):
809 def requirecap(self, name, purpose):
812 if self.capable(name):
810 if self.capable(name):
813 return
811 return
814
812
815 raise error.CapabilityError(
813 raise error.CapabilityError(
816 _('cannot %s; client or remote repository does not support the %r '
814 _('cannot %s; client or remote repository does not support the %r '
817 'capability') % (purpose, name))
815 'capability') % (purpose, name))
818
816
819 # End of ipeercapabilities.
817 # End of ipeercapabilities.
820
818
821 def _call(self, name, **args):
819 def _call(self, name, **args):
822 with self.commandexecutor() as e:
820 with self.commandexecutor() as e:
823 return e.callcommand(name, args).result()
821 return e.callcommand(name, args).result()
824
822
825 def commandexecutor(self):
823 def commandexecutor(self):
826 return httpv2executor(self.ui, self._opener, self._requestbuilder,
824 return httpv2executor(self.ui, self._opener, self._requestbuilder,
827 self._apiurl, self._descriptor)
825 self._apiurl, self._descriptor)
828
826
829 # Registry of API service names to metadata about peers that handle it.
827 # Registry of API service names to metadata about peers that handle it.
830 #
828 #
831 # The following keys are meaningful:
829 # The following keys are meaningful:
832 #
830 #
833 # init
831 # init
834 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
832 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
835 # apidescriptor) to create a peer.
833 # apidescriptor) to create a peer.
836 #
834 #
837 # priority
835 # priority
838 # Integer priority for the service. If we could choose from multiple
836 # Integer priority for the service. If we could choose from multiple
839 # services, we choose the one with the highest priority.
837 # services, we choose the one with the highest priority.
840 API_PEERS = {
838 API_PEERS = {
841 wireprototypes.HTTP_WIREPROTO_V2: {
839 wireprototypes.HTTP_WIREPROTO_V2: {
842 'init': httpv2peer,
840 'init': httpv2peer,
843 'priority': 50,
841 'priority': 50,
844 },
842 },
845 }
843 }
846
844
847 def performhandshake(ui, url, opener, requestbuilder):
845 def performhandshake(ui, url, opener, requestbuilder):
848 # The handshake is a request to the capabilities command.
846 # The handshake is a request to the capabilities command.
849
847
850 caps = None
848 caps = None
851 def capable(x):
849 def capable(x):
852 raise error.ProgrammingError('should not be called')
850 raise error.ProgrammingError('should not be called')
853
851
854 args = {}
852 args = {}
855
853
856 # The client advertises support for newer protocols by adding an
854 # The client advertises support for newer protocols by adding an
857 # X-HgUpgrade-* header with a list of supported APIs and an
855 # X-HgUpgrade-* header with a list of supported APIs and an
858 # X-HgProto-* header advertising which serializing formats it supports.
856 # X-HgProto-* header advertising which serializing formats it supports.
859 # We only support the HTTP version 2 transport and CBOR responses for
857 # We only support the HTTP version 2 transport and CBOR responses for
860 # now.
858 # now.
861 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
859 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
862
860
863 if advertisev2:
861 if advertisev2:
864 args['headers'] = {
862 args['headers'] = {
865 r'X-HgProto-1': r'cbor',
863 r'X-HgProto-1': r'cbor',
866 }
864 }
867
865
868 args['headers'].update(
866 args['headers'].update(
869 encodevalueinheaders(' '.join(sorted(API_PEERS)),
867 encodevalueinheaders(' '.join(sorted(API_PEERS)),
870 'X-HgUpgrade',
868 'X-HgUpgrade',
871 # We don't know the header limit this early.
869 # We don't know the header limit this early.
872 # So make it small.
870 # So make it small.
873 1024))
871 1024))
874
872
875 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
873 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
876 capable, url, 'capabilities',
874 capable, url, 'capabilities',
877 args)
875 args)
878 resp = sendrequest(ui, opener, req)
876 resp = sendrequest(ui, opener, req)
879
877
880 # The server may redirect us to the repo root, stripping the
878 # The server may redirect us to the repo root, stripping the
881 # ?cmd=capabilities query string from the URL. The server would likely
879 # ?cmd=capabilities query string from the URL. The server would likely
882 # return HTML in this case and ``parsev1commandresponse()`` would raise.
880 # return HTML in this case and ``parsev1commandresponse()`` would raise.
883 # We catch this special case and re-issue the capabilities request against
881 # We catch this special case and re-issue the capabilities request against
884 # the new URL.
882 # the new URL.
885 #
883 #
886 # We should ideally not do this, as a redirect that drops the query
884 # We should ideally not do this, as a redirect that drops the query
887 # string from the URL is arguably a server bug. (Garbage in, garbage out).
885 # string from the URL is arguably a server bug. (Garbage in, garbage out).
888 # However, Mercurial clients for several years appeared to handle this
886 # However, Mercurial clients for several years appeared to handle this
889 # issue without behavior degradation. And according to issue 5860, it may
887 # issue without behavior degradation. And according to issue 5860, it may
890 # be a longstanding bug in some server implementations. So we allow a
888 # be a longstanding bug in some server implementations. So we allow a
891 # redirect that drops the query string to "just work."
889 # redirect that drops the query string to "just work."
892 try:
890 try:
893 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
891 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
894 compressible=False,
892 compressible=False,
895 allowcbor=advertisev2)
893 allowcbor=advertisev2)
896 except RedirectedRepoError as e:
894 except RedirectedRepoError as e:
897 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
895 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
898 capable, e.respurl,
896 capable, e.respurl,
899 'capabilities', args)
897 'capabilities', args)
900 resp = sendrequest(ui, opener, req)
898 resp = sendrequest(ui, opener, req)
901 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
899 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
902 compressible=False,
900 compressible=False,
903 allowcbor=advertisev2)
901 allowcbor=advertisev2)
904
902
905 try:
903 try:
906 rawdata = resp.read()
904 rawdata = resp.read()
907 finally:
905 finally:
908 resp.close()
906 resp.close()
909
907
910 if not ct.startswith('application/mercurial-'):
908 if not ct.startswith('application/mercurial-'):
911 raise error.ProgrammingError('unexpected content-type: %s' % ct)
909 raise error.ProgrammingError('unexpected content-type: %s' % ct)
912
910
913 if advertisev2:
911 if advertisev2:
914 if ct == 'application/mercurial-cbor':
912 if ct == 'application/mercurial-cbor':
915 try:
913 try:
916 info = cbor.loads(rawdata)
914 info = cborutil.decodeall(rawdata)[0]
917 except cbor.CBORDecodeError:
915 except cborutil.CBORDecodeError:
918 raise error.Abort(_('error decoding CBOR from remote server'),
916 raise error.Abort(_('error decoding CBOR from remote server'),
919 hint=_('try again and consider contacting '
917 hint=_('try again and consider contacting '
920 'the server operator'))
918 'the server operator'))
921
919
922 # We got a legacy response. That's fine.
920 # We got a legacy response. That's fine.
923 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
921 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
924 info = {
922 info = {
925 'v1capabilities': set(rawdata.split())
923 'v1capabilities': set(rawdata.split())
926 }
924 }
927
925
928 else:
926 else:
929 raise error.RepoError(
927 raise error.RepoError(
930 _('unexpected response type from server: %s') % ct)
928 _('unexpected response type from server: %s') % ct)
931 else:
929 else:
932 info = {
930 info = {
933 'v1capabilities': set(rawdata.split())
931 'v1capabilities': set(rawdata.split())
934 }
932 }
935
933
936 return respurl, info
934 return respurl, info
937
935
938 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
936 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
939 """Construct an appropriate HTTP peer instance.
937 """Construct an appropriate HTTP peer instance.
940
938
941 ``opener`` is an ``url.opener`` that should be used to establish
939 ``opener`` is an ``url.opener`` that should be used to establish
942 connections, perform HTTP requests.
940 connections, perform HTTP requests.
943
941
944 ``requestbuilder`` is the type used for constructing HTTP requests.
942 ``requestbuilder`` is the type used for constructing HTTP requests.
945 It exists as an argument so extensions can override the default.
943 It exists as an argument so extensions can override the default.
946 """
944 """
947 u = util.url(path)
945 u = util.url(path)
948 if u.query or u.fragment:
946 if u.query or u.fragment:
949 raise error.Abort(_('unsupported URL component: "%s"') %
947 raise error.Abort(_('unsupported URL component: "%s"') %
950 (u.query or u.fragment))
948 (u.query or u.fragment))
951
949
952 # urllib cannot handle URLs with embedded user or passwd.
950 # urllib cannot handle URLs with embedded user or passwd.
953 url, authinfo = u.authinfo()
951 url, authinfo = u.authinfo()
954 ui.debug('using %s\n' % url)
952 ui.debug('using %s\n' % url)
955
953
956 opener = opener or urlmod.opener(ui, authinfo)
954 opener = opener or urlmod.opener(ui, authinfo)
957
955
958 respurl, info = performhandshake(ui, url, opener, requestbuilder)
956 respurl, info = performhandshake(ui, url, opener, requestbuilder)
959
957
960 # Given the intersection of APIs that both we and the server support,
958 # Given the intersection of APIs that both we and the server support,
961 # sort by their advertised priority and pick the first one.
959 # sort by their advertised priority and pick the first one.
962 #
960 #
963 # TODO consider making this request-based and interface driven. For
961 # TODO consider making this request-based and interface driven. For
964 # example, the caller could say "I want a peer that does X." It's quite
962 # example, the caller could say "I want a peer that does X." It's quite
965 # possible that not all peers would do that. Since we know the service
963 # possible that not all peers would do that. Since we know the service
966 # capabilities, we could filter out services not meeting the
964 # capabilities, we could filter out services not meeting the
967 # requirements. Possibly by consulting the interfaces defined by the
965 # requirements. Possibly by consulting the interfaces defined by the
968 # peer type.
966 # peer type.
969 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
967 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
970
968
971 preferredchoices = sorted(apipeerchoices,
969 preferredchoices = sorted(apipeerchoices,
972 key=lambda x: API_PEERS[x]['priority'],
970 key=lambda x: API_PEERS[x]['priority'],
973 reverse=True)
971 reverse=True)
974
972
975 for service in preferredchoices:
973 for service in preferredchoices:
976 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
974 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
977
975
978 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
976 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
979 requestbuilder,
977 requestbuilder,
980 info['apis'][service])
978 info['apis'][service])
981
979
982 # Failed to construct an API peer. Fall back to legacy.
980 # Failed to construct an API peer. Fall back to legacy.
983 return httppeer(ui, path, respurl, opener, requestbuilder,
981 return httppeer(ui, path, respurl, opener, requestbuilder,
984 info['v1capabilities'])
982 info['v1capabilities'])
985
983
986 def instance(ui, path, create, intents=None):
984 def instance(ui, path, create, intents=None):
987 if create:
985 if create:
988 raise error.Abort(_('cannot create new http repository'))
986 raise error.Abort(_('cannot create new http repository'))
989 try:
987 try:
990 if path.startswith('https:') and not urlmod.has_https:
988 if path.startswith('https:') and not urlmod.has_https:
991 raise error.Abort(_('Python support for SSL and HTTPS '
989 raise error.Abort(_('Python support for SSL and HTTPS '
992 'is not installed'))
990 'is not installed'))
993
991
994 inst = makepeer(ui, path)
992 inst = makepeer(ui, path)
995
993
996 return inst
994 return inst
997 except error.RepoError as httpexception:
995 except error.RepoError as httpexception:
998 try:
996 try:
999 r = statichttprepo.instance(ui, "static-" + path, create)
997 r = statichttprepo.instance(ui, "static-" + path, create)
1000 ui.note(_('(falling back to static-http)\n'))
998 ui.note(_('(falling back to static-http)\n'))
1001 return r
999 return r
1002 except error.RepoError:
1000 except error.RepoError:
1003 raise httpexception # use the original http RepoError instead
1001 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now