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