##// END OF EJS Templates
httppeer: move error handling and response wrapping into sendrequest...
Gregory Szorc -
r37568:b5862ee0 default
parent child Browse files
Show More
@@ -1,633 +1,635 b''
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import io
12 import io
13 import os
13 import os
14 import socket
14 import socket
15 import struct
15 import struct
16 import tempfile
16 import tempfile
17
17
18 from .i18n import _
18 from .i18n import _
19 from .thirdparty import (
19 from .thirdparty import (
20 cbor,
20 cbor,
21 )
21 )
22 from . 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 res = opener.open(req)
288 res = opener.open(req)
289 except urlerr.httperror as inst:
290 if inst.code == 401:
291 raise error.Abort(_('authorization failed'))
292 raise
293 except httplib.HTTPException as inst:
294 ui.debug('http error requesting %s\n' %
295 util.hidepassword(req.get_full_url()))
296 ui.traceback()
297 raise IOError(None, inst)
298 finally:
288 if ui.configbool('devel', 'debug.peer-request'):
299 if ui.configbool('devel', 'debug.peer-request'):
289 dbg(line % ' finished in %.4f seconds (%s)'
300 dbg(line % ' finished in %.4f seconds (%s)'
290 % (util.timer() - start, res.code))
301 % (util.timer() - start, res.code))
291
302
303 # Insert error handlers for common I/O failures.
304 _wraphttpresponse(res)
305
292 return res
306 return res
293
307
294 class httppeer(wireproto.wirepeer):
308 class httppeer(wireproto.wirepeer):
295 def __init__(self, ui, path, url, opener, requestbuilder):
309 def __init__(self, ui, path, url, opener, requestbuilder):
296 self.ui = ui
310 self.ui = ui
297 self._path = path
311 self._path = path
298 self._url = url
312 self._url = url
299 self._caps = None
313 self._caps = None
300 self._urlopener = opener
314 self._urlopener = opener
301 self._requestbuilder = requestbuilder
315 self._requestbuilder = requestbuilder
302
316
303 def __del__(self):
317 def __del__(self):
304 for h in self._urlopener.handlers:
318 for h in self._urlopener.handlers:
305 h.close()
319 h.close()
306 getattr(h, "close_all", lambda: None)()
320 getattr(h, "close_all", lambda: None)()
307
321
308 # Begin of ipeerconnection interface.
322 # Begin of ipeerconnection interface.
309
323
310 def url(self):
324 def url(self):
311 return self._path
325 return self._path
312
326
313 def local(self):
327 def local(self):
314 return None
328 return None
315
329
316 def peer(self):
330 def peer(self):
317 return self
331 return self
318
332
319 def canpush(self):
333 def canpush(self):
320 return True
334 return True
321
335
322 def close(self):
336 def close(self):
323 pass
337 pass
324
338
325 # End of ipeerconnection interface.
339 # End of ipeerconnection interface.
326
340
327 # Begin of ipeercommands interface.
341 # Begin of ipeercommands interface.
328
342
329 def capabilities(self):
343 def capabilities(self):
330 # self._fetchcaps() should have been called as part of peer
344 # self._fetchcaps() should have been called as part of peer
331 # handshake. So self._caps should always be set.
345 # handshake. So self._caps should always be set.
332 assert self._caps is not None
346 assert self._caps is not None
333 return self._caps
347 return self._caps
334
348
335 # End of ipeercommands interface.
349 # End of ipeercommands interface.
336
350
337 # look up capabilities only when needed
351 # look up capabilities only when needed
338
352
339 def _fetchcaps(self):
353 def _fetchcaps(self):
340 self._caps = set(self._call('capabilities').split())
354 self._caps = set(self._call('capabilities').split())
341
355
342 def _callstream(self, cmd, _compressible=False, **args):
356 def _callstream(self, cmd, _compressible=False, **args):
343 args = pycompat.byteskwargs(args)
357 args = pycompat.byteskwargs(args)
344
358
345 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
359 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
346 self._caps, self.capable,
360 self._caps, self.capable,
347 self._url, cmd, args)
361 self._url, cmd, args)
348
362
349 try:
350 resp = sendrequest(self.ui, self._urlopener, req)
363 resp = sendrequest(self.ui, self._urlopener, req)
351 except urlerr.httperror as inst:
352 if inst.code == 401:
353 raise error.Abort(_('authorization failed'))
354 raise
355 except httplib.HTTPException as inst:
356 self.ui.debug('http error while sending %s command\n' % cmd)
357 self.ui.traceback()
358 raise IOError(None, inst)
359
360 # Insert error handlers for common I/O failures.
361 _wraphttpresponse(resp)
362
364
363 # record the url we got redirected to
365 # record the url we got redirected to
364 resp_url = pycompat.bytesurl(resp.geturl())
366 resp_url = pycompat.bytesurl(resp.geturl())
365 if resp_url.endswith(qs):
367 if resp_url.endswith(qs):
366 resp_url = resp_url[:-len(qs)]
368 resp_url = resp_url[:-len(qs)]
367 if self._url.rstrip('/') != resp_url.rstrip('/'):
369 if self._url.rstrip('/') != resp_url.rstrip('/'):
368 if not self.ui.quiet:
370 if not self.ui.quiet:
369 self.ui.warn(_('real URL is %s\n') % resp_url)
371 self.ui.warn(_('real URL is %s\n') % resp_url)
370 self._url = resp_url
372 self._url = resp_url
371 try:
373 try:
372 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
374 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
373 except AttributeError:
375 except AttributeError:
374 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
376 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
375
377
376 safeurl = util.hidepassword(self._url)
378 safeurl = util.hidepassword(self._url)
377 if proto.startswith('application/hg-error'):
379 if proto.startswith('application/hg-error'):
378 raise error.OutOfBandError(resp.read())
380 raise error.OutOfBandError(resp.read())
379 # accept old "text/plain" and "application/hg-changegroup" for now
381 # accept old "text/plain" and "application/hg-changegroup" for now
380 if not (proto.startswith('application/mercurial-') or
382 if not (proto.startswith('application/mercurial-') or
381 (proto.startswith('text/plain')
383 (proto.startswith('text/plain')
382 and not resp.headers.get('content-length')) or
384 and not resp.headers.get('content-length')) or
383 proto.startswith('application/hg-changegroup')):
385 proto.startswith('application/hg-changegroup')):
384 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
386 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
385 raise error.RepoError(
387 raise error.RepoError(
386 _("'%s' does not appear to be an hg repository:\n"
388 _("'%s' does not appear to be an hg repository:\n"
387 "---%%<--- (%s)\n%s\n---%%<---\n")
389 "---%%<--- (%s)\n%s\n---%%<---\n")
388 % (safeurl, proto or 'no content-type', resp.read(1024)))
390 % (safeurl, proto or 'no content-type', resp.read(1024)))
389
391
390 if proto.startswith('application/mercurial-'):
392 if proto.startswith('application/mercurial-'):
391 try:
393 try:
392 version = proto.split('-', 1)[1]
394 version = proto.split('-', 1)[1]
393 version_info = tuple([int(n) for n in version.split('.')])
395 version_info = tuple([int(n) for n in version.split('.')])
394 except ValueError:
396 except ValueError:
395 raise error.RepoError(_("'%s' sent a broken Content-Type "
397 raise error.RepoError(_("'%s' sent a broken Content-Type "
396 "header (%s)") % (safeurl, proto))
398 "header (%s)") % (safeurl, proto))
397
399
398 # TODO consider switching to a decompression reader that uses
400 # TODO consider switching to a decompression reader that uses
399 # generators.
401 # generators.
400 if version_info == (0, 1):
402 if version_info == (0, 1):
401 if _compressible:
403 if _compressible:
402 return util.compengines['zlib'].decompressorreader(resp)
404 return util.compengines['zlib'].decompressorreader(resp)
403 return resp
405 return resp
404 elif version_info == (0, 2):
406 elif version_info == (0, 2):
405 # application/mercurial-0.2 always identifies the compression
407 # application/mercurial-0.2 always identifies the compression
406 # engine in the payload header.
408 # engine in the payload header.
407 elen = struct.unpack('B', resp.read(1))[0]
409 elen = struct.unpack('B', resp.read(1))[0]
408 ename = resp.read(elen)
410 ename = resp.read(elen)
409 engine = util.compengines.forwiretype(ename)
411 engine = util.compengines.forwiretype(ename)
410 return engine.decompressorreader(resp)
412 return engine.decompressorreader(resp)
411 else:
413 else:
412 raise error.RepoError(_("'%s' uses newer protocol %s") %
414 raise error.RepoError(_("'%s' uses newer protocol %s") %
413 (safeurl, version))
415 (safeurl, version))
414
416
415 if _compressible:
417 if _compressible:
416 return util.compengines['zlib'].decompressorreader(resp)
418 return util.compengines['zlib'].decompressorreader(resp)
417
419
418 return resp
420 return resp
419
421
420 def _call(self, cmd, **args):
422 def _call(self, cmd, **args):
421 fp = self._callstream(cmd, **args)
423 fp = self._callstream(cmd, **args)
422 try:
424 try:
423 return fp.read()
425 return fp.read()
424 finally:
426 finally:
425 # if using keepalive, allow connection to be reused
427 # if using keepalive, allow connection to be reused
426 fp.close()
428 fp.close()
427
429
428 def _callpush(self, cmd, cg, **args):
430 def _callpush(self, cmd, cg, **args):
429 # have to stream bundle to a temp file because we do not have
431 # have to stream bundle to a temp file because we do not have
430 # http 1.1 chunked transfer.
432 # http 1.1 chunked transfer.
431
433
432 types = self.capable('unbundle')
434 types = self.capable('unbundle')
433 try:
435 try:
434 types = types.split(',')
436 types = types.split(',')
435 except AttributeError:
437 except AttributeError:
436 # servers older than d1b16a746db6 will send 'unbundle' as a
438 # servers older than d1b16a746db6 will send 'unbundle' as a
437 # boolean capability. They only support headerless/uncompressed
439 # boolean capability. They only support headerless/uncompressed
438 # bundles.
440 # bundles.
439 types = [""]
441 types = [""]
440 for x in types:
442 for x in types:
441 if x in bundle2.bundletypes:
443 if x in bundle2.bundletypes:
442 type = x
444 type = x
443 break
445 break
444
446
445 tempname = bundle2.writebundle(self.ui, cg, None, type)
447 tempname = bundle2.writebundle(self.ui, cg, None, type)
446 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
448 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
447 headers = {r'Content-Type': r'application/mercurial-0.1'}
449 headers = {r'Content-Type': r'application/mercurial-0.1'}
448
450
449 try:
451 try:
450 r = self._call(cmd, data=fp, headers=headers, **args)
452 r = self._call(cmd, data=fp, headers=headers, **args)
451 vals = r.split('\n', 1)
453 vals = r.split('\n', 1)
452 if len(vals) < 2:
454 if len(vals) < 2:
453 raise error.ResponseError(_("unexpected response:"), r)
455 raise error.ResponseError(_("unexpected response:"), r)
454 return vals
456 return vals
455 except urlerr.httperror:
457 except urlerr.httperror:
456 # Catch and re-raise these so we don't try and treat them
458 # Catch and re-raise these so we don't try and treat them
457 # like generic socket errors. They lack any values in
459 # like generic socket errors. They lack any values in
458 # .args on Python 3 which breaks our socket.error block.
460 # .args on Python 3 which breaks our socket.error block.
459 raise
461 raise
460 except socket.error as err:
462 except socket.error as err:
461 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
463 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
462 raise error.Abort(_('push failed: %s') % err.args[1])
464 raise error.Abort(_('push failed: %s') % err.args[1])
463 raise error.Abort(err.args[1])
465 raise error.Abort(err.args[1])
464 finally:
466 finally:
465 fp.close()
467 fp.close()
466 os.unlink(tempname)
468 os.unlink(tempname)
467
469
468 def _calltwowaystream(self, cmd, fp, **args):
470 def _calltwowaystream(self, cmd, fp, **args):
469 fh = None
471 fh = None
470 fp_ = None
472 fp_ = None
471 filename = None
473 filename = None
472 try:
474 try:
473 # dump bundle to disk
475 # dump bundle to disk
474 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
476 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
475 fh = os.fdopen(fd, r"wb")
477 fh = os.fdopen(fd, r"wb")
476 d = fp.read(4096)
478 d = fp.read(4096)
477 while d:
479 while d:
478 fh.write(d)
480 fh.write(d)
479 d = fp.read(4096)
481 d = fp.read(4096)
480 fh.close()
482 fh.close()
481 # start http push
483 # start http push
482 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
484 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
483 headers = {r'Content-Type': r'application/mercurial-0.1'}
485 headers = {r'Content-Type': r'application/mercurial-0.1'}
484 return self._callstream(cmd, data=fp_, headers=headers, **args)
486 return self._callstream(cmd, data=fp_, headers=headers, **args)
485 finally:
487 finally:
486 if fp_ is not None:
488 if fp_ is not None:
487 fp_.close()
489 fp_.close()
488 if fh is not None:
490 if fh is not None:
489 fh.close()
491 fh.close()
490 os.unlink(filename)
492 os.unlink(filename)
491
493
492 def _callcompressable(self, cmd, **args):
494 def _callcompressable(self, cmd, **args):
493 return self._callstream(cmd, _compressible=True, **args)
495 return self._callstream(cmd, _compressible=True, **args)
494
496
495 def _abort(self, exception):
497 def _abort(self, exception):
496 raise exception
498 raise exception
497
499
498 # TODO implement interface for version 2 peers
500 # TODO implement interface for version 2 peers
499 class httpv2peer(object):
501 class httpv2peer(object):
500 def __init__(self, ui, repourl, opener):
502 def __init__(self, ui, repourl, opener):
501 self.ui = ui
503 self.ui = ui
502
504
503 if repourl.endswith('/'):
505 if repourl.endswith('/'):
504 repourl = repourl[:-1]
506 repourl = repourl[:-1]
505
507
506 self.url = repourl
508 self.url = repourl
507 self._opener = opener
509 self._opener = opener
508 # This is an its own attribute to facilitate extensions overriding
510 # This is an its own attribute to facilitate extensions overriding
509 # the default type.
511 # the default type.
510 self._requestbuilder = urlreq.request
512 self._requestbuilder = urlreq.request
511
513
512 def close(self):
514 def close(self):
513 pass
515 pass
514
516
515 # TODO require to be part of a batched primitive, use futures.
517 # TODO require to be part of a batched primitive, use futures.
516 def _call(self, name, **args):
518 def _call(self, name, **args):
517 """Call a wire protocol command with arguments."""
519 """Call a wire protocol command with arguments."""
518
520
519 # Having this early has a side-effect of importing wireprotov2server,
521 # Having this early has a side-effect of importing wireprotov2server,
520 # which has the side-effect of ensuring commands are registered.
522 # which has the side-effect of ensuring commands are registered.
521
523
522 # TODO modify user-agent to reflect v2.
524 # TODO modify user-agent to reflect v2.
523 headers = {
525 headers = {
524 r'Accept': wireprotov2server.FRAMINGTYPE,
526 r'Accept': wireprotov2server.FRAMINGTYPE,
525 r'Content-Type': wireprotov2server.FRAMINGTYPE,
527 r'Content-Type': wireprotov2server.FRAMINGTYPE,
526 }
528 }
527
529
528 # TODO permissions should come from capabilities results.
530 # TODO permissions should come from capabilities results.
529 permission = wireproto.commandsv2[name].permission
531 permission = wireproto.commandsv2[name].permission
530 if permission not in ('push', 'pull'):
532 if permission not in ('push', 'pull'):
531 raise error.ProgrammingError('unknown permission type: %s' %
533 raise error.ProgrammingError('unknown permission type: %s' %
532 permission)
534 permission)
533
535
534 permission = {
536 permission = {
535 'push': 'rw',
537 'push': 'rw',
536 'pull': 'ro',
538 'pull': 'ro',
537 }[permission]
539 }[permission]
538
540
539 url = '%s/api/%s/%s/%s' % (self.url, wireprotov2server.HTTPV2,
541 url = '%s/api/%s/%s/%s' % (self.url, wireprotov2server.HTTPV2,
540 permission, name)
542 permission, name)
541
543
542 # TODO this should be part of a generic peer for the frame-based
544 # TODO this should be part of a generic peer for the frame-based
543 # protocol.
545 # protocol.
544 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
546 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
545 buffersends=True)
547 buffersends=True)
546
548
547 request, action, meta = reactor.callcommand(name, args)
549 request, action, meta = reactor.callcommand(name, args)
548 assert action == 'noop'
550 assert action == 'noop'
549
551
550 action, meta = reactor.flushcommands()
552 action, meta = reactor.flushcommands()
551 assert action == 'sendframes'
553 assert action == 'sendframes'
552
554
553 body = b''.join(map(bytes, meta['framegen']))
555 body = b''.join(map(bytes, meta['framegen']))
554 req = self._requestbuilder(pycompat.strurl(url), body, headers)
556 req = self._requestbuilder(pycompat.strurl(url), body, headers)
555 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
557 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
556
558
557 # TODO unify this code with httppeer.
559 # TODO unify this code with httppeer.
558 try:
560 try:
559 res = self._opener.open(req)
561 res = self._opener.open(req)
560 except urlerr.httperror as e:
562 except urlerr.httperror as e:
561 if e.code == 401:
563 if e.code == 401:
562 raise error.Abort(_('authorization failed'))
564 raise error.Abort(_('authorization failed'))
563
565
564 raise
566 raise
565 except httplib.HTTPException as e:
567 except httplib.HTTPException as e:
566 self.ui.traceback()
568 self.ui.traceback()
567 raise IOError(None, e)
569 raise IOError(None, e)
568
570
569 # TODO validate response type, wrap response to handle I/O errors.
571 # TODO validate response type, wrap response to handle I/O errors.
570 # TODO more robust frame receiver.
572 # TODO more robust frame receiver.
571 results = []
573 results = []
572
574
573 while True:
575 while True:
574 frame = wireprotoframing.readframe(res)
576 frame = wireprotoframing.readframe(res)
575 if frame is None:
577 if frame is None:
576 break
578 break
577
579
578 self.ui.note(_('received %r\n') % frame)
580 self.ui.note(_('received %r\n') % frame)
579
581
580 action, meta = reactor.onframerecv(frame)
582 action, meta = reactor.onframerecv(frame)
581
583
582 if action == 'responsedata':
584 if action == 'responsedata':
583 if meta['cbor']:
585 if meta['cbor']:
584 payload = util.bytesio(meta['data'])
586 payload = util.bytesio(meta['data'])
585
587
586 decoder = cbor.CBORDecoder(payload)
588 decoder = cbor.CBORDecoder(payload)
587 while payload.tell() + 1 < len(meta['data']):
589 while payload.tell() + 1 < len(meta['data']):
588 results.append(decoder.decode())
590 results.append(decoder.decode())
589 else:
591 else:
590 results.append(meta['data'])
592 results.append(meta['data'])
591 else:
593 else:
592 error.ProgrammingError('unhandled action: %s' % action)
594 error.ProgrammingError('unhandled action: %s' % action)
593
595
594 return results
596 return results
595
597
596 def makepeer(ui, path, requestbuilder=urlreq.request):
598 def makepeer(ui, path, requestbuilder=urlreq.request):
597 """Construct an appropriate HTTP peer instance.
599 """Construct an appropriate HTTP peer instance.
598
600
599 ``requestbuilder`` is the type used for constructing HTTP requests.
601 ``requestbuilder`` is the type used for constructing HTTP requests.
600 It exists as an argument so extensions can override the default.
602 It exists as an argument so extensions can override the default.
601 """
603 """
602 u = util.url(path)
604 u = util.url(path)
603 if u.query or u.fragment:
605 if u.query or u.fragment:
604 raise error.Abort(_('unsupported URL component: "%s"') %
606 raise error.Abort(_('unsupported URL component: "%s"') %
605 (u.query or u.fragment))
607 (u.query or u.fragment))
606
608
607 # urllib cannot handle URLs with embedded user or passwd.
609 # urllib cannot handle URLs with embedded user or passwd.
608 url, authinfo = u.authinfo()
610 url, authinfo = u.authinfo()
609 ui.debug('using %s\n' % url)
611 ui.debug('using %s\n' % url)
610
612
611 opener = urlmod.opener(ui, authinfo)
613 opener = urlmod.opener(ui, authinfo)
612
614
613 return httppeer(ui, path, url, opener, requestbuilder)
615 return httppeer(ui, path, url, opener, requestbuilder)
614
616
615 def instance(ui, path, create):
617 def instance(ui, path, create):
616 if create:
618 if create:
617 raise error.Abort(_('cannot create new http repository'))
619 raise error.Abort(_('cannot create new http repository'))
618 try:
620 try:
619 if path.startswith('https:') and not urlmod.has_https:
621 if path.startswith('https:') and not urlmod.has_https:
620 raise error.Abort(_('Python support for SSL and HTTPS '
622 raise error.Abort(_('Python support for SSL and HTTPS '
621 'is not installed'))
623 'is not installed'))
622
624
623 inst = makepeer(ui, path)
625 inst = makepeer(ui, path)
624 inst._fetchcaps()
626 inst._fetchcaps()
625
627
626 return inst
628 return inst
627 except error.RepoError as httpexception:
629 except error.RepoError as httpexception:
628 try:
630 try:
629 r = statichttprepo.instance(ui, "static-" + path, create)
631 r = statichttprepo.instance(ui, "static-" + path, create)
630 ui.note(_('(falling back to static-http)\n'))
632 ui.note(_('(falling back to static-http)\n'))
631 return r
633 return r
632 except error.RepoError:
634 except error.RepoError:
633 raise httpexception # use the original http RepoError instead
635 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now