##// END OF EJS Templates
url: move _wraphttpresponse() from httpeer...
Gregory Szorc -
r40054:f80db6ad default
parent child Browse files
Show More
@@ -1,1006 +1,970 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 weakref
17 17
18 18 from .i18n import _
19 19 from . import (
20 20 bundle2,
21 21 error,
22 22 httpconnection,
23 23 pycompat,
24 24 repository,
25 25 statichttprepo,
26 26 url as urlmod,
27 27 util,
28 28 wireprotoframing,
29 29 wireprototypes,
30 30 wireprotov1peer,
31 31 wireprotov2peer,
32 32 wireprotov2server,
33 33 )
34 34 from .utils import (
35 35 cborutil,
36 36 interfaceutil,
37 37 stringutil,
38 38 )
39 39
40 40 httplib = util.httplib
41 41 urlerr = util.urlerr
42 42 urlreq = util.urlreq
43 43
44 44 def encodevalueinheaders(value, header, limit):
45 45 """Encode a string value into multiple HTTP headers.
46 46
47 47 ``value`` will be encoded into 1 or more HTTP headers with the names
48 48 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
49 49 name + value will be at most ``limit`` bytes long.
50 50
51 51 Returns an iterable of 2-tuples consisting of header names and
52 52 values as native strings.
53 53 """
54 54 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
55 55 # not bytes. This function always takes bytes in as arguments.
56 56 fmt = pycompat.strurl(header) + r'-%s'
57 57 # Note: it is *NOT* a bug that the last bit here is a bytestring
58 58 # and not a unicode: we're just getting the encoded length anyway,
59 59 # and using an r-string to make it portable between Python 2 and 3
60 60 # doesn't work because then the \r is a literal backslash-r
61 61 # instead of a carriage return.
62 62 valuelen = limit - len(fmt % r'000') - len(': \r\n')
63 63 result = []
64 64
65 65 n = 0
66 66 for i in pycompat.xrange(0, len(value), valuelen):
67 67 n += 1
68 68 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
69 69
70 70 return result
71 71
72 def _wraphttpresponse(resp):
73 """Wrap an HTTPResponse with common error handlers.
74
75 This ensures that any I/O from any consumer raises the appropriate
76 error and messaging.
77 """
78 origread = resp.read
79
80 class readerproxy(resp.__class__):
81 def read(self, size=None):
82 try:
83 return origread(size)
84 except httplib.IncompleteRead as e:
85 # e.expected is an integer if length known or None otherwise.
86 if e.expected:
87 got = len(e.partial)
88 total = e.expected + got
89 msg = _('HTTP request error (incomplete response; '
90 'expected %d bytes got %d)') % (total, got)
91 else:
92 msg = _('HTTP request error (incomplete response)')
93
94 raise error.PeerTransportError(
95 msg,
96 hint=_('this may be an intermittent network failure; '
97 'if the error persists, consider contacting the '
98 'network or server operator'))
99 except httplib.HTTPException as e:
100 raise error.PeerTransportError(
101 _('HTTP request error (%s)') % e,
102 hint=_('this may be an intermittent network failure; '
103 'if the error persists, consider contacting the '
104 'network or server operator'))
105
106 resp.__class__ = readerproxy
107
108 72 class _multifile(object):
109 73 def __init__(self, *fileobjs):
110 74 for f in fileobjs:
111 75 if not util.safehasattr(f, 'length'):
112 76 raise ValueError(
113 77 '_multifile only supports file objects that '
114 78 'have a length but this one does not:', type(f), f)
115 79 self._fileobjs = fileobjs
116 80 self._index = 0
117 81
118 82 @property
119 83 def length(self):
120 84 return sum(f.length for f in self._fileobjs)
121 85
122 86 def read(self, amt=None):
123 87 if amt <= 0:
124 88 return ''.join(f.read() for f in self._fileobjs)
125 89 parts = []
126 90 while amt and self._index < len(self._fileobjs):
127 91 parts.append(self._fileobjs[self._index].read(amt))
128 92 got = len(parts[-1])
129 93 if got < amt:
130 94 self._index += 1
131 95 amt -= got
132 96 return ''.join(parts)
133 97
134 98 def seek(self, offset, whence=os.SEEK_SET):
135 99 if whence != os.SEEK_SET:
136 100 raise NotImplementedError(
137 101 '_multifile does not support anything other'
138 102 ' than os.SEEK_SET for whence on seek()')
139 103 if offset != 0:
140 104 raise NotImplementedError(
141 105 '_multifile only supports seeking to start, but that '
142 106 'could be fixed if you need it')
143 107 for f in self._fileobjs:
144 108 f.seek(0)
145 109 self._index = 0
146 110
147 111 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
148 112 repobaseurl, cmd, args):
149 113 """Make an HTTP request to run a command for a version 1 client.
150 114
151 115 ``caps`` is a set of known server capabilities. The value may be
152 116 None if capabilities are not yet known.
153 117
154 118 ``capablefn`` is a function to evaluate a capability.
155 119
156 120 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
157 121 raw data to pass to it.
158 122 """
159 123 if cmd == 'pushkey':
160 124 args['data'] = ''
161 125 data = args.pop('data', None)
162 126 headers = args.pop('headers', {})
163 127
164 128 ui.debug("sending %s command\n" % cmd)
165 129 q = [('cmd', cmd)]
166 130 headersize = 0
167 131 # Important: don't use self.capable() here or else you end up
168 132 # with infinite recursion when trying to look up capabilities
169 133 # for the first time.
170 134 postargsok = caps is not None and 'httppostargs' in caps
171 135
172 136 # Send arguments via POST.
173 137 if postargsok and args:
174 138 strargs = urlreq.urlencode(sorted(args.items()))
175 139 if not data:
176 140 data = strargs
177 141 else:
178 142 if isinstance(data, bytes):
179 143 i = io.BytesIO(data)
180 144 i.length = len(data)
181 145 data = i
182 146 argsio = io.BytesIO(strargs)
183 147 argsio.length = len(strargs)
184 148 data = _multifile(argsio, data)
185 149 headers[r'X-HgArgs-Post'] = len(strargs)
186 150 elif args:
187 151 # Calling self.capable() can infinite loop if we are calling
188 152 # "capabilities". But that command should never accept wire
189 153 # protocol arguments. So this should never happen.
190 154 assert cmd != 'capabilities'
191 155 httpheader = capablefn('httpheader')
192 156 if httpheader:
193 157 headersize = int(httpheader.split(',', 1)[0])
194 158
195 159 # Send arguments via HTTP headers.
196 160 if headersize > 0:
197 161 # The headers can typically carry more data than the URL.
198 162 encargs = urlreq.urlencode(sorted(args.items()))
199 163 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
200 164 headersize):
201 165 headers[header] = value
202 166 # Send arguments via query string (Mercurial <1.9).
203 167 else:
204 168 q += sorted(args.items())
205 169
206 170 qs = '?%s' % urlreq.urlencode(q)
207 171 cu = "%s%s" % (repobaseurl, qs)
208 172 size = 0
209 173 if util.safehasattr(data, 'length'):
210 174 size = data.length
211 175 elif data is not None:
212 176 size = len(data)
213 177 if data is not None and r'Content-Type' not in headers:
214 178 headers[r'Content-Type'] = r'application/mercurial-0.1'
215 179
216 180 # Tell the server we accept application/mercurial-0.2 and multiple
217 181 # compression formats if the server is capable of emitting those
218 182 # payloads.
219 183 # Note: Keep this set empty by default, as client advertisement of
220 184 # protocol parameters should only occur after the handshake.
221 185 protoparams = set()
222 186
223 187 mediatypes = set()
224 188 if caps is not None:
225 189 mt = capablefn('httpmediatype')
226 190 if mt:
227 191 protoparams.add('0.1')
228 192 mediatypes = set(mt.split(','))
229 193
230 194 protoparams.add('partial-pull')
231 195
232 196 if '0.2tx' in mediatypes:
233 197 protoparams.add('0.2')
234 198
235 199 if '0.2tx' in mediatypes and capablefn('compression'):
236 200 # We /could/ compare supported compression formats and prune
237 201 # non-mutually supported or error if nothing is mutually supported.
238 202 # For now, send the full list to the server and have it error.
239 203 comps = [e.wireprotosupport().name for e in
240 204 util.compengines.supportedwireengines(util.CLIENTROLE)]
241 205 protoparams.add('comp=%s' % ','.join(comps))
242 206
243 207 if protoparams:
244 208 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
245 209 'X-HgProto',
246 210 headersize or 1024)
247 211 for header, value in protoheaders:
248 212 headers[header] = value
249 213
250 214 varyheaders = []
251 215 for header in headers:
252 216 if header.lower().startswith(r'x-hg'):
253 217 varyheaders.append(header)
254 218
255 219 if varyheaders:
256 220 headers[r'Vary'] = r','.join(sorted(varyheaders))
257 221
258 222 req = requestbuilder(pycompat.strurl(cu), data, headers)
259 223
260 224 if data is not None:
261 225 ui.debug("sending %d bytes\n" % size)
262 226 req.add_unredirected_header(r'Content-Length', r'%d' % size)
263 227
264 228 return req, cu, qs
265 229
266 230 def _reqdata(req):
267 231 """Get request data, if any. If no data, returns None."""
268 232 if pycompat.ispy3:
269 233 return req.data
270 234 if not req.has_data():
271 235 return None
272 236 return req.get_data()
273 237
274 238 def sendrequest(ui, opener, req):
275 239 """Send a prepared HTTP request.
276 240
277 241 Returns the response object.
278 242 """
279 243 dbg = ui.debug
280 244 if (ui.debugflag
281 245 and ui.configbool('devel', 'debug.peer-request')):
282 246 line = 'devel-peer-request: %s\n'
283 247 dbg(line % '%s %s' % (pycompat.bytesurl(req.get_method()),
284 248 pycompat.bytesurl(req.get_full_url())))
285 249 hgargssize = None
286 250
287 251 for header, value in sorted(req.header_items()):
288 252 header = pycompat.bytesurl(header)
289 253 value = pycompat.bytesurl(value)
290 254 if header.startswith('X-hgarg-'):
291 255 if hgargssize is None:
292 256 hgargssize = 0
293 257 hgargssize += len(value)
294 258 else:
295 259 dbg(line % ' %s %s' % (header, value))
296 260
297 261 if hgargssize is not None:
298 262 dbg(line % ' %d bytes of commands arguments in headers'
299 263 % hgargssize)
300 264 data = _reqdata(req)
301 265 if data is not None:
302 266 length = getattr(data, 'length', None)
303 267 if length is None:
304 268 length = len(data)
305 269 dbg(line % ' %d bytes of data' % length)
306 270
307 271 start = util.timer()
308 272
309 273 res = None
310 274 try:
311 275 res = opener.open(req)
312 276 except urlerr.httperror as inst:
313 277 if inst.code == 401:
314 278 raise error.Abort(_('authorization failed'))
315 279 raise
316 280 except httplib.HTTPException as inst:
317 281 ui.debug('http error requesting %s\n' %
318 282 util.hidepassword(req.get_full_url()))
319 283 ui.traceback()
320 284 raise IOError(None, inst)
321 285 finally:
322 286 if ui.debugflag and ui.configbool('devel', 'debug.peer-request'):
323 287 code = res.code if res else -1
324 288 dbg(line % ' finished in %.4f seconds (%d)'
325 289 % (util.timer() - start, code))
326 290
327 291 # Insert error handlers for common I/O failures.
328 _wraphttpresponse(res)
292 urlmod.wrapresponse(res)
329 293
330 294 return res
331 295
332 296 class RedirectedRepoError(error.RepoError):
333 297 def __init__(self, msg, respurl):
334 298 super(RedirectedRepoError, self).__init__(msg)
335 299 self.respurl = respurl
336 300
337 301 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
338 302 allowcbor=False):
339 303 # record the url we got redirected to
340 304 redirected = False
341 305 respurl = pycompat.bytesurl(resp.geturl())
342 306 if respurl.endswith(qs):
343 307 respurl = respurl[:-len(qs)]
344 308 qsdropped = False
345 309 else:
346 310 qsdropped = True
347 311
348 312 if baseurl.rstrip('/') != respurl.rstrip('/'):
349 313 redirected = True
350 314 if not ui.quiet:
351 315 ui.warn(_('real URL is %s\n') % respurl)
352 316
353 317 try:
354 318 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
355 319 except AttributeError:
356 320 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
357 321
358 322 safeurl = util.hidepassword(baseurl)
359 323 if proto.startswith('application/hg-error'):
360 324 raise error.OutOfBandError(resp.read())
361 325
362 326 # Pre 1.0 versions of Mercurial used text/plain and
363 327 # application/hg-changegroup. We don't support such old servers.
364 328 if not proto.startswith('application/mercurial-'):
365 329 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
366 330 msg = _("'%s' does not appear to be an hg repository:\n"
367 331 "---%%<--- (%s)\n%s\n---%%<---\n") % (
368 332 safeurl, proto or 'no content-type', resp.read(1024))
369 333
370 334 # Some servers may strip the query string from the redirect. We
371 335 # raise a special error type so callers can react to this specially.
372 336 if redirected and qsdropped:
373 337 raise RedirectedRepoError(msg, respurl)
374 338 else:
375 339 raise error.RepoError(msg)
376 340
377 341 try:
378 342 subtype = proto.split('-', 1)[1]
379 343
380 344 # Unless we end up supporting CBOR in the legacy wire protocol,
381 345 # this should ONLY be encountered for the initial capabilities
382 346 # request during handshake.
383 347 if subtype == 'cbor':
384 348 if allowcbor:
385 349 return respurl, proto, resp
386 350 else:
387 351 raise error.RepoError(_('unexpected CBOR response from '
388 352 'server'))
389 353
390 354 version_info = tuple([int(n) for n in subtype.split('.')])
391 355 except ValueError:
392 356 raise error.RepoError(_("'%s' sent a broken Content-Type "
393 357 "header (%s)") % (safeurl, proto))
394 358
395 359 # TODO consider switching to a decompression reader that uses
396 360 # generators.
397 361 if version_info == (0, 1):
398 362 if compressible:
399 363 resp = util.compengines['zlib'].decompressorreader(resp)
400 364
401 365 elif version_info == (0, 2):
402 366 # application/mercurial-0.2 always identifies the compression
403 367 # engine in the payload header.
404 368 elen = struct.unpack('B', util.readexactly(resp, 1))[0]
405 369 ename = util.readexactly(resp, elen)
406 370 engine = util.compengines.forwiretype(ename)
407 371
408 372 resp = engine.decompressorreader(resp)
409 373 else:
410 374 raise error.RepoError(_("'%s' uses newer protocol %s") %
411 375 (safeurl, subtype))
412 376
413 377 return respurl, proto, resp
414 378
415 379 class httppeer(wireprotov1peer.wirepeer):
416 380 def __init__(self, ui, path, url, opener, requestbuilder, caps):
417 381 self.ui = ui
418 382 self._path = path
419 383 self._url = url
420 384 self._caps = caps
421 385 self._urlopener = opener
422 386 self._requestbuilder = requestbuilder
423 387
424 388 def __del__(self):
425 389 for h in self._urlopener.handlers:
426 390 h.close()
427 391 getattr(h, "close_all", lambda: None)()
428 392
429 393 # Begin of ipeerconnection interface.
430 394
431 395 def url(self):
432 396 return self._path
433 397
434 398 def local(self):
435 399 return None
436 400
437 401 def peer(self):
438 402 return self
439 403
440 404 def canpush(self):
441 405 return True
442 406
443 407 def close(self):
444 408 pass
445 409
446 410 # End of ipeerconnection interface.
447 411
448 412 # Begin of ipeercommands interface.
449 413
450 414 def capabilities(self):
451 415 return self._caps
452 416
453 417 # End of ipeercommands interface.
454 418
455 419 def _callstream(self, cmd, _compressible=False, **args):
456 420 args = pycompat.byteskwargs(args)
457 421
458 422 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
459 423 self._caps, self.capable,
460 424 self._url, cmd, args)
461 425
462 426 resp = sendrequest(self.ui, self._urlopener, req)
463 427
464 428 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
465 429 resp, _compressible)
466 430
467 431 return resp
468 432
469 433 def _call(self, cmd, **args):
470 434 fp = self._callstream(cmd, **args)
471 435 try:
472 436 return fp.read()
473 437 finally:
474 438 # if using keepalive, allow connection to be reused
475 439 fp.close()
476 440
477 441 def _callpush(self, cmd, cg, **args):
478 442 # have to stream bundle to a temp file because we do not have
479 443 # http 1.1 chunked transfer.
480 444
481 445 types = self.capable('unbundle')
482 446 try:
483 447 types = types.split(',')
484 448 except AttributeError:
485 449 # servers older than d1b16a746db6 will send 'unbundle' as a
486 450 # boolean capability. They only support headerless/uncompressed
487 451 # bundles.
488 452 types = [""]
489 453 for x in types:
490 454 if x in bundle2.bundletypes:
491 455 type = x
492 456 break
493 457
494 458 tempname = bundle2.writebundle(self.ui, cg, None, type)
495 459 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
496 460 headers = {r'Content-Type': r'application/mercurial-0.1'}
497 461
498 462 try:
499 463 r = self._call(cmd, data=fp, headers=headers, **args)
500 464 vals = r.split('\n', 1)
501 465 if len(vals) < 2:
502 466 raise error.ResponseError(_("unexpected response:"), r)
503 467 return vals
504 468 except urlerr.httperror:
505 469 # Catch and re-raise these so we don't try and treat them
506 470 # like generic socket errors. They lack any values in
507 471 # .args on Python 3 which breaks our socket.error block.
508 472 raise
509 473 except socket.error as err:
510 474 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
511 475 raise error.Abort(_('push failed: %s') % err.args[1])
512 476 raise error.Abort(err.args[1])
513 477 finally:
514 478 fp.close()
515 479 os.unlink(tempname)
516 480
517 481 def _calltwowaystream(self, cmd, fp, **args):
518 482 fh = None
519 483 fp_ = None
520 484 filename = None
521 485 try:
522 486 # dump bundle to disk
523 487 fd, filename = pycompat.mkstemp(prefix="hg-bundle-", suffix=".hg")
524 488 fh = os.fdopen(fd, r"wb")
525 489 d = fp.read(4096)
526 490 while d:
527 491 fh.write(d)
528 492 d = fp.read(4096)
529 493 fh.close()
530 494 # start http push
531 495 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
532 496 headers = {r'Content-Type': r'application/mercurial-0.1'}
533 497 return self._callstream(cmd, data=fp_, headers=headers, **args)
534 498 finally:
535 499 if fp_ is not None:
536 500 fp_.close()
537 501 if fh is not None:
538 502 fh.close()
539 503 os.unlink(filename)
540 504
541 505 def _callcompressable(self, cmd, **args):
542 506 return self._callstream(cmd, _compressible=True, **args)
543 507
544 508 def _abort(self, exception):
545 509 raise exception
546 510
547 511 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests):
548 512 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
549 513 buffersends=True)
550 514
551 515 handler = wireprotov2peer.clienthandler(ui, reactor)
552 516
553 517 url = '%s/%s' % (apiurl, permission)
554 518
555 519 if len(requests) > 1:
556 520 url += '/multirequest'
557 521 else:
558 522 url += '/%s' % requests[0][0]
559 523
560 524 ui.debug('sending %d commands\n' % len(requests))
561 525 for command, args, f in requests:
562 526 ui.debug('sending command %s: %s\n' % (
563 527 command, stringutil.pprint(args, indent=2)))
564 528 assert not list(handler.callcommand(command, args, f))
565 529
566 530 # TODO stream this.
567 531 body = b''.join(map(bytes, handler.flushcommands()))
568 532
569 533 # TODO modify user-agent to reflect v2
570 534 headers = {
571 535 r'Accept': wireprotov2server.FRAMINGTYPE,
572 536 r'Content-Type': wireprotov2server.FRAMINGTYPE,
573 537 }
574 538
575 539 req = requestbuilder(pycompat.strurl(url), body, headers)
576 540 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
577 541
578 542 try:
579 543 res = opener.open(req)
580 544 except urlerr.httperror as e:
581 545 if e.code == 401:
582 546 raise error.Abort(_('authorization failed'))
583 547
584 548 raise
585 549 except httplib.HTTPException as e:
586 550 ui.traceback()
587 551 raise IOError(None, e)
588 552
589 553 return handler, res
590 554
591 555 class queuedcommandfuture(pycompat.futures.Future):
592 556 """Wraps result() on command futures to trigger submission on call."""
593 557
594 558 def result(self, timeout=None):
595 559 if self.done():
596 560 return pycompat.futures.Future.result(self, timeout)
597 561
598 562 self._peerexecutor.sendcommands()
599 563
600 564 # sendcommands() will restore the original __class__ and self.result
601 565 # will resolve to Future.result.
602 566 return self.result(timeout)
603 567
604 568 @interfaceutil.implementer(repository.ipeercommandexecutor)
605 569 class httpv2executor(object):
606 570 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor):
607 571 self._ui = ui
608 572 self._opener = opener
609 573 self._requestbuilder = requestbuilder
610 574 self._apiurl = apiurl
611 575 self._descriptor = descriptor
612 576 self._sent = False
613 577 self._closed = False
614 578 self._neededpermissions = set()
615 579 self._calls = []
616 580 self._futures = weakref.WeakSet()
617 581 self._responseexecutor = None
618 582 self._responsef = None
619 583
620 584 def __enter__(self):
621 585 return self
622 586
623 587 def __exit__(self, exctype, excvalue, exctb):
624 588 self.close()
625 589
626 590 def callcommand(self, command, args):
627 591 if self._sent:
628 592 raise error.ProgrammingError('callcommand() cannot be used after '
629 593 'commands are sent')
630 594
631 595 if self._closed:
632 596 raise error.ProgrammingError('callcommand() cannot be used after '
633 597 'close()')
634 598
635 599 # The service advertises which commands are available. So if we attempt
636 600 # to call an unknown command or pass an unknown argument, we can screen
637 601 # for this.
638 602 if command not in self._descriptor['commands']:
639 603 raise error.ProgrammingError(
640 604 'wire protocol command %s is not available' % command)
641 605
642 606 cmdinfo = self._descriptor['commands'][command]
643 607 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
644 608
645 609 if unknownargs:
646 610 raise error.ProgrammingError(
647 611 'wire protocol command %s does not accept argument: %s' % (
648 612 command, ', '.join(sorted(unknownargs))))
649 613
650 614 self._neededpermissions |= set(cmdinfo['permissions'])
651 615
652 616 # TODO we /could/ also validate types here, since the API descriptor
653 617 # includes types...
654 618
655 619 f = pycompat.futures.Future()
656 620
657 621 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
658 622 # could deadlock.
659 623 f.__class__ = queuedcommandfuture
660 624 f._peerexecutor = self
661 625
662 626 self._futures.add(f)
663 627 self._calls.append((command, args, f))
664 628
665 629 return f
666 630
667 631 def sendcommands(self):
668 632 if self._sent:
669 633 return
670 634
671 635 if not self._calls:
672 636 return
673 637
674 638 self._sent = True
675 639
676 640 # Unhack any future types so caller sees a clean type and so we
677 641 # break reference cycle.
678 642 for f in self._futures:
679 643 if isinstance(f, queuedcommandfuture):
680 644 f.__class__ = pycompat.futures.Future
681 645 f._peerexecutor = None
682 646
683 647 # Mark the future as running and filter out cancelled futures.
684 648 calls = [(command, args, f)
685 649 for command, args, f in self._calls
686 650 if f.set_running_or_notify_cancel()]
687 651
688 652 # Clear out references, prevent improper object usage.
689 653 self._calls = None
690 654
691 655 if not calls:
692 656 return
693 657
694 658 permissions = set(self._neededpermissions)
695 659
696 660 if 'push' in permissions and 'pull' in permissions:
697 661 permissions.remove('pull')
698 662
699 663 if len(permissions) > 1:
700 664 raise error.RepoError(_('cannot make request requiring multiple '
701 665 'permissions: %s') %
702 666 _(', ').join(sorted(permissions)))
703 667
704 668 permission = {
705 669 'push': 'rw',
706 670 'pull': 'ro',
707 671 }[permissions.pop()]
708 672
709 673 handler, resp = sendv2request(
710 674 self._ui, self._opener, self._requestbuilder, self._apiurl,
711 675 permission, calls)
712 676
713 677 # TODO we probably want to validate the HTTP code, media type, etc.
714 678
715 679 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
716 680 self._responsef = self._responseexecutor.submit(self._handleresponse,
717 681 handler, resp)
718 682
719 683 def close(self):
720 684 if self._closed:
721 685 return
722 686
723 687 self.sendcommands()
724 688
725 689 self._closed = True
726 690
727 691 if not self._responsef:
728 692 return
729 693
730 694 # TODO ^C here may not result in immediate program termination.
731 695
732 696 try:
733 697 self._responsef.result()
734 698 finally:
735 699 self._responseexecutor.shutdown(wait=True)
736 700 self._responsef = None
737 701 self._responseexecutor = None
738 702
739 703 # If any of our futures are still in progress, mark them as
740 704 # errored, otherwise a result() could wait indefinitely.
741 705 for f in self._futures:
742 706 if not f.done():
743 707 f.set_exception(error.ResponseError(
744 708 _('unfulfilled command response')))
745 709
746 710 self._futures = None
747 711
748 712 def _handleresponse(self, handler, resp):
749 713 # Called in a thread to read the response.
750 714
751 715 while handler.readframe(resp):
752 716 pass
753 717
754 718 # TODO implement interface for version 2 peers
755 719 @interfaceutil.implementer(repository.ipeerconnection,
756 720 repository.ipeercapabilities,
757 721 repository.ipeerrequests)
758 722 class httpv2peer(object):
759 723 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
760 724 apidescriptor):
761 725 self.ui = ui
762 726
763 727 if repourl.endswith('/'):
764 728 repourl = repourl[:-1]
765 729
766 730 self._url = repourl
767 731 self._apipath = apipath
768 732 self._apiurl = '%s/%s' % (repourl, apipath)
769 733 self._opener = opener
770 734 self._requestbuilder = requestbuilder
771 735 self._descriptor = apidescriptor
772 736
773 737 # Start of ipeerconnection.
774 738
775 739 def url(self):
776 740 return self._url
777 741
778 742 def local(self):
779 743 return None
780 744
781 745 def peer(self):
782 746 return self
783 747
784 748 def canpush(self):
785 749 # TODO change once implemented.
786 750 return False
787 751
788 752 def close(self):
789 753 pass
790 754
791 755 # End of ipeerconnection.
792 756
793 757 # Start of ipeercapabilities.
794 758
795 759 def capable(self, name):
796 760 # The capabilities used internally historically map to capabilities
797 761 # advertised from the "capabilities" wire protocol command. However,
798 762 # version 2 of that command works differently.
799 763
800 764 # Maps to commands that are available.
801 765 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
802 766 return True
803 767
804 768 # Other concepts.
805 769 if name in ('bundle2'):
806 770 return True
807 771
808 772 # Alias command-* to presence of command of that name.
809 773 if name.startswith('command-'):
810 774 return name[len('command-'):] in self._descriptor['commands']
811 775
812 776 return False
813 777
814 778 def requirecap(self, name, purpose):
815 779 if self.capable(name):
816 780 return
817 781
818 782 raise error.CapabilityError(
819 783 _('cannot %s; client or remote repository does not support the %r '
820 784 'capability') % (purpose, name))
821 785
822 786 # End of ipeercapabilities.
823 787
824 788 def _call(self, name, **args):
825 789 with self.commandexecutor() as e:
826 790 return e.callcommand(name, args).result()
827 791
828 792 def commandexecutor(self):
829 793 return httpv2executor(self.ui, self._opener, self._requestbuilder,
830 794 self._apiurl, self._descriptor)
831 795
832 796 # Registry of API service names to metadata about peers that handle it.
833 797 #
834 798 # The following keys are meaningful:
835 799 #
836 800 # init
837 801 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
838 802 # apidescriptor) to create a peer.
839 803 #
840 804 # priority
841 805 # Integer priority for the service. If we could choose from multiple
842 806 # services, we choose the one with the highest priority.
843 807 API_PEERS = {
844 808 wireprototypes.HTTP_WIREPROTO_V2: {
845 809 'init': httpv2peer,
846 810 'priority': 50,
847 811 },
848 812 }
849 813
850 814 def performhandshake(ui, url, opener, requestbuilder):
851 815 # The handshake is a request to the capabilities command.
852 816
853 817 caps = None
854 818 def capable(x):
855 819 raise error.ProgrammingError('should not be called')
856 820
857 821 args = {}
858 822
859 823 # The client advertises support for newer protocols by adding an
860 824 # X-HgUpgrade-* header with a list of supported APIs and an
861 825 # X-HgProto-* header advertising which serializing formats it supports.
862 826 # We only support the HTTP version 2 transport and CBOR responses for
863 827 # now.
864 828 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
865 829
866 830 if advertisev2:
867 831 args['headers'] = {
868 832 r'X-HgProto-1': r'cbor',
869 833 }
870 834
871 835 args['headers'].update(
872 836 encodevalueinheaders(' '.join(sorted(API_PEERS)),
873 837 'X-HgUpgrade',
874 838 # We don't know the header limit this early.
875 839 # So make it small.
876 840 1024))
877 841
878 842 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
879 843 capable, url, 'capabilities',
880 844 args)
881 845 resp = sendrequest(ui, opener, req)
882 846
883 847 # The server may redirect us to the repo root, stripping the
884 848 # ?cmd=capabilities query string from the URL. The server would likely
885 849 # return HTML in this case and ``parsev1commandresponse()`` would raise.
886 850 # We catch this special case and re-issue the capabilities request against
887 851 # the new URL.
888 852 #
889 853 # We should ideally not do this, as a redirect that drops the query
890 854 # string from the URL is arguably a server bug. (Garbage in, garbage out).
891 855 # However, Mercurial clients for several years appeared to handle this
892 856 # issue without behavior degradation. And according to issue 5860, it may
893 857 # be a longstanding bug in some server implementations. So we allow a
894 858 # redirect that drops the query string to "just work."
895 859 try:
896 860 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
897 861 compressible=False,
898 862 allowcbor=advertisev2)
899 863 except RedirectedRepoError as e:
900 864 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
901 865 capable, e.respurl,
902 866 'capabilities', args)
903 867 resp = sendrequest(ui, opener, req)
904 868 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
905 869 compressible=False,
906 870 allowcbor=advertisev2)
907 871
908 872 try:
909 873 rawdata = resp.read()
910 874 finally:
911 875 resp.close()
912 876
913 877 if not ct.startswith('application/mercurial-'):
914 878 raise error.ProgrammingError('unexpected content-type: %s' % ct)
915 879
916 880 if advertisev2:
917 881 if ct == 'application/mercurial-cbor':
918 882 try:
919 883 info = cborutil.decodeall(rawdata)[0]
920 884 except cborutil.CBORDecodeError:
921 885 raise error.Abort(_('error decoding CBOR from remote server'),
922 886 hint=_('try again and consider contacting '
923 887 'the server operator'))
924 888
925 889 # We got a legacy response. That's fine.
926 890 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
927 891 info = {
928 892 'v1capabilities': set(rawdata.split())
929 893 }
930 894
931 895 else:
932 896 raise error.RepoError(
933 897 _('unexpected response type from server: %s') % ct)
934 898 else:
935 899 info = {
936 900 'v1capabilities': set(rawdata.split())
937 901 }
938 902
939 903 return respurl, info
940 904
941 905 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
942 906 """Construct an appropriate HTTP peer instance.
943 907
944 908 ``opener`` is an ``url.opener`` that should be used to establish
945 909 connections, perform HTTP requests.
946 910
947 911 ``requestbuilder`` is the type used for constructing HTTP requests.
948 912 It exists as an argument so extensions can override the default.
949 913 """
950 914 u = util.url(path)
951 915 if u.query or u.fragment:
952 916 raise error.Abort(_('unsupported URL component: "%s"') %
953 917 (u.query or u.fragment))
954 918
955 919 # urllib cannot handle URLs with embedded user or passwd.
956 920 url, authinfo = u.authinfo()
957 921 ui.debug('using %s\n' % url)
958 922
959 923 opener = opener or urlmod.opener(ui, authinfo)
960 924
961 925 respurl, info = performhandshake(ui, url, opener, requestbuilder)
962 926
963 927 # Given the intersection of APIs that both we and the server support,
964 928 # sort by their advertised priority and pick the first one.
965 929 #
966 930 # TODO consider making this request-based and interface driven. For
967 931 # example, the caller could say "I want a peer that does X." It's quite
968 932 # possible that not all peers would do that. Since we know the service
969 933 # capabilities, we could filter out services not meeting the
970 934 # requirements. Possibly by consulting the interfaces defined by the
971 935 # peer type.
972 936 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
973 937
974 938 preferredchoices = sorted(apipeerchoices,
975 939 key=lambda x: API_PEERS[x]['priority'],
976 940 reverse=True)
977 941
978 942 for service in preferredchoices:
979 943 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
980 944
981 945 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
982 946 requestbuilder,
983 947 info['apis'][service])
984 948
985 949 # Failed to construct an API peer. Fall back to legacy.
986 950 return httppeer(ui, path, respurl, opener, requestbuilder,
987 951 info['v1capabilities'])
988 952
989 953 def instance(ui, path, create, intents=None, createopts=None):
990 954 if create:
991 955 raise error.Abort(_('cannot create new http repository'))
992 956 try:
993 957 if path.startswith('https:') and not urlmod.has_https:
994 958 raise error.Abort(_('Python support for SSL and HTTPS '
995 959 'is not installed'))
996 960
997 961 inst = makepeer(ui, path)
998 962
999 963 return inst
1000 964 except error.RepoError as httpexception:
1001 965 try:
1002 966 r = statichttprepo.instance(ui, "static-" + path, create)
1003 967 ui.note(_('(falling back to static-http)\n'))
1004 968 return r
1005 969 except error.RepoError:
1006 970 raise httpexception # use the original http RepoError instead
@@ -1,597 +1,633 b''
1 1 # url.py - HTTP handling for mercurial
2 2 #
3 3 # Copyright 2005, 2006, 2007, 2008 Matt Mackall <mpm@selenic.com>
4 4 # Copyright 2006, 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
5 5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import
11 11
12 12 import base64
13 13 import os
14 14 import socket
15 15 import sys
16 16
17 17 from .i18n import _
18 18 from . import (
19 19 encoding,
20 20 error,
21 21 httpconnection as httpconnectionmod,
22 22 keepalive,
23 23 pycompat,
24 24 sslutil,
25 25 urllibcompat,
26 26 util,
27 27 )
28 28 from .utils import (
29 29 stringutil,
30 30 )
31 31
32 32 httplib = util.httplib
33 33 stringio = util.stringio
34 34 urlerr = util.urlerr
35 35 urlreq = util.urlreq
36 36
37 37 def escape(s, quote=None):
38 38 '''Replace special characters "&", "<" and ">" to HTML-safe sequences.
39 39 If the optional flag quote is true, the quotation mark character (")
40 40 is also translated.
41 41
42 42 This is the same as cgi.escape in Python, but always operates on
43 43 bytes, whereas cgi.escape in Python 3 only works on unicodes.
44 44 '''
45 45 s = s.replace(b"&", b"&amp;")
46 46 s = s.replace(b"<", b"&lt;")
47 47 s = s.replace(b">", b"&gt;")
48 48 if quote:
49 49 s = s.replace(b'"', b"&quot;")
50 50 return s
51 51
52 52 class passwordmgr(object):
53 53 def __init__(self, ui, passwddb):
54 54 self.ui = ui
55 55 self.passwddb = passwddb
56 56
57 57 def add_password(self, realm, uri, user, passwd):
58 58 return self.passwddb.add_password(realm, uri, user, passwd)
59 59
60 60 def find_user_password(self, realm, authuri):
61 61 authinfo = self.passwddb.find_user_password(realm, authuri)
62 62 user, passwd = authinfo
63 63 if user and passwd:
64 64 self._writedebug(user, passwd)
65 65 return (user, passwd)
66 66
67 67 if not user or not passwd:
68 68 res = httpconnectionmod.readauthforuri(self.ui, authuri, user)
69 69 if res:
70 70 group, auth = res
71 71 user, passwd = auth.get('username'), auth.get('password')
72 72 self.ui.debug("using auth.%s.* for authentication\n" % group)
73 73 if not user or not passwd:
74 74 u = util.url(pycompat.bytesurl(authuri))
75 75 u.query = None
76 76 if not self.ui.interactive():
77 77 raise error.Abort(_('http authorization required for %s') %
78 78 util.hidepassword(bytes(u)))
79 79
80 80 self.ui.write(_("http authorization required for %s\n") %
81 81 util.hidepassword(bytes(u)))
82 82 self.ui.write(_("realm: %s\n") % pycompat.bytesurl(realm))
83 83 if user:
84 84 self.ui.write(_("user: %s\n") % user)
85 85 else:
86 86 user = self.ui.prompt(_("user:"), default=None)
87 87
88 88 if not passwd:
89 89 passwd = self.ui.getpass()
90 90
91 91 self.passwddb.add_password(realm, authuri, user, passwd)
92 92 self._writedebug(user, passwd)
93 93 return (user, passwd)
94 94
95 95 def _writedebug(self, user, passwd):
96 96 msg = _('http auth: user %s, password %s\n')
97 97 self.ui.debug(msg % (user, passwd and '*' * len(passwd) or 'not set'))
98 98
99 99 def find_stored_password(self, authuri):
100 100 return self.passwddb.find_user_password(None, authuri)
101 101
102 102 class proxyhandler(urlreq.proxyhandler):
103 103 def __init__(self, ui):
104 104 proxyurl = (ui.config("http_proxy", "host") or
105 105 encoding.environ.get('http_proxy'))
106 106 # XXX proxyauthinfo = None
107 107
108 108 if proxyurl:
109 109 # proxy can be proper url or host[:port]
110 110 if not (proxyurl.startswith('http:') or
111 111 proxyurl.startswith('https:')):
112 112 proxyurl = 'http://' + proxyurl + '/'
113 113 proxy = util.url(proxyurl)
114 114 if not proxy.user:
115 115 proxy.user = ui.config("http_proxy", "user")
116 116 proxy.passwd = ui.config("http_proxy", "passwd")
117 117
118 118 # see if we should use a proxy for this url
119 119 no_list = ["localhost", "127.0.0.1"]
120 120 no_list.extend([p.lower() for
121 121 p in ui.configlist("http_proxy", "no")])
122 122 no_list.extend([p.strip().lower() for
123 123 p in encoding.environ.get("no_proxy", '').split(',')
124 124 if p.strip()])
125 125 # "http_proxy.always" config is for running tests on localhost
126 126 if ui.configbool("http_proxy", "always"):
127 127 self.no_list = []
128 128 else:
129 129 self.no_list = no_list
130 130
131 131 proxyurl = bytes(proxy)
132 132 proxies = {'http': proxyurl, 'https': proxyurl}
133 133 ui.debug('proxying through %s\n' % util.hidepassword(proxyurl))
134 134 else:
135 135 proxies = {}
136 136
137 137 urlreq.proxyhandler.__init__(self, proxies)
138 138 self.ui = ui
139 139
140 140 def proxy_open(self, req, proxy, type_):
141 141 host = urllibcompat.gethost(req).split(':')[0]
142 142 for e in self.no_list:
143 143 if host == e:
144 144 return None
145 145 if e.startswith('*.') and host.endswith(e[2:]):
146 146 return None
147 147 if e.startswith('.') and host.endswith(e[1:]):
148 148 return None
149 149
150 150 return urlreq.proxyhandler.proxy_open(self, req, proxy, type_)
151 151
152 152 def _gen_sendfile(orgsend):
153 153 def _sendfile(self, data):
154 154 # send a file
155 155 if isinstance(data, httpconnectionmod.httpsendfile):
156 156 # if auth required, some data sent twice, so rewind here
157 157 data.seek(0)
158 158 for chunk in util.filechunkiter(data):
159 159 orgsend(self, chunk)
160 160 else:
161 161 orgsend(self, data)
162 162 return _sendfile
163 163
164 164 has_https = util.safehasattr(urlreq, 'httpshandler')
165 165
166 166 class httpconnection(keepalive.HTTPConnection):
167 167 # must be able to send big bundle as stream.
168 168 send = _gen_sendfile(keepalive.HTTPConnection.send)
169 169
170 170 def getresponse(self):
171 171 proxyres = getattr(self, 'proxyres', None)
172 172 if proxyres:
173 173 if proxyres.will_close:
174 174 self.close()
175 175 self.proxyres = None
176 176 return proxyres
177 177 return keepalive.HTTPConnection.getresponse(self)
178 178
179 179 # general transaction handler to support different ways to handle
180 180 # HTTPS proxying before and after Python 2.6.3.
181 181 def _generic_start_transaction(handler, h, req):
182 182 tunnel_host = getattr(req, '_tunnel_host', None)
183 183 if tunnel_host:
184 184 if tunnel_host[:7] not in ['http://', 'https:/']:
185 185 tunnel_host = 'https://' + tunnel_host
186 186 new_tunnel = True
187 187 else:
188 188 tunnel_host = urllibcompat.getselector(req)
189 189 new_tunnel = False
190 190
191 191 if new_tunnel or tunnel_host == urllibcompat.getfullurl(req): # has proxy
192 192 u = util.url(tunnel_host)
193 193 if new_tunnel or u.scheme == 'https': # only use CONNECT for HTTPS
194 194 h.realhostport = ':'.join([u.host, (u.port or '443')])
195 195 h.headers = req.headers.copy()
196 196 h.headers.update(handler.parent.addheaders)
197 197 return
198 198
199 199 h.realhostport = None
200 200 h.headers = None
201 201
202 202 def _generic_proxytunnel(self):
203 203 proxyheaders = dict(
204 204 [(x, self.headers[x]) for x in self.headers
205 205 if x.lower().startswith('proxy-')])
206 206 self.send('CONNECT %s HTTP/1.0\r\n' % self.realhostport)
207 207 for header in proxyheaders.iteritems():
208 208 self.send('%s: %s\r\n' % header)
209 209 self.send('\r\n')
210 210
211 211 # majority of the following code is duplicated from
212 212 # httplib.HTTPConnection as there are no adequate places to
213 213 # override functions to provide the needed functionality
214 214 res = self.response_class(self.sock,
215 215 strict=self.strict,
216 216 method=self._method)
217 217
218 218 while True:
219 219 version, status, reason = res._read_status()
220 220 if status != httplib.CONTINUE:
221 221 break
222 222 # skip lines that are all whitespace
223 223 list(iter(lambda: res.fp.readline().strip(), ''))
224 224 res.status = status
225 225 res.reason = reason.strip()
226 226
227 227 if res.status == 200:
228 228 # skip lines until we find a blank line
229 229 list(iter(res.fp.readline, '\r\n'))
230 230 return True
231 231
232 232 if version == 'HTTP/1.0':
233 233 res.version = 10
234 234 elif version.startswith('HTTP/1.'):
235 235 res.version = 11
236 236 elif version == 'HTTP/0.9':
237 237 res.version = 9
238 238 else:
239 239 raise httplib.UnknownProtocol(version)
240 240
241 241 if res.version == 9:
242 242 res.length = None
243 243 res.chunked = 0
244 244 res.will_close = 1
245 245 res.msg = httplib.HTTPMessage(stringio())
246 246 return False
247 247
248 248 res.msg = httplib.HTTPMessage(res.fp)
249 249 res.msg.fp = None
250 250
251 251 # are we using the chunked-style of transfer encoding?
252 252 trenc = res.msg.getheader('transfer-encoding')
253 253 if trenc and trenc.lower() == "chunked":
254 254 res.chunked = 1
255 255 res.chunk_left = None
256 256 else:
257 257 res.chunked = 0
258 258
259 259 # will the connection close at the end of the response?
260 260 res.will_close = res._check_close()
261 261
262 262 # do we have a Content-Length?
263 263 # NOTE: RFC 2616, section 4.4, #3 says we ignore this if
264 264 # transfer-encoding is "chunked"
265 265 length = res.msg.getheader('content-length')
266 266 if length and not res.chunked:
267 267 try:
268 268 res.length = int(length)
269 269 except ValueError:
270 270 res.length = None
271 271 else:
272 272 if res.length < 0: # ignore nonsensical negative lengths
273 273 res.length = None
274 274 else:
275 275 res.length = None
276 276
277 277 # does the body have a fixed length? (of zero)
278 278 if (status == httplib.NO_CONTENT or status == httplib.NOT_MODIFIED or
279 279 100 <= status < 200 or # 1xx codes
280 280 res._method == 'HEAD'):
281 281 res.length = 0
282 282
283 283 # if the connection remains open, and we aren't using chunked, and
284 284 # a content-length was not provided, then assume that the connection
285 285 # WILL close.
286 286 if (not res.will_close and
287 287 not res.chunked and
288 288 res.length is None):
289 289 res.will_close = 1
290 290
291 291 self.proxyres = res
292 292
293 293 return False
294 294
295 295 class httphandler(keepalive.HTTPHandler):
296 296 def http_open(self, req):
297 297 return self.do_open(httpconnection, req)
298 298
299 299 def _start_transaction(self, h, req):
300 300 _generic_start_transaction(self, h, req)
301 301 return keepalive.HTTPHandler._start_transaction(self, h, req)
302 302
303 303 class logginghttpconnection(keepalive.HTTPConnection):
304 304 def __init__(self, createconn, *args, **kwargs):
305 305 keepalive.HTTPConnection.__init__(self, *args, **kwargs)
306 306 self._create_connection = createconn
307 307
308 308 if sys.version_info < (2, 7, 7):
309 309 # copied from 2.7.14, since old implementations directly call
310 310 # socket.create_connection()
311 311 def connect(self):
312 312 self.sock = self._create_connection((self.host, self.port),
313 313 self.timeout,
314 314 self.source_address)
315 315 if self._tunnel_host:
316 316 self._tunnel()
317 317
318 318 class logginghttphandler(httphandler):
319 319 """HTTP handler that logs socket I/O."""
320 320 def __init__(self, logfh, name, observeropts):
321 321 super(logginghttphandler, self).__init__()
322 322
323 323 self._logfh = logfh
324 324 self._logname = name
325 325 self._observeropts = observeropts
326 326
327 327 # do_open() calls the passed class to instantiate an HTTPConnection. We
328 328 # pass in a callable method that creates a custom HTTPConnection instance
329 329 # whose callback to create the socket knows how to proxy the socket.
330 330 def http_open(self, req):
331 331 return self.do_open(self._makeconnection, req)
332 332
333 333 def _makeconnection(self, *args, **kwargs):
334 334 def createconnection(*args, **kwargs):
335 335 sock = socket.create_connection(*args, **kwargs)
336 336 return util.makeloggingsocket(self._logfh, sock, self._logname,
337 337 **self._observeropts)
338 338
339 339 return logginghttpconnection(createconnection, *args, **kwargs)
340 340
341 341 if has_https:
342 342 class httpsconnection(httplib.HTTPConnection):
343 343 response_class = keepalive.HTTPResponse
344 344 default_port = httplib.HTTPS_PORT
345 345 # must be able to send big bundle as stream.
346 346 send = _gen_sendfile(keepalive.safesend)
347 347 getresponse = keepalive.wrapgetresponse(httplib.HTTPConnection)
348 348
349 349 def __init__(self, host, port=None, key_file=None, cert_file=None,
350 350 *args, **kwargs):
351 351 httplib.HTTPConnection.__init__(self, host, port, *args, **kwargs)
352 352 self.key_file = key_file
353 353 self.cert_file = cert_file
354 354
355 355 def connect(self):
356 356 self.sock = socket.create_connection((self.host, self.port))
357 357
358 358 host = self.host
359 359 if self.realhostport: # use CONNECT proxy
360 360 _generic_proxytunnel(self)
361 361 host = self.realhostport.rsplit(':', 1)[0]
362 362 self.sock = sslutil.wrapsocket(
363 363 self.sock, self.key_file, self.cert_file, ui=self.ui,
364 364 serverhostname=host)
365 365 sslutil.validatesocket(self.sock)
366 366
367 367 class httpshandler(keepalive.KeepAliveHandler, urlreq.httpshandler):
368 368 def __init__(self, ui):
369 369 keepalive.KeepAliveHandler.__init__(self)
370 370 urlreq.httpshandler.__init__(self)
371 371 self.ui = ui
372 372 self.pwmgr = passwordmgr(self.ui,
373 373 self.ui.httppasswordmgrdb)
374 374
375 375 def _start_transaction(self, h, req):
376 376 _generic_start_transaction(self, h, req)
377 377 return keepalive.KeepAliveHandler._start_transaction(self, h, req)
378 378
379 379 def https_open(self, req):
380 380 # urllibcompat.getfullurl() does not contain credentials
381 381 # and we may need them to match the certificates.
382 382 url = urllibcompat.getfullurl(req)
383 383 user, password = self.pwmgr.find_stored_password(url)
384 384 res = httpconnectionmod.readauthforuri(self.ui, url, user)
385 385 if res:
386 386 group, auth = res
387 387 self.auth = auth
388 388 self.ui.debug("using auth.%s.* for authentication\n" % group)
389 389 else:
390 390 self.auth = None
391 391 return self.do_open(self._makeconnection, req)
392 392
393 393 def _makeconnection(self, host, port=None, *args, **kwargs):
394 394 keyfile = None
395 395 certfile = None
396 396
397 397 if len(args) >= 1: # key_file
398 398 keyfile = args[0]
399 399 if len(args) >= 2: # cert_file
400 400 certfile = args[1]
401 401 args = args[2:]
402 402
403 403 # if the user has specified different key/cert files in
404 404 # hgrc, we prefer these
405 405 if self.auth and 'key' in self.auth and 'cert' in self.auth:
406 406 keyfile = self.auth['key']
407 407 certfile = self.auth['cert']
408 408
409 409 conn = httpsconnection(host, port, keyfile, certfile, *args,
410 410 **kwargs)
411 411 conn.ui = self.ui
412 412 return conn
413 413
414 414 class httpdigestauthhandler(urlreq.httpdigestauthhandler):
415 415 def __init__(self, *args, **kwargs):
416 416 urlreq.httpdigestauthhandler.__init__(self, *args, **kwargs)
417 417 self.retried_req = None
418 418
419 419 def reset_retry_count(self):
420 420 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
421 421 # forever. We disable reset_retry_count completely and reset in
422 422 # http_error_auth_reqed instead.
423 423 pass
424 424
425 425 def http_error_auth_reqed(self, auth_header, host, req, headers):
426 426 # Reset the retry counter once for each request.
427 427 if req is not self.retried_req:
428 428 self.retried_req = req
429 429 self.retried = 0
430 430 return urlreq.httpdigestauthhandler.http_error_auth_reqed(
431 431 self, auth_header, host, req, headers)
432 432
433 433 class httpbasicauthhandler(urlreq.httpbasicauthhandler):
434 434 def __init__(self, *args, **kwargs):
435 435 self.auth = None
436 436 urlreq.httpbasicauthhandler.__init__(self, *args, **kwargs)
437 437 self.retried_req = None
438 438
439 439 def http_request(self, request):
440 440 if self.auth:
441 441 request.add_unredirected_header(self.auth_header, self.auth)
442 442
443 443 return request
444 444
445 445 def https_request(self, request):
446 446 if self.auth:
447 447 request.add_unredirected_header(self.auth_header, self.auth)
448 448
449 449 return request
450 450
451 451 def reset_retry_count(self):
452 452 # Python 2.6.5 will call this on 401 or 407 errors and thus loop
453 453 # forever. We disable reset_retry_count completely and reset in
454 454 # http_error_auth_reqed instead.
455 455 pass
456 456
457 457 def http_error_auth_reqed(self, auth_header, host, req, headers):
458 458 # Reset the retry counter once for each request.
459 459 if req is not self.retried_req:
460 460 self.retried_req = req
461 461 self.retried = 0
462 462 return urlreq.httpbasicauthhandler.http_error_auth_reqed(
463 463 self, auth_header, host, req, headers)
464 464
465 465 def retry_http_basic_auth(self, host, req, realm):
466 466 user, pw = self.passwd.find_user_password(
467 467 realm, urllibcompat.getfullurl(req))
468 468 if pw is not None:
469 469 raw = "%s:%s" % (pycompat.bytesurl(user), pycompat.bytesurl(pw))
470 470 auth = r'Basic %s' % pycompat.strurl(base64.b64encode(raw).strip())
471 471 if req.get_header(self.auth_header, None) == auth:
472 472 return None
473 473 self.auth = auth
474 474 req.add_unredirected_header(self.auth_header, auth)
475 475 return self.parent.open(req)
476 476 else:
477 477 return None
478 478
479 479 class cookiehandler(urlreq.basehandler):
480 480 def __init__(self, ui):
481 481 self.cookiejar = None
482 482
483 483 cookiefile = ui.config('auth', 'cookiefile')
484 484 if not cookiefile:
485 485 return
486 486
487 487 cookiefile = util.expandpath(cookiefile)
488 488 try:
489 489 cookiejar = util.cookielib.MozillaCookieJar(
490 490 pycompat.fsdecode(cookiefile))
491 491 cookiejar.load()
492 492 self.cookiejar = cookiejar
493 493 except util.cookielib.LoadError as e:
494 494 ui.warn(_('(error loading cookie file %s: %s; continuing without '
495 495 'cookies)\n') % (cookiefile, stringutil.forcebytestr(e)))
496 496
497 497 def http_request(self, request):
498 498 if self.cookiejar:
499 499 self.cookiejar.add_cookie_header(request)
500 500
501 501 return request
502 502
503 503 def https_request(self, request):
504 504 if self.cookiejar:
505 505 self.cookiejar.add_cookie_header(request)
506 506
507 507 return request
508 508
509 509 handlerfuncs = []
510 510
511 511 def opener(ui, authinfo=None, useragent=None, loggingfh=None,
512 512 loggingname=b's', loggingopts=None, sendaccept=True):
513 513 '''
514 514 construct an opener suitable for urllib2
515 515 authinfo will be added to the password manager
516 516
517 517 The opener can be configured to log socket events if the various
518 518 ``logging*`` arguments are specified.
519 519
520 520 ``loggingfh`` denotes a file object to log events to.
521 521 ``loggingname`` denotes the name of the to print when logging.
522 522 ``loggingopts`` is a dict of keyword arguments to pass to the constructed
523 523 ``util.socketobserver`` instance.
524 524
525 525 ``sendaccept`` allows controlling whether the ``Accept`` request header
526 526 is sent. The header is sent by default.
527 527 '''
528 528 handlers = []
529 529
530 530 if loggingfh:
531 531 handlers.append(logginghttphandler(loggingfh, loggingname,
532 532 loggingopts or {}))
533 533 # We don't yet support HTTPS when logging I/O. If we attempt to open
534 534 # an HTTPS URL, we'll likely fail due to unknown protocol.
535 535
536 536 else:
537 537 handlers.append(httphandler())
538 538 if has_https:
539 539 handlers.append(httpshandler(ui))
540 540
541 541 handlers.append(proxyhandler(ui))
542 542
543 543 passmgr = passwordmgr(ui, ui.httppasswordmgrdb)
544 544 if authinfo is not None:
545 545 realm, uris, user, passwd = authinfo
546 546 saveduser, savedpass = passmgr.find_stored_password(uris[0])
547 547 if user != saveduser or passwd:
548 548 passmgr.add_password(realm, uris, user, passwd)
549 549 ui.debug('http auth: user %s, password %s\n' %
550 550 (user, passwd and '*' * len(passwd) or 'not set'))
551 551
552 552 handlers.extend((httpbasicauthhandler(passmgr),
553 553 httpdigestauthhandler(passmgr)))
554 554 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
555 555 handlers.append(cookiehandler(ui))
556 556 opener = urlreq.buildopener(*handlers)
557 557
558 558 # The user agent should should *NOT* be used by servers for e.g.
559 559 # protocol detection or feature negotiation: there are other
560 560 # facilities for that.
561 561 #
562 562 # "mercurial/proto-1.0" was the original user agent string and
563 563 # exists for backwards compatibility reasons.
564 564 #
565 565 # The "(Mercurial %s)" string contains the distribution
566 566 # name and version. Other client implementations should choose their
567 567 # own distribution name. Since servers should not be using the user
568 568 # agent string for anything, clients should be able to define whatever
569 569 # user agent they deem appropriate.
570 570 #
571 571 # The custom user agent is for lfs, because unfortunately some servers
572 572 # do look at this value.
573 573 if not useragent:
574 574 agent = 'mercurial/proto-1.0 (Mercurial %s)' % util.version()
575 575 opener.addheaders = [(r'User-agent', pycompat.sysstr(agent))]
576 576 else:
577 577 opener.addheaders = [(r'User-agent', pycompat.sysstr(useragent))]
578 578
579 579 # This header should only be needed by wire protocol requests. But it has
580 580 # been sent on all requests since forever. We keep sending it for backwards
581 581 # compatibility reasons. Modern versions of the wire protocol use
582 582 # X-HgProto-<N> for advertising client support.
583 583 if sendaccept:
584 584 opener.addheaders.append((r'Accept', r'application/mercurial-0.1'))
585 585
586 586 return opener
587 587
588 588 def open(ui, url_, data=None):
589 589 u = util.url(url_)
590 590 if u.scheme:
591 591 u.scheme = u.scheme.lower()
592 592 url_, authinfo = u.authinfo()
593 593 else:
594 594 path = util.normpath(os.path.abspath(url_))
595 595 url_ = 'file://' + pycompat.bytesurl(urlreq.pathname2url(path))
596 596 authinfo = None
597 597 return opener(ui, authinfo).open(pycompat.strurl(url_), data)
598
599 def wrapresponse(resp):
600 """Wrap a response object with common error handlers.
601
602 This ensures that any I/O from any consumer raises the appropriate
603 error and messaging.
604 """
605 origread = resp.read
606
607 class readerproxy(resp.__class__):
608 def read(self, size=None):
609 try:
610 return origread(size)
611 except httplib.IncompleteRead as e:
612 # e.expected is an integer if length known or None otherwise.
613 if e.expected:
614 got = len(e.partial)
615 total = e.expected + got
616 msg = _('HTTP request error (incomplete response; '
617 'expected %d bytes got %d)') % (total, got)
618 else:
619 msg = _('HTTP request error (incomplete response)')
620
621 raise error.PeerTransportError(
622 msg,
623 hint=_('this may be an intermittent network failure; '
624 'if the error persists, consider contacting the '
625 'network or server operator'))
626 except httplib.HTTPException as e:
627 raise error.PeerTransportError(
628 _('HTTP request error (%s)') % e,
629 hint=_('this may be an intermittent network failure; '
630 'if the error persists, consider contacting the '
631 'network or server operator'))
632
633 resp.__class__ = readerproxy
General Comments 0
You need to be logged in to leave comments. Login now