##// END OF EJS Templates
clonebundles: allow manifest to specify sha256 digest of bundles
Joerg Sonnenberger -
r52875:aa7f4a45 default
parent child Browse files
Show More
@@ -6,6 +6,7
6 from __future__ import annotations
6 from __future__ import annotations
7
7
8 import collections
8 import collections
9 import re
9 import typing
10 import typing
10
11
11 from typing import (
12 from typing import (
@@ -27,6 +28,7 from . import (
27 error,
28 error,
28 requirements as requirementsmod,
29 requirements as requirementsmod,
29 sslutil,
30 sslutil,
31 url as urlmod,
30 util,
32 util,
31 )
33 )
32 from .utils import stringutil
34 from .utils import stringutil
@@ -406,6 +408,9 def isstreamclonespec(bundlespec):
406 return False
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 def filterclonebundleentries(
414 def filterclonebundleentries(
410 repo, entries, streamclonerequested=False, pullbundles=False
415 repo, entries, streamclonerequested=False, pullbundles=False
411 ):
416 ):
@@ -481,6 +486,43 def filterclonebundleentries(
481 )
486 )
482 continue
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 newentries.append(entry)
526 newentries.append(entry)
485
527
486 return newentries
528 return newentries
@@ -2900,8 +2900,23 def _maybeapplyclonebundle(pullop):
2900 entries = bundlecaches.sortclonebundleentries(repo.ui, entries)
2900 entries = bundlecaches.sortclonebundleentries(repo.ui, entries)
2901
2901
2902 url = entries[0][b'URL']
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 repo.ui.status(_(b'applying clone bundle from %s\n') % url)
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 repo.ui.status(_(b'finished applying clone bundle\n'))
2920 repo.ui.status(_(b'finished applying clone bundle\n'))
2906 # Bundle failed.
2921 # Bundle failed.
2907 #
2922 #
@@ -2930,14 +2945,14 def inline_clone_bundle_open(ui, url, pe
2930 return util.chunkbuffer(peerclonebundle)
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 """Attempt to apply a bundle from a URL."""
2949 """Attempt to apply a bundle from a URL."""
2935 with repo.lock(), repo.transaction(b'bundleurl') as tr:
2950 with repo.lock(), repo.transaction(b'bundleurl') as tr:
2936 try:
2951 try:
2937 if url.startswith(bundlecaches.CLONEBUNDLESCHEME):
2952 if url.startswith(bundlecaches.CLONEBUNDLESCHEME):
2938 fh = inline_clone_bundle_open(ui, url, peer)
2953 fh = inline_clone_bundle_open(ui, url, peer)
2939 else:
2954 else:
2940 fh = urlmod.open(ui, url)
2955 fh = urlmod.open(ui, url, digest=digest)
2941 cg = readbundle(ui, fh, b'stream')
2956 cg = readbundle(ui, fh, b'stream')
2942
2957
2943 if isinstance(cg, streamclone.streamcloneapplier):
2958 if isinstance(cg, streamclone.streamcloneapplier):
@@ -10,9 +10,11
10 from __future__ import annotations
10 from __future__ import annotations
11
11
12 import base64
12 import base64
13 import hashlib
13 import socket
14 import socket
14
15
15 from .i18n import _
16 from .i18n import _
17 from .node import hex
16 from . import (
18 from . import (
17 encoding,
19 encoding,
18 error,
20 error,
@@ -499,6 +501,71 class readlinehandler(urlreq.basehandler
499 https_response = http_response
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 handlerfuncs = []
569 handlerfuncs = []
503
570
504
571
@@ -510,6 +577,7 def opener(
510 loggingname=b's',
577 loggingname=b's',
511 loggingopts=None,
578 loggingopts=None,
512 sendaccept=True,
579 sendaccept=True,
580 digest=None,
513 ):
581 ):
514 """
582 """
515 construct an opener suitable for urllib2
583 construct an opener suitable for urllib2
@@ -562,6 +630,8 def opener(
562 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
630 handlers.extend([h(ui, passmgr) for h in handlerfuncs])
563 handlers.append(urlreq.httpcookieprocessor(cookiejar=load_cookiejar(ui)))
631 handlers.append(urlreq.httpcookieprocessor(cookiejar=load_cookiejar(ui)))
564 handlers.append(readlinehandler())
632 handlers.append(readlinehandler())
633 if digest:
634 handlers.append(digesthandler(digest))
565 opener = urlreq.buildopener(*handlers)
635 opener = urlreq.buildopener(*handlers)
566
636
567 # keepalive.py's handlers will populate these attributes if they exist.
637 # keepalive.py's handlers will populate these attributes if they exist.
@@ -600,7 +670,7 def opener(
600 return opener
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 u = urlutil.url(url_)
674 u = urlutil.url(url_)
605 if u.scheme:
675 if u.scheme:
606 u.scheme = u.scheme.lower()
676 u.scheme = u.scheme.lower()
@@ -611,7 +681,7 def open(ui, url_, data=None, sendaccept
611 urlreq.pathname2url(pycompat.fsdecode(path))
681 urlreq.pathname2url(pycompat.fsdecode(path))
612 )
682 )
613 authinfo = None
683 authinfo = None
614 return opener(ui, authinfo, sendaccept=sendaccept).open(
684 return opener(ui, authinfo, sendaccept=sendaccept, digest=digest).open(
615 pycompat.strurl(url_), data
685 pycompat.strurl(url_), data
616 )
686 )
617
687
@@ -743,6 +743,66 on a 32MB system.
743 (sent 4 HTTP requests and * bytes; received * bytes in responses) (glob)
743 (sent 4 HTTP requests and * bytes; received * bytes in responses) (glob)
744 $ killdaemons.py
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 Testing a clone bundles that involves revlog splitting (issue6811)
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