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