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