##// END OF EJS Templates
httppeer: remove redundant code to fetch capabilities...
Gregory Szorc -
r36237:a463f375 default
parent child Browse files
Show More
@@ -1,512 +1,508 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,
25 url,
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 self._requestbuilder = None
142 u = util.url(path)
142 u = util.url(path)
143 if u.query or u.fragment:
143 if u.query or u.fragment:
144 raise error.Abort(_('unsupported URL component: "%s"') %
144 raise error.Abort(_('unsupported URL component: "%s"') %
145 (u.query or u.fragment))
145 (u.query or u.fragment))
146
146
147 # urllib cannot handle URLs with embedded user or passwd
147 # urllib cannot handle URLs with embedded user or passwd
148 self._url, authinfo = u.authinfo()
148 self._url, authinfo = u.authinfo()
149
149
150 self._ui = ui
150 self._ui = ui
151 ui.debug('using %s\n' % self._url)
151 ui.debug('using %s\n' % self._url)
152
152
153 self._urlopener = url.opener(ui, authinfo)
153 self._urlopener = url.opener(ui, authinfo)
154 self._requestbuilder = urlreq.request
154 self._requestbuilder = urlreq.request
155
155
156 def __del__(self):
156 def __del__(self):
157 urlopener = getattr(self, '_urlopener', None)
157 urlopener = getattr(self, '_urlopener', None)
158 if urlopener:
158 if urlopener:
159 for h in urlopener.handlers:
159 for h in urlopener.handlers:
160 h.close()
160 h.close()
161 getattr(h, "close_all", lambda: None)()
161 getattr(h, "close_all", lambda: None)()
162
162
163 def _openurl(self, req):
163 def _openurl(self, req):
164 if (self._ui.debugflag
164 if (self._ui.debugflag
165 and self._ui.configbool('devel', 'debug.peer-request')):
165 and self._ui.configbool('devel', 'debug.peer-request')):
166 dbg = self._ui.debug
166 dbg = self._ui.debug
167 line = 'devel-peer-request: %s\n'
167 line = 'devel-peer-request: %s\n'
168 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
168 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
169 hgargssize = None
169 hgargssize = None
170
170
171 for header, value in sorted(req.header_items()):
171 for header, value in sorted(req.header_items()):
172 if header.startswith('X-hgarg-'):
172 if header.startswith('X-hgarg-'):
173 if hgargssize is None:
173 if hgargssize is None:
174 hgargssize = 0
174 hgargssize = 0
175 hgargssize += len(value)
175 hgargssize += len(value)
176 else:
176 else:
177 dbg(line % ' %s %s' % (header, value))
177 dbg(line % ' %s %s' % (header, value))
178
178
179 if hgargssize is not None:
179 if hgargssize is not None:
180 dbg(line % ' %d bytes of commands arguments in headers'
180 dbg(line % ' %d bytes of commands arguments in headers'
181 % hgargssize)
181 % hgargssize)
182
182
183 if req.has_data():
183 if req.has_data():
184 data = req.get_data()
184 data = req.get_data()
185 length = getattr(data, 'length', None)
185 length = getattr(data, 'length', None)
186 if length is None:
186 if length is None:
187 length = len(data)
187 length = len(data)
188 dbg(line % ' %d bytes of data' % length)
188 dbg(line % ' %d bytes of data' % length)
189
189
190 start = util.timer()
190 start = util.timer()
191
191
192 ret = self._urlopener.open(req)
192 ret = self._urlopener.open(req)
193 if self._ui.configbool('devel', 'debug.peer-request'):
193 if self._ui.configbool('devel', 'debug.peer-request'):
194 dbg(line % ' finished in %.4f seconds (%s)'
194 dbg(line % ' finished in %.4f seconds (%s)'
195 % (util.timer() - start, ret.code))
195 % (util.timer() - start, ret.code))
196 return ret
196 return ret
197
197
198 # Begin of _basepeer interface.
198 # Begin of _basepeer interface.
199
199
200 @util.propertycache
200 @util.propertycache
201 def ui(self):
201 def ui(self):
202 return self._ui
202 return self._ui
203
203
204 def url(self):
204 def url(self):
205 return self._path
205 return self._path
206
206
207 def local(self):
207 def local(self):
208 return None
208 return None
209
209
210 def peer(self):
210 def peer(self):
211 return self
211 return self
212
212
213 def canpush(self):
213 def canpush(self):
214 return True
214 return True
215
215
216 def close(self):
216 def close(self):
217 pass
217 pass
218
218
219 # End of _basepeer interface.
219 # End of _basepeer interface.
220
220
221 # Begin of _basewirepeer interface.
221 # Begin of _basewirepeer interface.
222
222
223 def capabilities(self):
223 def capabilities(self):
224 if self._caps is None:
224 # self._fetchcaps() should have been called as part of peer
225 try:
225 # handshake. So self._caps should always be set.
226 self._fetchcaps()
226 assert self._caps is not None
227 except error.RepoError:
228 self._caps = set()
229 self.ui.debug('capabilities: %s\n' %
230 (' '.join(self._caps or ['none'])))
231 return self._caps
227 return self._caps
232
228
233 # End of _basewirepeer interface.
229 # End of _basewirepeer interface.
234
230
235 # look up capabilities only when needed
231 # look up capabilities only when needed
236
232
237 def _fetchcaps(self):
233 def _fetchcaps(self):
238 self._caps = set(self._call('capabilities').split())
234 self._caps = set(self._call('capabilities').split())
239
235
240 def _callstream(self, cmd, _compressible=False, **args):
236 def _callstream(self, cmd, _compressible=False, **args):
241 args = pycompat.byteskwargs(args)
237 args = pycompat.byteskwargs(args)
242 if cmd == 'pushkey':
238 if cmd == 'pushkey':
243 args['data'] = ''
239 args['data'] = ''
244 data = args.pop('data', None)
240 data = args.pop('data', None)
245 headers = args.pop('headers', {})
241 headers = args.pop('headers', {})
246
242
247 self.ui.debug("sending %s command\n" % cmd)
243 self.ui.debug("sending %s command\n" % cmd)
248 q = [('cmd', cmd)]
244 q = [('cmd', cmd)]
249 headersize = 0
245 headersize = 0
250 varyheaders = []
246 varyheaders = []
251 # Important: don't use self.capable() here or else you end up
247 # Important: don't use self.capable() here or else you end up
252 # with infinite recursion when trying to look up capabilities
248 # with infinite recursion when trying to look up capabilities
253 # for the first time.
249 # for the first time.
254 postargsok = self._caps is not None and 'httppostargs' in self._caps
250 postargsok = self._caps is not None and 'httppostargs' in self._caps
255
251
256 # Send arguments via POST.
252 # Send arguments via POST.
257 if postargsok and args:
253 if postargsok and args:
258 strargs = urlreq.urlencode(sorted(args.items()))
254 strargs = urlreq.urlencode(sorted(args.items()))
259 if not data:
255 if not data:
260 data = strargs
256 data = strargs
261 else:
257 else:
262 if isinstance(data, bytes):
258 if isinstance(data, bytes):
263 i = io.BytesIO(data)
259 i = io.BytesIO(data)
264 i.length = len(data)
260 i.length = len(data)
265 data = i
261 data = i
266 argsio = io.BytesIO(strargs)
262 argsio = io.BytesIO(strargs)
267 argsio.length = len(strargs)
263 argsio.length = len(strargs)
268 data = _multifile(argsio, data)
264 data = _multifile(argsio, data)
269 headers[r'X-HgArgs-Post'] = len(strargs)
265 headers[r'X-HgArgs-Post'] = len(strargs)
270 elif args:
266 elif args:
271 # Calling self.capable() can infinite loop if we are calling
267 # Calling self.capable() can infinite loop if we are calling
272 # "capabilities". But that command should never accept wire
268 # "capabilities". But that command should never accept wire
273 # protocol arguments. So this should never happen.
269 # protocol arguments. So this should never happen.
274 assert cmd != 'capabilities'
270 assert cmd != 'capabilities'
275 httpheader = self.capable('httpheader')
271 httpheader = self.capable('httpheader')
276 if httpheader:
272 if httpheader:
277 headersize = int(httpheader.split(',', 1)[0])
273 headersize = int(httpheader.split(',', 1)[0])
278
274
279 # Send arguments via HTTP headers.
275 # Send arguments via HTTP headers.
280 if headersize > 0:
276 if headersize > 0:
281 # The headers can typically carry more data than the URL.
277 # The headers can typically carry more data than the URL.
282 encargs = urlreq.urlencode(sorted(args.items()))
278 encargs = urlreq.urlencode(sorted(args.items()))
283 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
279 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
284 headersize):
280 headersize):
285 headers[header] = value
281 headers[header] = value
286 varyheaders.append(header)
282 varyheaders.append(header)
287 # Send arguments via query string (Mercurial <1.9).
283 # Send arguments via query string (Mercurial <1.9).
288 else:
284 else:
289 q += sorted(args.items())
285 q += sorted(args.items())
290
286
291 qs = '?%s' % urlreq.urlencode(q)
287 qs = '?%s' % urlreq.urlencode(q)
292 cu = "%s%s" % (self._url, qs)
288 cu = "%s%s" % (self._url, qs)
293 size = 0
289 size = 0
294 if util.safehasattr(data, 'length'):
290 if util.safehasattr(data, 'length'):
295 size = data.length
291 size = data.length
296 elif data is not None:
292 elif data is not None:
297 size = len(data)
293 size = len(data)
298 if size and self.ui.configbool('ui', 'usehttp2'):
294 if size and self.ui.configbool('ui', 'usehttp2'):
299 headers[r'Expect'] = r'100-Continue'
295 headers[r'Expect'] = r'100-Continue'
300 headers[r'X-HgHttp2'] = r'1'
296 headers[r'X-HgHttp2'] = r'1'
301 if data is not None and r'Content-Type' not in headers:
297 if data is not None and r'Content-Type' not in headers:
302 headers[r'Content-Type'] = r'application/mercurial-0.1'
298 headers[r'Content-Type'] = r'application/mercurial-0.1'
303
299
304 # Tell the server we accept application/mercurial-0.2 and multiple
300 # Tell the server we accept application/mercurial-0.2 and multiple
305 # compression formats if the server is capable of emitting those
301 # compression formats if the server is capable of emitting those
306 # payloads.
302 # payloads.
307 protoparams = []
303 protoparams = []
308
304
309 mediatypes = set()
305 mediatypes = set()
310 if self._caps is not None:
306 if self._caps is not None:
311 mt = self.capable('httpmediatype')
307 mt = self.capable('httpmediatype')
312 if mt:
308 if mt:
313 protoparams.append('0.1')
309 protoparams.append('0.1')
314 mediatypes = set(mt.split(','))
310 mediatypes = set(mt.split(','))
315
311
316 if '0.2tx' in mediatypes:
312 if '0.2tx' in mediatypes:
317 protoparams.append('0.2')
313 protoparams.append('0.2')
318
314
319 if '0.2tx' in mediatypes and self.capable('compression'):
315 if '0.2tx' in mediatypes and self.capable('compression'):
320 # We /could/ compare supported compression formats and prune
316 # We /could/ compare supported compression formats and prune
321 # non-mutually supported or error if nothing is mutually supported.
317 # non-mutually supported or error if nothing is mutually supported.
322 # For now, send the full list to the server and have it error.
318 # For now, send the full list to the server and have it error.
323 comps = [e.wireprotosupport().name for e in
319 comps = [e.wireprotosupport().name for e in
324 util.compengines.supportedwireengines(util.CLIENTROLE)]
320 util.compengines.supportedwireengines(util.CLIENTROLE)]
325 protoparams.append('comp=%s' % ','.join(comps))
321 protoparams.append('comp=%s' % ','.join(comps))
326
322
327 if protoparams:
323 if protoparams:
328 protoheaders = encodevalueinheaders(' '.join(protoparams),
324 protoheaders = encodevalueinheaders(' '.join(protoparams),
329 'X-HgProto',
325 'X-HgProto',
330 headersize or 1024)
326 headersize or 1024)
331 for header, value in protoheaders:
327 for header, value in protoheaders:
332 headers[header] = value
328 headers[header] = value
333 varyheaders.append(header)
329 varyheaders.append(header)
334
330
335 if varyheaders:
331 if varyheaders:
336 headers[r'Vary'] = r','.join(varyheaders)
332 headers[r'Vary'] = r','.join(varyheaders)
337
333
338 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
334 req = self._requestbuilder(pycompat.strurl(cu), data, headers)
339
335
340 if data is not None:
336 if data is not None:
341 self.ui.debug("sending %s bytes\n" % size)
337 self.ui.debug("sending %s bytes\n" % size)
342 req.add_unredirected_header('Content-Length', '%d' % size)
338 req.add_unredirected_header('Content-Length', '%d' % size)
343 try:
339 try:
344 resp = self._openurl(req)
340 resp = self._openurl(req)
345 except urlerr.httperror as inst:
341 except urlerr.httperror as inst:
346 if inst.code == 401:
342 if inst.code == 401:
347 raise error.Abort(_('authorization failed'))
343 raise error.Abort(_('authorization failed'))
348 raise
344 raise
349 except httplib.HTTPException as inst:
345 except httplib.HTTPException as inst:
350 self.ui.debug('http error while sending %s command\n' % cmd)
346 self.ui.debug('http error while sending %s command\n' % cmd)
351 self.ui.traceback()
347 self.ui.traceback()
352 raise IOError(None, inst)
348 raise IOError(None, inst)
353
349
354 # Insert error handlers for common I/O failures.
350 # Insert error handlers for common I/O failures.
355 _wraphttpresponse(resp)
351 _wraphttpresponse(resp)
356
352
357 # record the url we got redirected to
353 # record the url we got redirected to
358 resp_url = pycompat.bytesurl(resp.geturl())
354 resp_url = pycompat.bytesurl(resp.geturl())
359 if resp_url.endswith(qs):
355 if resp_url.endswith(qs):
360 resp_url = resp_url[:-len(qs)]
356 resp_url = resp_url[:-len(qs)]
361 if self._url.rstrip('/') != resp_url.rstrip('/'):
357 if self._url.rstrip('/') != resp_url.rstrip('/'):
362 if not self.ui.quiet:
358 if not self.ui.quiet:
363 self.ui.warn(_('real URL is %s\n') % resp_url)
359 self.ui.warn(_('real URL is %s\n') % resp_url)
364 self._url = resp_url
360 self._url = resp_url
365 try:
361 try:
366 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
362 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
367 except AttributeError:
363 except AttributeError:
368 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
364 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
369
365
370 safeurl = util.hidepassword(self._url)
366 safeurl = util.hidepassword(self._url)
371 if proto.startswith('application/hg-error'):
367 if proto.startswith('application/hg-error'):
372 raise error.OutOfBandError(resp.read())
368 raise error.OutOfBandError(resp.read())
373 # accept old "text/plain" and "application/hg-changegroup" for now
369 # accept old "text/plain" and "application/hg-changegroup" for now
374 if not (proto.startswith('application/mercurial-') or
370 if not (proto.startswith('application/mercurial-') or
375 (proto.startswith('text/plain')
371 (proto.startswith('text/plain')
376 and not resp.headers.get('content-length')) or
372 and not resp.headers.get('content-length')) or
377 proto.startswith('application/hg-changegroup')):
373 proto.startswith('application/hg-changegroup')):
378 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
374 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
379 raise error.RepoError(
375 raise error.RepoError(
380 _("'%s' does not appear to be an hg repository:\n"
376 _("'%s' does not appear to be an hg repository:\n"
381 "---%%<--- (%s)\n%s\n---%%<---\n")
377 "---%%<--- (%s)\n%s\n---%%<---\n")
382 % (safeurl, proto or 'no content-type', resp.read(1024)))
378 % (safeurl, proto or 'no content-type', resp.read(1024)))
383
379
384 if proto.startswith('application/mercurial-'):
380 if proto.startswith('application/mercurial-'):
385 try:
381 try:
386 version = proto.split('-', 1)[1]
382 version = proto.split('-', 1)[1]
387 version_info = tuple([int(n) for n in version.split('.')])
383 version_info = tuple([int(n) for n in version.split('.')])
388 except ValueError:
384 except ValueError:
389 raise error.RepoError(_("'%s' sent a broken Content-Type "
385 raise error.RepoError(_("'%s' sent a broken Content-Type "
390 "header (%s)") % (safeurl, proto))
386 "header (%s)") % (safeurl, proto))
391
387
392 # TODO consider switching to a decompression reader that uses
388 # TODO consider switching to a decompression reader that uses
393 # generators.
389 # generators.
394 if version_info == (0, 1):
390 if version_info == (0, 1):
395 if _compressible:
391 if _compressible:
396 return util.compengines['zlib'].decompressorreader(resp)
392 return util.compengines['zlib'].decompressorreader(resp)
397 return resp
393 return resp
398 elif version_info == (0, 2):
394 elif version_info == (0, 2):
399 # application/mercurial-0.2 always identifies the compression
395 # application/mercurial-0.2 always identifies the compression
400 # engine in the payload header.
396 # engine in the payload header.
401 elen = struct.unpack('B', resp.read(1))[0]
397 elen = struct.unpack('B', resp.read(1))[0]
402 ename = resp.read(elen)
398 ename = resp.read(elen)
403 engine = util.compengines.forwiretype(ename)
399 engine = util.compengines.forwiretype(ename)
404 return engine.decompressorreader(resp)
400 return engine.decompressorreader(resp)
405 else:
401 else:
406 raise error.RepoError(_("'%s' uses newer protocol %s") %
402 raise error.RepoError(_("'%s' uses newer protocol %s") %
407 (safeurl, version))
403 (safeurl, version))
408
404
409 if _compressible:
405 if _compressible:
410 return util.compengines['zlib'].decompressorreader(resp)
406 return util.compengines['zlib'].decompressorreader(resp)
411
407
412 return resp
408 return resp
413
409
414 def _call(self, cmd, **args):
410 def _call(self, cmd, **args):
415 fp = self._callstream(cmd, **args)
411 fp = self._callstream(cmd, **args)
416 try:
412 try:
417 return fp.read()
413 return fp.read()
418 finally:
414 finally:
419 # if using keepalive, allow connection to be reused
415 # if using keepalive, allow connection to be reused
420 fp.close()
416 fp.close()
421
417
422 def _callpush(self, cmd, cg, **args):
418 def _callpush(self, cmd, cg, **args):
423 # have to stream bundle to a temp file because we do not have
419 # have to stream bundle to a temp file because we do not have
424 # http 1.1 chunked transfer.
420 # http 1.1 chunked transfer.
425
421
426 types = self.capable('unbundle')
422 types = self.capable('unbundle')
427 try:
423 try:
428 types = types.split(',')
424 types = types.split(',')
429 except AttributeError:
425 except AttributeError:
430 # servers older than d1b16a746db6 will send 'unbundle' as a
426 # servers older than d1b16a746db6 will send 'unbundle' as a
431 # boolean capability. They only support headerless/uncompressed
427 # boolean capability. They only support headerless/uncompressed
432 # bundles.
428 # bundles.
433 types = [""]
429 types = [""]
434 for x in types:
430 for x in types:
435 if x in bundle2.bundletypes:
431 if x in bundle2.bundletypes:
436 type = x
432 type = x
437 break
433 break
438
434
439 tempname = bundle2.writebundle(self.ui, cg, None, type)
435 tempname = bundle2.writebundle(self.ui, cg, None, type)
440 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
436 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
441 headers = {'Content-Type': 'application/mercurial-0.1'}
437 headers = {'Content-Type': 'application/mercurial-0.1'}
442
438
443 try:
439 try:
444 r = self._call(cmd, data=fp, headers=headers, **args)
440 r = self._call(cmd, data=fp, headers=headers, **args)
445 vals = r.split('\n', 1)
441 vals = r.split('\n', 1)
446 if len(vals) < 2:
442 if len(vals) < 2:
447 raise error.ResponseError(_("unexpected response:"), r)
443 raise error.ResponseError(_("unexpected response:"), r)
448 return vals
444 return vals
449 except socket.error as err:
445 except socket.error as err:
450 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
446 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
451 raise error.Abort(_('push failed: %s') % err.args[1])
447 raise error.Abort(_('push failed: %s') % err.args[1])
452 raise error.Abort(err.args[1])
448 raise error.Abort(err.args[1])
453 finally:
449 finally:
454 fp.close()
450 fp.close()
455 os.unlink(tempname)
451 os.unlink(tempname)
456
452
457 def _calltwowaystream(self, cmd, fp, **args):
453 def _calltwowaystream(self, cmd, fp, **args):
458 fh = None
454 fh = None
459 fp_ = None
455 fp_ = None
460 filename = None
456 filename = None
461 try:
457 try:
462 # dump bundle to disk
458 # dump bundle to disk
463 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
459 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
464 fh = os.fdopen(fd, pycompat.sysstr("wb"))
460 fh = os.fdopen(fd, pycompat.sysstr("wb"))
465 d = fp.read(4096)
461 d = fp.read(4096)
466 while d:
462 while d:
467 fh.write(d)
463 fh.write(d)
468 d = fp.read(4096)
464 d = fp.read(4096)
469 fh.close()
465 fh.close()
470 # start http push
466 # start http push
471 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
467 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
472 headers = {'Content-Type': 'application/mercurial-0.1'}
468 headers = {'Content-Type': 'application/mercurial-0.1'}
473 return self._callstream(cmd, data=fp_, headers=headers, **args)
469 return self._callstream(cmd, data=fp_, headers=headers, **args)
474 finally:
470 finally:
475 if fp_ is not None:
471 if fp_ is not None:
476 fp_.close()
472 fp_.close()
477 if fh is not None:
473 if fh is not None:
478 fh.close()
474 fh.close()
479 os.unlink(filename)
475 os.unlink(filename)
480
476
481 def _callcompressable(self, cmd, **args):
477 def _callcompressable(self, cmd, **args):
482 return self._callstream(cmd, _compressible=True, **args)
478 return self._callstream(cmd, _compressible=True, **args)
483
479
484 def _abort(self, exception):
480 def _abort(self, exception):
485 raise exception
481 raise exception
486
482
487 class httpspeer(httppeer):
483 class httpspeer(httppeer):
488 def __init__(self, ui, path):
484 def __init__(self, ui, path):
489 if not url.has_https:
485 if not url.has_https:
490 raise error.Abort(_('Python support for SSL and HTTPS '
486 raise error.Abort(_('Python support for SSL and HTTPS '
491 'is not installed'))
487 'is not installed'))
492 httppeer.__init__(self, ui, path)
488 httppeer.__init__(self, ui, path)
493
489
494 def instance(ui, path, create):
490 def instance(ui, path, create):
495 if create:
491 if create:
496 raise error.Abort(_('cannot create new http repository'))
492 raise error.Abort(_('cannot create new http repository'))
497 try:
493 try:
498 if path.startswith('https:'):
494 if path.startswith('https:'):
499 inst = httpspeer(ui, path)
495 inst = httpspeer(ui, path)
500 else:
496 else:
501 inst = httppeer(ui, path)
497 inst = httppeer(ui, path)
502
498
503 inst._fetchcaps()
499 inst._fetchcaps()
504
500
505 return inst
501 return inst
506 except error.RepoError as httpexception:
502 except error.RepoError as httpexception:
507 try:
503 try:
508 r = statichttprepo.instance(ui, "static-" + path, create)
504 r = statichttprepo.instance(ui, "static-" + path, create)
509 ui.note(_('(falling back to static-http)\n'))
505 ui.note(_('(falling back to static-http)\n'))
510 return r
506 return r
511 except error.RepoError:
507 except error.RepoError:
512 raise httpexception # use the original http RepoError instead
508 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now