##// END OF EJS Templates
httppeer: extract common response handling into own function...
Gregory Szorc -
r37569:946eb204 default
parent child Browse files
Show More
@@ -1,635 +1,643
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 . import (
22 from . import (
23 bundle2,
23 bundle2,
24 error,
24 error,
25 httpconnection,
25 httpconnection,
26 pycompat,
26 pycompat,
27 statichttprepo,
27 statichttprepo,
28 url as urlmod,
28 url as urlmod,
29 util,
29 util,
30 wireproto,
30 wireproto,
31 wireprotoframing,
31 wireprotoframing,
32 wireprotov2server,
32 wireprotov2server,
33 )
33 )
34
34
35 httplib = util.httplib
35 httplib = util.httplib
36 urlerr = util.urlerr
36 urlerr = util.urlerr
37 urlreq = util.urlreq
37 urlreq = util.urlreq
38
38
39 def encodevalueinheaders(value, header, limit):
39 def encodevalueinheaders(value, header, limit):
40 """Encode a string value into multiple HTTP headers.
40 """Encode a string value into multiple HTTP headers.
41
41
42 ``value`` will be encoded into 1 or more HTTP headers with the names
42 ``value`` will be encoded into 1 or more HTTP headers with the names
43 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
43 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
44 name + value will be at most ``limit`` bytes long.
44 name + value will be at most ``limit`` bytes long.
45
45
46 Returns an iterable of 2-tuples consisting of header names and
46 Returns an iterable of 2-tuples consisting of header names and
47 values as native strings.
47 values as native strings.
48 """
48 """
49 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
49 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
50 # not bytes. This function always takes bytes in as arguments.
50 # not bytes. This function always takes bytes in as arguments.
51 fmt = pycompat.strurl(header) + r'-%s'
51 fmt = pycompat.strurl(header) + r'-%s'
52 # Note: it is *NOT* a bug that the last bit here is a bytestring
52 # Note: it is *NOT* a bug that the last bit here is a bytestring
53 # and not a unicode: we're just getting the encoded length anyway,
53 # and not a unicode: we're just getting the encoded length anyway,
54 # and using an r-string to make it portable between Python 2 and 3
54 # and using an r-string to make it portable between Python 2 and 3
55 # doesn't work because then the \r is a literal backslash-r
55 # doesn't work because then the \r is a literal backslash-r
56 # instead of a carriage return.
56 # instead of a carriage return.
57 valuelen = limit - len(fmt % r'000') - len(': \r\n')
57 valuelen = limit - len(fmt % r'000') - len(': \r\n')
58 result = []
58 result = []
59
59
60 n = 0
60 n = 0
61 for i in xrange(0, len(value), valuelen):
61 for i in xrange(0, len(value), valuelen):
62 n += 1
62 n += 1
63 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
63 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
64
64
65 return result
65 return result
66
66
67 def _wraphttpresponse(resp):
67 def _wraphttpresponse(resp):
68 """Wrap an HTTPResponse with common error handlers.
68 """Wrap an HTTPResponse with common error handlers.
69
69
70 This ensures that any I/O from any consumer raises the appropriate
70 This ensures that any I/O from any consumer raises the appropriate
71 error and messaging.
71 error and messaging.
72 """
72 """
73 origread = resp.read
73 origread = resp.read
74
74
75 class readerproxy(resp.__class__):
75 class readerproxy(resp.__class__):
76 def read(self, size=None):
76 def read(self, size=None):
77 try:
77 try:
78 return origread(size)
78 return origread(size)
79 except httplib.IncompleteRead as e:
79 except httplib.IncompleteRead as e:
80 # e.expected is an integer if length known or None otherwise.
80 # e.expected is an integer if length known or None otherwise.
81 if e.expected:
81 if e.expected:
82 msg = _('HTTP request error (incomplete response; '
82 msg = _('HTTP request error (incomplete response; '
83 'expected %d bytes got %d)') % (e.expected,
83 'expected %d bytes got %d)') % (e.expected,
84 len(e.partial))
84 len(e.partial))
85 else:
85 else:
86 msg = _('HTTP request error (incomplete response)')
86 msg = _('HTTP request error (incomplete response)')
87
87
88 raise error.PeerTransportError(
88 raise error.PeerTransportError(
89 msg,
89 msg,
90 hint=_('this may be an intermittent network failure; '
90 hint=_('this may be an intermittent network failure; '
91 'if the error persists, consider contacting the '
91 'if the error persists, consider contacting the '
92 'network or server operator'))
92 'network or server operator'))
93 except httplib.HTTPException as e:
93 except httplib.HTTPException as e:
94 raise error.PeerTransportError(
94 raise error.PeerTransportError(
95 _('HTTP request error (%s)') % e,
95 _('HTTP request error (%s)') % e,
96 hint=_('this may be an intermittent network failure; '
96 hint=_('this may be an intermittent network failure; '
97 'if the error persists, consider contacting the '
97 'if the error persists, consider contacting the '
98 'network or server operator'))
98 'network or server operator'))
99
99
100 resp.__class__ = readerproxy
100 resp.__class__ = readerproxy
101
101
102 class _multifile(object):
102 class _multifile(object):
103 def __init__(self, *fileobjs):
103 def __init__(self, *fileobjs):
104 for f in fileobjs:
104 for f in fileobjs:
105 if not util.safehasattr(f, 'length'):
105 if not util.safehasattr(f, 'length'):
106 raise ValueError(
106 raise ValueError(
107 '_multifile only supports file objects that '
107 '_multifile only supports file objects that '
108 'have a length but this one does not:', type(f), f)
108 'have a length but this one does not:', type(f), f)
109 self._fileobjs = fileobjs
109 self._fileobjs = fileobjs
110 self._index = 0
110 self._index = 0
111
111
112 @property
112 @property
113 def length(self):
113 def length(self):
114 return sum(f.length for f in self._fileobjs)
114 return sum(f.length for f in self._fileobjs)
115
115
116 def read(self, amt=None):
116 def read(self, amt=None):
117 if amt <= 0:
117 if amt <= 0:
118 return ''.join(f.read() for f in self._fileobjs)
118 return ''.join(f.read() for f in self._fileobjs)
119 parts = []
119 parts = []
120 while amt and self._index < len(self._fileobjs):
120 while amt and self._index < len(self._fileobjs):
121 parts.append(self._fileobjs[self._index].read(amt))
121 parts.append(self._fileobjs[self._index].read(amt))
122 got = len(parts[-1])
122 got = len(parts[-1])
123 if got < amt:
123 if got < amt:
124 self._index += 1
124 self._index += 1
125 amt -= got
125 amt -= got
126 return ''.join(parts)
126 return ''.join(parts)
127
127
128 def seek(self, offset, whence=os.SEEK_SET):
128 def seek(self, offset, whence=os.SEEK_SET):
129 if whence != os.SEEK_SET:
129 if whence != os.SEEK_SET:
130 raise NotImplementedError(
130 raise NotImplementedError(
131 '_multifile does not support anything other'
131 '_multifile does not support anything other'
132 ' than os.SEEK_SET for whence on seek()')
132 ' than os.SEEK_SET for whence on seek()')
133 if offset != 0:
133 if offset != 0:
134 raise NotImplementedError(
134 raise NotImplementedError(
135 '_multifile only supports seeking to start, but that '
135 '_multifile only supports seeking to start, but that '
136 'could be fixed if you need it')
136 'could be fixed if you need it')
137 for f in self._fileobjs:
137 for f in self._fileobjs:
138 f.seek(0)
138 f.seek(0)
139 self._index = 0
139 self._index = 0
140
140
141 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
141 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
142 repobaseurl, cmd, args):
142 repobaseurl, cmd, args):
143 """Make an HTTP request to run a command for a version 1 client.
143 """Make an HTTP request to run a command for a version 1 client.
144
144
145 ``caps`` is a set of known server capabilities. The value may be
145 ``caps`` is a set of known server capabilities. The value may be
146 None if capabilities are not yet known.
146 None if capabilities are not yet known.
147
147
148 ``capablefn`` is a function to evaluate a capability.
148 ``capablefn`` is a function to evaluate a capability.
149
149
150 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
150 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
151 raw data to pass to it.
151 raw data to pass to it.
152 """
152 """
153 if cmd == 'pushkey':
153 if cmd == 'pushkey':
154 args['data'] = ''
154 args['data'] = ''
155 data = args.pop('data', None)
155 data = args.pop('data', None)
156 headers = args.pop('headers', {})
156 headers = args.pop('headers', {})
157
157
158 ui.debug("sending %s command\n" % cmd)
158 ui.debug("sending %s command\n" % cmd)
159 q = [('cmd', cmd)]
159 q = [('cmd', cmd)]
160 headersize = 0
160 headersize = 0
161 varyheaders = []
161 varyheaders = []
162 # Important: don't use self.capable() here or else you end up
162 # Important: don't use self.capable() here or else you end up
163 # with infinite recursion when trying to look up capabilities
163 # with infinite recursion when trying to look up capabilities
164 # for the first time.
164 # for the first time.
165 postargsok = caps is not None and 'httppostargs' in caps
165 postargsok = caps is not None and 'httppostargs' in caps
166
166
167 # Send arguments via POST.
167 # Send arguments via POST.
168 if postargsok and args:
168 if postargsok and args:
169 strargs = urlreq.urlencode(sorted(args.items()))
169 strargs = urlreq.urlencode(sorted(args.items()))
170 if not data:
170 if not data:
171 data = strargs
171 data = strargs
172 else:
172 else:
173 if isinstance(data, bytes):
173 if isinstance(data, bytes):
174 i = io.BytesIO(data)
174 i = io.BytesIO(data)
175 i.length = len(data)
175 i.length = len(data)
176 data = i
176 data = i
177 argsio = io.BytesIO(strargs)
177 argsio = io.BytesIO(strargs)
178 argsio.length = len(strargs)
178 argsio.length = len(strargs)
179 data = _multifile(argsio, data)
179 data = _multifile(argsio, data)
180 headers[r'X-HgArgs-Post'] = len(strargs)
180 headers[r'X-HgArgs-Post'] = len(strargs)
181 elif args:
181 elif args:
182 # Calling self.capable() can infinite loop if we are calling
182 # Calling self.capable() can infinite loop if we are calling
183 # "capabilities". But that command should never accept wire
183 # "capabilities". But that command should never accept wire
184 # protocol arguments. So this should never happen.
184 # protocol arguments. So this should never happen.
185 assert cmd != 'capabilities'
185 assert cmd != 'capabilities'
186 httpheader = capablefn('httpheader')
186 httpheader = capablefn('httpheader')
187 if httpheader:
187 if httpheader:
188 headersize = int(httpheader.split(',', 1)[0])
188 headersize = int(httpheader.split(',', 1)[0])
189
189
190 # Send arguments via HTTP headers.
190 # Send arguments via HTTP headers.
191 if headersize > 0:
191 if headersize > 0:
192 # The headers can typically carry more data than the URL.
192 # The headers can typically carry more data than the URL.
193 encargs = urlreq.urlencode(sorted(args.items()))
193 encargs = urlreq.urlencode(sorted(args.items()))
194 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
194 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
195 headersize):
195 headersize):
196 headers[header] = value
196 headers[header] = value
197 varyheaders.append(header)
197 varyheaders.append(header)
198 # Send arguments via query string (Mercurial <1.9).
198 # Send arguments via query string (Mercurial <1.9).
199 else:
199 else:
200 q += sorted(args.items())
200 q += sorted(args.items())
201
201
202 qs = '?%s' % urlreq.urlencode(q)
202 qs = '?%s' % urlreq.urlencode(q)
203 cu = "%s%s" % (repobaseurl, qs)
203 cu = "%s%s" % (repobaseurl, qs)
204 size = 0
204 size = 0
205 if util.safehasattr(data, 'length'):
205 if util.safehasattr(data, 'length'):
206 size = data.length
206 size = data.length
207 elif data is not None:
207 elif data is not None:
208 size = len(data)
208 size = len(data)
209 if data is not None and r'Content-Type' not in headers:
209 if data is not None and r'Content-Type' not in headers:
210 headers[r'Content-Type'] = r'application/mercurial-0.1'
210 headers[r'Content-Type'] = r'application/mercurial-0.1'
211
211
212 # Tell the server we accept application/mercurial-0.2 and multiple
212 # Tell the server we accept application/mercurial-0.2 and multiple
213 # compression formats if the server is capable of emitting those
213 # compression formats if the server is capable of emitting those
214 # payloads.
214 # payloads.
215 protoparams = {'partial-pull'}
215 protoparams = {'partial-pull'}
216
216
217 mediatypes = set()
217 mediatypes = set()
218 if caps is not None:
218 if caps is not None:
219 mt = capablefn('httpmediatype')
219 mt = capablefn('httpmediatype')
220 if mt:
220 if mt:
221 protoparams.add('0.1')
221 protoparams.add('0.1')
222 mediatypes = set(mt.split(','))
222 mediatypes = set(mt.split(','))
223
223
224 if '0.2tx' in mediatypes:
224 if '0.2tx' in mediatypes:
225 protoparams.add('0.2')
225 protoparams.add('0.2')
226
226
227 if '0.2tx' in mediatypes and capablefn('compression'):
227 if '0.2tx' in mediatypes and capablefn('compression'):
228 # We /could/ compare supported compression formats and prune
228 # We /could/ compare supported compression formats and prune
229 # non-mutually supported or error if nothing is mutually supported.
229 # non-mutually supported or error if nothing is mutually supported.
230 # For now, send the full list to the server and have it error.
230 # For now, send the full list to the server and have it error.
231 comps = [e.wireprotosupport().name for e in
231 comps = [e.wireprotosupport().name for e in
232 util.compengines.supportedwireengines(util.CLIENTROLE)]
232 util.compengines.supportedwireengines(util.CLIENTROLE)]
233 protoparams.add('comp=%s' % ','.join(comps))
233 protoparams.add('comp=%s' % ','.join(comps))
234
234
235 if protoparams:
235 if protoparams:
236 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
236 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
237 'X-HgProto',
237 'X-HgProto',
238 headersize or 1024)
238 headersize or 1024)
239 for header, value in protoheaders:
239 for header, value in protoheaders:
240 headers[header] = value
240 headers[header] = value
241 varyheaders.append(header)
241 varyheaders.append(header)
242
242
243 if varyheaders:
243 if varyheaders:
244 headers[r'Vary'] = r','.join(varyheaders)
244 headers[r'Vary'] = r','.join(varyheaders)
245
245
246 req = requestbuilder(pycompat.strurl(cu), data, headers)
246 req = requestbuilder(pycompat.strurl(cu), data, headers)
247
247
248 if data is not None:
248 if data is not None:
249 ui.debug("sending %d bytes\n" % size)
249 ui.debug("sending %d bytes\n" % size)
250 req.add_unredirected_header(r'Content-Length', r'%d' % size)
250 req.add_unredirected_header(r'Content-Length', r'%d' % size)
251
251
252 return req, cu, qs
252 return req, cu, qs
253
253
254 def sendrequest(ui, opener, req):
254 def sendrequest(ui, opener, req):
255 """Send a prepared HTTP request.
255 """Send a prepared HTTP request.
256
256
257 Returns the response object.
257 Returns the response object.
258 """
258 """
259 if (ui.debugflag
259 if (ui.debugflag
260 and ui.configbool('devel', 'debug.peer-request')):
260 and ui.configbool('devel', 'debug.peer-request')):
261 dbg = ui.debug
261 dbg = ui.debug
262 line = 'devel-peer-request: %s\n'
262 line = 'devel-peer-request: %s\n'
263 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
263 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
264 hgargssize = None
264 hgargssize = None
265
265
266 for header, value in sorted(req.header_items()):
266 for header, value in sorted(req.header_items()):
267 if header.startswith('X-hgarg-'):
267 if header.startswith('X-hgarg-'):
268 if hgargssize is None:
268 if hgargssize is None:
269 hgargssize = 0
269 hgargssize = 0
270 hgargssize += len(value)
270 hgargssize += len(value)
271 else:
271 else:
272 dbg(line % ' %s %s' % (header, value))
272 dbg(line % ' %s %s' % (header, value))
273
273
274 if hgargssize is not None:
274 if hgargssize is not None:
275 dbg(line % ' %d bytes of commands arguments in headers'
275 dbg(line % ' %d bytes of commands arguments in headers'
276 % hgargssize)
276 % hgargssize)
277
277
278 if req.has_data():
278 if req.has_data():
279 data = req.get_data()
279 data = req.get_data()
280 length = getattr(data, 'length', None)
280 length = getattr(data, 'length', None)
281 if length is None:
281 if length is None:
282 length = len(data)
282 length = len(data)
283 dbg(line % ' %d bytes of data' % length)
283 dbg(line % ' %d bytes of data' % length)
284
284
285 start = util.timer()
285 start = util.timer()
286
286
287 try:
287 try:
288 res = opener.open(req)
288 res = opener.open(req)
289 except urlerr.httperror as inst:
289 except urlerr.httperror as inst:
290 if inst.code == 401:
290 if inst.code == 401:
291 raise error.Abort(_('authorization failed'))
291 raise error.Abort(_('authorization failed'))
292 raise
292 raise
293 except httplib.HTTPException as inst:
293 except httplib.HTTPException as inst:
294 ui.debug('http error requesting %s\n' %
294 ui.debug('http error requesting %s\n' %
295 util.hidepassword(req.get_full_url()))
295 util.hidepassword(req.get_full_url()))
296 ui.traceback()
296 ui.traceback()
297 raise IOError(None, inst)
297 raise IOError(None, inst)
298 finally:
298 finally:
299 if ui.configbool('devel', 'debug.peer-request'):
299 if ui.configbool('devel', 'debug.peer-request'):
300 dbg(line % ' finished in %.4f seconds (%s)'
300 dbg(line % ' finished in %.4f seconds (%s)'
301 % (util.timer() - start, res.code))
301 % (util.timer() - start, res.code))
302
302
303 # Insert error handlers for common I/O failures.
303 # Insert error handlers for common I/O failures.
304 _wraphttpresponse(res)
304 _wraphttpresponse(res)
305
305
306 return res
306 return res
307
307
308 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible):
309 # record the url we got redirected to
310 respurl = pycompat.bytesurl(resp.geturl())
311 if respurl.endswith(qs):
312 respurl = respurl[:-len(qs)]
313 if baseurl.rstrip('/') != respurl.rstrip('/'):
314 if not ui.quiet:
315 ui.warn(_('real URL is %s\n') % respurl)
316
317 try:
318 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
319 except AttributeError:
320 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
321
322 safeurl = util.hidepassword(baseurl)
323 if proto.startswith('application/hg-error'):
324 raise error.OutOfBandError(resp.read())
325 # accept old "text/plain" and "application/hg-changegroup" for now
326 if not (proto.startswith('application/mercurial-') or
327 (proto.startswith('text/plain')
328 and not resp.headers.get('content-length')) or
329 proto.startswith('application/hg-changegroup')):
330 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
331 raise error.RepoError(
332 _("'%s' does not appear to be an hg repository:\n"
333 "---%%<--- (%s)\n%s\n---%%<---\n")
334 % (safeurl, proto or 'no content-type', resp.read(1024)))
335
336 if proto.startswith('application/mercurial-'):
337 try:
338 version = proto.split('-', 1)[1]
339 version_info = tuple([int(n) for n in version.split('.')])
340 except ValueError:
341 raise error.RepoError(_("'%s' sent a broken Content-Type "
342 "header (%s)") % (safeurl, proto))
343
344 # TODO consider switching to a decompression reader that uses
345 # generators.
346 if version_info == (0, 1):
347 if compressible:
348 resp = util.compengines['zlib'].decompressorreader(resp)
349
350 return respurl, resp
351
352 elif version_info == (0, 2):
353 # application/mercurial-0.2 always identifies the compression
354 # engine in the payload header.
355 elen = struct.unpack('B', resp.read(1))[0]
356 ename = resp.read(elen)
357 engine = util.compengines.forwiretype(ename)
358 return respurl, engine.decompressorreader(resp)
359 else:
360 raise error.RepoError(_("'%s' uses newer protocol %s") %
361 (safeurl, version))
362
363 if compressible:
364 resp = util.compengines['zlib'].decompressorreader(resp)
365
366 return respurl, resp
367
308 class httppeer(wireproto.wirepeer):
368 class httppeer(wireproto.wirepeer):
309 def __init__(self, ui, path, url, opener, requestbuilder):
369 def __init__(self, ui, path, url, opener, requestbuilder):
310 self.ui = ui
370 self.ui = ui
311 self._path = path
371 self._path = path
312 self._url = url
372 self._url = url
313 self._caps = None
373 self._caps = None
314 self._urlopener = opener
374 self._urlopener = opener
315 self._requestbuilder = requestbuilder
375 self._requestbuilder = requestbuilder
316
376
317 def __del__(self):
377 def __del__(self):
318 for h in self._urlopener.handlers:
378 for h in self._urlopener.handlers:
319 h.close()
379 h.close()
320 getattr(h, "close_all", lambda: None)()
380 getattr(h, "close_all", lambda: None)()
321
381
322 # Begin of ipeerconnection interface.
382 # Begin of ipeerconnection interface.
323
383
324 def url(self):
384 def url(self):
325 return self._path
385 return self._path
326
386
327 def local(self):
387 def local(self):
328 return None
388 return None
329
389
330 def peer(self):
390 def peer(self):
331 return self
391 return self
332
392
333 def canpush(self):
393 def canpush(self):
334 return True
394 return True
335
395
336 def close(self):
396 def close(self):
337 pass
397 pass
338
398
339 # End of ipeerconnection interface.
399 # End of ipeerconnection interface.
340
400
341 # Begin of ipeercommands interface.
401 # Begin of ipeercommands interface.
342
402
343 def capabilities(self):
403 def capabilities(self):
344 # self._fetchcaps() should have been called as part of peer
404 # self._fetchcaps() should have been called as part of peer
345 # handshake. So self._caps should always be set.
405 # handshake. So self._caps should always be set.
346 assert self._caps is not None
406 assert self._caps is not None
347 return self._caps
407 return self._caps
348
408
349 # End of ipeercommands interface.
409 # End of ipeercommands interface.
350
410
351 # look up capabilities only when needed
411 # look up capabilities only when needed
352
412
353 def _fetchcaps(self):
413 def _fetchcaps(self):
354 self._caps = set(self._call('capabilities').split())
414 self._caps = set(self._call('capabilities').split())
355
415
356 def _callstream(self, cmd, _compressible=False, **args):
416 def _callstream(self, cmd, _compressible=False, **args):
357 args = pycompat.byteskwargs(args)
417 args = pycompat.byteskwargs(args)
358
418
359 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
419 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
360 self._caps, self.capable,
420 self._caps, self.capable,
361 self._url, cmd, args)
421 self._url, cmd, args)
362
422
363 resp = sendrequest(self.ui, self._urlopener, req)
423 resp = sendrequest(self.ui, self._urlopener, req)
364
424
365 # record the url we got redirected to
425 self._url, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
366 resp_url = pycompat.bytesurl(resp.geturl())
426 resp, _compressible)
367 if resp_url.endswith(qs):
368 resp_url = resp_url[:-len(qs)]
369 if self._url.rstrip('/') != resp_url.rstrip('/'):
370 if not self.ui.quiet:
371 self.ui.warn(_('real URL is %s\n') % resp_url)
372 self._url = resp_url
373 try:
374 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
375 except AttributeError:
376 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
377
378 safeurl = util.hidepassword(self._url)
379 if proto.startswith('application/hg-error'):
380 raise error.OutOfBandError(resp.read())
381 # accept old "text/plain" and "application/hg-changegroup" for now
382 if not (proto.startswith('application/mercurial-') or
383 (proto.startswith('text/plain')
384 and not resp.headers.get('content-length')) or
385 proto.startswith('application/hg-changegroup')):
386 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
387 raise error.RepoError(
388 _("'%s' does not appear to be an hg repository:\n"
389 "---%%<--- (%s)\n%s\n---%%<---\n")
390 % (safeurl, proto or 'no content-type', resp.read(1024)))
391
392 if proto.startswith('application/mercurial-'):
393 try:
394 version = proto.split('-', 1)[1]
395 version_info = tuple([int(n) for n in version.split('.')])
396 except ValueError:
397 raise error.RepoError(_("'%s' sent a broken Content-Type "
398 "header (%s)") % (safeurl, proto))
399
400 # TODO consider switching to a decompression reader that uses
401 # generators.
402 if version_info == (0, 1):
403 if _compressible:
404 return util.compengines['zlib'].decompressorreader(resp)
405 return resp
406 elif version_info == (0, 2):
407 # application/mercurial-0.2 always identifies the compression
408 # engine in the payload header.
409 elen = struct.unpack('B', resp.read(1))[0]
410 ename = resp.read(elen)
411 engine = util.compengines.forwiretype(ename)
412 return engine.decompressorreader(resp)
413 else:
414 raise error.RepoError(_("'%s' uses newer protocol %s") %
415 (safeurl, version))
416
417 if _compressible:
418 return util.compengines['zlib'].decompressorreader(resp)
419
427
420 return resp
428 return resp
421
429
422 def _call(self, cmd, **args):
430 def _call(self, cmd, **args):
423 fp = self._callstream(cmd, **args)
431 fp = self._callstream(cmd, **args)
424 try:
432 try:
425 return fp.read()
433 return fp.read()
426 finally:
434 finally:
427 # if using keepalive, allow connection to be reused
435 # if using keepalive, allow connection to be reused
428 fp.close()
436 fp.close()
429
437
430 def _callpush(self, cmd, cg, **args):
438 def _callpush(self, cmd, cg, **args):
431 # have to stream bundle to a temp file because we do not have
439 # have to stream bundle to a temp file because we do not have
432 # http 1.1 chunked transfer.
440 # http 1.1 chunked transfer.
433
441
434 types = self.capable('unbundle')
442 types = self.capable('unbundle')
435 try:
443 try:
436 types = types.split(',')
444 types = types.split(',')
437 except AttributeError:
445 except AttributeError:
438 # servers older than d1b16a746db6 will send 'unbundle' as a
446 # servers older than d1b16a746db6 will send 'unbundle' as a
439 # boolean capability. They only support headerless/uncompressed
447 # boolean capability. They only support headerless/uncompressed
440 # bundles.
448 # bundles.
441 types = [""]
449 types = [""]
442 for x in types:
450 for x in types:
443 if x in bundle2.bundletypes:
451 if x in bundle2.bundletypes:
444 type = x
452 type = x
445 break
453 break
446
454
447 tempname = bundle2.writebundle(self.ui, cg, None, type)
455 tempname = bundle2.writebundle(self.ui, cg, None, type)
448 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
456 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
449 headers = {r'Content-Type': r'application/mercurial-0.1'}
457 headers = {r'Content-Type': r'application/mercurial-0.1'}
450
458
451 try:
459 try:
452 r = self._call(cmd, data=fp, headers=headers, **args)
460 r = self._call(cmd, data=fp, headers=headers, **args)
453 vals = r.split('\n', 1)
461 vals = r.split('\n', 1)
454 if len(vals) < 2:
462 if len(vals) < 2:
455 raise error.ResponseError(_("unexpected response:"), r)
463 raise error.ResponseError(_("unexpected response:"), r)
456 return vals
464 return vals
457 except urlerr.httperror:
465 except urlerr.httperror:
458 # Catch and re-raise these so we don't try and treat them
466 # Catch and re-raise these so we don't try and treat them
459 # like generic socket errors. They lack any values in
467 # like generic socket errors. They lack any values in
460 # .args on Python 3 which breaks our socket.error block.
468 # .args on Python 3 which breaks our socket.error block.
461 raise
469 raise
462 except socket.error as err:
470 except socket.error as err:
463 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
471 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
464 raise error.Abort(_('push failed: %s') % err.args[1])
472 raise error.Abort(_('push failed: %s') % err.args[1])
465 raise error.Abort(err.args[1])
473 raise error.Abort(err.args[1])
466 finally:
474 finally:
467 fp.close()
475 fp.close()
468 os.unlink(tempname)
476 os.unlink(tempname)
469
477
470 def _calltwowaystream(self, cmd, fp, **args):
478 def _calltwowaystream(self, cmd, fp, **args):
471 fh = None
479 fh = None
472 fp_ = None
480 fp_ = None
473 filename = None
481 filename = None
474 try:
482 try:
475 # dump bundle to disk
483 # dump bundle to disk
476 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
484 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
477 fh = os.fdopen(fd, r"wb")
485 fh = os.fdopen(fd, r"wb")
478 d = fp.read(4096)
486 d = fp.read(4096)
479 while d:
487 while d:
480 fh.write(d)
488 fh.write(d)
481 d = fp.read(4096)
489 d = fp.read(4096)
482 fh.close()
490 fh.close()
483 # start http push
491 # start http push
484 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
492 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
485 headers = {r'Content-Type': r'application/mercurial-0.1'}
493 headers = {r'Content-Type': r'application/mercurial-0.1'}
486 return self._callstream(cmd, data=fp_, headers=headers, **args)
494 return self._callstream(cmd, data=fp_, headers=headers, **args)
487 finally:
495 finally:
488 if fp_ is not None:
496 if fp_ is not None:
489 fp_.close()
497 fp_.close()
490 if fh is not None:
498 if fh is not None:
491 fh.close()
499 fh.close()
492 os.unlink(filename)
500 os.unlink(filename)
493
501
494 def _callcompressable(self, cmd, **args):
502 def _callcompressable(self, cmd, **args):
495 return self._callstream(cmd, _compressible=True, **args)
503 return self._callstream(cmd, _compressible=True, **args)
496
504
497 def _abort(self, exception):
505 def _abort(self, exception):
498 raise exception
506 raise exception
499
507
500 # TODO implement interface for version 2 peers
508 # TODO implement interface for version 2 peers
501 class httpv2peer(object):
509 class httpv2peer(object):
502 def __init__(self, ui, repourl, opener):
510 def __init__(self, ui, repourl, opener):
503 self.ui = ui
511 self.ui = ui
504
512
505 if repourl.endswith('/'):
513 if repourl.endswith('/'):
506 repourl = repourl[:-1]
514 repourl = repourl[:-1]
507
515
508 self.url = repourl
516 self.url = repourl
509 self._opener = opener
517 self._opener = opener
510 # This is an its own attribute to facilitate extensions overriding
518 # This is an its own attribute to facilitate extensions overriding
511 # the default type.
519 # the default type.
512 self._requestbuilder = urlreq.request
520 self._requestbuilder = urlreq.request
513
521
514 def close(self):
522 def close(self):
515 pass
523 pass
516
524
517 # TODO require to be part of a batched primitive, use futures.
525 # TODO require to be part of a batched primitive, use futures.
518 def _call(self, name, **args):
526 def _call(self, name, **args):
519 """Call a wire protocol command with arguments."""
527 """Call a wire protocol command with arguments."""
520
528
521 # Having this early has a side-effect of importing wireprotov2server,
529 # Having this early has a side-effect of importing wireprotov2server,
522 # which has the side-effect of ensuring commands are registered.
530 # which has the side-effect of ensuring commands are registered.
523
531
524 # TODO modify user-agent to reflect v2.
532 # TODO modify user-agent to reflect v2.
525 headers = {
533 headers = {
526 r'Accept': wireprotov2server.FRAMINGTYPE,
534 r'Accept': wireprotov2server.FRAMINGTYPE,
527 r'Content-Type': wireprotov2server.FRAMINGTYPE,
535 r'Content-Type': wireprotov2server.FRAMINGTYPE,
528 }
536 }
529
537
530 # TODO permissions should come from capabilities results.
538 # TODO permissions should come from capabilities results.
531 permission = wireproto.commandsv2[name].permission
539 permission = wireproto.commandsv2[name].permission
532 if permission not in ('push', 'pull'):
540 if permission not in ('push', 'pull'):
533 raise error.ProgrammingError('unknown permission type: %s' %
541 raise error.ProgrammingError('unknown permission type: %s' %
534 permission)
542 permission)
535
543
536 permission = {
544 permission = {
537 'push': 'rw',
545 'push': 'rw',
538 'pull': 'ro',
546 'pull': 'ro',
539 }[permission]
547 }[permission]
540
548
541 url = '%s/api/%s/%s/%s' % (self.url, wireprotov2server.HTTPV2,
549 url = '%s/api/%s/%s/%s' % (self.url, wireprotov2server.HTTPV2,
542 permission, name)
550 permission, name)
543
551
544 # TODO this should be part of a generic peer for the frame-based
552 # TODO this should be part of a generic peer for the frame-based
545 # protocol.
553 # protocol.
546 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
554 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
547 buffersends=True)
555 buffersends=True)
548
556
549 request, action, meta = reactor.callcommand(name, args)
557 request, action, meta = reactor.callcommand(name, args)
550 assert action == 'noop'
558 assert action == 'noop'
551
559
552 action, meta = reactor.flushcommands()
560 action, meta = reactor.flushcommands()
553 assert action == 'sendframes'
561 assert action == 'sendframes'
554
562
555 body = b''.join(map(bytes, meta['framegen']))
563 body = b''.join(map(bytes, meta['framegen']))
556 req = self._requestbuilder(pycompat.strurl(url), body, headers)
564 req = self._requestbuilder(pycompat.strurl(url), body, headers)
557 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
565 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
558
566
559 # TODO unify this code with httppeer.
567 # TODO unify this code with httppeer.
560 try:
568 try:
561 res = self._opener.open(req)
569 res = self._opener.open(req)
562 except urlerr.httperror as e:
570 except urlerr.httperror as e:
563 if e.code == 401:
571 if e.code == 401:
564 raise error.Abort(_('authorization failed'))
572 raise error.Abort(_('authorization failed'))
565
573
566 raise
574 raise
567 except httplib.HTTPException as e:
575 except httplib.HTTPException as e:
568 self.ui.traceback()
576 self.ui.traceback()
569 raise IOError(None, e)
577 raise IOError(None, e)
570
578
571 # TODO validate response type, wrap response to handle I/O errors.
579 # TODO validate response type, wrap response to handle I/O errors.
572 # TODO more robust frame receiver.
580 # TODO more robust frame receiver.
573 results = []
581 results = []
574
582
575 while True:
583 while True:
576 frame = wireprotoframing.readframe(res)
584 frame = wireprotoframing.readframe(res)
577 if frame is None:
585 if frame is None:
578 break
586 break
579
587
580 self.ui.note(_('received %r\n') % frame)
588 self.ui.note(_('received %r\n') % frame)
581
589
582 action, meta = reactor.onframerecv(frame)
590 action, meta = reactor.onframerecv(frame)
583
591
584 if action == 'responsedata':
592 if action == 'responsedata':
585 if meta['cbor']:
593 if meta['cbor']:
586 payload = util.bytesio(meta['data'])
594 payload = util.bytesio(meta['data'])
587
595
588 decoder = cbor.CBORDecoder(payload)
596 decoder = cbor.CBORDecoder(payload)
589 while payload.tell() + 1 < len(meta['data']):
597 while payload.tell() + 1 < len(meta['data']):
590 results.append(decoder.decode())
598 results.append(decoder.decode())
591 else:
599 else:
592 results.append(meta['data'])
600 results.append(meta['data'])
593 else:
601 else:
594 error.ProgrammingError('unhandled action: %s' % action)
602 error.ProgrammingError('unhandled action: %s' % action)
595
603
596 return results
604 return results
597
605
598 def makepeer(ui, path, requestbuilder=urlreq.request):
606 def makepeer(ui, path, requestbuilder=urlreq.request):
599 """Construct an appropriate HTTP peer instance.
607 """Construct an appropriate HTTP peer instance.
600
608
601 ``requestbuilder`` is the type used for constructing HTTP requests.
609 ``requestbuilder`` is the type used for constructing HTTP requests.
602 It exists as an argument so extensions can override the default.
610 It exists as an argument so extensions can override the default.
603 """
611 """
604 u = util.url(path)
612 u = util.url(path)
605 if u.query or u.fragment:
613 if u.query or u.fragment:
606 raise error.Abort(_('unsupported URL component: "%s"') %
614 raise error.Abort(_('unsupported URL component: "%s"') %
607 (u.query or u.fragment))
615 (u.query or u.fragment))
608
616
609 # urllib cannot handle URLs with embedded user or passwd.
617 # urllib cannot handle URLs with embedded user or passwd.
610 url, authinfo = u.authinfo()
618 url, authinfo = u.authinfo()
611 ui.debug('using %s\n' % url)
619 ui.debug('using %s\n' % url)
612
620
613 opener = urlmod.opener(ui, authinfo)
621 opener = urlmod.opener(ui, authinfo)
614
622
615 return httppeer(ui, path, url, opener, requestbuilder)
623 return httppeer(ui, path, url, opener, requestbuilder)
616
624
617 def instance(ui, path, create):
625 def instance(ui, path, create):
618 if create:
626 if create:
619 raise error.Abort(_('cannot create new http repository'))
627 raise error.Abort(_('cannot create new http repository'))
620 try:
628 try:
621 if path.startswith('https:') and not urlmod.has_https:
629 if path.startswith('https:') and not urlmod.has_https:
622 raise error.Abort(_('Python support for SSL and HTTPS '
630 raise error.Abort(_('Python support for SSL and HTTPS '
623 'is not installed'))
631 'is not installed'))
624
632
625 inst = makepeer(ui, path)
633 inst = makepeer(ui, path)
626 inst._fetchcaps()
634 inst._fetchcaps()
627
635
628 return inst
636 return inst
629 except error.RepoError as httpexception:
637 except error.RepoError as httpexception:
630 try:
638 try:
631 r = statichttprepo.instance(ui, "static-" + path, create)
639 r = statichttprepo.instance(ui, "static-" + path, create)
632 ui.note(_('(falling back to static-http)\n'))
640 ui.note(_('(falling back to static-http)\n'))
633 return r
641 return r
634 except error.RepoError:
642 except error.RepoError:
635 raise httpexception # use the original http RepoError instead
643 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now