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