##// END OF EJS Templates
httppeer: basic implementation of capabilities interface...
Gregory Szorc -
r37629:ae873087 default
parent child Browse files
Show More
@@ -1,789 +1,816 b''
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import io
12 import io
13 import os
13 import os
14 import socket
14 import socket
15 import struct
15 import struct
16 import tempfile
16 import tempfile
17
17
18 from .i18n import _
18 from .i18n import _
19 from .thirdparty import (
19 from .thirdparty import (
20 cbor,
20 cbor,
21 )
21 )
22 from .thirdparty.zope import (
22 from .thirdparty.zope import (
23 interface as zi,
23 interface as zi,
24 )
24 )
25 from . import (
25 from . import (
26 bundle2,
26 bundle2,
27 error,
27 error,
28 httpconnection,
28 httpconnection,
29 pycompat,
29 pycompat,
30 repository,
30 repository,
31 statichttprepo,
31 statichttprepo,
32 url as urlmod,
32 url as urlmod,
33 util,
33 util,
34 wireproto,
34 wireproto,
35 wireprotoframing,
35 wireprotoframing,
36 wireprototypes,
36 wireprototypes,
37 wireprotov2server,
37 wireprotov2server,
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 xrange(0, len(value), valuelen):
66 for i in 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 msg = _('HTTP request error (incomplete response; '
87 msg = _('HTTP request error (incomplete response; '
88 'expected %d bytes got %d)') % (e.expected,
88 'expected %d bytes got %d)') % (e.expected,
89 len(e.partial))
89 len(e.partial))
90 else:
90 else:
91 msg = _('HTTP request error (incomplete response)')
91 msg = _('HTTP request error (incomplete response)')
92
92
93 raise error.PeerTransportError(
93 raise error.PeerTransportError(
94 msg,
94 msg,
95 hint=_('this may be an intermittent network failure; '
95 hint=_('this may be an intermittent network failure; '
96 'if the error persists, consider contacting the '
96 'if the error persists, consider contacting the '
97 'network or server operator'))
97 'network or server operator'))
98 except httplib.HTTPException as e:
98 except httplib.HTTPException as e:
99 raise error.PeerTransportError(
99 raise error.PeerTransportError(
100 _('HTTP request error (%s)') % e,
100 _('HTTP request error (%s)') % e,
101 hint=_('this may be an intermittent network failure; '
101 hint=_('this may be an intermittent network failure; '
102 'if the error persists, consider contacting the '
102 'if the error persists, consider contacting the '
103 'network or server operator'))
103 'network or server operator'))
104
104
105 resp.__class__ = readerproxy
105 resp.__class__ = readerproxy
106
106
107 class _multifile(object):
107 class _multifile(object):
108 def __init__(self, *fileobjs):
108 def __init__(self, *fileobjs):
109 for f in fileobjs:
109 for f in fileobjs:
110 if not util.safehasattr(f, 'length'):
110 if not util.safehasattr(f, 'length'):
111 raise ValueError(
111 raise ValueError(
112 '_multifile only supports file objects that '
112 '_multifile only supports file objects that '
113 'have a length but this one does not:', type(f), f)
113 'have a length but this one does not:', type(f), f)
114 self._fileobjs = fileobjs
114 self._fileobjs = fileobjs
115 self._index = 0
115 self._index = 0
116
116
117 @property
117 @property
118 def length(self):
118 def length(self):
119 return sum(f.length for f in self._fileobjs)
119 return sum(f.length for f in self._fileobjs)
120
120
121 def read(self, amt=None):
121 def read(self, amt=None):
122 if amt <= 0:
122 if amt <= 0:
123 return ''.join(f.read() for f in self._fileobjs)
123 return ''.join(f.read() for f in self._fileobjs)
124 parts = []
124 parts = []
125 while amt and self._index < len(self._fileobjs):
125 while amt and self._index < len(self._fileobjs):
126 parts.append(self._fileobjs[self._index].read(amt))
126 parts.append(self._fileobjs[self._index].read(amt))
127 got = len(parts[-1])
127 got = len(parts[-1])
128 if got < amt:
128 if got < amt:
129 self._index += 1
129 self._index += 1
130 amt -= got
130 amt -= got
131 return ''.join(parts)
131 return ''.join(parts)
132
132
133 def seek(self, offset, whence=os.SEEK_SET):
133 def seek(self, offset, whence=os.SEEK_SET):
134 if whence != os.SEEK_SET:
134 if whence != os.SEEK_SET:
135 raise NotImplementedError(
135 raise NotImplementedError(
136 '_multifile does not support anything other'
136 '_multifile does not support anything other'
137 ' than os.SEEK_SET for whence on seek()')
137 ' than os.SEEK_SET for whence on seek()')
138 if offset != 0:
138 if offset != 0:
139 raise NotImplementedError(
139 raise NotImplementedError(
140 '_multifile only supports seeking to start, but that '
140 '_multifile only supports seeking to start, but that '
141 'could be fixed if you need it')
141 'could be fixed if you need it')
142 for f in self._fileobjs:
142 for f in self._fileobjs:
143 f.seek(0)
143 f.seek(0)
144 self._index = 0
144 self._index = 0
145
145
146 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
146 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
147 repobaseurl, cmd, args):
147 repobaseurl, cmd, args):
148 """Make an HTTP request to run a command for a version 1 client.
148 """Make an HTTP request to run a command for a version 1 client.
149
149
150 ``caps`` is a set of known server capabilities. The value may be
150 ``caps`` is a set of known server capabilities. The value may be
151 None if capabilities are not yet known.
151 None if capabilities are not yet known.
152
152
153 ``capablefn`` is a function to evaluate a capability.
153 ``capablefn`` is a function to evaluate a capability.
154
154
155 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
155 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
156 raw data to pass to it.
156 raw data to pass to it.
157 """
157 """
158 if cmd == 'pushkey':
158 if cmd == 'pushkey':
159 args['data'] = ''
159 args['data'] = ''
160 data = args.pop('data', None)
160 data = args.pop('data', None)
161 headers = args.pop('headers', {})
161 headers = args.pop('headers', {})
162
162
163 ui.debug("sending %s command\n" % cmd)
163 ui.debug("sending %s command\n" % cmd)
164 q = [('cmd', cmd)]
164 q = [('cmd', cmd)]
165 headersize = 0
165 headersize = 0
166 # Important: don't use self.capable() here or else you end up
166 # Important: don't use self.capable() here or else you end up
167 # with infinite recursion when trying to look up capabilities
167 # with infinite recursion when trying to look up capabilities
168 # for the first time.
168 # for the first time.
169 postargsok = caps is not None and 'httppostargs' in caps
169 postargsok = caps is not None and 'httppostargs' in caps
170
170
171 # Send arguments via POST.
171 # Send arguments via POST.
172 if postargsok and args:
172 if postargsok and args:
173 strargs = urlreq.urlencode(sorted(args.items()))
173 strargs = urlreq.urlencode(sorted(args.items()))
174 if not data:
174 if not data:
175 data = strargs
175 data = strargs
176 else:
176 else:
177 if isinstance(data, bytes):
177 if isinstance(data, bytes):
178 i = io.BytesIO(data)
178 i = io.BytesIO(data)
179 i.length = len(data)
179 i.length = len(data)
180 data = i
180 data = i
181 argsio = io.BytesIO(strargs)
181 argsio = io.BytesIO(strargs)
182 argsio.length = len(strargs)
182 argsio.length = len(strargs)
183 data = _multifile(argsio, data)
183 data = _multifile(argsio, data)
184 headers[r'X-HgArgs-Post'] = len(strargs)
184 headers[r'X-HgArgs-Post'] = len(strargs)
185 elif args:
185 elif args:
186 # Calling self.capable() can infinite loop if we are calling
186 # Calling self.capable() can infinite loop if we are calling
187 # "capabilities". But that command should never accept wire
187 # "capabilities". But that command should never accept wire
188 # protocol arguments. So this should never happen.
188 # protocol arguments. So this should never happen.
189 assert cmd != 'capabilities'
189 assert cmd != 'capabilities'
190 httpheader = capablefn('httpheader')
190 httpheader = capablefn('httpheader')
191 if httpheader:
191 if httpheader:
192 headersize = int(httpheader.split(',', 1)[0])
192 headersize = int(httpheader.split(',', 1)[0])
193
193
194 # Send arguments via HTTP headers.
194 # Send arguments via HTTP headers.
195 if headersize > 0:
195 if headersize > 0:
196 # The headers can typically carry more data than the URL.
196 # The headers can typically carry more data than the URL.
197 encargs = urlreq.urlencode(sorted(args.items()))
197 encargs = urlreq.urlencode(sorted(args.items()))
198 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
198 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
199 headersize):
199 headersize):
200 headers[header] = value
200 headers[header] = value
201 # Send arguments via query string (Mercurial <1.9).
201 # Send arguments via query string (Mercurial <1.9).
202 else:
202 else:
203 q += sorted(args.items())
203 q += sorted(args.items())
204
204
205 qs = '?%s' % urlreq.urlencode(q)
205 qs = '?%s' % urlreq.urlencode(q)
206 cu = "%s%s" % (repobaseurl, qs)
206 cu = "%s%s" % (repobaseurl, qs)
207 size = 0
207 size = 0
208 if util.safehasattr(data, 'length'):
208 if util.safehasattr(data, 'length'):
209 size = data.length
209 size = data.length
210 elif data is not None:
210 elif data is not None:
211 size = len(data)
211 size = len(data)
212 if data is not None and r'Content-Type' not in headers:
212 if data is not None and r'Content-Type' not in headers:
213 headers[r'Content-Type'] = r'application/mercurial-0.1'
213 headers[r'Content-Type'] = r'application/mercurial-0.1'
214
214
215 # Tell the server we accept application/mercurial-0.2 and multiple
215 # Tell the server we accept application/mercurial-0.2 and multiple
216 # compression formats if the server is capable of emitting those
216 # compression formats if the server is capable of emitting those
217 # payloads.
217 # payloads.
218 # Note: Keep this set empty by default, as client advertisement of
218 # Note: Keep this set empty by default, as client advertisement of
219 # protocol parameters should only occur after the handshake.
219 # protocol parameters should only occur after the handshake.
220 protoparams = set()
220 protoparams = set()
221
221
222 mediatypes = set()
222 mediatypes = set()
223 if caps is not None:
223 if caps is not None:
224 mt = capablefn('httpmediatype')
224 mt = capablefn('httpmediatype')
225 if mt:
225 if mt:
226 protoparams.add('0.1')
226 protoparams.add('0.1')
227 mediatypes = set(mt.split(','))
227 mediatypes = set(mt.split(','))
228
228
229 protoparams.add('partial-pull')
229 protoparams.add('partial-pull')
230
230
231 if '0.2tx' in mediatypes:
231 if '0.2tx' in mediatypes:
232 protoparams.add('0.2')
232 protoparams.add('0.2')
233
233
234 if '0.2tx' in mediatypes and capablefn('compression'):
234 if '0.2tx' in mediatypes and capablefn('compression'):
235 # We /could/ compare supported compression formats and prune
235 # We /could/ compare supported compression formats and prune
236 # non-mutually supported or error if nothing is mutually supported.
236 # non-mutually supported or error if nothing is mutually supported.
237 # For now, send the full list to the server and have it error.
237 # For now, send the full list to the server and have it error.
238 comps = [e.wireprotosupport().name for e in
238 comps = [e.wireprotosupport().name for e in
239 util.compengines.supportedwireengines(util.CLIENTROLE)]
239 util.compengines.supportedwireengines(util.CLIENTROLE)]
240 protoparams.add('comp=%s' % ','.join(comps))
240 protoparams.add('comp=%s' % ','.join(comps))
241
241
242 if protoparams:
242 if protoparams:
243 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
243 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
244 'X-HgProto',
244 'X-HgProto',
245 headersize or 1024)
245 headersize or 1024)
246 for header, value in protoheaders:
246 for header, value in protoheaders:
247 headers[header] = value
247 headers[header] = value
248
248
249 varyheaders = []
249 varyheaders = []
250 for header in headers:
250 for header in headers:
251 if header.lower().startswith(r'x-hg'):
251 if header.lower().startswith(r'x-hg'):
252 varyheaders.append(header)
252 varyheaders.append(header)
253
253
254 if varyheaders:
254 if varyheaders:
255 headers[r'Vary'] = r','.join(sorted(varyheaders))
255 headers[r'Vary'] = r','.join(sorted(varyheaders))
256
256
257 req = requestbuilder(pycompat.strurl(cu), data, headers)
257 req = requestbuilder(pycompat.strurl(cu), data, headers)
258
258
259 if data is not None:
259 if data is not None:
260 ui.debug("sending %d bytes\n" % size)
260 ui.debug("sending %d bytes\n" % size)
261 req.add_unredirected_header(r'Content-Length', r'%d' % size)
261 req.add_unredirected_header(r'Content-Length', r'%d' % size)
262
262
263 return req, cu, qs
263 return req, cu, qs
264
264
265 def sendrequest(ui, opener, req):
265 def sendrequest(ui, opener, req):
266 """Send a prepared HTTP request.
266 """Send a prepared HTTP request.
267
267
268 Returns the response object.
268 Returns the response object.
269 """
269 """
270 if (ui.debugflag
270 if (ui.debugflag
271 and ui.configbool('devel', 'debug.peer-request')):
271 and ui.configbool('devel', 'debug.peer-request')):
272 dbg = ui.debug
272 dbg = ui.debug
273 line = 'devel-peer-request: %s\n'
273 line = 'devel-peer-request: %s\n'
274 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
274 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
275 hgargssize = None
275 hgargssize = None
276
276
277 for header, value in sorted(req.header_items()):
277 for header, value in sorted(req.header_items()):
278 if header.startswith('X-hgarg-'):
278 if header.startswith('X-hgarg-'):
279 if hgargssize is None:
279 if hgargssize is None:
280 hgargssize = 0
280 hgargssize = 0
281 hgargssize += len(value)
281 hgargssize += len(value)
282 else:
282 else:
283 dbg(line % ' %s %s' % (header, value))
283 dbg(line % ' %s %s' % (header, value))
284
284
285 if hgargssize is not None:
285 if hgargssize is not None:
286 dbg(line % ' %d bytes of commands arguments in headers'
286 dbg(line % ' %d bytes of commands arguments in headers'
287 % hgargssize)
287 % hgargssize)
288
288
289 if req.has_data():
289 if req.has_data():
290 data = req.get_data()
290 data = req.get_data()
291 length = getattr(data, 'length', None)
291 length = getattr(data, 'length', None)
292 if length is None:
292 if length is None:
293 length = len(data)
293 length = len(data)
294 dbg(line % ' %d bytes of data' % length)
294 dbg(line % ' %d bytes of data' % length)
295
295
296 start = util.timer()
296 start = util.timer()
297
297
298 try:
298 try:
299 res = opener.open(req)
299 res = opener.open(req)
300 except urlerr.httperror as inst:
300 except urlerr.httperror as inst:
301 if inst.code == 401:
301 if inst.code == 401:
302 raise error.Abort(_('authorization failed'))
302 raise error.Abort(_('authorization failed'))
303 raise
303 raise
304 except httplib.HTTPException as inst:
304 except httplib.HTTPException as inst:
305 ui.debug('http error requesting %s\n' %
305 ui.debug('http error requesting %s\n' %
306 util.hidepassword(req.get_full_url()))
306 util.hidepassword(req.get_full_url()))
307 ui.traceback()
307 ui.traceback()
308 raise IOError(None, inst)
308 raise IOError(None, inst)
309 finally:
309 finally:
310 if ui.configbool('devel', 'debug.peer-request'):
310 if ui.configbool('devel', 'debug.peer-request'):
311 dbg(line % ' finished in %.4f seconds (%s)'
311 dbg(line % ' finished in %.4f seconds (%s)'
312 % (util.timer() - start, res.code))
312 % (util.timer() - start, res.code))
313
313
314 # Insert error handlers for common I/O failures.
314 # Insert error handlers for common I/O failures.
315 _wraphttpresponse(res)
315 _wraphttpresponse(res)
316
316
317 return res
317 return res
318
318
319 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
319 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
320 allowcbor=False):
320 allowcbor=False):
321 # record the url we got redirected to
321 # record the url we got redirected to
322 respurl = pycompat.bytesurl(resp.geturl())
322 respurl = pycompat.bytesurl(resp.geturl())
323 if respurl.endswith(qs):
323 if respurl.endswith(qs):
324 respurl = respurl[:-len(qs)]
324 respurl = respurl[:-len(qs)]
325 if baseurl.rstrip('/') != respurl.rstrip('/'):
325 if baseurl.rstrip('/') != respurl.rstrip('/'):
326 if not ui.quiet:
326 if not ui.quiet:
327 ui.warn(_('real URL is %s\n') % respurl)
327 ui.warn(_('real URL is %s\n') % respurl)
328
328
329 try:
329 try:
330 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
330 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
331 except AttributeError:
331 except AttributeError:
332 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
332 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
333
333
334 safeurl = util.hidepassword(baseurl)
334 safeurl = util.hidepassword(baseurl)
335 if proto.startswith('application/hg-error'):
335 if proto.startswith('application/hg-error'):
336 raise error.OutOfBandError(resp.read())
336 raise error.OutOfBandError(resp.read())
337
337
338 # Pre 1.0 versions of Mercurial used text/plain and
338 # Pre 1.0 versions of Mercurial used text/plain and
339 # application/hg-changegroup. We don't support such old servers.
339 # application/hg-changegroup. We don't support such old servers.
340 if not proto.startswith('application/mercurial-'):
340 if not proto.startswith('application/mercurial-'):
341 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
341 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
342 raise error.RepoError(
342 raise error.RepoError(
343 _("'%s' does not appear to be an hg repository:\n"
343 _("'%s' does not appear to be an hg repository:\n"
344 "---%%<--- (%s)\n%s\n---%%<---\n")
344 "---%%<--- (%s)\n%s\n---%%<---\n")
345 % (safeurl, proto or 'no content-type', resp.read(1024)))
345 % (safeurl, proto or 'no content-type', resp.read(1024)))
346
346
347 try:
347 try:
348 subtype = proto.split('-', 1)[1]
348 subtype = proto.split('-', 1)[1]
349
349
350 # Unless we end up supporting CBOR in the legacy wire protocol,
350 # Unless we end up supporting CBOR in the legacy wire protocol,
351 # this should ONLY be encountered for the initial capabilities
351 # this should ONLY be encountered for the initial capabilities
352 # request during handshake.
352 # request during handshake.
353 if subtype == 'cbor':
353 if subtype == 'cbor':
354 if allowcbor:
354 if allowcbor:
355 return respurl, proto, resp
355 return respurl, proto, resp
356 else:
356 else:
357 raise error.RepoError(_('unexpected CBOR response from '
357 raise error.RepoError(_('unexpected CBOR response from '
358 'server'))
358 'server'))
359
359
360 version_info = tuple([int(n) for n in subtype.split('.')])
360 version_info = tuple([int(n) for n in subtype.split('.')])
361 except ValueError:
361 except ValueError:
362 raise error.RepoError(_("'%s' sent a broken Content-Type "
362 raise error.RepoError(_("'%s' sent a broken Content-Type "
363 "header (%s)") % (safeurl, proto))
363 "header (%s)") % (safeurl, proto))
364
364
365 # TODO consider switching to a decompression reader that uses
365 # TODO consider switching to a decompression reader that uses
366 # generators.
366 # generators.
367 if version_info == (0, 1):
367 if version_info == (0, 1):
368 if compressible:
368 if compressible:
369 resp = util.compengines['zlib'].decompressorreader(resp)
369 resp = util.compengines['zlib'].decompressorreader(resp)
370
370
371 elif version_info == (0, 2):
371 elif version_info == (0, 2):
372 # application/mercurial-0.2 always identifies the compression
372 # application/mercurial-0.2 always identifies the compression
373 # engine in the payload header.
373 # engine in the payload header.
374 elen = struct.unpack('B', resp.read(1))[0]
374 elen = struct.unpack('B', resp.read(1))[0]
375 ename = resp.read(elen)
375 ename = resp.read(elen)
376 engine = util.compengines.forwiretype(ename)
376 engine = util.compengines.forwiretype(ename)
377
377
378 resp = engine.decompressorreader(resp)
378 resp = engine.decompressorreader(resp)
379 else:
379 else:
380 raise error.RepoError(_("'%s' uses newer protocol %s") %
380 raise error.RepoError(_("'%s' uses newer protocol %s") %
381 (safeurl, subtype))
381 (safeurl, subtype))
382
382
383 return respurl, proto, resp
383 return respurl, proto, resp
384
384
385 class httppeer(wireproto.wirepeer):
385 class httppeer(wireproto.wirepeer):
386 def __init__(self, ui, path, url, opener, requestbuilder, caps):
386 def __init__(self, ui, path, url, opener, requestbuilder, caps):
387 self.ui = ui
387 self.ui = ui
388 self._path = path
388 self._path = path
389 self._url = url
389 self._url = url
390 self._caps = caps
390 self._caps = caps
391 self._urlopener = opener
391 self._urlopener = opener
392 self._requestbuilder = requestbuilder
392 self._requestbuilder = requestbuilder
393
393
394 def __del__(self):
394 def __del__(self):
395 for h in self._urlopener.handlers:
395 for h in self._urlopener.handlers:
396 h.close()
396 h.close()
397 getattr(h, "close_all", lambda: None)()
397 getattr(h, "close_all", lambda: None)()
398
398
399 # Begin of ipeerconnection interface.
399 # Begin of ipeerconnection interface.
400
400
401 def url(self):
401 def url(self):
402 return self._path
402 return self._path
403
403
404 def local(self):
404 def local(self):
405 return None
405 return None
406
406
407 def peer(self):
407 def peer(self):
408 return self
408 return self
409
409
410 def canpush(self):
410 def canpush(self):
411 return True
411 return True
412
412
413 def close(self):
413 def close(self):
414 pass
414 pass
415
415
416 # End of ipeerconnection interface.
416 # End of ipeerconnection interface.
417
417
418 # Begin of ipeercommands interface.
418 # Begin of ipeercommands interface.
419
419
420 def capabilities(self):
420 def capabilities(self):
421 return self._caps
421 return self._caps
422
422
423 # End of ipeercommands interface.
423 # End of ipeercommands interface.
424
424
425 # look up capabilities only when needed
425 # look up capabilities only when needed
426
426
427 def _callstream(self, cmd, _compressible=False, **args):
427 def _callstream(self, cmd, _compressible=False, **args):
428 args = pycompat.byteskwargs(args)
428 args = pycompat.byteskwargs(args)
429
429
430 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
430 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
431 self._caps, self.capable,
431 self._caps, self.capable,
432 self._url, cmd, args)
432 self._url, cmd, args)
433
433
434 resp = sendrequest(self.ui, self._urlopener, req)
434 resp = sendrequest(self.ui, self._urlopener, req)
435
435
436 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
436 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
437 resp, _compressible)
437 resp, _compressible)
438
438
439 return resp
439 return resp
440
440
441 def _call(self, cmd, **args):
441 def _call(self, cmd, **args):
442 fp = self._callstream(cmd, **args)
442 fp = self._callstream(cmd, **args)
443 try:
443 try:
444 return fp.read()
444 return fp.read()
445 finally:
445 finally:
446 # if using keepalive, allow connection to be reused
446 # if using keepalive, allow connection to be reused
447 fp.close()
447 fp.close()
448
448
449 def _callpush(self, cmd, cg, **args):
449 def _callpush(self, cmd, cg, **args):
450 # have to stream bundle to a temp file because we do not have
450 # have to stream bundle to a temp file because we do not have
451 # http 1.1 chunked transfer.
451 # http 1.1 chunked transfer.
452
452
453 types = self.capable('unbundle')
453 types = self.capable('unbundle')
454 try:
454 try:
455 types = types.split(',')
455 types = types.split(',')
456 except AttributeError:
456 except AttributeError:
457 # servers older than d1b16a746db6 will send 'unbundle' as a
457 # servers older than d1b16a746db6 will send 'unbundle' as a
458 # boolean capability. They only support headerless/uncompressed
458 # boolean capability. They only support headerless/uncompressed
459 # bundles.
459 # bundles.
460 types = [""]
460 types = [""]
461 for x in types:
461 for x in types:
462 if x in bundle2.bundletypes:
462 if x in bundle2.bundletypes:
463 type = x
463 type = x
464 break
464 break
465
465
466 tempname = bundle2.writebundle(self.ui, cg, None, type)
466 tempname = bundle2.writebundle(self.ui, cg, None, type)
467 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
467 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
468 headers = {r'Content-Type': r'application/mercurial-0.1'}
468 headers = {r'Content-Type': r'application/mercurial-0.1'}
469
469
470 try:
470 try:
471 r = self._call(cmd, data=fp, headers=headers, **args)
471 r = self._call(cmd, data=fp, headers=headers, **args)
472 vals = r.split('\n', 1)
472 vals = r.split('\n', 1)
473 if len(vals) < 2:
473 if len(vals) < 2:
474 raise error.ResponseError(_("unexpected response:"), r)
474 raise error.ResponseError(_("unexpected response:"), r)
475 return vals
475 return vals
476 except urlerr.httperror:
476 except urlerr.httperror:
477 # Catch and re-raise these so we don't try and treat them
477 # Catch and re-raise these so we don't try and treat them
478 # like generic socket errors. They lack any values in
478 # like generic socket errors. They lack any values in
479 # .args on Python 3 which breaks our socket.error block.
479 # .args on Python 3 which breaks our socket.error block.
480 raise
480 raise
481 except socket.error as err:
481 except socket.error as err:
482 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
482 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
483 raise error.Abort(_('push failed: %s') % err.args[1])
483 raise error.Abort(_('push failed: %s') % err.args[1])
484 raise error.Abort(err.args[1])
484 raise error.Abort(err.args[1])
485 finally:
485 finally:
486 fp.close()
486 fp.close()
487 os.unlink(tempname)
487 os.unlink(tempname)
488
488
489 def _calltwowaystream(self, cmd, fp, **args):
489 def _calltwowaystream(self, cmd, fp, **args):
490 fh = None
490 fh = None
491 fp_ = None
491 fp_ = None
492 filename = None
492 filename = None
493 try:
493 try:
494 # dump bundle to disk
494 # dump bundle to disk
495 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
495 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
496 fh = os.fdopen(fd, r"wb")
496 fh = os.fdopen(fd, r"wb")
497 d = fp.read(4096)
497 d = fp.read(4096)
498 while d:
498 while d:
499 fh.write(d)
499 fh.write(d)
500 d = fp.read(4096)
500 d = fp.read(4096)
501 fh.close()
501 fh.close()
502 # start http push
502 # start http push
503 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
503 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
504 headers = {r'Content-Type': r'application/mercurial-0.1'}
504 headers = {r'Content-Type': r'application/mercurial-0.1'}
505 return self._callstream(cmd, data=fp_, headers=headers, **args)
505 return self._callstream(cmd, data=fp_, headers=headers, **args)
506 finally:
506 finally:
507 if fp_ is not None:
507 if fp_ is not None:
508 fp_.close()
508 fp_.close()
509 if fh is not None:
509 if fh is not None:
510 fh.close()
510 fh.close()
511 os.unlink(filename)
511 os.unlink(filename)
512
512
513 def _callcompressable(self, cmd, **args):
513 def _callcompressable(self, cmd, **args):
514 return self._callstream(cmd, _compressible=True, **args)
514 return self._callstream(cmd, _compressible=True, **args)
515
515
516 def _abort(self, exception):
516 def _abort(self, exception):
517 raise exception
517 raise exception
518
518
519 # TODO implement interface for version 2 peers
519 # TODO implement interface for version 2 peers
520 @zi.implementer(repository.ipeerconnection)
520 @zi.implementer(repository.ipeerconnection, repository.ipeercapabilities)
521 class httpv2peer(object):
521 class httpv2peer(object):
522 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
522 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
523 apidescriptor):
523 apidescriptor):
524 self.ui = ui
524 self.ui = ui
525
525
526 if repourl.endswith('/'):
526 if repourl.endswith('/'):
527 repourl = repourl[:-1]
527 repourl = repourl[:-1]
528
528
529 self._url = repourl
529 self._url = repourl
530 self._apipath = apipath
530 self._apipath = apipath
531 self._opener = opener
531 self._opener = opener
532 self._requestbuilder = requestbuilder
532 self._requestbuilder = requestbuilder
533 self._descriptor = apidescriptor
533 self._descriptor = apidescriptor
534
534
535 # Start of ipeerconnection.
535 # Start of ipeerconnection.
536
536
537 def url(self):
537 def url(self):
538 return self._url
538 return self._url
539
539
540 def local(self):
540 def local(self):
541 return None
541 return None
542
542
543 def peer(self):
543 def peer(self):
544 return self
544 return self
545
545
546 def canpush(self):
546 def canpush(self):
547 # TODO change once implemented.
547 # TODO change once implemented.
548 return False
548 return False
549
549
550 def close(self):
550 def close(self):
551 pass
551 pass
552
552
553 # End of ipeerconnection.
553 # End of ipeerconnection.
554
554
555 # Start of ipeercapabilities.
556
557 def capable(self, name):
558 # The capabilities used internally historically map to capabilities
559 # advertised from the "capabilities" wire protocol command. However,
560 # version 2 of that command works differently.
561
562 # Maps to commands that are available.
563 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
564 return True
565
566 # Other concepts.
567 if name in ('bundle2',):
568 return True
569
570 return False
571
572 def requirecap(self, name, purpose):
573 if self.capable(name):
574 return
575
576 raise error.CapabilityError(
577 _('cannot %s; client or remote repository does not support the %r '
578 'capability') % (purpose, name))
579
580 # End of ipeercapabilities.
581
555 # TODO require to be part of a batched primitive, use futures.
582 # TODO require to be part of a batched primitive, use futures.
556 def _call(self, name, **args):
583 def _call(self, name, **args):
557 """Call a wire protocol command with arguments."""
584 """Call a wire protocol command with arguments."""
558
585
559 # Having this early has a side-effect of importing wireprotov2server,
586 # Having this early has a side-effect of importing wireprotov2server,
560 # which has the side-effect of ensuring commands are registered.
587 # which has the side-effect of ensuring commands are registered.
561
588
562 # TODO modify user-agent to reflect v2.
589 # TODO modify user-agent to reflect v2.
563 headers = {
590 headers = {
564 r'Accept': wireprotov2server.FRAMINGTYPE,
591 r'Accept': wireprotov2server.FRAMINGTYPE,
565 r'Content-Type': wireprotov2server.FRAMINGTYPE,
592 r'Content-Type': wireprotov2server.FRAMINGTYPE,
566 }
593 }
567
594
568 # TODO permissions should come from capabilities results.
595 # TODO permissions should come from capabilities results.
569 permission = wireproto.commandsv2[name].permission
596 permission = wireproto.commandsv2[name].permission
570 if permission not in ('push', 'pull'):
597 if permission not in ('push', 'pull'):
571 raise error.ProgrammingError('unknown permission type: %s' %
598 raise error.ProgrammingError('unknown permission type: %s' %
572 permission)
599 permission)
573
600
574 permission = {
601 permission = {
575 'push': 'rw',
602 'push': 'rw',
576 'pull': 'ro',
603 'pull': 'ro',
577 }[permission]
604 }[permission]
578
605
579 url = '%s/%s/%s/%s' % (self._url, self._apipath, permission, name)
606 url = '%s/%s/%s/%s' % (self._url, self._apipath, permission, name)
580
607
581 # TODO this should be part of a generic peer for the frame-based
608 # TODO this should be part of a generic peer for the frame-based
582 # protocol.
609 # protocol.
583 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
610 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
584 buffersends=True)
611 buffersends=True)
585
612
586 request, action, meta = reactor.callcommand(name, args)
613 request, action, meta = reactor.callcommand(name, args)
587 assert action == 'noop'
614 assert action == 'noop'
588
615
589 action, meta = reactor.flushcommands()
616 action, meta = reactor.flushcommands()
590 assert action == 'sendframes'
617 assert action == 'sendframes'
591
618
592 body = b''.join(map(bytes, meta['framegen']))
619 body = b''.join(map(bytes, meta['framegen']))
593 req = self._requestbuilder(pycompat.strurl(url), body, headers)
620 req = self._requestbuilder(pycompat.strurl(url), body, headers)
594 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
621 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
595
622
596 # TODO unify this code with httppeer.
623 # TODO unify this code with httppeer.
597 try:
624 try:
598 res = self._opener.open(req)
625 res = self._opener.open(req)
599 except urlerr.httperror as e:
626 except urlerr.httperror as e:
600 if e.code == 401:
627 if e.code == 401:
601 raise error.Abort(_('authorization failed'))
628 raise error.Abort(_('authorization failed'))
602
629
603 raise
630 raise
604 except httplib.HTTPException as e:
631 except httplib.HTTPException as e:
605 self.ui.traceback()
632 self.ui.traceback()
606 raise IOError(None, e)
633 raise IOError(None, e)
607
634
608 # TODO validate response type, wrap response to handle I/O errors.
635 # TODO validate response type, wrap response to handle I/O errors.
609 # TODO more robust frame receiver.
636 # TODO more robust frame receiver.
610 results = []
637 results = []
611
638
612 while True:
639 while True:
613 frame = wireprotoframing.readframe(res)
640 frame = wireprotoframing.readframe(res)
614 if frame is None:
641 if frame is None:
615 break
642 break
616
643
617 self.ui.note(_('received %r\n') % frame)
644 self.ui.note(_('received %r\n') % frame)
618
645
619 action, meta = reactor.onframerecv(frame)
646 action, meta = reactor.onframerecv(frame)
620
647
621 if action == 'responsedata':
648 if action == 'responsedata':
622 if meta['cbor']:
649 if meta['cbor']:
623 payload = util.bytesio(meta['data'])
650 payload = util.bytesio(meta['data'])
624
651
625 decoder = cbor.CBORDecoder(payload)
652 decoder = cbor.CBORDecoder(payload)
626 while payload.tell() + 1 < len(meta['data']):
653 while payload.tell() + 1 < len(meta['data']):
627 results.append(decoder.decode())
654 results.append(decoder.decode())
628 else:
655 else:
629 results.append(meta['data'])
656 results.append(meta['data'])
630 else:
657 else:
631 error.ProgrammingError('unhandled action: %s' % action)
658 error.ProgrammingError('unhandled action: %s' % action)
632
659
633 return results
660 return results
634
661
635 # Registry of API service names to metadata about peers that handle it.
662 # Registry of API service names to metadata about peers that handle it.
636 #
663 #
637 # The following keys are meaningful:
664 # The following keys are meaningful:
638 #
665 #
639 # init
666 # init
640 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
667 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
641 # apidescriptor) to create a peer.
668 # apidescriptor) to create a peer.
642 #
669 #
643 # priority
670 # priority
644 # Integer priority for the service. If we could choose from multiple
671 # Integer priority for the service. If we could choose from multiple
645 # services, we choose the one with the highest priority.
672 # services, we choose the one with the highest priority.
646 API_PEERS = {
673 API_PEERS = {
647 wireprototypes.HTTPV2: {
674 wireprototypes.HTTPV2: {
648 'init': httpv2peer,
675 'init': httpv2peer,
649 'priority': 50,
676 'priority': 50,
650 },
677 },
651 }
678 }
652
679
653 def performhandshake(ui, url, opener, requestbuilder):
680 def performhandshake(ui, url, opener, requestbuilder):
654 # The handshake is a request to the capabilities command.
681 # The handshake is a request to the capabilities command.
655
682
656 caps = None
683 caps = None
657 def capable(x):
684 def capable(x):
658 raise error.ProgrammingError('should not be called')
685 raise error.ProgrammingError('should not be called')
659
686
660 args = {}
687 args = {}
661
688
662 # The client advertises support for newer protocols by adding an
689 # The client advertises support for newer protocols by adding an
663 # X-HgUpgrade-* header with a list of supported APIs and an
690 # X-HgUpgrade-* header with a list of supported APIs and an
664 # X-HgProto-* header advertising which serializing formats it supports.
691 # X-HgProto-* header advertising which serializing formats it supports.
665 # We only support the HTTP version 2 transport and CBOR responses for
692 # We only support the HTTP version 2 transport and CBOR responses for
666 # now.
693 # now.
667 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
694 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
668
695
669 if advertisev2:
696 if advertisev2:
670 args['headers'] = {
697 args['headers'] = {
671 r'X-HgProto-1': r'cbor',
698 r'X-HgProto-1': r'cbor',
672 }
699 }
673
700
674 args['headers'].update(
701 args['headers'].update(
675 encodevalueinheaders(' '.join(sorted(API_PEERS)),
702 encodevalueinheaders(' '.join(sorted(API_PEERS)),
676 'X-HgUpgrade',
703 'X-HgUpgrade',
677 # We don't know the header limit this early.
704 # We don't know the header limit this early.
678 # So make it small.
705 # So make it small.
679 1024))
706 1024))
680
707
681 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
708 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
682 capable, url, 'capabilities',
709 capable, url, 'capabilities',
683 args)
710 args)
684
711
685 resp = sendrequest(ui, opener, req)
712 resp = sendrequest(ui, opener, req)
686
713
687 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
714 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
688 compressible=False,
715 compressible=False,
689 allowcbor=advertisev2)
716 allowcbor=advertisev2)
690
717
691 try:
718 try:
692 rawdata = resp.read()
719 rawdata = resp.read()
693 finally:
720 finally:
694 resp.close()
721 resp.close()
695
722
696 if not ct.startswith('application/mercurial-'):
723 if not ct.startswith('application/mercurial-'):
697 raise error.ProgrammingError('unexpected content-type: %s' % ct)
724 raise error.ProgrammingError('unexpected content-type: %s' % ct)
698
725
699 if advertisev2:
726 if advertisev2:
700 if ct == 'application/mercurial-cbor':
727 if ct == 'application/mercurial-cbor':
701 try:
728 try:
702 info = cbor.loads(rawdata)
729 info = cbor.loads(rawdata)
703 except cbor.CBORDecodeError:
730 except cbor.CBORDecodeError:
704 raise error.Abort(_('error decoding CBOR from remote server'),
731 raise error.Abort(_('error decoding CBOR from remote server'),
705 hint=_('try again and consider contacting '
732 hint=_('try again and consider contacting '
706 'the server operator'))
733 'the server operator'))
707
734
708 # We got a legacy response. That's fine.
735 # We got a legacy response. That's fine.
709 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
736 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
710 info = {
737 info = {
711 'v1capabilities': set(rawdata.split())
738 'v1capabilities': set(rawdata.split())
712 }
739 }
713
740
714 else:
741 else:
715 raise error.RepoError(
742 raise error.RepoError(
716 _('unexpected response type from server: %s') % ct)
743 _('unexpected response type from server: %s') % ct)
717 else:
744 else:
718 info = {
745 info = {
719 'v1capabilities': set(rawdata.split())
746 'v1capabilities': set(rawdata.split())
720 }
747 }
721
748
722 return respurl, info
749 return respurl, info
723
750
724 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
751 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
725 """Construct an appropriate HTTP peer instance.
752 """Construct an appropriate HTTP peer instance.
726
753
727 ``opener`` is an ``url.opener`` that should be used to establish
754 ``opener`` is an ``url.opener`` that should be used to establish
728 connections, perform HTTP requests.
755 connections, perform HTTP requests.
729
756
730 ``requestbuilder`` is the type used for constructing HTTP requests.
757 ``requestbuilder`` is the type used for constructing HTTP requests.
731 It exists as an argument so extensions can override the default.
758 It exists as an argument so extensions can override the default.
732 """
759 """
733 u = util.url(path)
760 u = util.url(path)
734 if u.query or u.fragment:
761 if u.query or u.fragment:
735 raise error.Abort(_('unsupported URL component: "%s"') %
762 raise error.Abort(_('unsupported URL component: "%s"') %
736 (u.query or u.fragment))
763 (u.query or u.fragment))
737
764
738 # urllib cannot handle URLs with embedded user or passwd.
765 # urllib cannot handle URLs with embedded user or passwd.
739 url, authinfo = u.authinfo()
766 url, authinfo = u.authinfo()
740 ui.debug('using %s\n' % url)
767 ui.debug('using %s\n' % url)
741
768
742 opener = opener or urlmod.opener(ui, authinfo)
769 opener = opener or urlmod.opener(ui, authinfo)
743
770
744 respurl, info = performhandshake(ui, url, opener, requestbuilder)
771 respurl, info = performhandshake(ui, url, opener, requestbuilder)
745
772
746 # Given the intersection of APIs that both we and the server support,
773 # Given the intersection of APIs that both we and the server support,
747 # sort by their advertised priority and pick the first one.
774 # sort by their advertised priority and pick the first one.
748 #
775 #
749 # TODO consider making this request-based and interface driven. For
776 # TODO consider making this request-based and interface driven. For
750 # example, the caller could say "I want a peer that does X." It's quite
777 # example, the caller could say "I want a peer that does X." It's quite
751 # possible that not all peers would do that. Since we know the service
778 # possible that not all peers would do that. Since we know the service
752 # capabilities, we could filter out services not meeting the
779 # capabilities, we could filter out services not meeting the
753 # requirements. Possibly by consulting the interfaces defined by the
780 # requirements. Possibly by consulting the interfaces defined by the
754 # peer type.
781 # peer type.
755 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
782 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
756
783
757 preferredchoices = sorted(apipeerchoices,
784 preferredchoices = sorted(apipeerchoices,
758 key=lambda x: API_PEERS[x]['priority'],
785 key=lambda x: API_PEERS[x]['priority'],
759 reverse=True)
786 reverse=True)
760
787
761 for service in preferredchoices:
788 for service in preferredchoices:
762 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
789 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
763
790
764 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
791 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
765 requestbuilder,
792 requestbuilder,
766 info['apis'][service])
793 info['apis'][service])
767
794
768 # Failed to construct an API peer. Fall back to legacy.
795 # Failed to construct an API peer. Fall back to legacy.
769 return httppeer(ui, path, respurl, opener, requestbuilder,
796 return httppeer(ui, path, respurl, opener, requestbuilder,
770 info['v1capabilities'])
797 info['v1capabilities'])
771
798
772 def instance(ui, path, create):
799 def instance(ui, path, create):
773 if create:
800 if create:
774 raise error.Abort(_('cannot create new http repository'))
801 raise error.Abort(_('cannot create new http repository'))
775 try:
802 try:
776 if path.startswith('https:') and not urlmod.has_https:
803 if path.startswith('https:') and not urlmod.has_https:
777 raise error.Abort(_('Python support for SSL and HTTPS '
804 raise error.Abort(_('Python support for SSL and HTTPS '
778 'is not installed'))
805 'is not installed'))
779
806
780 inst = makepeer(ui, path)
807 inst = makepeer(ui, path)
781
808
782 return inst
809 return inst
783 except error.RepoError as httpexception:
810 except error.RepoError as httpexception:
784 try:
811 try:
785 r = statichttprepo.instance(ui, "static-" + path, create)
812 r = statichttprepo.instance(ui, "static-" + path, create)
786 ui.note(_('(falling back to static-http)\n'))
813 ui.note(_('(falling back to static-http)\n'))
787 return r
814 return r
788 except error.RepoError:
815 except error.RepoError:
789 raise httpexception # use the original http RepoError instead
816 raise httpexception # use the original http RepoError instead
@@ -1,152 +1,154 b''
1 # Test that certain objects conform to well-defined interfaces.
1 # Test that certain objects conform to well-defined interfaces.
2
2
3 from __future__ import absolute_import, print_function
3 from __future__ import absolute_import, print_function
4
4
5 import os
5 import os
6
6
7 from mercurial.thirdparty.zope import (
7 from mercurial.thirdparty.zope import (
8 interface as zi,
8 interface as zi,
9 )
9 )
10 from mercurial.thirdparty.zope.interface import (
10 from mercurial.thirdparty.zope.interface import (
11 verify as ziverify,
11 verify as ziverify,
12 )
12 )
13 from mercurial import (
13 from mercurial import (
14 bundlerepo,
14 bundlerepo,
15 filelog,
15 filelog,
16 httppeer,
16 httppeer,
17 localrepo,
17 localrepo,
18 repository,
18 repository,
19 sshpeer,
19 sshpeer,
20 statichttprepo,
20 statichttprepo,
21 ui as uimod,
21 ui as uimod,
22 unionrepo,
22 unionrepo,
23 vfs as vfsmod,
23 vfs as vfsmod,
24 wireprotoserver,
24 wireprotoserver,
25 wireprototypes,
25 wireprototypes,
26 wireprotov2server,
26 wireprotov2server,
27 )
27 )
28
28
29 rootdir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
29 rootdir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
30
30
31 def checkzobject(o, allowextra=False):
31 def checkzobject(o, allowextra=False):
32 """Verify an object with a zope interface."""
32 """Verify an object with a zope interface."""
33 ifaces = zi.providedBy(o)
33 ifaces = zi.providedBy(o)
34 if not ifaces:
34 if not ifaces:
35 print('%r does not provide any zope interfaces' % o)
35 print('%r does not provide any zope interfaces' % o)
36 return
36 return
37
37
38 # Run zope.interface's built-in verification routine. This verifies that
38 # Run zope.interface's built-in verification routine. This verifies that
39 # everything that is supposed to be present is present.
39 # everything that is supposed to be present is present.
40 for iface in ifaces:
40 for iface in ifaces:
41 ziverify.verifyObject(iface, o)
41 ziverify.verifyObject(iface, o)
42
42
43 if allowextra:
43 if allowextra:
44 return
44 return
45
45
46 # Now verify that the object provides no extra public attributes that
46 # Now verify that the object provides no extra public attributes that
47 # aren't declared as part of interfaces.
47 # aren't declared as part of interfaces.
48 allowed = set()
48 allowed = set()
49 for iface in ifaces:
49 for iface in ifaces:
50 allowed |= set(iface.names(all=True))
50 allowed |= set(iface.names(all=True))
51
51
52 public = {a for a in dir(o) if not a.startswith('_')}
52 public = {a for a in dir(o) if not a.startswith('_')}
53
53
54 for attr in sorted(public - allowed):
54 for attr in sorted(public - allowed):
55 print('public attribute not declared in interfaces: %s.%s' % (
55 print('public attribute not declared in interfaces: %s.%s' % (
56 o.__class__.__name__, attr))
56 o.__class__.__name__, attr))
57
57
58 # Facilitates testing localpeer.
58 # Facilitates testing localpeer.
59 class dummyrepo(object):
59 class dummyrepo(object):
60 def __init__(self):
60 def __init__(self):
61 self.ui = uimod.ui()
61 self.ui = uimod.ui()
62 def filtered(self, name):
62 def filtered(self, name):
63 pass
63 pass
64 def _restrictcapabilities(self, caps):
64 def _restrictcapabilities(self, caps):
65 pass
65 pass
66
66
67 class dummyopener(object):
67 class dummyopener(object):
68 handlers = []
68 handlers = []
69
69
70 # Facilitates testing sshpeer without requiring a server.
70 # Facilitates testing sshpeer without requiring a server.
71 class badpeer(httppeer.httppeer):
71 class badpeer(httppeer.httppeer):
72 def __init__(self):
72 def __init__(self):
73 super(badpeer, self).__init__(None, None, None, dummyopener(), None,
73 super(badpeer, self).__init__(None, None, None, dummyopener(), None,
74 None)
74 None)
75 self.badattribute = True
75 self.badattribute = True
76
76
77 def badmethod(self):
77 def badmethod(self):
78 pass
78 pass
79
79
80 class dummypipe(object):
80 class dummypipe(object):
81 def close(self):
81 def close(self):
82 pass
82 pass
83
83
84 def main():
84 def main():
85 ui = uimod.ui()
85 ui = uimod.ui()
86 # Needed so we can open a local repo with obsstore without a warning.
86 # Needed so we can open a local repo with obsstore without a warning.
87 ui.setconfig('experimental', 'evolution.createmarkers', True)
87 ui.setconfig('experimental', 'evolution.createmarkers', True)
88
88
89 checkzobject(badpeer())
89 checkzobject(badpeer())
90
90
91 ziverify.verifyClass(repository.ipeerbaselegacycommands,
91 ziverify.verifyClass(repository.ipeerbaselegacycommands,
92 httppeer.httppeer)
92 httppeer.httppeer)
93 checkzobject(httppeer.httppeer(None, None, None, dummyopener(), None, None))
93 checkzobject(httppeer.httppeer(None, None, None, dummyopener(), None, None))
94
94
95 ziverify.verifyClass(repository.ipeerconnection,
95 ziverify.verifyClass(repository.ipeerconnection,
96 httppeer.httpv2peer)
96 httppeer.httpv2peer)
97 ziverify.verifyClass(repository.ipeercapabilities,
98 httppeer.httpv2peer)
97 checkzobject(httppeer.httpv2peer(None, '', None, None, None, None))
99 checkzobject(httppeer.httpv2peer(None, '', None, None, None, None))
98
100
99 ziverify.verifyClass(repository.ipeerbase,
101 ziverify.verifyClass(repository.ipeerbase,
100 localrepo.localpeer)
102 localrepo.localpeer)
101 checkzobject(localrepo.localpeer(dummyrepo()))
103 checkzobject(localrepo.localpeer(dummyrepo()))
102
104
103 ziverify.verifyClass(repository.ipeerbaselegacycommands,
105 ziverify.verifyClass(repository.ipeerbaselegacycommands,
104 sshpeer.sshv1peer)
106 sshpeer.sshv1peer)
105 checkzobject(sshpeer.sshv1peer(ui, 'ssh://localhost/foo', None, dummypipe(),
107 checkzobject(sshpeer.sshv1peer(ui, 'ssh://localhost/foo', None, dummypipe(),
106 dummypipe(), None, None))
108 dummypipe(), None, None))
107
109
108 ziverify.verifyClass(repository.ipeerbaselegacycommands,
110 ziverify.verifyClass(repository.ipeerbaselegacycommands,
109 sshpeer.sshv2peer)
111 sshpeer.sshv2peer)
110 checkzobject(sshpeer.sshv2peer(ui, 'ssh://localhost/foo', None, dummypipe(),
112 checkzobject(sshpeer.sshv2peer(ui, 'ssh://localhost/foo', None, dummypipe(),
111 dummypipe(), None, None))
113 dummypipe(), None, None))
112
114
113 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
115 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
114 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
116 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
115
117
116 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
118 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
117 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
119 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
118
120
119 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
121 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
120 checkzobject(unionrepo.unionpeer(dummyrepo()))
122 checkzobject(unionrepo.unionpeer(dummyrepo()))
121
123
122 ziverify.verifyClass(repository.completelocalrepository,
124 ziverify.verifyClass(repository.completelocalrepository,
123 localrepo.localrepository)
125 localrepo.localrepository)
124 repo = localrepo.localrepository(ui, rootdir)
126 repo = localrepo.localrepository(ui, rootdir)
125 checkzobject(repo)
127 checkzobject(repo)
126
128
127 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
129 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
128 wireprotoserver.sshv1protocolhandler)
130 wireprotoserver.sshv1protocolhandler)
129 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
131 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
130 wireprotoserver.sshv2protocolhandler)
132 wireprotoserver.sshv2protocolhandler)
131 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
133 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
132 wireprotoserver.httpv1protocolhandler)
134 wireprotoserver.httpv1protocolhandler)
133 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
135 ziverify.verifyClass(wireprototypes.baseprotocolhandler,
134 wireprotov2server.httpv2protocolhandler)
136 wireprotov2server.httpv2protocolhandler)
135
137
136 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
138 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
137 checkzobject(sshv1)
139 checkzobject(sshv1)
138 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
140 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
139 checkzobject(sshv2)
141 checkzobject(sshv2)
140
142
141 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
143 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
142 checkzobject(httpv1)
144 checkzobject(httpv1)
143 httpv2 = wireprotov2server.httpv2protocolhandler(None, None)
145 httpv2 = wireprotov2server.httpv2protocolhandler(None, None)
144 checkzobject(httpv2)
146 checkzobject(httpv2)
145
147
146 ziverify.verifyClass(repository.ifilestorage, filelog.filelog)
148 ziverify.verifyClass(repository.ifilestorage, filelog.filelog)
147
149
148 vfs = vfsmod.vfs('.')
150 vfs = vfsmod.vfs('.')
149 fl = filelog.filelog(vfs, 'dummy.i')
151 fl = filelog.filelog(vfs, 'dummy.i')
150 checkzobject(fl, allowextra=True)
152 checkzobject(fl, allowextra=True)
151
153
152 main()
154 main()
General Comments 0
You need to be logged in to leave comments. Login now