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