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