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