Show More
@@ -6,6 +6,7 | |||
|
6 | 6 | from __future__ import annotations |
|
7 | 7 | |
|
8 | 8 | import collections |
|
9 | import re | |
|
9 | 10 | import typing |
|
10 | 11 | |
|
11 | 12 | from typing import ( |
@@ -27,6 +28,7 from . import ( | |||
|
27 | 28 | error, |
|
28 | 29 | requirements as requirementsmod, |
|
29 | 30 | sslutil, |
|
31 | url as urlmod, | |
|
30 | 32 | util, |
|
31 | 33 | ) |
|
32 | 34 | from .utils import stringutil |
@@ -406,6 +408,9 def isstreamclonespec(bundlespec): | |||
|
406 | 408 | return False |
|
407 | 409 | |
|
408 | 410 | |
|
411 | digest_regex = re.compile(b'^[a-z0-9]+:[0-9a-f]+(,[a-z0-9]+:[0-9a-f]+)*$') | |
|
412 | ||
|
413 | ||
|
409 | 414 | def filterclonebundleentries( |
|
410 | 415 | repo, entries, streamclonerequested=False, pullbundles=False |
|
411 | 416 | ): |
@@ -481,6 +486,43 def filterclonebundleentries( | |||
|
481 | 486 | ) |
|
482 | 487 | continue |
|
483 | 488 | |
|
489 | if b'DIGEST' in entry: | |
|
490 | if not digest_regex.match(entry[b'DIGEST']): | |
|
491 | repo.ui.debug( | |
|
492 | b'filtering %s due to a bad DIGEST attribute\n' % url | |
|
493 | ) | |
|
494 | continue | |
|
495 | supported = 0 | |
|
496 | seen = {} | |
|
497 | for digest_entry in entry[b'DIGEST'].split(b','): | |
|
498 | algo, digest = digest_entry.split(b':') | |
|
499 | if algo not in seen: | |
|
500 | seen[algo] = digest | |
|
501 | elif seen[algo] != digest: | |
|
502 | repo.ui.debug( | |
|
503 | b'filtering %s due to conflicting %s digests\n' | |
|
504 | % (url, algo) | |
|
505 | ) | |
|
506 | supported = 0 | |
|
507 | break | |
|
508 | digester = urlmod.digesthandler.digest_algorithms.get(algo) | |
|
509 | if digester is None: | |
|
510 | continue | |
|
511 | if len(digest) != digester().digest_size * 2: | |
|
512 | repo.ui.debug( | |
|
513 | b'filtering %s due to a bad %s digest\n' % (url, algo) | |
|
514 | ) | |
|
515 | supported = 0 | |
|
516 | break | |
|
517 | supported += 1 | |
|
518 | else: | |
|
519 | if supported == 0: | |
|
520 | repo.ui.debug( | |
|
521 | b'filtering %s due to lack of supported digest\n' % url | |
|
522 | ) | |
|
523 | if supported == 0: | |
|
524 | continue | |
|
525 | ||
|
484 | 526 | newentries.append(entry) |
|
485 | 527 | |
|
486 | 528 | return newentries |
@@ -2900,8 +2900,23 def _maybeapplyclonebundle(pullop): | |||
|
2900 | 2900 | entries = bundlecaches.sortclonebundleentries(repo.ui, entries) |
|
2901 | 2901 | |
|
2902 | 2902 | url = entries[0][b'URL'] |
|
2903 | digest = entries[0].get(b'DIGEST') | |
|
2904 | if digest: | |
|
2905 | algorithms = urlmod.digesthandler.digest_algorithms.keys() | |
|
2906 | preference = dict(zip(algorithms, range(len(algorithms)))) | |
|
2907 | best_entry = None | |
|
2908 | best_preference = len(preference) | |
|
2909 | for digest_entry in digest.split(b','): | |
|
2910 | cur_algo, cur_digest = digest_entry.split(b':') | |
|
2911 | if cur_algo not in preference: | |
|
2912 | continue | |
|
2913 | if preference[cur_algo] < best_preference: | |
|
2914 | best_entry = digest_entry | |
|
2915 | best_preference = preference[cur_algo] | |
|
2916 | digest = best_entry | |
|
2917 | ||
|
2903 | 2918 | repo.ui.status(_(b'applying clone bundle from %s\n') % url) |
|
2904 | if trypullbundlefromurl(repo.ui, repo, url, remote): | |
|
2919 | if trypullbundlefromurl(repo.ui, repo, url, remote, digest): | |
|
2905 | 2920 | repo.ui.status(_(b'finished applying clone bundle\n')) |
|
2906 | 2921 | # Bundle failed. |
|
2907 | 2922 | # |
@@ -2930,14 +2945,14 def inline_clone_bundle_open(ui, url, pe | |||
|
2930 | 2945 | return util.chunkbuffer(peerclonebundle) |
|
2931 | 2946 | |
|
2932 | 2947 | |
|
2933 | def trypullbundlefromurl(ui, repo, url, peer): | |
|
2948 | def trypullbundlefromurl(ui, repo, url, peer, digest): | |
|
2934 | 2949 | """Attempt to apply a bundle from a URL.""" |
|
2935 | 2950 | with repo.lock(), repo.transaction(b'bundleurl') as tr: |
|
2936 | 2951 | try: |
|
2937 | 2952 | if url.startswith(bundlecaches.CLONEBUNDLESCHEME): |
|
2938 | 2953 | fh = inline_clone_bundle_open(ui, url, peer) |
|
2939 | 2954 | else: |
|
2940 | fh = urlmod.open(ui, url) | |
|
2955 | fh = urlmod.open(ui, url, digest=digest) | |
|
2941 | 2956 | cg = readbundle(ui, fh, b'stream') |
|
2942 | 2957 | |
|
2943 | 2958 | if isinstance(cg, streamclone.streamcloneapplier): |
@@ -10,9 +10,11 | |||
|
10 | 10 | from __future__ import annotations |
|
11 | 11 | |
|
12 | 12 | import base64 |
|
13 | import hashlib | |
|
13 | 14 | import socket |
|
14 | 15 | |
|
15 | 16 | from .i18n import _ |
|
17 | from .node import hex | |
|
16 | 18 | from . import ( |
|
17 | 19 | encoding, |
|
18 | 20 | error, |
@@ -499,6 +501,71 class readlinehandler(urlreq.basehandler | |||
|
499 | 501 | https_response = http_response |
|
500 | 502 | |
|
501 | 503 | |
|
504 | class digesthandler(urlreq.basehandler): | |
|
505 | # exchange.py assumes the algorithms are listed in order of preference, | |
|
506 | # earlier entries are prefered. | |
|
507 | digest_algorithms = { | |
|
508 | b'sha256': hashlib.sha256, | |
|
509 | b'sha512': hashlib.sha512, | |
|
510 | } | |
|
511 | ||
|
512 | def __init__(self, digest): | |
|
513 | if b':' not in digest: | |
|
514 | raise error.Abort(_(b'invalid digest specification')) | |
|
515 | algo, checksum = digest.split(b':') | |
|
516 | if algo not in self.digest_algorithms: | |
|
517 | raise error.Abort(_(b'unsupported digest algorithm: %s') % algo) | |
|
518 | self._digest = checksum | |
|
519 | self._hasher = self.digest_algorithms[algo]() | |
|
520 | ||
|
521 | def http_response(self, request, response): | |
|
522 | class digestresponse(response.__class__): | |
|
523 | def _digest_input(self, data): | |
|
524 | self._hasher.update(data) | |
|
525 | self._digest_consumed += len(data) | |
|
526 | if self._digest_finished: | |
|
527 | digest = hex(self._hasher.digest()) | |
|
528 | if digest != self._digest: | |
|
529 | raise error.SecurityError( | |
|
530 | _( | |
|
531 | b'file with digest %s expected, but %s found for %d bytes' | |
|
532 | ) | |
|
533 | % ( | |
|
534 | pycompat.bytestr(self._digest), | |
|
535 | pycompat.bytestr(digest), | |
|
536 | self._digest_consumed, | |
|
537 | ) | |
|
538 | ) | |
|
539 | ||
|
540 | def read(self, amt=None): | |
|
541 | data = super().read(amt) | |
|
542 | self._digest_input(data) | |
|
543 | return data | |
|
544 | ||
|
545 | def readline(self): | |
|
546 | data = super().readline() | |
|
547 | self._digest_input(data) | |
|
548 | return data | |
|
549 | ||
|
550 | def readinto(self, dest): | |
|
551 | got = super().readinto(dest) | |
|
552 | self._digest_input(dest[:got]) | |
|
553 | return got | |
|
554 | ||
|
555 | def _close_conn(self): | |
|
556 | self._digest_finished = True | |
|
557 | return super().close() | |
|
558 | ||
|
559 | response.__class__ = digestresponse | |
|
560 | response._digest = self._digest | |
|
561 | response._digest_consumed = 0 | |
|
562 | response._hasher = self._hasher.copy() | |
|
563 | response._digest_finished = False | |
|
564 | return response | |
|
565 | ||
|
566 | https_response = http_response | |
|
567 | ||
|
568 | ||
|
502 | 569 | handlerfuncs = [] |
|
503 | 570 | |
|
504 | 571 | |
@@ -510,6 +577,7 def opener( | |||
|
510 | 577 | loggingname=b's', |
|
511 | 578 | loggingopts=None, |
|
512 | 579 | sendaccept=True, |
|
580 | digest=None, | |
|
513 | 581 | ): |
|
514 | 582 | """ |
|
515 | 583 | construct an opener suitable for urllib2 |
@@ -562,6 +630,8 def opener( | |||
|
562 | 630 | handlers.extend([h(ui, passmgr) for h in handlerfuncs]) |
|
563 | 631 | handlers.append(urlreq.httpcookieprocessor(cookiejar=load_cookiejar(ui))) |
|
564 | 632 | handlers.append(readlinehandler()) |
|
633 | if digest: | |
|
634 | handlers.append(digesthandler(digest)) | |
|
565 | 635 | opener = urlreq.buildopener(*handlers) |
|
566 | 636 | |
|
567 | 637 | # keepalive.py's handlers will populate these attributes if they exist. |
@@ -600,7 +670,7 def opener( | |||
|
600 | 670 | return opener |
|
601 | 671 | |
|
602 | 672 | |
|
603 | def open(ui, url_, data=None, sendaccept=True): | |
|
673 | def open(ui, url_, data=None, sendaccept=True, digest=None): | |
|
604 | 674 | u = urlutil.url(url_) |
|
605 | 675 | if u.scheme: |
|
606 | 676 | u.scheme = u.scheme.lower() |
@@ -611,7 +681,7 def open(ui, url_, data=None, sendaccept | |||
|
611 | 681 | urlreq.pathname2url(pycompat.fsdecode(path)) |
|
612 | 682 | ) |
|
613 | 683 | authinfo = None |
|
614 | return opener(ui, authinfo, sendaccept=sendaccept).open( | |
|
684 | return opener(ui, authinfo, sendaccept=sendaccept, digest=digest).open( | |
|
615 | 685 | pycompat.strurl(url_), data |
|
616 | 686 | ) |
|
617 | 687 |
@@ -743,6 +743,66 on a 32MB system. | |||
|
743 | 743 | (sent 4 HTTP requests and * bytes; received * bytes in responses) (glob) |
|
744 | 744 | $ killdaemons.py |
|
745 | 745 | |
|
746 | Testing a clone bundle with digest | |
|
747 | ================================== | |
|
748 | ||
|
749 | $ "$PYTHON" $TESTDIR/dumbhttp.py -p $HGPORT1 --pid http.pid | |
|
750 | $ cat http.pid >> $DAEMON_PIDS | |
|
751 | $ hg -R server serve -d -p $HGPORT --pid-file hg.pid --accesslog access.log | |
|
752 | $ cat hg.pid >> $DAEMON_PIDS | |
|
753 | ||
|
754 | $ digest=$("$PYTHON" -c "import hashlib; print (hashlib.sha256(open('gz-a.hg', 'rb').read()).hexdigest())") | |
|
755 | $ cat > server/.hg/clonebundles.manifest << EOF | |
|
756 | > http://localhost:$HGPORT1/gz-a.hg BUNDLESPEC=gzip-v2 DIGEST=sha256:${digest} | |
|
757 | > EOF | |
|
758 | $ hg clone -U http://localhost:$HGPORT digest-valid | |
|
759 | applying clone bundle from http://localhost:$HGPORT1/gz-a.hg | |
|
760 | adding changesets | |
|
761 | adding manifests | |
|
762 | adding file changes | |
|
763 | added 2 changesets with 2 changes to 2 files | |
|
764 | finished applying clone bundle | |
|
765 | searching for changes | |
|
766 | no changes found | |
|
767 | 2 local changesets published | |
|
768 | $ digest_bad=$("$PYTHON" -c "import hashlib; print (hashlib.sha256(open('gz-a.hg', 'rb').read()+b'.').hexdigest())") | |
|
769 | $ cat > server/.hg/clonebundles.manifest << EOF | |
|
770 | > http://localhost:$HGPORT1/gz-a.hg BUNDLESPEC=gzip-v2 DIGEST=sha256:${digest_bad} | |
|
771 | > EOF | |
|
772 | $ hg clone -U http://localhost:$HGPORT digest-invalid | |
|
773 | applying clone bundle from http://localhost:$HGPORT1/gz-a.hg | |
|
774 | abort: file with digest [0-9a-f]* expected, but [0-9a-f]* found for [0-9]* bytes (re) | |
|
775 | [150] | |
|
776 | $ cat > server/.hg/clonebundles.manifest << EOF | |
|
777 | > http://localhost:$HGPORT1/bad-a.hg BUNDLESPEC=gzip-v2 DIGEST=sha256:xx | |
|
778 | > http://localhost:$HGPORT1/bad-b.hg BUNDLESPEC=gzip-v2 DIGEST=xxx:0000 | |
|
779 | > http://localhost:$HGPORT1/bad-c.hg BUNDLESPEC=gzip-v2 DIGEST=sha256:0000 | |
|
780 | > http://localhost:$HGPORT1/bad-d.hg BUNDLESPEC=gzip-v2 DIGEST=xxx:00,xxx:01 | |
|
781 | > http://localhost:$HGPORT1/gz-a.hg BUNDLESPEC=gzip-v2 DIGEST=sha256:${digest_bad} | |
|
782 | > EOF | |
|
783 | $ hg clone --debug -U http://localhost:$HGPORT digest-malformed | |
|
784 | using http://localhost:$HGPORT/ | |
|
785 | sending capabilities command | |
|
786 | sending clonebundles_manifest command | |
|
787 | filtering http://localhost:$HGPORT1/bad-a.hg due to a bad DIGEST attribute | |
|
788 | filtering http://localhost:$HGPORT1/bad-b.hg due to lack of supported digest | |
|
789 | filtering http://localhost:$HGPORT1/bad-c.hg due to a bad sha256 digest | |
|
790 | filtering http://localhost:$HGPORT1/bad-d.hg due to conflicting xxx digests | |
|
791 | applying clone bundle from http://localhost:$HGPORT1/gz-a.hg | |
|
792 | bundle2-input-bundle: 1 params with-transaction | |
|
793 | bundle2-input-bundle: 0 parts total | |
|
794 | \(sent [0-9]* HTTP requests and [0-9]* bytes; received [0-9]* bytes in responses\) (re) | |
|
795 | abort: file with digest [0-9a-f]* expected, but [0-9a-f]* found for [0-9]* bytes (re) | |
|
796 | [150] | |
|
797 | $ cat > server/.hg/clonebundles.manifest << EOF | |
|
798 | > http://localhost:$HGPORT1/gz-a.hg BUNDLESPEC=gzip-v2 DIGEST=sha512:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000,sha256:0000000000000000000000000000000000000000000000000000000000000000 | |
|
799 | > EOF | |
|
800 | $ hg clone -U http://localhost:$HGPORT digest-preference | |
|
801 | applying clone bundle from http://localhost:$HGPORT1/gz-a.hg | |
|
802 | abort: file with digest 0{64} expected, but [0-9a-f]+ found for [0-9]+ bytes (re) | |
|
803 | [150] | |
|
804 | $ killdaemons.py | |
|
805 | ||
|
746 | 806 | Testing a clone bundles that involves revlog splitting (issue6811) |
|
747 | 807 | ================================================================== |
|
748 | 808 |
General Comments 0
You need to be logged in to leave comments.
Login now