##// END OF EJS Templates
httppeer: unify hint message for PeerTransportError...
FUJIWARA Katsunori -
r32087:b59a292d stable
parent child Browse files
Show More
@@ -1,402 +1,402 b''
1 1 # httppeer.py - HTTP repository proxy classes for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from __future__ import absolute_import
10 10
11 11 import errno
12 12 import os
13 13 import socket
14 14 import struct
15 15 import tempfile
16 16
17 17 from .i18n import _
18 18 from .node import nullid
19 19 from . import (
20 20 bundle2,
21 21 error,
22 22 httpconnection,
23 23 pycompat,
24 24 statichttprepo,
25 25 url,
26 26 util,
27 27 wireproto,
28 28 )
29 29
30 30 httplib = util.httplib
31 31 urlerr = util.urlerr
32 32 urlreq = util.urlreq
33 33
34 34 def encodevalueinheaders(value, header, limit):
35 35 """Encode a string value into multiple HTTP headers.
36 36
37 37 ``value`` will be encoded into 1 or more HTTP headers with the names
38 38 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
39 39 name + value will be at most ``limit`` bytes long.
40 40
41 41 Returns an iterable of 2-tuples consisting of header names and values.
42 42 """
43 43 fmt = header + '-%s'
44 44 valuelen = limit - len(fmt % '000') - len(': \r\n')
45 45 result = []
46 46
47 47 n = 0
48 48 for i in xrange(0, len(value), valuelen):
49 49 n += 1
50 50 result.append((fmt % str(n), value[i:i + valuelen]))
51 51
52 52 return result
53 53
54 54 def _wraphttpresponse(resp):
55 55 """Wrap an HTTPResponse with common error handlers.
56 56
57 57 This ensures that any I/O from any consumer raises the appropriate
58 58 error and messaging.
59 59 """
60 60 origread = resp.read
61 61
62 62 class readerproxy(resp.__class__):
63 63 def read(self, size=None):
64 64 try:
65 65 return origread(size)
66 66 except httplib.IncompleteRead as e:
67 67 # e.expected is an integer if length known or None otherwise.
68 68 if e.expected:
69 69 msg = _('HTTP request error (incomplete response; '
70 70 'expected %d bytes got %d)') % (e.expected,
71 71 len(e.partial))
72 72 else:
73 73 msg = _('HTTP request error (incomplete response)')
74 74
75 75 raise error.PeerTransportError(
76 76 msg,
77 77 hint=_('this may be an intermittent network failure; '
78 78 'if the error persists, consider contacting the '
79 79 'network or server operator'))
80 80 except httplib.HTTPException as e:
81 81 raise error.PeerTransportError(
82 82 _('HTTP request error (%s)') % e,
83 hint=_('this may be an intermittent failure; '
83 hint=_('this may be an intermittent network failure; '
84 84 'if the error persists, consider contacting the '
85 85 'network or server operator'))
86 86
87 87 resp.__class__ = readerproxy
88 88
89 89 class httppeer(wireproto.wirepeer):
90 90 def __init__(self, ui, path):
91 91 self.path = path
92 92 self.caps = None
93 93 self.handler = None
94 94 self.urlopener = None
95 95 self.requestbuilder = None
96 96 u = util.url(path)
97 97 if u.query or u.fragment:
98 98 raise error.Abort(_('unsupported URL component: "%s"') %
99 99 (u.query or u.fragment))
100 100
101 101 # urllib cannot handle URLs with embedded user or passwd
102 102 self._url, authinfo = u.authinfo()
103 103
104 104 self.ui = ui
105 105 self.ui.debug('using %s\n' % self._url)
106 106
107 107 self.urlopener = url.opener(ui, authinfo)
108 108 self.requestbuilder = urlreq.request
109 109
110 110 def __del__(self):
111 111 urlopener = getattr(self, 'urlopener', None)
112 112 if urlopener:
113 113 for h in urlopener.handlers:
114 114 h.close()
115 115 getattr(h, "close_all", lambda : None)()
116 116
117 117 def url(self):
118 118 return self.path
119 119
120 120 # look up capabilities only when needed
121 121
122 122 def _fetchcaps(self):
123 123 self.caps = set(self._call('capabilities').split())
124 124
125 125 def _capabilities(self):
126 126 if self.caps is None:
127 127 try:
128 128 self._fetchcaps()
129 129 except error.RepoError:
130 130 self.caps = set()
131 131 self.ui.debug('capabilities: %s\n' %
132 132 (' '.join(self.caps or ['none'])))
133 133 return self.caps
134 134
135 135 def lock(self):
136 136 raise error.Abort(_('operation not supported over http'))
137 137
138 138 def _callstream(self, cmd, _compressible=False, **args):
139 139 if cmd == 'pushkey':
140 140 args['data'] = ''
141 141 data = args.pop('data', None)
142 142 headers = args.pop('headers', {})
143 143
144 144 self.ui.debug("sending %s command\n" % cmd)
145 145 q = [('cmd', cmd)]
146 146 headersize = 0
147 147 varyheaders = []
148 148 # Important: don't use self.capable() here or else you end up
149 149 # with infinite recursion when trying to look up capabilities
150 150 # for the first time.
151 151 postargsok = self.caps is not None and 'httppostargs' in self.caps
152 152 # TODO: support for httppostargs when data is a file-like
153 153 # object rather than a basestring
154 154 canmungedata = not data or isinstance(data, basestring)
155 155 if postargsok and canmungedata:
156 156 strargs = urlreq.urlencode(sorted(args.items()))
157 157 if strargs:
158 158 if not data:
159 159 data = strargs
160 160 elif isinstance(data, basestring):
161 161 data = strargs + data
162 162 headers['X-HgArgs-Post'] = len(strargs)
163 163 else:
164 164 if len(args) > 0:
165 165 httpheader = self.capable('httpheader')
166 166 if httpheader:
167 167 headersize = int(httpheader.split(',', 1)[0])
168 168 if headersize > 0:
169 169 # The headers can typically carry more data than the URL.
170 170 encargs = urlreq.urlencode(sorted(args.items()))
171 171 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
172 172 headersize):
173 173 headers[header] = value
174 174 varyheaders.append(header)
175 175 else:
176 176 q += sorted(args.items())
177 177 qs = '?%s' % urlreq.urlencode(q)
178 178 cu = "%s%s" % (self._url, qs)
179 179 size = 0
180 180 if util.safehasattr(data, 'length'):
181 181 size = data.length
182 182 elif data is not None:
183 183 size = len(data)
184 184 if size and self.ui.configbool('ui', 'usehttp2', False):
185 185 headers['Expect'] = '100-Continue'
186 186 headers['X-HgHttp2'] = '1'
187 187 if data is not None and 'Content-Type' not in headers:
188 188 headers['Content-Type'] = 'application/mercurial-0.1'
189 189
190 190 # Tell the server we accept application/mercurial-0.2 and multiple
191 191 # compression formats if the server is capable of emitting those
192 192 # payloads.
193 193 protoparams = []
194 194
195 195 mediatypes = set()
196 196 if self.caps is not None:
197 197 mt = self.capable('httpmediatype')
198 198 if mt:
199 199 protoparams.append('0.1')
200 200 mediatypes = set(mt.split(','))
201 201
202 202 if '0.2tx' in mediatypes:
203 203 protoparams.append('0.2')
204 204
205 205 if '0.2tx' in mediatypes and self.capable('compression'):
206 206 # We /could/ compare supported compression formats and prune
207 207 # non-mutually supported or error if nothing is mutually supported.
208 208 # For now, send the full list to the server and have it error.
209 209 comps = [e.wireprotosupport().name for e in
210 210 util.compengines.supportedwireengines(util.CLIENTROLE)]
211 211 protoparams.append('comp=%s' % ','.join(comps))
212 212
213 213 if protoparams:
214 214 protoheaders = encodevalueinheaders(' '.join(protoparams),
215 215 'X-HgProto',
216 216 headersize or 1024)
217 217 for header, value in protoheaders:
218 218 headers[header] = value
219 219 varyheaders.append(header)
220 220
221 221 if varyheaders:
222 222 headers['Vary'] = ','.join(varyheaders)
223 223
224 224 req = self.requestbuilder(cu, data, headers)
225 225
226 226 if data is not None:
227 227 self.ui.debug("sending %s bytes\n" % size)
228 228 req.add_unredirected_header('Content-Length', '%d' % size)
229 229 try:
230 230 resp = self.urlopener.open(req)
231 231 except urlerr.httperror as inst:
232 232 if inst.code == 401:
233 233 raise error.Abort(_('authorization failed'))
234 234 raise
235 235 except httplib.HTTPException as inst:
236 236 self.ui.debug('http error while sending %s command\n' % cmd)
237 237 self.ui.traceback()
238 238 raise IOError(None, inst)
239 239
240 240 # Insert error handlers for common I/O failures.
241 241 _wraphttpresponse(resp)
242 242
243 243 # record the url we got redirected to
244 244 resp_url = resp.geturl()
245 245 if resp_url.endswith(qs):
246 246 resp_url = resp_url[:-len(qs)]
247 247 if self._url.rstrip('/') != resp_url.rstrip('/'):
248 248 if not self.ui.quiet:
249 249 self.ui.warn(_('real URL is %s\n') % resp_url)
250 250 self._url = resp_url
251 251 try:
252 252 proto = resp.getheader('content-type')
253 253 except AttributeError:
254 254 proto = resp.headers.get('content-type', '')
255 255
256 256 safeurl = util.hidepassword(self._url)
257 257 if proto.startswith('application/hg-error'):
258 258 raise error.OutOfBandError(resp.read())
259 259 # accept old "text/plain" and "application/hg-changegroup" for now
260 260 if not (proto.startswith('application/mercurial-') or
261 261 (proto.startswith('text/plain')
262 262 and not resp.headers.get('content-length')) or
263 263 proto.startswith('application/hg-changegroup')):
264 264 self.ui.debug("requested URL: '%s'\n" % util.hidepassword(cu))
265 265 raise error.RepoError(
266 266 _("'%s' does not appear to be an hg repository:\n"
267 267 "---%%<--- (%s)\n%s\n---%%<---\n")
268 268 % (safeurl, proto or 'no content-type', resp.read(1024)))
269 269
270 270 if proto.startswith('application/mercurial-'):
271 271 try:
272 272 version = proto.split('-', 1)[1]
273 273 version_info = tuple([int(n) for n in version.split('.')])
274 274 except ValueError:
275 275 raise error.RepoError(_("'%s' sent a broken Content-Type "
276 276 "header (%s)") % (safeurl, proto))
277 277
278 278 # TODO consider switching to a decompression reader that uses
279 279 # generators.
280 280 if version_info == (0, 1):
281 281 if _compressible:
282 282 return util.compengines['zlib'].decompressorreader(resp)
283 283 return resp
284 284 elif version_info == (0, 2):
285 285 # application/mercurial-0.2 always identifies the compression
286 286 # engine in the payload header.
287 287 elen = struct.unpack('B', resp.read(1))[0]
288 288 ename = resp.read(elen)
289 289 engine = util.compengines.forwiretype(ename)
290 290 return engine.decompressorreader(resp)
291 291 else:
292 292 raise error.RepoError(_("'%s' uses newer protocol %s") %
293 293 (safeurl, version))
294 294
295 295 if _compressible:
296 296 return util.compengines['zlib'].decompressorreader(resp)
297 297
298 298 return resp
299 299
300 300 def _call(self, cmd, **args):
301 301 fp = self._callstream(cmd, **args)
302 302 try:
303 303 return fp.read()
304 304 finally:
305 305 # if using keepalive, allow connection to be reused
306 306 fp.close()
307 307
308 308 def _callpush(self, cmd, cg, **args):
309 309 # have to stream bundle to a temp file because we do not have
310 310 # http 1.1 chunked transfer.
311 311
312 312 types = self.capable('unbundle')
313 313 try:
314 314 types = types.split(',')
315 315 except AttributeError:
316 316 # servers older than d1b16a746db6 will send 'unbundle' as a
317 317 # boolean capability. They only support headerless/uncompressed
318 318 # bundles.
319 319 types = [""]
320 320 for x in types:
321 321 if x in bundle2.bundletypes:
322 322 type = x
323 323 break
324 324
325 325 tempname = bundle2.writebundle(self.ui, cg, None, type)
326 326 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
327 327 headers = {'Content-Type': 'application/mercurial-0.1'}
328 328
329 329 try:
330 330 r = self._call(cmd, data=fp, headers=headers, **args)
331 331 vals = r.split('\n', 1)
332 332 if len(vals) < 2:
333 333 raise error.ResponseError(_("unexpected response:"), r)
334 334 return vals
335 335 except socket.error as err:
336 336 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
337 337 raise error.Abort(_('push failed: %s') % err.args[1])
338 338 raise error.Abort(err.args[1])
339 339 finally:
340 340 fp.close()
341 341 os.unlink(tempname)
342 342
343 343 def _calltwowaystream(self, cmd, fp, **args):
344 344 fh = None
345 345 fp_ = None
346 346 filename = None
347 347 try:
348 348 # dump bundle to disk
349 349 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
350 350 fh = os.fdopen(fd, pycompat.sysstr("wb"))
351 351 d = fp.read(4096)
352 352 while d:
353 353 fh.write(d)
354 354 d = fp.read(4096)
355 355 fh.close()
356 356 # start http push
357 357 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
358 358 headers = {'Content-Type': 'application/mercurial-0.1'}
359 359 return self._callstream(cmd, data=fp_, headers=headers, **args)
360 360 finally:
361 361 if fp_ is not None:
362 362 fp_.close()
363 363 if fh is not None:
364 364 fh.close()
365 365 os.unlink(filename)
366 366
367 367 def _callcompressable(self, cmd, **args):
368 368 return self._callstream(cmd, _compressible=True, **args)
369 369
370 370 def _abort(self, exception):
371 371 raise exception
372 372
373 373 class httpspeer(httppeer):
374 374 def __init__(self, ui, path):
375 375 if not url.has_https:
376 376 raise error.Abort(_('Python support for SSL and HTTPS '
377 377 'is not installed'))
378 378 httppeer.__init__(self, ui, path)
379 379
380 380 def instance(ui, path, create):
381 381 if create:
382 382 raise error.Abort(_('cannot create new http repository'))
383 383 try:
384 384 if path.startswith('https:'):
385 385 inst = httpspeer(ui, path)
386 386 else:
387 387 inst = httppeer(ui, path)
388 388 try:
389 389 # Try to do useful work when checking compatibility.
390 390 # Usually saves a roundtrip since we want the caps anyway.
391 391 inst._fetchcaps()
392 392 except error.RepoError:
393 393 # No luck, try older compatibility check.
394 394 inst.between([(nullid, nullid)])
395 395 return inst
396 396 except error.RepoError as httpexception:
397 397 try:
398 398 r = statichttprepo.instance(ui, "static-" + path, create)
399 399 ui.note(_('(falling back to static-http)\n'))
400 400 return r
401 401 except error.RepoError:
402 402 raise httpexception # use the original http RepoError instead
General Comments 0
You need to be logged in to leave comments. Login now