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