From 50e21a90347a8a4bcc88487245c93f0379811dde Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 1 Dec 2021 09:55:44 -0500 Subject: [PATCH 01/55] Split StorageServer into generic part and Foolscap part. --- src/allmydata/storage/server.py | 127 ++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 49 deletions(-) diff --git a/src/allmydata/storage/server.py b/src/allmydata/storage/server.py index 7dc277e39..9d3ac4012 100644 --- a/src/allmydata/storage/server.py +++ b/src/allmydata/storage/server.py @@ -12,7 +12,7 @@ if PY2: # strings. Omit bytes so we don't leak future's custom bytes. from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, pow, round, super, dict, list, object, range, str, max, min # noqa: F401 else: - from typing import Dict + from typing import Dict, Tuple import os, re import six @@ -56,12 +56,11 @@ NUM_RE=re.compile("^[0-9]+$") DEFAULT_RENEWAL_TIME = 31 * 24 * 60 * 60 -@implementer(RIStorageServer, IStatsProducer) -class StorageServer(service.MultiService, Referenceable): +@implementer(IStatsProducer) +class StorageServer(service.MultiService): """ - A filesystem-based implementation of ``RIStorageServer``. + Implement the business logic for the storage server. """ - name = 'storage' LeaseCheckerClass = LeaseCheckingCrawler def __init__(self, storedir, nodeid, reserved_space=0, @@ -125,16 +124,8 @@ class StorageServer(service.MultiService, Referenceable): self.lease_checker.setServiceParent(self) self._clock = clock - # Currently being-written Bucketwriters. For Foolscap, lifetime is tied - # to connection: when disconnection happens, the BucketWriters are - # removed. For HTTP, this makes no sense, so there will be - # timeout-based cleanup; see - # https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3807. - # Map in-progress filesystem path -> BucketWriter: self._bucket_writers = {} # type: Dict[str,BucketWriter] - # Canaries and disconnect markers for BucketWriters created via Foolscap: - self._bucket_writer_disconnect_markers = {} # type: Dict[BucketWriter,(IRemoteReference, object)] def stopService(self): # Cancel any in-progress uploads: @@ -263,7 +254,7 @@ class StorageServer(service.MultiService, Referenceable): space += bw.allocated_size() return space - def remote_get_version(self): + def get_version(self): remaining_space = self.get_available_space() if remaining_space is None: # We're on a platform that has no API to get disk stats. @@ -284,7 +275,7 @@ class StorageServer(service.MultiService, Referenceable): } return version - def _allocate_buckets(self, storage_index, + def allocate_buckets(self, storage_index, renew_secret, cancel_secret, sharenums, allocated_size, owner_num=0, renew_leases=True): @@ -371,21 +362,6 @@ class StorageServer(service.MultiService, Referenceable): self.add_latency("allocate", self._clock.seconds() - start) return alreadygot, bucketwriters - def remote_allocate_buckets(self, storage_index, - renew_secret, cancel_secret, - sharenums, allocated_size, - canary, owner_num=0): - """Foolscap-specific ``allocate_buckets()`` API.""" - alreadygot, bucketwriters = self._allocate_buckets( - storage_index, renew_secret, cancel_secret, sharenums, allocated_size, - owner_num=owner_num, renew_leases=True, - ) - # Abort BucketWriters if disconnection happens. - for bw in bucketwriters.values(): - disconnect_marker = canary.notifyOnDisconnect(bw.disconnected) - self._bucket_writer_disconnect_markers[bw] = (canary, disconnect_marker) - return alreadygot, bucketwriters - def _iter_share_files(self, storage_index): for shnum, filename in self._get_bucket_shares(storage_index): with open(filename, 'rb') as f: @@ -401,8 +377,7 @@ class StorageServer(service.MultiService, Referenceable): continue # non-sharefile yield sf - def remote_add_lease(self, storage_index, renew_secret, cancel_secret, - owner_num=1): + def add_lease(self, storage_index, renew_secret, cancel_secret, owner_num=1): start = self._clock.seconds() self.count("add-lease") new_expire_time = self._clock.seconds() + DEFAULT_RENEWAL_TIME @@ -414,7 +389,7 @@ class StorageServer(service.MultiService, Referenceable): self.add_latency("add-lease", self._clock.seconds() - start) return None - def remote_renew_lease(self, storage_index, renew_secret): + def renew_lease(self, storage_index, renew_secret): start = self._clock.seconds() self.count("renew") new_expire_time = self._clock.seconds() + DEFAULT_RENEWAL_TIME @@ -448,7 +423,7 @@ class StorageServer(service.MultiService, Referenceable): # Commonly caused by there being no buckets at all. pass - def remote_get_buckets(self, storage_index): + def get_buckets(self, storage_index): start = self._clock.seconds() self.count("get") si_s = si_b2a(storage_index) @@ -698,18 +673,6 @@ class StorageServer(service.MultiService, Referenceable): self.add_latency("writev", self._clock.seconds() - start) return (testv_is_good, read_data) - def remote_slot_testv_and_readv_and_writev(self, storage_index, - secrets, - test_and_write_vectors, - read_vector): - return self.slot_testv_and_readv_and_writev( - storage_index, - secrets, - test_and_write_vectors, - read_vector, - renew_leases=True, - ) - def _allocate_slot_share(self, bucketdir, secrets, sharenum, owner_num=0): (write_enabler, renew_secret, cancel_secret) = secrets @@ -720,7 +683,7 @@ class StorageServer(service.MultiService, Referenceable): self) return share - def remote_slot_readv(self, storage_index, shares, readv): + def slot_readv(self, storage_index, shares, readv): start = self._clock.seconds() self.count("readv") si_s = si_b2a(storage_index) @@ -747,8 +710,8 @@ class StorageServer(service.MultiService, Referenceable): self.add_latency("readv", self._clock.seconds() - start) return datavs - def remote_advise_corrupt_share(self, share_type, storage_index, shnum, - reason): + def advise_corrupt_share(self, share_type, storage_index, shnum, + reason): # This is a remote API, I believe, so this has to be bytes for legacy # protocol backwards compatibility reasons. assert isinstance(share_type, bytes) @@ -774,3 +737,69 @@ class StorageServer(service.MultiService, Referenceable): share_type=share_type, si=si_s, shnum=shnum, reason=reason, level=log.SCARY, umid="SGx2fA") return None + + +@implementer(RIStorageServer) +class FoolscapStorageServer(Referenceable): # type: ignore # warner/foolscap#78 + """ + A filesystem-based implementation of ``RIStorageServer``. + + For Foolscap, BucketWriter lifetime is tied to connection: when + disconnection happens, the BucketWriters are removed. + """ + name = 'storage' + + def __init__(self, storage_server): # type: (StorageServer) -> None + self._server = storage_server + + # Canaries and disconnect markers for BucketWriters created via Foolscap: + self._bucket_writer_disconnect_markers = {} # type: Dict[BucketWriter,Tuple[IRemoteReference, object]] + + + def remote_get_version(self): + return self.get_version() + + def remote_allocate_buckets(self, storage_index, + renew_secret, cancel_secret, + sharenums, allocated_size, + canary, owner_num=0): + """Foolscap-specific ``allocate_buckets()`` API.""" + alreadygot, bucketwriters = self._server.allocate_buckets( + storage_index, renew_secret, cancel_secret, sharenums, allocated_size, + owner_num=owner_num, renew_leases=True, + ) + # Abort BucketWriters if disconnection happens. + for bw in bucketwriters.values(): + disconnect_marker = canary.notifyOnDisconnect(bw.disconnected) + self._bucket_writer_disconnect_markers[bw] = (canary, disconnect_marker) + return alreadygot, bucketwriters + + def remote_add_lease(self, storage_index, renew_secret, cancel_secret, + owner_num=1): + return self._server.add_lease(storage_index, renew_secret, cancel_secret) + + def remote_renew_lease(self, storage_index, renew_secret): + return self._server.renew_lease(storage_index, renew_secret) + + def remote_get_buckets(self, storage_index): + return self._server.get_buckets(storage_index) + + def remote_slot_testv_and_readv_and_writev(self, storage_index, + secrets, + test_and_write_vectors, + read_vector): + return self._server.slot_testv_and_readv_and_writev( + storage_index, + secrets, + test_and_write_vectors, + read_vector, + renew_leases=True, + ) + + def remote_slot_readv(self, storage_index, shares, readv): + return self._server.slot_readv(self, storage_index, shares, readv) + + def remote_advise_corrupt_share(self, share_type, storage_index, shnum, + reason): + return self._server.advise_corrupt_share(share_type, storage_index, shnum, + reason) From 541b28f4693c900f83fc767c5e012918e98d3b9a Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 2 Dec 2021 09:36:56 -0500 Subject: [PATCH 02/55] News file. --- newsfragments/3849.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3849.minor diff --git a/newsfragments/3849.minor b/newsfragments/3849.minor new file mode 100644 index 000000000..e69de29bb From f7cb4d5c92e33b2b25286e4adb8c3ba4d32755bd Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 2 Dec 2021 10:02:46 -0500 Subject: [PATCH 03/55] Hook up the new FoolscapStorageServer, and fix enough bugs, such that almost all end-to-end and integration tests pass. --- src/allmydata/client.py | 4 ++-- src/allmydata/storage/immutable.py | 8 ++++---- src/allmydata/storage/server.py | 24 +++++++++++++++++++----- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/allmydata/client.py b/src/allmydata/client.py index a2f88ebd6..645e157b6 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -36,7 +36,7 @@ from twisted.python.filepath import FilePath import allmydata from allmydata.crypto import rsa, ed25519 from allmydata.crypto.util import remove_prefix -from allmydata.storage.server import StorageServer +from allmydata.storage.server import StorageServer, FoolscapStorageServer from allmydata import storage_client from allmydata.immutable.upload import Uploader from allmydata.immutable.offloaded import Helper @@ -834,7 +834,7 @@ class _Client(node.Node, pollmixin.PollMixin): if anonymous_storage_enabled(self.config): furl_file = self.config.get_private_path("storage.furl").encode(get_filesystem_encoding()) - furl = self.tub.registerReference(ss, furlFile=furl_file) + furl = self.tub.registerReference(FoolscapStorageServer(ss), furlFile=furl_file) announcement["anonymous-storage-FURL"] = furl enabled_storage_servers = self._enable_storage_servers( diff --git a/src/allmydata/storage/immutable.py b/src/allmydata/storage/immutable.py index 08b83cd87..173a43e8e 100644 --- a/src/allmydata/storage/immutable.py +++ b/src/allmydata/storage/immutable.py @@ -382,7 +382,7 @@ class BucketReader(Referenceable): # type: ignore # warner/foolscap#78 return data def remote_advise_corrupt_share(self, reason): - return self.ss.remote_advise_corrupt_share(b"immutable", - self.storage_index, - self.shnum, - reason) + return self.ss.advise_corrupt_share(b"immutable", + self.storage_index, + self.shnum, + reason) diff --git a/src/allmydata/storage/server.py b/src/allmydata/storage/server.py index 9d3ac4012..5dd8cd0bc 100644 --- a/src/allmydata/storage/server.py +++ b/src/allmydata/storage/server.py @@ -127,6 +127,9 @@ class StorageServer(service.MultiService): # Map in-progress filesystem path -> BucketWriter: self._bucket_writers = {} # type: Dict[str,BucketWriter] + # These callables will be called with BucketWriters that closed: + self._call_on_bucket_writer_close = [] + def stopService(self): # Cancel any in-progress uploads: for bw in list(self._bucket_writers.values()): @@ -405,9 +408,14 @@ class StorageServer(service.MultiService): if self.stats_provider: self.stats_provider.count('storage_server.bytes_added', consumed_size) del self._bucket_writers[bw.incominghome] - if bw in self._bucket_writer_disconnect_markers: - canary, disconnect_marker = self._bucket_writer_disconnect_markers.pop(bw) - canary.dontNotifyOnDisconnect(disconnect_marker) + for handler in self._call_on_bucket_writer_close: + handler(bw) + + def register_bucket_writer_close_handler(self, handler): + """ + The handler will be called with any ``BucketWriter`` that closes. + """ + self._call_on_bucket_writer_close.append(handler) def _get_bucket_shares(self, storage_index): """Return a list of (shnum, pathname) tuples for files that hold @@ -755,9 +763,15 @@ class FoolscapStorageServer(Referenceable): # type: ignore # warner/foolscap#78 # Canaries and disconnect markers for BucketWriters created via Foolscap: self._bucket_writer_disconnect_markers = {} # type: Dict[BucketWriter,Tuple[IRemoteReference, object]] + self._server.register_bucket_writer_close_handler(self._bucket_writer_closed) + + def _bucket_writer_closed(self, bw): + if bw in self._bucket_writer_disconnect_markers: + canary, disconnect_marker = self._bucket_writer_disconnect_markers.pop(bw) + canary.dontNotifyOnDisconnect(disconnect_marker) def remote_get_version(self): - return self.get_version() + return self._server.get_version() def remote_allocate_buckets(self, storage_index, renew_secret, cancel_secret, @@ -797,7 +811,7 @@ class FoolscapStorageServer(Referenceable): # type: ignore # warner/foolscap#78 ) def remote_slot_readv(self, storage_index, shares, readv): - return self._server.slot_readv(self, storage_index, shares, readv) + return self._server.slot_readv(storage_index, shares, readv) def remote_advise_corrupt_share(self, share_type, storage_index, shnum, reason): From 476c41e49ec973db18b13d8f3b4e96a70e7c0933 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 2 Dec 2021 10:29:52 -0500 Subject: [PATCH 04/55] Split out Foolscap code from BucketReader/Writer. --- src/allmydata/storage/immutable.py | 60 +++++++++++++++++++++++------- src/allmydata/storage/server.py | 18 ++++++++- 2 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/allmydata/storage/immutable.py b/src/allmydata/storage/immutable.py index 173a43e8e..5fea7e2b6 100644 --- a/src/allmydata/storage/immutable.py +++ b/src/allmydata/storage/immutable.py @@ -230,8 +230,10 @@ class ShareFile(object): return space_freed -@implementer(RIBucketWriter) -class BucketWriter(Referenceable): # type: ignore # warner/foolscap#78 +class BucketWriter(object): + """ + Keep track of the process of writing to a ShareFile. + """ def __init__(self, ss, incominghome, finalhome, max_size, lease_info, clock): self.ss = ss @@ -251,7 +253,7 @@ class BucketWriter(Referenceable): # type: ignore # warner/foolscap#78 def allocated_size(self): return self._max_size - def remote_write(self, offset, data): + def write(self, offset, data): # Delay the timeout, since we received data: self._timeout.reset(30 * 60) start = self._clock.seconds() @@ -275,9 +277,6 @@ class BucketWriter(Referenceable): # type: ignore # warner/foolscap#78 self.ss.add_latency("write", self._clock.seconds() - start) self.ss.count("write") - def remote_close(self): - self.close() - def close(self): precondition(not self.closed) self._timeout.cancel() @@ -329,13 +328,10 @@ class BucketWriter(Referenceable): # type: ignore # warner/foolscap#78 facility="tahoe.storage", level=log.UNUSUAL) self.abort() - def remote_abort(self): + def abort(self): log.msg("storage: aborting sharefile %s" % self.incominghome, facility="tahoe.storage", level=log.UNUSUAL) - self.abort() self.ss.count("abort") - - def abort(self): if self.closed: return @@ -358,8 +354,28 @@ class BucketWriter(Referenceable): # type: ignore # warner/foolscap#78 self._timeout.cancel() -@implementer(RIBucketReader) -class BucketReader(Referenceable): # type: ignore # warner/foolscap#78 +@implementer(RIBucketWriter) +class FoolscapBucketWriter(Referenceable): # type: ignore # warner/foolscap#78 + """ + Foolscap-specific BucketWriter. + """ + def __init__(self, bucket_writer): + self._bucket_writer = bucket_writer + + def remote_write(self, offset, data): + return self._bucket_writer.write(offset, data) + + def remote_close(self): + return self._bucket_writer.close() + + def remote_abort(self): + return self._bucket_writer.abort() + + +class BucketReader(object): + """ + Manage the process for reading from a ``ShareFile``. + """ def __init__(self, ss, sharefname, storage_index=None, shnum=None): self.ss = ss @@ -374,15 +390,31 @@ class BucketReader(Referenceable): # type: ignore # warner/foolscap#78 ), self.shnum) - def remote_read(self, offset, length): + def read(self, offset, length): start = time.time() data = self._share_file.read_share_data(offset, length) self.ss.add_latency("read", time.time() - start) self.ss.count("read") return data - def remote_advise_corrupt_share(self, reason): + def advise_corrupt_share(self, reason): return self.ss.advise_corrupt_share(b"immutable", self.storage_index, self.shnum, reason) + + +@implementer(RIBucketReader) +class FoolscapBucketReader(Referenceable): # type: ignore # warner/foolscap#78 + """ + Foolscap wrapper for ``BucketReader`` + """ + + def __init__(self, bucket_reader): + self._bucket_reader = bucket_reader + + def remote_read(self, offset, length): + return self._bucket_reader.read(offset, length) + + def remote_advise_corrupt_share(self, reason): + return self._bucket_reader.advise_corrupt_share(reason) diff --git a/src/allmydata/storage/server.py b/src/allmydata/storage/server.py index 5dd8cd0bc..8c790b66f 100644 --- a/src/allmydata/storage/server.py +++ b/src/allmydata/storage/server.py @@ -33,7 +33,10 @@ from allmydata.storage.lease import LeaseInfo from allmydata.storage.mutable import MutableShareFile, EmptyShare, \ create_mutable_sharefile from allmydata.mutable.layout import MAX_MUTABLE_SHARE_SIZE -from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader +from allmydata.storage.immutable import ( + ShareFile, BucketWriter, BucketReader, FoolscapBucketWriter, + FoolscapBucketReader, +) from allmydata.storage.crawler import BucketCountingCrawler from allmydata.storage.expirer import LeaseCheckingCrawler @@ -782,10 +785,18 @@ class FoolscapStorageServer(Referenceable): # type: ignore # warner/foolscap#78 storage_index, renew_secret, cancel_secret, sharenums, allocated_size, owner_num=owner_num, renew_leases=True, ) + # Abort BucketWriters if disconnection happens. for bw in bucketwriters.values(): disconnect_marker = canary.notifyOnDisconnect(bw.disconnected) self._bucket_writer_disconnect_markers[bw] = (canary, disconnect_marker) + + # Wrap BucketWriters with Foolscap adapter: + bucketwriters = { + k: FoolscapBucketWriter(bw) + for (k, bw) in bucketwriters.items() + } + return alreadygot, bucketwriters def remote_add_lease(self, storage_index, renew_secret, cancel_secret, @@ -796,7 +807,10 @@ class FoolscapStorageServer(Referenceable): # type: ignore # warner/foolscap#78 return self._server.renew_lease(storage_index, renew_secret) def remote_get_buckets(self, storage_index): - return self._server.get_buckets(storage_index) + return { + k: FoolscapBucketReader(bucket) + for (k, bucket) in self._server.get_buckets(storage_index).items() + } def remote_slot_testv_and_readv_and_writev(self, storage_index, secrets, From 8c3d61a94e6da2e590831afc86afa85e0b0d6e36 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 2 Dec 2021 10:49:23 -0500 Subject: [PATCH 05/55] Bit more backwards compatible. --- src/allmydata/storage/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/storage/server.py b/src/allmydata/storage/server.py index 8c790b66f..bb116ce8e 100644 --- a/src/allmydata/storage/server.py +++ b/src/allmydata/storage/server.py @@ -626,7 +626,7 @@ class StorageServer(service.MultiService): secrets, test_and_write_vectors, read_vector, - renew_leases, + renew_leases=True, ): """ Read data from shares and conditionally write some data to them. From 439e5f2998ac178f7773190842dba0a9855da893 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 2 Dec 2021 10:49:30 -0500 Subject: [PATCH 06/55] Insofar as possible, switch to testing without the Foolscap API. --- src/allmydata/test/test_storage.py | 303 +++++++++++++------------ src/allmydata/test/test_storage_web.py | 24 +- 2 files changed, 165 insertions(+), 162 deletions(-) diff --git a/src/allmydata/test/test_storage.py b/src/allmydata/test/test_storage.py index bc87e168d..bfc1a7cd4 100644 --- a/src/allmydata/test/test_storage.py +++ b/src/allmydata/test/test_storage.py @@ -31,10 +31,15 @@ from hypothesis import given, strategies import itertools from allmydata import interfaces from allmydata.util import fileutil, hashutil, base32 -from allmydata.storage.server import StorageServer, DEFAULT_RENEWAL_TIME +from allmydata.storage.server import ( + StorageServer, DEFAULT_RENEWAL_TIME, FoolscapStorageServer, +) from allmydata.storage.shares import get_share_file from allmydata.storage.mutable import MutableShareFile -from allmydata.storage.immutable import BucketWriter, BucketReader, ShareFile +from allmydata.storage.immutable import ( + BucketWriter, BucketReader, ShareFile, FoolscapBucketWriter, + FoolscapBucketReader, +) from allmydata.storage.common import storage_index_to_dir, \ UnknownMutableContainerVersionError, UnknownImmutableContainerVersionError, \ si_b2a, si_a2b @@ -129,25 +134,25 @@ class Bucket(unittest.TestCase): def test_create(self): incoming, final = self.make_workdir("test_create") bw = BucketWriter(self, incoming, final, 200, self.make_lease(), Clock()) - bw.remote_write(0, b"a"*25) - bw.remote_write(25, b"b"*25) - bw.remote_write(50, b"c"*25) - bw.remote_write(75, b"d"*7) - bw.remote_close() + bw.write(0, b"a"*25) + bw.write(25, b"b"*25) + bw.write(50, b"c"*25) + bw.write(75, b"d"*7) + bw.close() def test_readwrite(self): incoming, final = self.make_workdir("test_readwrite") bw = BucketWriter(self, incoming, final, 200, self.make_lease(), Clock()) - bw.remote_write(0, b"a"*25) - bw.remote_write(25, b"b"*25) - bw.remote_write(50, b"c"*7) # last block may be short - bw.remote_close() + bw.write(0, b"a"*25) + bw.write(25, b"b"*25) + bw.write(50, b"c"*7) # last block may be short + bw.close() # now read from it br = BucketReader(self, bw.finalhome) - self.failUnlessEqual(br.remote_read(0, 25), b"a"*25) - self.failUnlessEqual(br.remote_read(25, 25), b"b"*25) - self.failUnlessEqual(br.remote_read(50, 7), b"c"*7) + self.failUnlessEqual(br.read(0, 25), b"a"*25) + self.failUnlessEqual(br.read(25, 25), b"b"*25) + self.failUnlessEqual(br.read(50, 7), b"c"*7) def test_write_past_size_errors(self): """Writing beyond the size of the bucket throws an exception.""" @@ -157,7 +162,7 @@ class Bucket(unittest.TestCase): ) bw = BucketWriter(self, incoming, final, 200, self.make_lease(), Clock()) with self.assertRaises(DataTooLargeError): - bw.remote_write(offset, b"a" * length) + bw.write(offset, b"a" * length) @given( maybe_overlapping_offset=strategies.integers(min_value=0, max_value=98), @@ -177,25 +182,25 @@ class Bucket(unittest.TestCase): self, incoming, final, length, self.make_lease(), Clock() ) # Three writes: 10-19, 30-39, 50-59. This allows for a bunch of holes. - bw.remote_write(10, expected_data[10:20]) - bw.remote_write(30, expected_data[30:40]) - bw.remote_write(50, expected_data[50:60]) + bw.write(10, expected_data[10:20]) + bw.write(30, expected_data[30:40]) + bw.write(50, expected_data[50:60]) # Then, an overlapping write but with matching data: - bw.remote_write( + bw.write( maybe_overlapping_offset, expected_data[ maybe_overlapping_offset:maybe_overlapping_offset + maybe_overlapping_length ] ) # Now fill in the holes: - bw.remote_write(0, expected_data[0:10]) - bw.remote_write(20, expected_data[20:30]) - bw.remote_write(40, expected_data[40:50]) - bw.remote_write(60, expected_data[60:]) - bw.remote_close() + bw.write(0, expected_data[0:10]) + bw.write(20, expected_data[20:30]) + bw.write(40, expected_data[40:50]) + bw.write(60, expected_data[60:]) + bw.close() br = BucketReader(self, bw.finalhome) - self.assertEqual(br.remote_read(0, length), expected_data) + self.assertEqual(br.read(0, length), expected_data) @given( @@ -215,21 +220,21 @@ class Bucket(unittest.TestCase): self, incoming, final, length, self.make_lease(), Clock() ) # Three writes: 10-19, 30-39, 50-59. This allows for a bunch of holes. - bw.remote_write(10, b"1" * 10) - bw.remote_write(30, b"1" * 10) - bw.remote_write(50, b"1" * 10) + bw.write(10, b"1" * 10) + bw.write(30, b"1" * 10) + bw.write(50, b"1" * 10) # Then, write something that might overlap with some of them, but # conflicts. Then fill in holes left by first three writes. Conflict is # inevitable. with self.assertRaises(ConflictingWriteError): - bw.remote_write( + bw.write( maybe_overlapping_offset, b'X' * min(maybe_overlapping_length, length - maybe_overlapping_offset), ) - bw.remote_write(0, b"1" * 10) - bw.remote_write(20, b"1" * 10) - bw.remote_write(40, b"1" * 10) - bw.remote_write(60, b"1" * 40) + bw.write(0, b"1" * 10) + bw.write(20, b"1" * 10) + bw.write(40, b"1" * 10) + bw.write(60, b"1" * 40) def test_read_past_end_of_share_data(self): # test vector for immutable files (hard-coded contents of an immutable share @@ -274,15 +279,15 @@ class Bucket(unittest.TestCase): # Now read from it. br = BucketReader(mockstorageserver, final) - self.failUnlessEqual(br.remote_read(0, len(share_data)), share_data) + self.failUnlessEqual(br.read(0, len(share_data)), share_data) # Read past the end of share data to get the cancel secret. read_length = len(share_data) + len(ownernumber) + len(renewsecret) + len(cancelsecret) - result_of_read = br.remote_read(0, read_length) + result_of_read = br.read(0, read_length) self.failUnlessEqual(result_of_read, share_data) - result_of_read = br.remote_read(0, len(share_data)+1) + result_of_read = br.read(0, len(share_data)+1) self.failUnlessEqual(result_of_read, share_data) def _assert_timeout_only_after_30_minutes(self, clock, bw): @@ -320,7 +325,7 @@ class Bucket(unittest.TestCase): clock.advance(29 * 60) # .. but we receive a write! So that should delay the timeout again to # another 30 minutes. - bw.remote_write(0, b"hello") + bw.write(0, b"hello") self._assert_timeout_only_after_30_minutes(clock, bw) def test_bucket_closing_cancels_timeout(self): @@ -374,7 +379,7 @@ class BucketProxy(unittest.TestCase): fileutil.make_dirs(basedir) fileutil.make_dirs(os.path.join(basedir, "tmp")) bw = BucketWriter(self, incoming, final, size, self.make_lease(), Clock()) - rb = RemoteBucket(bw) + rb = RemoteBucket(FoolscapBucketWriter(bw)) return bw, rb, final def make_lease(self): @@ -446,7 +451,7 @@ class BucketProxy(unittest.TestCase): # now read everything back def _start_reading(res): br = BucketReader(self, sharefname) - rb = RemoteBucket(br) + rb = RemoteBucket(FoolscapBucketReader(br)) server = NoNetworkServer(b"abc", None) rbp = rbp_class(rb, server, storage_index=b"") self.failUnlessIn("to peer", repr(rbp)) @@ -514,20 +519,20 @@ class Server(unittest.TestCase): def test_declares_fixed_1528(self): ss = self.create("test_declares_fixed_1528") - ver = ss.remote_get_version() + ver = ss.get_version() sv1 = ver[b'http://allmydata.org/tahoe/protocols/storage/v1'] self.failUnless(sv1.get(b'prevents-read-past-end-of-share-data'), sv1) def test_declares_maximum_share_sizes(self): ss = self.create("test_declares_maximum_share_sizes") - ver = ss.remote_get_version() + ver = ss.get_version() sv1 = ver[b'http://allmydata.org/tahoe/protocols/storage/v1'] self.failUnlessIn(b'maximum-immutable-share-size', sv1) self.failUnlessIn(b'maximum-mutable-share-size', sv1) def test_declares_available_space(self): ss = self.create("test_declares_available_space") - ver = ss.remote_get_version() + ver = ss.get_version() sv1 = ver[b'http://allmydata.org/tahoe/protocols/storage/v1'] self.failUnlessIn(b'available-space', sv1) @@ -538,7 +543,9 @@ class Server(unittest.TestCase): """ renew_secret = hashutil.my_renewal_secret_hash(b"%d" % next(self._lease_secret)) cancel_secret = hashutil.my_cancel_secret_hash(b"%d" % next(self._lease_secret)) - return ss._allocate_buckets( + if isinstance(ss, FoolscapStorageServer): + ss = ss._server + return ss.allocate_buckets( storage_index, renew_secret, cancel_secret, sharenums, size, @@ -562,12 +569,12 @@ class Server(unittest.TestCase): shnum, bucket = list(writers.items())[0] # This test is going to hammer your filesystem if it doesn't make a sparse file for this. :-( - bucket.remote_write(2**32, b"ab") - bucket.remote_close() + bucket.write(2**32, b"ab") + bucket.close() - readers = ss.remote_get_buckets(b"allocate") + readers = ss.get_buckets(b"allocate") reader = readers[shnum] - self.failUnlessEqual(reader.remote_read(2**32, 2), b"ab") + self.failUnlessEqual(reader.read(2**32, 2), b"ab") def test_dont_overfill_dirs(self): """ @@ -578,8 +585,8 @@ class Server(unittest.TestCase): ss = self.create("test_dont_overfill_dirs") already, writers = self.allocate(ss, b"storageindex", [0], 10) for i, wb in writers.items(): - wb.remote_write(0, b"%10d" % i) - wb.remote_close() + wb.write(0, b"%10d" % i) + wb.close() storedir = os.path.join(self.workdir("test_dont_overfill_dirs"), "shares") children_of_storedir = set(os.listdir(storedir)) @@ -588,8 +595,8 @@ class Server(unittest.TestCase): # chars the same as the first storageindex. already, writers = self.allocate(ss, b"storageindey", [0], 10) for i, wb in writers.items(): - wb.remote_write(0, b"%10d" % i) - wb.remote_close() + wb.write(0, b"%10d" % i) + wb.close() storedir = os.path.join(self.workdir("test_dont_overfill_dirs"), "shares") new_children_of_storedir = set(os.listdir(storedir)) @@ -599,8 +606,8 @@ class Server(unittest.TestCase): ss = self.create("test_remove_incoming") already, writers = self.allocate(ss, b"vid", list(range(3)), 10) for i,wb in writers.items(): - wb.remote_write(0, b"%10d" % i) - wb.remote_close() + wb.write(0, b"%10d" % i) + wb.close() incoming_share_dir = wb.incominghome incoming_bucket_dir = os.path.dirname(incoming_share_dir) incoming_prefix_dir = os.path.dirname(incoming_bucket_dir) @@ -619,32 +626,32 @@ class Server(unittest.TestCase): # Now abort the writers. for writer in writers.values(): - writer.remote_abort() + writer.abort() self.failUnlessEqual(ss.allocated_size(), 0) def test_allocate(self): ss = self.create("test_allocate") - self.failUnlessEqual(ss.remote_get_buckets(b"allocate"), {}) + self.failUnlessEqual(ss.get_buckets(b"allocate"), {}) already,writers = self.allocate(ss, b"allocate", [0,1,2], 75) self.failUnlessEqual(already, set()) self.failUnlessEqual(set(writers.keys()), set([0,1,2])) # while the buckets are open, they should not count as readable - self.failUnlessEqual(ss.remote_get_buckets(b"allocate"), {}) + self.failUnlessEqual(ss.get_buckets(b"allocate"), {}) # close the buckets for i,wb in writers.items(): - wb.remote_write(0, b"%25d" % i) - wb.remote_close() + wb.write(0, b"%25d" % i) + wb.close() # aborting a bucket that was already closed is a no-op - wb.remote_abort() + wb.abort() # now they should be readable - b = ss.remote_get_buckets(b"allocate") + b = ss.get_buckets(b"allocate") self.failUnlessEqual(set(b.keys()), set([0,1,2])) - self.failUnlessEqual(b[0].remote_read(0, 25), b"%25d" % 0) + self.failUnlessEqual(b[0].read(0, 25), b"%25d" % 0) b_str = str(b[0]) self.failUnlessIn("BucketReader", b_str) self.failUnlessIn("mfwgy33dmf2g 0", b_str) @@ -665,15 +672,15 @@ class Server(unittest.TestCase): # aborting the writes should remove the tempfiles for i,wb in writers2.items(): - wb.remote_abort() + wb.abort() already2,writers2 = self.allocate(ss, b"allocate", [2,3,4,5], 75) self.failUnlessEqual(already2, set([0,1,2])) self.failUnlessEqual(set(writers2.keys()), set([5])) for i,wb in writers2.items(): - wb.remote_abort() + wb.abort() for i,wb in writers.items(): - wb.remote_abort() + wb.abort() def test_allocate_without_lease_renewal(self): """ @@ -696,8 +703,8 @@ class Server(unittest.TestCase): ss, storage_index, [0], 1, renew_leases=False, ) (writer,) = writers.values() - writer.remote_write(0, b"x") - writer.remote_close() + writer.write(0, b"x") + writer.close() # It should have a lease granted at the current time. shares = dict(ss._get_bucket_shares(storage_index)) @@ -719,8 +726,8 @@ class Server(unittest.TestCase): ss, storage_index, [1], 1, renew_leases=False, ) (writer,) = writers.values() - writer.remote_write(0, b"x") - writer.remote_close() + writer.write(0, b"x") + writer.close() # The first share's lease expiration time is unchanged. shares = dict(ss._get_bucket_shares(storage_index)) @@ -736,8 +743,8 @@ class Server(unittest.TestCase): def test_bad_container_version(self): ss = self.create("test_bad_container_version") a,w = self.allocate(ss, b"si1", [0], 10) - w[0].remote_write(0, b"\xff"*10) - w[0].remote_close() + w[0].write(0, b"\xff"*10) + w[0].close() fn = os.path.join(ss.sharedir, storage_index_to_dir(b"si1"), "0") f = open(fn, "rb+") @@ -745,15 +752,15 @@ class Server(unittest.TestCase): f.write(struct.pack(">L", 0)) # this is invalid: minimum used is v1 f.close() - ss.remote_get_buckets(b"allocate") + ss.get_buckets(b"allocate") e = self.failUnlessRaises(UnknownImmutableContainerVersionError, - ss.remote_get_buckets, b"si1") + ss.get_buckets, b"si1") self.failUnlessIn(" had version 0 but we wanted 1", str(e)) def test_disconnect(self): # simulate a disconnection - ss = self.create("test_disconnect") + ss = FoolscapStorageServer(self.create("test_disconnect")) renew_secret = b"r" * 32 cancel_secret = b"c" * 32 canary = FakeCanary() @@ -789,7 +796,7 @@ class Server(unittest.TestCase): } self.patch(fileutil, 'get_disk_stats', call_get_disk_stats) - ss = self.create("test_reserved_space", reserved_space=reserved) + ss = FoolscapStorageServer(self.create("test_reserved_space", reserved_space=reserved)) # 15k available, 10k reserved, leaves 5k for shares # a newly created and filled share incurs this much overhead, beyond @@ -810,28 +817,28 @@ class Server(unittest.TestCase): self.failUnlessEqual(len(writers), 3) # now the StorageServer should have 3000 bytes provisionally # allocated, allowing only 2000 more to be claimed - self.failUnlessEqual(len(ss._bucket_writers), 3) + self.failUnlessEqual(len(ss._server._bucket_writers), 3) # allocating 1001-byte shares only leaves room for one canary2 = FakeCanary() already2, writers2 = self.allocate(ss, b"vid2", [0,1,2], 1001, canary2) self.failUnlessEqual(len(writers2), 1) - self.failUnlessEqual(len(ss._bucket_writers), 4) + self.failUnlessEqual(len(ss._server._bucket_writers), 4) # we abandon the first set, so their provisional allocation should be # returned canary.disconnected() - self.failUnlessEqual(len(ss._bucket_writers), 1) + self.failUnlessEqual(len(ss._server._bucket_writers), 1) # now we have a provisional allocation of 1001 bytes # and we close the second set, so their provisional allocation should # become real, long-term allocation, and grows to include the # overhead. for bw in writers2.values(): - bw.remote_write(0, b"a"*25) - bw.remote_close() - self.failUnlessEqual(len(ss._bucket_writers), 0) + bw.write(0, b"a"*25) + bw.close() + self.failUnlessEqual(len(ss._server._bucket_writers), 0) # this also changes the amount reported as available by call_get_disk_stats allocated = 1001 + OVERHEAD + LEASE_SIZE @@ -848,12 +855,12 @@ class Server(unittest.TestCase): canary=canary3, ) self.failUnlessEqual(len(writers3), 39) - self.failUnlessEqual(len(ss._bucket_writers), 39) + self.failUnlessEqual(len(ss._server._bucket_writers), 39) canary3.disconnected() - self.failUnlessEqual(len(ss._bucket_writers), 0) - ss.disownServiceParent() + self.failUnlessEqual(len(ss._server._bucket_writers), 0) + ss._server.disownServiceParent() del ss def test_seek(self): @@ -882,24 +889,22 @@ class Server(unittest.TestCase): Given a StorageServer, create a bucket with 5 shares and return renewal and cancellation secrets. """ - canary = FakeCanary() sharenums = list(range(5)) size = 100 # Creating a bucket also creates a lease: rs, cs = (hashutil.my_renewal_secret_hash(b"%d" % next(self._lease_secret)), hashutil.my_cancel_secret_hash(b"%d" % next(self._lease_secret))) - already, writers = ss.remote_allocate_buckets(storage_index, rs, cs, - sharenums, size, canary) + already, writers = ss.allocate_buckets(storage_index, rs, cs, + sharenums, size) self.failUnlessEqual(len(already), expected_already) self.failUnlessEqual(len(writers), expected_writers) for wb in writers.values(): - wb.remote_close() + wb.close() return rs, cs def test_leases(self): ss = self.create("test_leases") - canary = FakeCanary() sharenums = list(range(5)) size = 100 @@ -919,54 +924,54 @@ class Server(unittest.TestCase): # and a third lease, using add-lease rs2a,cs2a = (hashutil.my_renewal_secret_hash(b"%d" % next(self._lease_secret)), hashutil.my_cancel_secret_hash(b"%d" % next(self._lease_secret))) - ss.remote_add_lease(b"si1", rs2a, cs2a) + ss.add_lease(b"si1", rs2a, cs2a) (lease1, lease2, lease3) = ss.get_leases(b"si1") self.assertTrue(lease1.is_renew_secret(rs1)) self.assertTrue(lease2.is_renew_secret(rs2)) self.assertTrue(lease3.is_renew_secret(rs2a)) # add-lease on a missing storage index is silently ignored - self.assertIsNone(ss.remote_add_lease(b"si18", b"", b"")) + self.assertIsNone(ss.add_lease(b"si18", b"", b"")) # check that si0 is readable - readers = ss.remote_get_buckets(b"si0") + readers = ss.get_buckets(b"si0") self.failUnlessEqual(len(readers), 5) # renew the first lease. Only the proper renew_secret should work - ss.remote_renew_lease(b"si0", rs0) - self.failUnlessRaises(IndexError, ss.remote_renew_lease, b"si0", cs0) - self.failUnlessRaises(IndexError, ss.remote_renew_lease, b"si0", rs1) + ss.renew_lease(b"si0", rs0) + self.failUnlessRaises(IndexError, ss.renew_lease, b"si0", cs0) + self.failUnlessRaises(IndexError, ss.renew_lease, b"si0", rs1) # check that si0 is still readable - readers = ss.remote_get_buckets(b"si0") + readers = ss.get_buckets(b"si0") self.failUnlessEqual(len(readers), 5) # There is no such method as remote_cancel_lease for now -- see # ticket #1528. - self.failIf(hasattr(ss, 'remote_cancel_lease'), \ - "ss should not have a 'remote_cancel_lease' method/attribute") + self.failIf(hasattr(FoolscapStorageServer(ss), 'remote_cancel_lease'), \ + "ss should not have a 'remote_cancel_lease' method/attribute") # test overlapping uploads rs3,cs3 = (hashutil.my_renewal_secret_hash(b"%d" % next(self._lease_secret)), hashutil.my_cancel_secret_hash(b"%d" % next(self._lease_secret))) rs4,cs4 = (hashutil.my_renewal_secret_hash(b"%d" % next(self._lease_secret)), hashutil.my_cancel_secret_hash(b"%d" % next(self._lease_secret))) - already,writers = ss.remote_allocate_buckets(b"si3", rs3, cs3, - sharenums, size, canary) + already,writers = ss.allocate_buckets(b"si3", rs3, cs3, + sharenums, size) self.failUnlessEqual(len(already), 0) self.failUnlessEqual(len(writers), 5) - already2,writers2 = ss.remote_allocate_buckets(b"si3", rs4, cs4, - sharenums, size, canary) + already2,writers2 = ss.allocate_buckets(b"si3", rs4, cs4, + sharenums, size) self.failUnlessEqual(len(already2), 0) self.failUnlessEqual(len(writers2), 0) for wb in writers.values(): - wb.remote_close() + wb.close() leases = list(ss.get_leases(b"si3")) self.failUnlessEqual(len(leases), 1) - already3,writers3 = ss.remote_allocate_buckets(b"si3", rs4, cs4, - sharenums, size, canary) + already3,writers3 = ss.allocate_buckets(b"si3", rs4, cs4, + sharenums, size) self.failUnlessEqual(len(already3), 5) self.failUnlessEqual(len(writers3), 0) @@ -991,7 +996,7 @@ class Server(unittest.TestCase): clock.advance(123456) # Adding a lease with matching renewal secret just renews it: - ss.remote_add_lease(b"si0", renewal_secret, cancel_secret) + ss.add_lease(b"si0", renewal_secret, cancel_secret) [lease] = ss.get_leases(b"si0") self.assertEqual(lease.get_expiration_time(), 123 + 123456 + DEFAULT_RENEWAL_TIME) @@ -1027,14 +1032,14 @@ class Server(unittest.TestCase): self.failUnlessEqual(already, set()) self.failUnlessEqual(set(writers.keys()), set([0,1,2])) for i,wb in writers.items(): - wb.remote_write(0, b"%25d" % i) - wb.remote_close() + wb.write(0, b"%25d" % i) + wb.close() # since we discard the data, the shares should be present but sparse. # Since we write with some seeks, the data we read back will be all # zeros. - b = ss.remote_get_buckets(b"vid") + b = ss.get_buckets(b"vid") self.failUnlessEqual(set(b.keys()), set([0,1,2])) - self.failUnlessEqual(b[0].remote_read(0, 25), b"\x00" * 25) + self.failUnlessEqual(b[0].read(0, 25), b"\x00" * 25) def test_advise_corruption(self): workdir = self.workdir("test_advise_corruption") @@ -1042,8 +1047,8 @@ class Server(unittest.TestCase): ss.setServiceParent(self.sparent) si0_s = base32.b2a(b"si0") - ss.remote_advise_corrupt_share(b"immutable", b"si0", 0, - b"This share smells funny.\n") + ss.advise_corrupt_share(b"immutable", b"si0", 0, + b"This share smells funny.\n") reportdir = os.path.join(workdir, "corruption-advisories") reports = os.listdir(reportdir) self.failUnlessEqual(len(reports), 1) @@ -1062,12 +1067,12 @@ class Server(unittest.TestCase): already,writers = self.allocate(ss, b"si1", [1], 75) self.failUnlessEqual(already, set()) self.failUnlessEqual(set(writers.keys()), set([1])) - writers[1].remote_write(0, b"data") - writers[1].remote_close() + writers[1].write(0, b"data") + writers[1].close() - b = ss.remote_get_buckets(b"si1") + b = ss.get_buckets(b"si1") self.failUnlessEqual(set(b.keys()), set([1])) - b[1].remote_advise_corrupt_share(b"This share tastes like dust.\n") + b[1].advise_corrupt_share(b"This share tastes like dust.\n") reports = os.listdir(reportdir) self.failUnlessEqual(len(reports), 2) @@ -1125,7 +1130,7 @@ class MutableServer(unittest.TestCase): write_enabler = self.write_enabler(we_tag) renew_secret = self.renew_secret(lease_tag) cancel_secret = self.cancel_secret(lease_tag) - rstaraw = ss.remote_slot_testv_and_readv_and_writev + rstaraw = ss.slot_testv_and_readv_and_writev testandwritev = dict( [ (shnum, ([], [], None) ) for shnum in sharenums ] ) readv = [] @@ -1146,7 +1151,7 @@ class MutableServer(unittest.TestCase): f.seek(0) f.write(b"BAD MAGIC") f.close() - read = ss.remote_slot_readv + read = ss.slot_readv e = self.failUnlessRaises(UnknownMutableContainerVersionError, read, b"si1", [0], [(0,10)]) self.failUnlessIn(" had magic ", str(e)) @@ -1156,8 +1161,8 @@ class MutableServer(unittest.TestCase): ss = self.create("test_container_size") self.allocate(ss, b"si1", b"we1", next(self._lease_secret), set([0,1,2]), 100) - read = ss.remote_slot_readv - rstaraw = ss.remote_slot_testv_and_readv_and_writev + read = ss.slot_readv + rstaraw = ss.slot_testv_and_readv_and_writev secrets = ( self.write_enabler(b"we1"), self.renew_secret(b"we1"), self.cancel_secret(b"we1") ) @@ -1237,7 +1242,7 @@ class MutableServer(unittest.TestCase): # Also see if the server explicitly declares that it supports this # feature. - ver = ss.remote_get_version() + ver = ss.get_version() storage_v1_ver = ver[b"http://allmydata.org/tahoe/protocols/storage/v1"] self.failUnless(storage_v1_ver.get(b"fills-holes-with-zero-bytes")) @@ -1255,7 +1260,7 @@ class MutableServer(unittest.TestCase): self.allocate(ss, b"si1", b"we1", next(self._lease_secret), set([0,1,2]), 100) - read = ss.remote_slot_readv + read = ss.slot_readv self.failUnlessEqual(read(b"si1", [0], [(0, 10)]), {0: [b""]}) self.failUnlessEqual(read(b"si1", [], [(0, 10)]), @@ -1268,7 +1273,7 @@ class MutableServer(unittest.TestCase): self.renew_secret(b"we1"), self.cancel_secret(b"we1") ) data = b"".join([ (b"%d" % i) * 10 for i in range(10) ]) - write = ss.remote_slot_testv_and_readv_and_writev + write = ss.slot_testv_and_readv_and_writev answer = write(b"si1", secrets, {0: ([], [(0,data)], None)}, []) @@ -1278,7 +1283,7 @@ class MutableServer(unittest.TestCase): {0: [b"00000000001111111111"]}) self.failUnlessEqual(read(b"si1", [0], [(95,10)]), {0: [b"99999"]}) - #self.failUnlessEqual(s0.remote_get_length(), 100) + #self.failUnlessEqual(s0.get_length(), 100) bad_secrets = (b"bad write enabler", secrets[1], secrets[2]) f = self.failUnlessRaises(BadWriteEnablerError, @@ -1312,8 +1317,8 @@ class MutableServer(unittest.TestCase): self.renew_secret(b"we1"), self.cancel_secret(b"we1") ) data = b"".join([ (b"%d" % i) * 10 for i in range(10) ]) - write = ss.remote_slot_testv_and_readv_and_writev - read = ss.remote_slot_readv + write = ss.slot_testv_and_readv_and_writev + read = ss.slot_readv def reset(): write(b"si1", secrets, @@ -1357,8 +1362,8 @@ class MutableServer(unittest.TestCase): self.renew_secret(b"we1"), self.cancel_secret(b"we1") ) data = b"".join([ (b"%d" % i) * 10 for i in range(10) ]) - write = ss.remote_slot_testv_and_readv_and_writev - read = ss.remote_slot_readv + write = ss.slot_testv_and_readv_and_writev + read = ss.slot_readv data = [(b"%d" % i) * 100 for i in range(3)] rc = write(b"si1", secrets, {0: ([], [(0,data[0])], None), @@ -1389,8 +1394,8 @@ class MutableServer(unittest.TestCase): self.renew_secret(b"we1-%d" % n), self.cancel_secret(b"we1-%d" % n) ) data = b"".join([ (b"%d" % i) * 10 for i in range(10) ]) - write = ss.remote_slot_testv_and_readv_and_writev - read = ss.remote_slot_readv + write = ss.slot_testv_and_readv_and_writev + read = ss.slot_readv rc = write(b"si1", secrets(0), {0: ([], [(0,data)], None)}, []) self.failUnlessEqual(rc, (True, {})) @@ -1406,7 +1411,7 @@ class MutableServer(unittest.TestCase): self.failUnlessEqual(len(list(s0.get_leases())), 1) # add-lease on a missing storage index is silently ignored - self.failUnlessEqual(ss.remote_add_lease(b"si18", b"", b""), None) + self.failUnlessEqual(ss.add_lease(b"si18", b"", b""), None) # re-allocate the slots and use the same secrets, that should update # the lease @@ -1414,7 +1419,7 @@ class MutableServer(unittest.TestCase): self.failUnlessEqual(len(list(s0.get_leases())), 1) # renew it directly - ss.remote_renew_lease(b"si1", secrets(0)[1]) + ss.renew_lease(b"si1", secrets(0)[1]) self.failUnlessEqual(len(list(s0.get_leases())), 1) # now allocate them with a bunch of different secrets, to trigger the @@ -1422,7 +1427,7 @@ class MutableServer(unittest.TestCase): write(b"si1", secrets(1), {0: ([], [(0,data)], None)}, []) self.failUnlessEqual(len(list(s0.get_leases())), 2) secrets2 = secrets(2) - ss.remote_add_lease(b"si1", secrets2[1], secrets2[2]) + ss.add_lease(b"si1", secrets2[1], secrets2[2]) self.failUnlessEqual(len(list(s0.get_leases())), 3) write(b"si1", secrets(3), {0: ([], [(0,data)], None)}, []) write(b"si1", secrets(4), {0: ([], [(0,data)], None)}, []) @@ -1440,11 +1445,11 @@ class MutableServer(unittest.TestCase): # read back the leases, make sure they're still intact. self.compare_leases_without_timestamps(all_leases, list(s0.get_leases())) - ss.remote_renew_lease(b"si1", secrets(0)[1]) - ss.remote_renew_lease(b"si1", secrets(1)[1]) - ss.remote_renew_lease(b"si1", secrets(2)[1]) - ss.remote_renew_lease(b"si1", secrets(3)[1]) - ss.remote_renew_lease(b"si1", secrets(4)[1]) + ss.renew_lease(b"si1", secrets(0)[1]) + ss.renew_lease(b"si1", secrets(1)[1]) + ss.renew_lease(b"si1", secrets(2)[1]) + ss.renew_lease(b"si1", secrets(3)[1]) + ss.renew_lease(b"si1", secrets(4)[1]) self.compare_leases_without_timestamps(all_leases, list(s0.get_leases())) # get a new copy of the leases, with the current timestamps. Reading # data and failing to renew/cancel leases should leave the timestamps @@ -1455,7 +1460,7 @@ class MutableServer(unittest.TestCase): # examine the exception thus raised, make sure the old nodeid is # present, to provide for share migration e = self.failUnlessRaises(IndexError, - ss.remote_renew_lease, b"si1", + ss.renew_lease, b"si1", secrets(20)[1]) e_s = str(e) self.failUnlessIn("Unable to renew non-existent lease", e_s) @@ -1490,7 +1495,7 @@ class MutableServer(unittest.TestCase): self.renew_secret(b"we1-%d" % n), self.cancel_secret(b"we1-%d" % n) ) data = b"".join([ (b"%d" % i) * 10 for i in range(10) ]) - write = ss.remote_slot_testv_and_readv_and_writev + write = ss.slot_testv_and_readv_and_writev write_enabler, renew_secret, cancel_secret = secrets(0) rc = write(b"si1", (write_enabler, renew_secret, cancel_secret), {0: ([], [(0,data)], None)}, []) @@ -1506,7 +1511,7 @@ class MutableServer(unittest.TestCase): clock.advance(835) # Adding a lease renews it: - ss.remote_add_lease(b"si1", renew_secret, cancel_secret) + ss.add_lease(b"si1", renew_secret, cancel_secret) [lease] = s0.get_leases() self.assertEqual(lease.get_expiration_time(), 235 + 835 + DEFAULT_RENEWAL_TIME) @@ -1515,8 +1520,8 @@ class MutableServer(unittest.TestCase): ss = self.create("test_remove") self.allocate(ss, b"si1", b"we1", next(self._lease_secret), set([0,1,2]), 100) - readv = ss.remote_slot_readv - writev = ss.remote_slot_testv_and_readv_and_writev + readv = ss.slot_readv + writev = ss.slot_testv_and_readv_and_writev secrets = ( self.write_enabler(b"we1"), self.renew_secret(b"we1"), self.cancel_secret(b"we1") ) @@ -1620,7 +1625,7 @@ class MutableServer(unittest.TestCase): # We don't even need to create any shares to exercise this # functionality. Just go straight to sending a truncate-to-zero # write. - testv_is_good, read_data = ss.remote_slot_testv_and_readv_and_writev( + testv_is_good, read_data = ss.slot_testv_and_readv_and_writev( storage_index=storage_index, secrets=secrets, test_and_write_vectors={ @@ -1638,7 +1643,7 @@ class MDMFProxies(unittest.TestCase, ShouldFailMixin): self.sparent = LoggingServiceParent() self._lease_secret = itertools.count() self.ss = self.create("MDMFProxies storage test server") - self.rref = RemoteBucket(self.ss) + self.rref = RemoteBucket(FoolscapStorageServer(self.ss)) self.storage_server = _StorageServer(lambda: self.rref) self.secrets = (self.write_enabler(b"we_secret"), self.renew_secret(b"renew_secret"), @@ -1805,7 +1810,7 @@ class MDMFProxies(unittest.TestCase, ShouldFailMixin): If tail_segment=True, then I will write a share that has a smaller tail segment than other segments. """ - write = self.ss.remote_slot_testv_and_readv_and_writev + write = self.ss.slot_testv_and_readv_and_writev data = self.build_test_mdmf_share(tail_segment, empty) # Finally, we write the whole thing to the storage server in one # pass. @@ -1873,7 +1878,7 @@ class MDMFProxies(unittest.TestCase, ShouldFailMixin): empty=False): # Some tests need SDMF shares to verify that we can still # read them. This method writes one, which resembles but is not - write = self.ss.remote_slot_testv_and_readv_and_writev + write = self.ss.slot_testv_and_readv_and_writev share = self.build_test_sdmf_share(empty) testvs = [(0, 1, b"eq", b"")] tws = {} @@ -2205,7 +2210,7 @@ class MDMFProxies(unittest.TestCase, ShouldFailMixin): # blocks. mw = self._make_new_mw(b"si1", 0) # Test writing some blocks. - read = self.ss.remote_slot_readv + read = self.ss.slot_readv expected_private_key_offset = struct.calcsize(MDMFHEADER) expected_sharedata_offset = struct.calcsize(MDMFHEADER) + \ PRIVATE_KEY_SIZE + \ @@ -2996,7 +3001,7 @@ class MDMFProxies(unittest.TestCase, ShouldFailMixin): d = sdmfr.finish_publishing() def _then(ignored): self.failUnlessEqual(self.rref.write_count, 1) - read = self.ss.remote_slot_readv + read = self.ss.slot_readv self.failUnlessEqual(read(b"si1", [0], [(0, len(data))]), {0: [data]}) d.addCallback(_then) @@ -3053,7 +3058,7 @@ class MDMFProxies(unittest.TestCase, ShouldFailMixin): sdmfw.finish_publishing()) def _then_again(results): self.failUnless(results[0]) - read = self.ss.remote_slot_readv + read = self.ss.slot_readv self.failUnlessEqual(read(b"si1", [0], [(1, 8)]), {0: [struct.pack(">Q", 1)]}) self.failUnlessEqual(read(b"si1", [0], [(9, len(data) - 9)]), diff --git a/src/allmydata/test/test_storage_web.py b/src/allmydata/test/test_storage_web.py index 38e380223..5d72fbd82 100644 --- a/src/allmydata/test/test_storage_web.py +++ b/src/allmydata/test/test_storage_web.py @@ -38,7 +38,6 @@ from allmydata.web.storage import ( StorageStatusElement, remove_prefix ) -from .common_util import FakeCanary from .common_web import ( render, @@ -289,28 +288,27 @@ class LeaseCrawler(unittest.TestCase, pollmixin.PollMixin): mutable_si_3, rs3, cs3, we3 = make_mutable(b"\x03" * 16) rs3a, cs3a = make_extra_lease(mutable_si_3, 1) sharenums = [0] - canary = FakeCanary() # note: 'tahoe debug dump-share' will not handle this file, since the # inner contents are not a valid CHK share data = b"\xff" * 1000 - a,w = ss.remote_allocate_buckets(immutable_si_0, rs0, cs0, sharenums, - 1000, canary) - w[0].remote_write(0, data) - w[0].remote_close() + a,w = ss.allocate_buckets(immutable_si_0, rs0, cs0, sharenums, + 1000) + w[0].write(0, data) + w[0].close() - a,w = ss.remote_allocate_buckets(immutable_si_1, rs1, cs1, sharenums, - 1000, canary) - w[0].remote_write(0, data) - w[0].remote_close() - ss.remote_add_lease(immutable_si_1, rs1a, cs1a) + a,w = ss.allocate_buckets(immutable_si_1, rs1, cs1, sharenums, + 1000) + w[0].write(0, data) + w[0].close() + ss.add_lease(immutable_si_1, rs1a, cs1a) - writev = ss.remote_slot_testv_and_readv_and_writev + writev = ss.slot_testv_and_readv_and_writev writev(mutable_si_2, (we2, rs2, cs2), {0: ([], [(0,data)], len(data))}, []) writev(mutable_si_3, (we3, rs3, cs3), {0: ([], [(0,data)], len(data))}, []) - ss.remote_add_lease(mutable_si_3, rs3a, cs3a) + ss.add_lease(mutable_si_3, rs3a, cs3a) self.sis = [immutable_si_0, immutable_si_1, mutable_si_2, mutable_si_3] self.renew_secrets = [rs0, rs1, rs1a, rs2, rs3, rs3a] From 90f8480cf0c2fb97fd5e420c6e9ae029dad203b7 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 3 Dec 2021 13:09:27 -0500 Subject: [PATCH 07/55] Make more of the unittests pass again with the StorageServer factoring. --- src/allmydata/storage/http_server.py | 2 +- src/allmydata/storage/server.py | 1 + src/allmydata/test/no_network.py | 6 ++++-- src/allmydata/test/test_checker.py | 6 +++--- src/allmydata/test/test_client.py | 2 +- src/allmydata/test/test_crawler.py | 14 +++++++------- src/allmydata/test/test_helper.py | 7 ++++--- src/allmydata/test/test_hung_server.py | 4 ++-- src/allmydata/test/test_storage_http.py | 2 +- 9 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index 327892ecd..6297ef484 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -91,4 +91,4 @@ class HTTPServer(object): @_authorized_route(_app, "/v1/version", methods=["GET"]) def version(self, request, authorization): - return self._cbor(request, self._storage_server.remote_get_version()) + return self._cbor(request, self._storage_server.get_version()) diff --git a/src/allmydata/storage/server.py b/src/allmydata/storage/server.py index bb116ce8e..0df9f23d8 100644 --- a/src/allmydata/storage/server.py +++ b/src/allmydata/storage/server.py @@ -64,6 +64,7 @@ class StorageServer(service.MultiService): """ Implement the business logic for the storage server. """ + name = "storage" LeaseCheckerClass = LeaseCheckingCrawler def __init__(self, storedir, nodeid, reserved_space=0, diff --git a/src/allmydata/test/no_network.py b/src/allmydata/test/no_network.py index b9fa99005..97cb371e6 100644 --- a/src/allmydata/test/no_network.py +++ b/src/allmydata/test/no_network.py @@ -50,7 +50,9 @@ from allmydata.util.assertutil import _assert from allmydata import uri as tahoe_uri from allmydata.client import _Client -from allmydata.storage.server import StorageServer, storage_index_to_dir +from allmydata.storage.server import ( + StorageServer, storage_index_to_dir, FoolscapStorageServer, +) from allmydata.util import fileutil, idlib, hashutil from allmydata.util.hashutil import permute_server_hash from allmydata.util.fileutil import abspath_expanduser_unicode @@ -417,7 +419,7 @@ class NoNetworkGrid(service.MultiService): ss.setServiceParent(middleman) serverid = ss.my_nodeid self.servers_by_number[i] = ss - wrapper = wrap_storage_server(ss) + wrapper = wrap_storage_server(FoolscapStorageServer(ss)) self.wrappers_by_id[serverid] = wrapper self.proxies_by_id[serverid] = NoNetworkServer(serverid, wrapper) self.rebuild_serverlist() diff --git a/src/allmydata/test/test_checker.py b/src/allmydata/test/test_checker.py index f56ecd089..3d64d4976 100644 --- a/src/allmydata/test/test_checker.py +++ b/src/allmydata/test/test_checker.py @@ -773,13 +773,13 @@ class AddLease(GridTestMixin, unittest.TestCase): d.addCallback(_check_cr, "mutable-normal") really_did_break = [] - # now break the server's remote_add_lease call + # now break the server's add_lease call def _break_add_lease(ign): def broken_add_lease(*args, **kwargs): really_did_break.append(1) raise KeyError("intentional failure, should be ignored") - assert self.g.servers_by_number[0].remote_add_lease - self.g.servers_by_number[0].remote_add_lease = broken_add_lease + assert self.g.servers_by_number[0].add_lease + self.g.servers_by_number[0].add_lease = broken_add_lease d.addCallback(_break_add_lease) # and confirm that the files still look healthy diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py index a2572e735..c65a2fa2c 100644 --- a/src/allmydata/test/test_client.py +++ b/src/allmydata/test/test_client.py @@ -601,7 +601,7 @@ class Basic(testutil.ReallyEqualMixin, unittest.TestCase): "enabled = true\n") c = yield client.create_client(basedir) ss = c.getServiceNamed("storage") - verdict = ss.remote_get_version() + verdict = ss.get_version() self.failUnlessReallyEqual(verdict[b"application-version"], allmydata.__full_version__.encode("ascii")) self.failIfEqual(str(allmydata.__version__), "unknown") diff --git a/src/allmydata/test/test_crawler.py b/src/allmydata/test/test_crawler.py index a9be90c43..80d732986 100644 --- a/src/allmydata/test/test_crawler.py +++ b/src/allmydata/test/test_crawler.py @@ -27,7 +27,7 @@ from allmydata.util import fileutil, hashutil, pollmixin from allmydata.storage.server import StorageServer, si_b2a from allmydata.storage.crawler import ShareCrawler, TimeSliceExceeded -from allmydata.test.common_util import StallMixin, FakeCanary +from allmydata.test.common_util import StallMixin class BucketEnumeratingCrawler(ShareCrawler): cpu_slice = 500 # make sure it can complete in a single slice @@ -124,12 +124,12 @@ class Basic(unittest.TestCase, StallMixin, pollmixin.PollMixin): def write(self, i, ss, serverid, tail=0): si = self.si(i) si = si[:-1] + bytes(bytearray((tail,))) - had,made = ss.remote_allocate_buckets(si, - self.rs(i, serverid), - self.cs(i, serverid), - set([0]), 99, FakeCanary()) - made[0].remote_write(0, b"data") - made[0].remote_close() + had,made = ss.allocate_buckets(si, + self.rs(i, serverid), + self.cs(i, serverid), + set([0]), 99) + made[0].write(0, b"data") + made[0].close() return si_b2a(si) def test_immediate(self): diff --git a/src/allmydata/test/test_helper.py b/src/allmydata/test/test_helper.py index 3faffbe0d..933a2b591 100644 --- a/src/allmydata/test/test_helper.py +++ b/src/allmydata/test/test_helper.py @@ -39,6 +39,7 @@ from allmydata.crypto import aes from allmydata.storage.server import ( si_b2a, StorageServer, + FoolscapStorageServer, ) from allmydata.storage_client import StorageFarmBroker from allmydata.immutable.layout import ( @@ -427,7 +428,7 @@ class CHKCheckerAndUEBFetcherTests(SyncTestCase): """ storage_index = b"a" * 16 serverid = b"b" * 20 - storage = StorageServer(self.mktemp(), serverid) + storage = FoolscapStorageServer(StorageServer(self.mktemp(), serverid)) rref_without_ueb = LocalWrapper(storage, fireNow) yield write_bad_share(rref_without_ueb, storage_index) server_without_ueb = NoNetworkServer(serverid, rref_without_ueb) @@ -451,7 +452,7 @@ class CHKCheckerAndUEBFetcherTests(SyncTestCase): """ storage_index = b"a" * 16 serverid = b"b" * 20 - storage = StorageServer(self.mktemp(), serverid) + storage = FoolscapStorageServer(StorageServer(self.mktemp(), serverid)) rref_with_ueb = LocalWrapper(storage, fireNow) ueb = { "needed_shares": 2, @@ -487,7 +488,7 @@ class CHKCheckerAndUEBFetcherTests(SyncTestCase): in [b"b", b"c"] ) storages = list( - StorageServer(self.mktemp(), serverid) + FoolscapStorageServer(StorageServer(self.mktemp(), serverid)) for serverid in serverids ) diff --git a/src/allmydata/test/test_hung_server.py b/src/allmydata/test/test_hung_server.py index 490315500..162b1d79c 100644 --- a/src/allmydata/test/test_hung_server.py +++ b/src/allmydata/test/test_hung_server.py @@ -73,7 +73,7 @@ class HungServerDownloadTest(GridTestMixin, ShouldFailMixin, PollMixin, def _copy_share(self, share, to_server): (sharenum, sharefile) = share (id, ss) = to_server - shares_dir = os.path.join(ss.original.storedir, "shares") + shares_dir = os.path.join(ss.original._server.storedir, "shares") si = uri.from_string(self.uri).get_storage_index() si_dir = os.path.join(shares_dir, storage_index_to_dir(si)) if not os.path.exists(si_dir): @@ -82,7 +82,7 @@ class HungServerDownloadTest(GridTestMixin, ShouldFailMixin, PollMixin, shutil.copy(sharefile, new_sharefile) self.shares = self.find_uri_shares(self.uri) # Make sure that the storage server has the share. - self.failUnless((sharenum, ss.original.my_nodeid, new_sharefile) + self.failUnless((sharenum, ss.original._server.my_nodeid, new_sharefile) in self.shares) def _corrupt_share(self, share, corruptor_func): diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 442e154a0..6504ac97f 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -65,5 +65,5 @@ class HTTPTests(TestCase): The client can return the version. """ version = yield self.client.get_version() - expected_version = self.storage_server.remote_get_version() + expected_version = self.storage_server.get_version() self.assertEqual(version, expected_version) From b32374c8bcee4120dd89e37fe2472aabf48abce2 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 16 Dec 2021 10:39:58 -0500 Subject: [PATCH 08/55] Secret header parsing. --- src/allmydata/storage/http_server.py | 38 +++++++++++++ src/allmydata/test/test_storage_http.py | 73 ++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 1 deletion(-) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index 6297ef484..47722180d 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -13,8 +13,12 @@ if PY2: # fmt: off from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 # fmt: on +else: + from typing import Dict, List, Set from functools import wraps +from enum import Enum +from base64 import b64decode from klein import Klein from twisted.web import http @@ -26,6 +30,40 @@ from .server import StorageServer from .http_client import swissnum_auth_header +class Secrets(Enum): + """Different kinds of secrets the client may send.""" + LEASE_RENEW = "lease-renew-secret" + LEASE_CANCEL = "lease-cancel-secret" + UPLOAD = "upload-secret" + + +class ClientSecretsException(Exception): + """The client did not send the appropriate secrets.""" + + +def _extract_secrets(header_values, required_secrets): # type: (List[str], Set[Secrets]) -> Dict[Secrets, bytes] + """ + Given list of values of ``X-Tahoe-Authorization`` headers, and required + secrets, return dictionary mapping secrets to decoded values. + + If too few secrets were given, or too many, a ``ClientSecretsException`` is + raised. + """ + key_to_enum = {e.value: e for e in Secrets} + result = {} + try: + for header_value in header_values: + key, value = header_value.strip().split(" ", 1) + result[key_to_enum[key]] = b64decode(value) + except (ValueError, KeyError) as e: + raise ClientSecretsException("Bad header value(s): {}".format(header_values)) + if result.keys() != required_secrets: + raise ClientSecretsException( + "Expected {} secrets, got {}".format(required_secrets, result.keys()) + ) + return result + + def _authorization_decorator(f): """ Check the ``Authorization`` header, and (TODO: in later revision of code) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index e413a0624..2a84d477f 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -15,6 +15,7 @@ if PY2: # fmt: on from unittest import SkipTest +from base64 import b64encode from twisted.trial.unittest import TestCase from twisted.internet.defer import inlineCallbacks @@ -23,10 +24,80 @@ from treq.testing import StubTreq from hyperlink import DecodedURL from ..storage.server import StorageServer -from ..storage.http_server import HTTPServer +from ..storage.http_server import HTTPServer, _extract_secrets, Secrets, ClientSecretsException from ..storage.http_client import StorageClient, ClientException +class ExtractSecretsTests(TestCase): + """ + Tests for ``_extract_secrets``. + """ + def test_extract_secrets(self): + """ + ``_extract_secrets()`` returns a dictionary with the extracted secrets + if the input secrets match the required secrets. + """ + secret1 = b"\xFF\x11ZEBRa" + secret2 = b"\x34\xF2lalalalalala" + lease_secret = "lease-renew-secret " + str(b64encode(secret1), "ascii").strip() + upload_secret = "upload-secret " + str(b64encode(secret2), "ascii").strip() + + # No secrets needed, none given: + self.assertEqual(_extract_secrets([], set()), {}) + + # One secret: + self.assertEqual( + _extract_secrets([lease_secret], + {Secrets.LEASE_RENEW}), + {Secrets.LEASE_RENEW: secret1} + ) + + # Two secrets: + self.assertEqual( + _extract_secrets([upload_secret, lease_secret], + {Secrets.LEASE_RENEW, Secrets.UPLOAD}), + {Secrets.LEASE_RENEW: secret1, Secrets.UPLOAD: secret2} + ) + + def test_wrong_number_of_secrets(self): + """ + If the wrong number of secrets are passed to ``_extract_secrets``, a + ``ClientSecretsException`` is raised. + """ + secret1 = b"\xFF\x11ZEBRa" + lease_secret = "lease-renew-secret " + str(b64encode(secret1), "ascii").strip() + + # Missing secret: + with self.assertRaises(ClientSecretsException): + _extract_secrets([], {Secrets.LEASE_RENEW}) + + # Wrong secret: + with self.assertRaises(ClientSecretsException): + _extract_secrets([lease_secret], {Secrets.UPLOAD}) + + # Extra secret: + with self.assertRaises(ClientSecretsException): + _extract_secrets([lease_secret], {}) + + def test_bad_secrets(self): + """ + Bad inputs to ``_extract_secrets`` result in + ``ClientSecretsException``. + """ + + # Missing value. + with self.assertRaises(ClientSecretsException): + _extract_secrets(["lease-renew-secret"], {Secrets.LEASE_RENEW}) + + # Garbage prefix + with self.assertRaises(ClientSecretsException): + _extract_secrets(["FOO eA=="], {}) + + # Not base64. + with self.assertRaises(ClientSecretsException): + _extract_secrets(["lease-renew-secret x"], {Secrets.LEASE_RENEW}) + + class HTTPTests(TestCase): """ Tests of HTTP client talking to the HTTP server. From 1737340df69ff443c1e548d9126066c05e00bf30 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 16 Dec 2021 10:52:02 -0500 Subject: [PATCH 09/55] Document response codes some more. --- docs/proposed/http-storage-node-protocol.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/proposed/http-storage-node-protocol.rst b/docs/proposed/http-storage-node-protocol.rst index 490d3f3ca..0d8cee466 100644 --- a/docs/proposed/http-storage-node-protocol.rst +++ b/docs/proposed/http-storage-node-protocol.rst @@ -369,6 +369,19 @@ The authentication *type* used is ``Tahoe-LAFS``. The swissnum from the NURL used to locate the storage service is used as the *credentials*. If credentials are not presented or the swissnum is not associated with a storage service then no storage processing is performed and the request receives an ``401 UNAUTHORIZED`` response. +There are also, for some endpoints, secrets sent via ``X-Tahoe-Authorization`` headers. +If these are: + +1. Missing. +2. The wrong length. +3. Not the expected kind of secret. +4. They are otherwise unparseable before they are actually semantically used. + +the server will respond with ``400 BAD REQUEST``. +401 is not used because this isn't an authorization problem, this is a "you sent garbage and should know better" bug. + +If authorization using the secret fails, then a ``401 UNAUTHORIZED`` response should be sent. + General ~~~~~~~ From 87fa9ac2a8e507b385c4b0845c50d34cf6d30da6 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 16 Dec 2021 11:17:11 -0500 Subject: [PATCH 10/55] Infrastructure for sending secrets. --- src/allmydata/storage/http_client.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py index f8a7590aa..774ecbde1 100644 --- a/src/allmydata/storage/http_client.py +++ b/src/allmydata/storage/http_client.py @@ -19,7 +19,7 @@ else: from typing import Union from treq.testing import StubTreq -import base64 +from base64 import b64encode # TODO Make sure to import Python version? from cbor2 import loads @@ -44,7 +44,7 @@ def _decode_cbor(response): def swissnum_auth_header(swissnum): # type: (bytes) -> bytes """Return value for ``Authentication`` header.""" - return b"Tahoe-LAFS " + base64.b64encode(swissnum).strip() + return b"Tahoe-LAFS " + b64encode(swissnum).strip() class StorageClient(object): @@ -68,12 +68,25 @@ class StorageClient(object): ) return headers + def _request(self, method, url, secrets, **kwargs): + """ + Like ``treq.request()``, but additional argument of secrets mapping + ``http_server.Secret`` to the bytes value of the secret. + """ + headers = self._get_headers() + for key, value in secrets.items(): + headers.addRawHeader( + "X-Tahoe-Authorization", + b"{} {}".format(key.value.encode("ascii"), b64encode(value).strip()) + ) + return self._treq.request(method, url, headers=headers, **kwargs) + @inlineCallbacks def get_version(self): """ Return the version metadata for the server. """ url = self._base_url.click("/v1/version") - response = yield self._treq.get(url, headers=self._get_headers()) + response = yield self._request("GET", url, {}) decoded_response = yield _decode_cbor(response) returnValue(decoded_response) From da52a9aedeebe39c908f1616bac6ce887d340066 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 16 Dec 2021 11:17:32 -0500 Subject: [PATCH 11/55] Test for server-side secret handling. --- src/allmydata/test/test_storage_http.py | 88 +++++++++++++++++++++---- 1 file changed, 75 insertions(+), 13 deletions(-) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 2a84d477f..648530852 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -21,10 +21,14 @@ from twisted.trial.unittest import TestCase from twisted.internet.defer import inlineCallbacks from treq.testing import StubTreq +from klein import Klein from hyperlink import DecodedURL from ..storage.server import StorageServer -from ..storage.http_server import HTTPServer, _extract_secrets, Secrets, ClientSecretsException +from ..storage.http_server import ( + HTTPServer, _extract_secrets, Secrets, ClientSecretsException, + _authorized_route, +) from ..storage.http_client import StorageClient, ClientException @@ -98,22 +102,80 @@ class ExtractSecretsTests(TestCase): _extract_secrets(["lease-renew-secret x"], {Secrets.LEASE_RENEW}) -class HTTPTests(TestCase): +SWISSNUM_FOR_TEST = b"abcd" + + +class TestApp(object): + """HTTP API for testing purposes.""" + + _app = Klein() + _swissnum = SWISSNUM_FOR_TEST # Match what the test client is using + + @_authorized_route(_app, {Secrets.UPLOAD}, "/upload_secret", methods=["GET"]) + def validate_upload_secret(self, request, authorization): + if authorization == {Secrets.UPLOAD: b"abc"}: + return "OK" + else: + return "BAD: {}".format(authorization) + + +class RoutingTests(TestCase): """ - Tests of HTTP client talking to the HTTP server. + Tests for the HTTP routing infrastructure. + """ + def setUp(self): + self._http_server = TestApp() + self.client = StorageClient( + DecodedURL.from_text("http://127.0.0.1"), + SWISSNUM_FOR_TEST, + treq=StubTreq(self._http_server._app.resource()), + ) + + @inlineCallbacks + def test_authorization_enforcement(self): + """ + The requirement for secrets is enforced; if they are not given, a 400 + response code is returned. + """ + secret = b"abc" + + # Without secret, get a 400 error. + response = yield self.client._request( + "GET", "http://127.0.0.1/upload_secret", {} + ) + self.assertEqual(response.code, 400) + + # With secret, we're good. + response = yield self.client._request( + "GET", "http://127.0.0.1/upload_secret", {Secrets.UPLOAD, b"abc"} + ) + self.assertEqual(response.code, 200) + + +def setup_http_test(self): + """ + Setup HTTP tests; call from ``setUp``. + """ + if PY2: + raise SkipTest("Not going to bother supporting Python 2") + self.storage_server = StorageServer(self.mktemp(), b"\x00" * 20) + # TODO what should the swissnum _actually_ be? + self._http_server = HTTPServer(self.storage_server, SWISSNUM_FOR_TEST) + self.client = StorageClient( + DecodedURL.from_text("http://127.0.0.1"), + SWISSNUM_FOR_TEST, + treq=StubTreq(self._http_server.get_resource()), + ) + + +class GenericHTTPAPITests(TestCase): + """ + Tests of HTTP client talking to the HTTP server, for generic HTTP API + endpoints and concerns. """ def setUp(self): - if PY2: - raise SkipTest("Not going to bother supporting Python 2") - self.storage_server = StorageServer(self.mktemp(), b"\x00" * 20) - # TODO what should the swissnum _actually_ be? - self._http_server = HTTPServer(self.storage_server, b"abcd") - self.client = StorageClient( - DecodedURL.from_text("http://127.0.0.1"), - b"abcd", - treq=StubTreq(self._http_server.get_resource()), - ) + setup_http_test(self) @inlineCallbacks def test_bad_authentication(self): From 816dc0c73ff3ed7522c63198c6659c17e39f7837 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 16 Dec 2021 11:42:06 -0500 Subject: [PATCH 12/55] X-Tahoe-Authorization can be validated and are passed to server methods. --- src/allmydata/storage/http_client.py | 2 +- src/allmydata/storage/http_server.py | 51 ++++++++++++++++--------- src/allmydata/test/test_storage_http.py | 2 +- 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/allmydata/storage/http_client.py b/src/allmydata/storage/http_client.py index 774ecbde1..72e1af080 100644 --- a/src/allmydata/storage/http_client.py +++ b/src/allmydata/storage/http_client.py @@ -77,7 +77,7 @@ class StorageClient(object): for key, value in secrets.items(): headers.addRawHeader( "X-Tahoe-Authorization", - b"{} {}".format(key.value.encode("ascii"), b64encode(value).strip()) + b"%s %s" % (key.value.encode("ascii"), b64encode(value).strip()) ) return self._treq.request(method, url, headers=headers, **kwargs) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index 47722180d..a26a2c266 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -54,8 +54,10 @@ def _extract_secrets(header_values, required_secrets): # type: (List[str], Set[ try: for header_value in header_values: key, value = header_value.strip().split(" ", 1) + # TODO enforce secret is 32 bytes long for lease secrets. dunno + # about upload secret. result[key_to_enum[key]] = b64decode(value) - except (ValueError, KeyError) as e: + except (ValueError, KeyError): raise ClientSecretsException("Bad header value(s): {}".format(header_values)) if result.keys() != required_secrets: raise ClientSecretsException( @@ -64,38 +66,45 @@ def _extract_secrets(header_values, required_secrets): # type: (List[str], Set[ return result -def _authorization_decorator(f): +def _authorization_decorator(required_secrets): """ Check the ``Authorization`` header, and (TODO: in later revision of code) extract ``X-Tahoe-Authorization`` headers and pass them in. """ + def decorator(f): + @wraps(f) + def route(self, request, *args, **kwargs): + if request.requestHeaders.getRawHeaders("Authorization", [None])[0] != str( + swissnum_auth_header(self._swissnum), "ascii" + ): + request.setResponseCode(http.UNAUTHORIZED) + return b"" + authorization = request.requestHeaders.getRawHeaders("X-Tahoe-Authorization", []) + try: + secrets = _extract_secrets(authorization, required_secrets) + except ClientSecretsException: + request.setResponseCode(400) + return b"" + return f(self, request, secrets, *args, **kwargs) - @wraps(f) - def route(self, request, *args, **kwargs): - if request.requestHeaders.getRawHeaders("Authorization", [None])[0] != str( - swissnum_auth_header(self._swissnum), "ascii" - ): - request.setResponseCode(http.UNAUTHORIZED) - return b"" - # authorization = request.requestHeaders.getRawHeaders("X-Tahoe-Authorization", []) - # For now, just a placeholder: - authorization = None - return f(self, request, authorization, *args, **kwargs) + return route - return route + return decorator -def _authorized_route(app, *route_args, **route_kwargs): +def _authorized_route(app, required_secrets, *route_args, **route_kwargs): """ Like Klein's @route, but with additional support for checking the ``Authorization`` header as well as ``X-Tahoe-Authorization`` headers. The - latter will (TODO: in later revision of code) get passed in as second - argument to wrapped functions. + latter will get passed in as second argument to wrapped functions, a + dictionary mapping a ``Secret`` value to the uploaded secret. + + :param required_secrets: Set of required ``Secret`` types. """ def decorator(f): @app.route(*route_args, **route_kwargs) - @_authorization_decorator + @_authorization_decorator(required_secrets) def handle_route(*args, **kwargs): return f(*args, **kwargs) @@ -127,6 +136,10 @@ class HTTPServer(object): # TODO if data is big, maybe want to use a temporary file eventually... return dumps(data) - @_authorized_route(_app, "/v1/version", methods=["GET"]) + + ##### Generic APIs ##### + + @_authorized_route(_app, set(), "/v1/version", methods=["GET"]) def version(self, request, authorization): + """Return version information.""" return self._cbor(request, self._storage_server.get_version()) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 648530852..b28f4aafb 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -147,7 +147,7 @@ class RoutingTests(TestCase): # With secret, we're good. response = yield self.client._request( - "GET", "http://127.0.0.1/upload_secret", {Secrets.UPLOAD, b"abc"} + "GET", "http://127.0.0.1/upload_secret", {Secrets.UPLOAD: b"abc"} ) self.assertEqual(response.code, 200) From d2ce80dab88df8431a2188ae53bd40f2debe5ee4 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 16 Dec 2021 11:42:44 -0500 Subject: [PATCH 13/55] News file. --- newsfragments/3848.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3848.minor diff --git a/newsfragments/3848.minor b/newsfragments/3848.minor new file mode 100644 index 000000000..e69de29bb From fb0be6b8944dcf9b1254e5d3991ddd8d2ff6ad3c Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 16 Dec 2021 11:46:35 -0500 Subject: [PATCH 14/55] Enforce length of lease secrets. --- src/allmydata/storage/http_server.py | 12 +++++++----- src/allmydata/test/test_storage_http.py | 10 ++++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index a26a2c266..1143acce9 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -49,14 +49,16 @@ def _extract_secrets(header_values, required_secrets): # type: (List[str], Set[ If too few secrets were given, or too many, a ``ClientSecretsException`` is raised. """ - key_to_enum = {e.value: e for e in Secrets} + string_key_to_enum = {e.value: e for e in Secrets} result = {} try: for header_value in header_values: - key, value = header_value.strip().split(" ", 1) - # TODO enforce secret is 32 bytes long for lease secrets. dunno - # about upload secret. - result[key_to_enum[key]] = b64decode(value) + string_key, string_value = header_value.strip().split(" ", 1) + key = string_key_to_enum[string_key] + value = b64decode(string_value) + if key in (Secrets.LEASE_CANCEL, Secrets.LEASE_RENEW) and len(value) != 32: + raise ClientSecretsException("Lease secrets must be 32 bytes long") + result[key] = value except (ValueError, KeyError): raise ClientSecretsException("Bad header value(s): {}".format(header_values)) if result.keys() != required_secrets: diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index b28f4aafb..9d4ef3738 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -41,8 +41,8 @@ class ExtractSecretsTests(TestCase): ``_extract_secrets()`` returns a dictionary with the extracted secrets if the input secrets match the required secrets. """ - secret1 = b"\xFF\x11ZEBRa" - secret2 = b"\x34\xF2lalalalalala" + secret1 = b"\xFF" * 32 + secret2 = b"\x34" * 32 lease_secret = "lease-renew-secret " + str(b64encode(secret1), "ascii").strip() upload_secret = "upload-secret " + str(b64encode(secret2), "ascii").strip() @@ -101,6 +101,12 @@ class ExtractSecretsTests(TestCase): with self.assertRaises(ClientSecretsException): _extract_secrets(["lease-renew-secret x"], {Secrets.LEASE_RENEW}) + # Wrong length lease secrets (must be 32 bytes long). + with self.assertRaises(ClientSecretsException): + _extract_secrets(["lease-renew-secret eA=="], {Secrets.LEASE_RENEW}) + with self.assertRaises(ClientSecretsException): + _extract_secrets(["lease-upload-secret eA=="], {Secrets.LEASE_RENEW}) + SWISSNUM_FOR_TEST = b"abcd" From 428a9d0573628fdc91c5efd7fc3a2f94bbdd19bf Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 16 Dec 2021 11:47:40 -0500 Subject: [PATCH 15/55] Lint fix. --- src/allmydata/test/test_storage_http.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 9d4ef3738..b9aa59f3e 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -143,8 +143,6 @@ class RoutingTests(TestCase): The requirement for secrets is enforced; if they are not given, a 400 response code is returned. """ - secret = b"abc" - # Without secret, get a 400 error. response = yield self.client._request( "GET", "http://127.0.0.1/upload_secret", {} From 81b95f3335c7178fb64cb131d4423780cf04cd29 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 16 Dec 2021 11:53:31 -0500 Subject: [PATCH 16/55] Ensure secret was validated. --- src/allmydata/test/test_storage_http.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index b9aa59f3e..16420b266 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -119,8 +119,8 @@ class TestApp(object): @_authorized_route(_app, {Secrets.UPLOAD}, "/upload_secret", methods=["GET"]) def validate_upload_secret(self, request, authorization): - if authorization == {Secrets.UPLOAD: b"abc"}: - return "OK" + if authorization == {Secrets.UPLOAD: b"MAGIC"}: + return "GOOD SECRET" else: return "BAD: {}".format(authorization) @@ -151,9 +151,10 @@ class RoutingTests(TestCase): # With secret, we're good. response = yield self.client._request( - "GET", "http://127.0.0.1/upload_secret", {Secrets.UPLOAD: b"abc"} + "GET", "http://127.0.0.1/upload_secret", {Secrets.UPLOAD: b"MAGIC"} ) self.assertEqual(response.code, 200) + self.assertEqual((yield response.content()), b"GOOD SECRET") def setup_http_test(self): From a529ba7d5ea84c2b9f4cef48f6e5ef778cb5fbbc Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 17 Dec 2021 09:14:09 -0500 Subject: [PATCH 17/55] More skipping on Python 2. --- src/allmydata/test/test_storage_http.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 16420b266..aba33fad3 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -36,6 +36,10 @@ class ExtractSecretsTests(TestCase): """ Tests for ``_extract_secrets``. """ + def setUp(self): + if PY2: + raise SkipTest("Not going to bother supporting Python 2") + def test_extract_secrets(self): """ ``_extract_secrets()`` returns a dictionary with the extracted secrets @@ -130,6 +134,8 @@ class RoutingTests(TestCase): Tests for the HTTP routing infrastructure. """ def setUp(self): + if PY2: + raise SkipTest("Not going to bother supporting Python 2") self._http_server = TestApp() self.client = StorageClient( DecodedURL.from_text("http://127.0.0.1"), From 291b4e1896f52a07fbe92df7b67f90372ca0f052 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Mon, 20 Dec 2021 11:17:27 -0500 Subject: [PATCH 18/55] Use more secure comparison to prevent timing-based side-channel attacks. --- src/allmydata/storage/http_server.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index 1143acce9..386368364 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -28,10 +28,12 @@ from cbor2 import dumps from .server import StorageServer from .http_client import swissnum_auth_header +from ..util.hashutil import timing_safe_compare class Secrets(Enum): """Different kinds of secrets the client may send.""" + LEASE_RENEW = "lease-renew-secret" LEASE_CANCEL = "lease-cancel-secret" UPLOAD = "upload-secret" @@ -41,7 +43,9 @@ class ClientSecretsException(Exception): """The client did not send the appropriate secrets.""" -def _extract_secrets(header_values, required_secrets): # type: (List[str], Set[Secrets]) -> Dict[Secrets, bytes] +def _extract_secrets( + header_values, required_secrets +): # type: (List[str], Set[Secrets]) -> Dict[Secrets, bytes] """ Given list of values of ``X-Tahoe-Authorization`` headers, and required secrets, return dictionary mapping secrets to decoded values. @@ -73,15 +77,21 @@ def _authorization_decorator(required_secrets): Check the ``Authorization`` header, and (TODO: in later revision of code) extract ``X-Tahoe-Authorization`` headers and pass them in. """ + def decorator(f): @wraps(f) def route(self, request, *args, **kwargs): - if request.requestHeaders.getRawHeaders("Authorization", [None])[0] != str( - swissnum_auth_header(self._swissnum), "ascii" + if not timing_safe_compare( + request.requestHeaders.getRawHeaders("Authorization", [None])[0].encode( + "utf-8" + ), + swissnum_auth_header(self._swissnum), ): request.setResponseCode(http.UNAUTHORIZED) return b"" - authorization = request.requestHeaders.getRawHeaders("X-Tahoe-Authorization", []) + authorization = request.requestHeaders.getRawHeaders( + "X-Tahoe-Authorization", [] + ) try: secrets = _extract_secrets(authorization, required_secrets) except ClientSecretsException: @@ -138,7 +148,6 @@ class HTTPServer(object): # TODO if data is big, maybe want to use a temporary file eventually... return dumps(data) - ##### Generic APIs ##### @_authorized_route(_app, set(), "/v1/version", methods=["GET"]) From 1721865b20d0d160891f91365dc477fae26ffcb0 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Mon, 20 Dec 2021 13:46:19 -0500 Subject: [PATCH 19/55] No longer TODO. --- src/allmydata/storage/http_server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/allmydata/storage/http_server.py b/src/allmydata/storage/http_server.py index 386368364..83bbbe49d 100644 --- a/src/allmydata/storage/http_server.py +++ b/src/allmydata/storage/http_server.py @@ -74,8 +74,8 @@ def _extract_secrets( def _authorization_decorator(required_secrets): """ - Check the ``Authorization`` header, and (TODO: in later revision of code) - extract ``X-Tahoe-Authorization`` headers and pass them in. + Check the ``Authorization`` header, and extract ``X-Tahoe-Authorization`` + headers and pass them in. """ def decorator(f): From 2bda2a01278d1aba5f3a13c772c9c7b887119bdd Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 21 Dec 2021 11:10:53 -0500 Subject: [PATCH 20/55] Switch to using a fixture. --- src/allmydata/test/test_storage_http.py | 77 +++++++++++++++---------- 1 file changed, 46 insertions(+), 31 deletions(-) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index aba33fad3..1cf650875 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -17,28 +17,34 @@ if PY2: from unittest import SkipTest from base64 import b64encode -from twisted.trial.unittest import TestCase from twisted.internet.defer import inlineCallbacks +from fixtures import Fixture, TempDir from treq.testing import StubTreq from klein import Klein from hyperlink import DecodedURL +from .common import AsyncTestCase, SyncTestCase from ..storage.server import StorageServer from ..storage.http_server import ( - HTTPServer, _extract_secrets, Secrets, ClientSecretsException, + HTTPServer, + _extract_secrets, + Secrets, + ClientSecretsException, _authorized_route, ) from ..storage.http_client import StorageClient, ClientException -class ExtractSecretsTests(TestCase): +class ExtractSecretsTests(SyncTestCase): """ Tests for ``_extract_secrets``. """ + def setUp(self): if PY2: raise SkipTest("Not going to bother supporting Python 2") + super(ExtractSecretsTests, self).setUp() def test_extract_secrets(self): """ @@ -55,16 +61,16 @@ class ExtractSecretsTests(TestCase): # One secret: self.assertEqual( - _extract_secrets([lease_secret], - {Secrets.LEASE_RENEW}), - {Secrets.LEASE_RENEW: secret1} + _extract_secrets([lease_secret], {Secrets.LEASE_RENEW}), + {Secrets.LEASE_RENEW: secret1}, ) # Two secrets: self.assertEqual( - _extract_secrets([upload_secret, lease_secret], - {Secrets.LEASE_RENEW, Secrets.UPLOAD}), - {Secrets.LEASE_RENEW: secret1, Secrets.UPLOAD: secret2} + _extract_secrets( + [upload_secret, lease_secret], {Secrets.LEASE_RENEW, Secrets.UPLOAD} + ), + {Secrets.LEASE_RENEW: secret1, Secrets.UPLOAD: secret2}, ) def test_wrong_number_of_secrets(self): @@ -129,19 +135,23 @@ class TestApp(object): return "BAD: {}".format(authorization) -class RoutingTests(TestCase): +class RoutingTests(AsyncTestCase): """ Tests for the HTTP routing infrastructure. """ + def setUp(self): if PY2: raise SkipTest("Not going to bother supporting Python 2") + super(RoutingTests, self).setUp() + # Could be a fixture, but will only be used in this test class so not + # going to bother: self._http_server = TestApp() self.client = StorageClient( - DecodedURL.from_text("http://127.0.0.1"), - SWISSNUM_FOR_TEST, - treq=StubTreq(self._http_server._app.resource()), - ) + DecodedURL.from_text("http://127.0.0.1"), + SWISSNUM_FOR_TEST, + treq=StubTreq(self._http_server._app.resource()), + ) @inlineCallbacks def test_authorization_enforcement(self): @@ -163,30 +173,35 @@ class RoutingTests(TestCase): self.assertEqual((yield response.content()), b"GOOD SECRET") -def setup_http_test(self): +class HttpTestFixture(Fixture): """ - Setup HTTP tests; call from ``setUp``. + Setup HTTP tests' infrastructure, the storage server and corresponding + client. """ - if PY2: - raise SkipTest("Not going to bother supporting Python 2") - self.storage_server = StorageServer(self.mktemp(), b"\x00" * 20) - # TODO what should the swissnum _actually_ be? - self._http_server = HTTPServer(self.storage_server, SWISSNUM_FOR_TEST) - self.client = StorageClient( - DecodedURL.from_text("http://127.0.0.1"), - SWISSNUM_FOR_TEST, - treq=StubTreq(self._http_server.get_resource()), - ) + + def _setUp(self): + self.tempdir = self.useFixture(TempDir()) + self.storage_server = StorageServer(self.tempdir.path, b"\x00" * 20) + # TODO what should the swissnum _actually_ be? + self.http_server = HTTPServer(self.storage_server, SWISSNUM_FOR_TEST) + self.client = StorageClient( + DecodedURL.from_text("http://127.0.0.1"), + SWISSNUM_FOR_TEST, + treq=StubTreq(self.http_server.get_resource()), + ) -class GenericHTTPAPITests(TestCase): +class GenericHTTPAPITests(AsyncTestCase): """ Tests of HTTP client talking to the HTTP server, for generic HTTP API endpoints and concerns. """ def setUp(self): - setup_http_test(self) + if PY2: + raise SkipTest("Not going to bother supporting Python 2") + super(GenericHTTPAPITests, self).setUp() + self.http = self.useFixture(HttpTestFixture()) @inlineCallbacks def test_bad_authentication(self): @@ -197,7 +212,7 @@ class GenericHTTPAPITests(TestCase): client = StorageClient( DecodedURL.from_text("http://127.0.0.1"), b"something wrong", - treq=StubTreq(self._http_server.get_resource()), + treq=StubTreq(self.http.http_server.get_resource()), ) with self.assertRaises(ClientException) as e: yield client.get_version() @@ -211,14 +226,14 @@ class GenericHTTPAPITests(TestCase): We ignore available disk space and max immutable share size, since that might change across calls. """ - version = yield self.client.get_version() + version = yield self.http.client.get_version() version[b"http://allmydata.org/tahoe/protocols/storage/v1"].pop( b"available-space" ) version[b"http://allmydata.org/tahoe/protocols/storage/v1"].pop( b"maximum-immutable-share-size" ) - expected_version = self.storage_server.get_version() + expected_version = self.http.storage_server.get_version() expected_version[b"http://allmydata.org/tahoe/protocols/storage/v1"].pop( b"available-space" ) From b1f4e82adfc1d2ff8482b1edb28e0139e7ef0000 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 21 Dec 2021 11:55:16 -0500 Subject: [PATCH 21/55] Switch to using hypothesis. --- src/allmydata/test/test_storage_http.py | 31 +++++++++---------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 1cf650875..2a3a5bc90 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -19,6 +19,7 @@ from base64 import b64encode from twisted.internet.defer import inlineCallbacks +from hypothesis import given, strategies as st from fixtures import Fixture, TempDir from treq.testing import StubTreq from klein import Klein @@ -46,32 +47,22 @@ class ExtractSecretsTests(SyncTestCase): raise SkipTest("Not going to bother supporting Python 2") super(ExtractSecretsTests, self).setUp() - def test_extract_secrets(self): + @given(secret_types=st.sets(st.sampled_from(Secrets))) + def test_extract_secrets(self, secret_types): """ ``_extract_secrets()`` returns a dictionary with the extracted secrets if the input secrets match the required secrets. """ - secret1 = b"\xFF" * 32 - secret2 = b"\x34" * 32 - lease_secret = "lease-renew-secret " + str(b64encode(secret1), "ascii").strip() - upload_secret = "upload-secret " + str(b64encode(secret2), "ascii").strip() + secrets = {s: bytes([i] * 32) for (i, s) in enumerate(secret_types)} + headers = [ + "{} {}".format( + secret_type.value, str(b64encode(secrets[secret_type]), "ascii").strip() + ) + for secret_type in secret_types + ] # No secrets needed, none given: - self.assertEqual(_extract_secrets([], set()), {}) - - # One secret: - self.assertEqual( - _extract_secrets([lease_secret], {Secrets.LEASE_RENEW}), - {Secrets.LEASE_RENEW: secret1}, - ) - - # Two secrets: - self.assertEqual( - _extract_secrets( - [upload_secret, lease_secret], {Secrets.LEASE_RENEW, Secrets.UPLOAD} - ), - {Secrets.LEASE_RENEW: secret1, Secrets.UPLOAD: secret2}, - ) + self.assertEqual(_extract_secrets(headers, secret_types), secrets) def test_wrong_number_of_secrets(self): """ From 776f19cbb2f96607f583dd53ce3c90cbc1353ea8 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Tue, 21 Dec 2021 12:34:02 -0500 Subject: [PATCH 22/55] Even more hypothesis, this time for secrets' contents. --- src/allmydata/test/test_storage_http.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 2a3a5bc90..3dc6bac96 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -47,13 +47,25 @@ class ExtractSecretsTests(SyncTestCase): raise SkipTest("Not going to bother supporting Python 2") super(ExtractSecretsTests, self).setUp() - @given(secret_types=st.sets(st.sampled_from(Secrets))) - def test_extract_secrets(self, secret_types): + @given( + params=st.sets(st.sampled_from(Secrets)).flatmap( + lambda secret_types: st.tuples( + st.just(secret_types), + st.lists( + st.binary(min_size=32, max_size=32), + min_size=len(secret_types), + max_size=len(secret_types), + ), + ) + ) + ) + def test_extract_secrets(self, params): """ ``_extract_secrets()`` returns a dictionary with the extracted secrets if the input secrets match the required secrets. """ - secrets = {s: bytes([i] * 32) for (i, s) in enumerate(secret_types)} + secret_types, secrets = params + secrets = {t: s for (t, s) in zip(secret_types, secrets)} headers = [ "{} {}".format( secret_type.value, str(b64encode(secrets[secret_type]), "ascii").strip() From 8b4d166a54ebc13c43743cd4263612234f6c68d4 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 22 Dec 2021 11:44:45 -0500 Subject: [PATCH 23/55] Use hypothesis for another test. --- src/allmydata/test/test_storage_http.py | 78 +++++++++++++------------ 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 3dc6bac96..80bd2661b 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -19,7 +19,7 @@ from base64 import b64encode from twisted.internet.defer import inlineCallbacks -from hypothesis import given, strategies as st +from hypothesis import assume, given, strategies as st from fixtures import Fixture, TempDir from treq.testing import StubTreq from klein import Klein @@ -37,6 +37,35 @@ from ..storage.http_server import ( from ..storage.http_client import StorageClient, ClientException +def _post_process(params): + secret_types, secrets = params + secrets = {t: s for (t, s) in zip(secret_types, secrets)} + headers = [ + "{} {}".format( + secret_type.value, str(b64encode(secrets[secret_type]), "ascii").strip() + ) + for secret_type in secret_types + ] + return secrets, headers + + +# Creates a tuple of ({Secret enum value: secret_bytes}, [http headers with secrets]). +SECRETS_STRATEGY = ( + st.sets(st.sampled_from(Secrets)) + .flatmap( + lambda secret_types: st.tuples( + st.just(secret_types), + st.lists( + st.binary(min_size=32, max_size=32), + min_size=len(secret_types), + max_size=len(secret_types), + ), + ) + ) + .map(_post_process) +) + + class ExtractSecretsTests(SyncTestCase): """ Tests for ``_extract_secrets``. @@ -47,54 +76,31 @@ class ExtractSecretsTests(SyncTestCase): raise SkipTest("Not going to bother supporting Python 2") super(ExtractSecretsTests, self).setUp() - @given( - params=st.sets(st.sampled_from(Secrets)).flatmap( - lambda secret_types: st.tuples( - st.just(secret_types), - st.lists( - st.binary(min_size=32, max_size=32), - min_size=len(secret_types), - max_size=len(secret_types), - ), - ) - ) - ) - def test_extract_secrets(self, params): + @given(secrets_to_send=SECRETS_STRATEGY) + def test_extract_secrets(self, secrets_to_send): """ ``_extract_secrets()`` returns a dictionary with the extracted secrets if the input secrets match the required secrets. """ - secret_types, secrets = params - secrets = {t: s for (t, s) in zip(secret_types, secrets)} - headers = [ - "{} {}".format( - secret_type.value, str(b64encode(secrets[secret_type]), "ascii").strip() - ) - for secret_type in secret_types - ] + secrets, headers = secrets_to_send # No secrets needed, none given: - self.assertEqual(_extract_secrets(headers, secret_types), secrets) + self.assertEqual(_extract_secrets(headers, secrets.keys()), secrets) - def test_wrong_number_of_secrets(self): + @given( + secrets_to_send=SECRETS_STRATEGY, + secrets_to_require=st.sets(st.sampled_from(Secrets)), + ) + def test_wrong_number_of_secrets(self, secrets_to_send, secrets_to_require): """ If the wrong number of secrets are passed to ``_extract_secrets``, a ``ClientSecretsException`` is raised. """ - secret1 = b"\xFF\x11ZEBRa" - lease_secret = "lease-renew-secret " + str(b64encode(secret1), "ascii").strip() + secrets_to_send, headers = secrets_to_send + assume(secrets_to_send.keys() != secrets_to_require) - # Missing secret: with self.assertRaises(ClientSecretsException): - _extract_secrets([], {Secrets.LEASE_RENEW}) - - # Wrong secret: - with self.assertRaises(ClientSecretsException): - _extract_secrets([lease_secret], {Secrets.UPLOAD}) - - # Extra secret: - with self.assertRaises(ClientSecretsException): - _extract_secrets([lease_secret], {}) + _extract_secrets(headers, secrets_to_require) def test_bad_secrets(self): """ From 7a0c83e71be89f9a1efc631f7cb75df8b063fad8 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 22 Dec 2021 11:52:13 -0500 Subject: [PATCH 24/55] Split up test. --- src/allmydata/test/test_storage_http.py | 30 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 80bd2661b..160cf8479 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -102,29 +102,43 @@ class ExtractSecretsTests(SyncTestCase): with self.assertRaises(ClientSecretsException): _extract_secrets(headers, secrets_to_require) - def test_bad_secrets(self): + def test_bad_secret_missing_value(self): """ - Bad inputs to ``_extract_secrets`` result in + Missing value in ``_extract_secrets`` result in ``ClientSecretsException``. """ - - # Missing value. with self.assertRaises(ClientSecretsException): _extract_secrets(["lease-renew-secret"], {Secrets.LEASE_RENEW}) - # Garbage prefix + def test_bad_secret_unknown_prefix(self): + """ + Missing value in ``_extract_secrets`` result in + ``ClientSecretsException``. + """ with self.assertRaises(ClientSecretsException): _extract_secrets(["FOO eA=="], {}) - # Not base64. + def test_bad_secret_not_base64(self): + """ + A non-base64 value in ``_extract_secrets`` result in + ``ClientSecretsException``. + """ with self.assertRaises(ClientSecretsException): _extract_secrets(["lease-renew-secret x"], {Secrets.LEASE_RENEW}) - # Wrong length lease secrets (must be 32 bytes long). + def test_bad_secret_wrong_length_lease_renew(self): + """ + Lease renewal secrets must be 32-bytes long. + """ with self.assertRaises(ClientSecretsException): _extract_secrets(["lease-renew-secret eA=="], {Secrets.LEASE_RENEW}) + + def test_bad_secret_wrong_length_lease_cancel(self): + """ + Lease cancel secrets must be 32-bytes long. + """ with self.assertRaises(ClientSecretsException): - _extract_secrets(["lease-upload-secret eA=="], {Secrets.LEASE_RENEW}) + _extract_secrets(["lease-cancel-secret eA=="], {Secrets.LEASE_RENEW}) SWISSNUM_FOR_TEST = b"abcd" From 58a71517c1fdc74b735876541d2dfa0ddfb2e5c4 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Wed, 22 Dec 2021 13:16:43 -0500 Subject: [PATCH 25/55] Correct way to skip with testtools. --- src/allmydata/test/test_storage_http.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/test_storage_http.py b/src/allmydata/test/test_storage_http.py index 160cf8479..181b6d347 100644 --- a/src/allmydata/test/test_storage_http.py +++ b/src/allmydata/test/test_storage_http.py @@ -14,7 +14,6 @@ if PY2: from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 # fmt: on -from unittest import SkipTest from base64 import b64encode from twisted.internet.defer import inlineCallbacks @@ -73,7 +72,7 @@ class ExtractSecretsTests(SyncTestCase): def setUp(self): if PY2: - raise SkipTest("Not going to bother supporting Python 2") + self.skipTest("Not going to bother supporting Python 2") super(ExtractSecretsTests, self).setUp() @given(secrets_to_send=SECRETS_STRATEGY) @@ -165,7 +164,7 @@ class RoutingTests(AsyncTestCase): def setUp(self): if PY2: - raise SkipTest("Not going to bother supporting Python 2") + self.skipTest("Not going to bother supporting Python 2") super(RoutingTests, self).setUp() # Could be a fixture, but will only be used in this test class so not # going to bother: @@ -222,7 +221,7 @@ class GenericHTTPAPITests(AsyncTestCase): def setUp(self): if PY2: - raise SkipTest("Not going to bother supporting Python 2") + self.skipTest("Not going to bother supporting Python 2") super(GenericHTTPAPITests, self).setUp() self.http = self.useFixture(HttpTestFixture()) From e9aaaaccc4c9783a9d1eb74ca241d72797ceceec Mon Sep 17 00:00:00 2001 From: meejah Date: Wed, 22 Dec 2021 15:31:09 -0700 Subject: [PATCH 26/55] test for json welcome page --- src/allmydata/test/web/test_root.py | 88 ++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/web/test_root.py b/src/allmydata/test/web/test_root.py index 1d5e45ba4..b0789b1d2 100644 --- a/src/allmydata/test/web/test_root.py +++ b/src/allmydata/test/web/test_root.py @@ -11,6 +11,7 @@ if PY2: from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 import time +import json from urllib.parse import ( quote, @@ -24,14 +25,23 @@ from twisted.web.template import Tag from twisted.web.test.requesthelper import DummyRequest from twisted.application import service from testtools.twistedsupport import succeeded -from twisted.internet.defer import inlineCallbacks +from twisted.internet.defer import ( + inlineCallbacks, + succeed, +) from ...storage_client import ( NativeStorageServer, StorageFarmBroker, ) -from ...web.root import RootElement +from ...web.root import ( + RootElement, + Root, +) from ...util.connection_status import ConnectionStatus +from ...crypto.ed25519 import ( + create_signing_keypair, +) from allmydata.web.root import URIHandler from allmydata.client import _Client @@ -47,6 +57,7 @@ from ..common import ( from ..common import ( SyncTestCase, + AsyncTestCase, ) from testtools.matchers import ( @@ -138,3 +149,76 @@ class RenderServiceRow(SyncTestCase): self.assertThat(item.slotData.get("version"), Equals("")) self.assertThat(item.slotData.get("nickname"), Equals("")) + + +class RenderRoot(AsyncTestCase): + + @inlineCallbacks + def test_root_json(self): + """ + """ + ann = { + "anonymous-storage-FURL": "pb://w2hqnbaa25yw4qgcvghl5psa3srpfgw3@tcp:127.0.0.1:51309/vucto2z4fxment3vfxbqecblbf6zyp6x", + "permutation-seed-base32": "w2hqnbaa25yw4qgcvghl5psa3srpfgw3", + } + srv = NativeStorageServer(b"server_id", ann, None, {}, EMPTY_CLIENT_CONFIG) + srv.get_connection_status = lambda: ConnectionStatus(False, "summary", {}, 0, 0) + + class FakeClient(_Client): + history = [] + stats_provider = object() + nickname = "" + nodeid = b"asdf" + _node_public_key = create_signing_keypair()[1] + introducer_clients = [] + helper = None + + def __init__(self): + service.MultiService.__init__(self) + self.storage_broker = StorageFarmBroker( + permute_peers=True, + tub_maker=None, + node_config=EMPTY_CLIENT_CONFIG, + ) + self.storage_broker.test_add_server(b"test-srv", srv) + + root = Root(FakeClient(), now_fn=time.time) + + lines = [] + + req = DummyRequest(b"") + req.fields = {} + req.args = { + "t": ["json"], + } + + # for some reason, DummyRequest is already finished when we + # try to add a notifyFinish handler, so override that + # behavior. + + def nop(): + return succeed(None) + req.notifyFinish = nop + req.write = lines.append + + yield root.render(req) + + raw_js = b"".join(lines).decode("utf8") + self.assertThat( + json.loads(raw_js), + Equals({ + "introducers": { + "statuses": [] + }, + "servers": [ + { + "connection_status": "summary", + "nodeid": "server_id", + "last_received_data": 0, + "version": None, + "available_space": None, + "nickname": "" + } + ] + }) + ) From 94b540215f6c32db026cbcaf588f22a9ebdfa866 Mon Sep 17 00:00:00 2001 From: meejah Date: Wed, 22 Dec 2021 15:32:30 -0700 Subject: [PATCH 27/55] args are bytes --- src/allmydata/test/web/test_root.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/web/test_root.py b/src/allmydata/test/web/test_root.py index b0789b1d2..44b91fa48 100644 --- a/src/allmydata/test/web/test_root.py +++ b/src/allmydata/test/web/test_root.py @@ -189,7 +189,7 @@ class RenderRoot(AsyncTestCase): req = DummyRequest(b"") req.fields = {} req.args = { - "t": ["json"], + b"t": [b"json"], } # for some reason, DummyRequest is already finished when we From 5be5714bb378a9ad7180f7878ce75b96120afc5c Mon Sep 17 00:00:00 2001 From: meejah Date: Wed, 22 Dec 2021 15:32:40 -0700 Subject: [PATCH 28/55] fix; get rid of sorting --- src/allmydata/web/root.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/allmydata/web/root.py b/src/allmydata/web/root.py index 1debc1d10..f1a8569d6 100644 --- a/src/allmydata/web/root.py +++ b/src/allmydata/web/root.py @@ -297,14 +297,12 @@ class Root(MultiFormatResource): } return json.dumps(result, indent=1) + "\n" - def _describe_known_servers(self, broker): - return sorted(list( + return list( self._describe_server(server) for server in broker.get_known_servers() - ), key=lambda o: sorted(o.items())) - + ) def _describe_server(self, server): status = server.get_connection_status() From 872ce021c85b48321fe389200661cf3f087e959f Mon Sep 17 00:00:00 2001 From: meejah Date: Wed, 22 Dec 2021 15:32:59 -0700 Subject: [PATCH 29/55] news --- newsfragments/3852.minor | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3852.minor diff --git a/newsfragments/3852.minor b/newsfragments/3852.minor new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/newsfragments/3852.minor @@ -0,0 +1 @@ + From 2f94fdf372116f18fde2d764b0331edb995303fb Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 6 Jan 2022 12:47:44 -0500 Subject: [PATCH 30/55] Extra testing coverage, including reproducer for #3854. --- src/allmydata/test/web/test_webish.py | 48 +++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/web/test_webish.py b/src/allmydata/test/web/test_webish.py index 12a04a6eb..4a77d21ae 100644 --- a/src/allmydata/test/web/test_webish.py +++ b/src/allmydata/test/web/test_webish.py @@ -90,10 +90,11 @@ class TahoeLAFSRequestTests(SyncTestCase): """ self._fields_test(b"GET", {}, b"", Equals(None)) - def test_form_fields(self): + def test_form_fields_if_filename_set(self): """ When a ``POST`` request is received, form fields are parsed into - ``TahoeLAFSRequest.fields``. + ``TahoeLAFSRequest.fields`` and the body is bytes (presuming ``filename`` + is set). """ form_data, boundary = multipart_formdata([ [param(u"name", u"foo"), @@ -121,6 +122,49 @@ class TahoeLAFSRequestTests(SyncTestCase): ), ) + def test_form_fields_if_name_is_file(self): + """ + When a ``POST`` request is received, form fields are parsed into + ``TahoeLAFSRequest.fields`` and the body is bytes when ``name`` + is set to ``"file"``. + """ + form_data, boundary = multipart_formdata([ + [param(u"name", u"foo"), + body(u"bar"), + ], + [param(u"name", u"file"), + body(u"some file contents"), + ], + ]) + self._fields_test( + b"POST", + {b"content-type": b"multipart/form-data; boundary=" + bytes(boundary, 'ascii')}, + form_data.encode("ascii"), + AfterPreprocessing( + lambda fs: { + k: fs.getvalue(k) + for k + in fs.keys() + }, + Equals({ + "foo": "bar", + "file": b"some file contents", + }), + ), + ) + + def test_form_fields_require_correct_mime_type(self): + """ + The body of a ``POST`` is not parsed into fields if its mime type is + not ``multipart/form-data``. + + Reproducer for https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3854 + """ + data = u'{"lalala": "lolo"}' + data = data.encode("utf-8") + self._fields_test(b"POST", {"content-type": "application/json"}, + data, Equals(None)) + class TahoeLAFSSiteTests(SyncTestCase): """ From 9f5d7c6d22d40183aaab480990d83c122049495d Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 6 Jan 2022 13:09:25 -0500 Subject: [PATCH 31/55] Fix a bug where we did unnecessary parsing. --- src/allmydata/webish.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py index 9b63a220c..559b475cb 100644 --- a/src/allmydata/webish.py +++ b/src/allmydata/webish.py @@ -114,7 +114,8 @@ class TahoeLAFSRequest(Request, object): self.path, argstring = x self.args = parse_qs(argstring, 1) - if self.method == b'POST': + content_type = (self.requestHeaders.getRawHeaders("content-type") or [""])[0] + if self.method == b'POST' and content_type.split(";")[0] == "multipart/form-data": # We use FieldStorage here because it performs better than # cgi.parse_multipart(self.content, pdict) which is what # twisted.web.http.Request uses. From 310b77aef0765bf26a28ac4ed8d03f10c05dbb49 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 6 Jan 2022 13:10:13 -0500 Subject: [PATCH 32/55] News file. --- newsfragments/3854.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3854.bugfix diff --git a/newsfragments/3854.bugfix b/newsfragments/3854.bugfix new file mode 100644 index 000000000..d12e174f9 --- /dev/null +++ b/newsfragments/3854.bugfix @@ -0,0 +1 @@ +Fixed regression on Python 3 where JSON HTTP POSTs failed to be processed. \ No newline at end of file From 2864ff872d4ddb1f4a16f1669e597e6e7ab3565a Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Thu, 6 Jan 2022 13:34:56 -0500 Subject: [PATCH 33/55] Another MIME type that needs to be handled by FieldStorage. --- src/allmydata/webish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/webish.py b/src/allmydata/webish.py index 559b475cb..519b3e1f0 100644 --- a/src/allmydata/webish.py +++ b/src/allmydata/webish.py @@ -115,7 +115,7 @@ class TahoeLAFSRequest(Request, object): self.args = parse_qs(argstring, 1) content_type = (self.requestHeaders.getRawHeaders("content-type") or [""])[0] - if self.method == b'POST' and content_type.split(";")[0] == "multipart/form-data": + if self.method == b'POST' and content_type.split(";")[0] in ("multipart/form-data", "application/x-www-form-urlencoded"): # We use FieldStorage here because it performs better than # cgi.parse_multipart(self.content, pdict) which is what # twisted.web.http.Request uses. From 983f90116b7b120d30a01b336f59bf1c0a62b9f2 Mon Sep 17 00:00:00 2001 From: meejah Date: Thu, 6 Jan 2022 13:15:31 -0700 Subject: [PATCH 34/55] check differently, don't depend on order --- src/allmydata/test/web/test_web.py | 50 +++++++++++++++++------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/src/allmydata/test/web/test_web.py b/src/allmydata/test/web/test_web.py index 1c9d6b65c..03cd6e560 100644 --- a/src/allmydata/test/web/test_web.py +++ b/src/allmydata/test/web/test_web.py @@ -820,29 +820,37 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixi """ d = self.GET("/?t=json") def _check(res): + """ + Check that the results are correct. + We can't depend on the order of servers in the output + """ decoded = json.loads(res) - expected = { - u'introducers': { - u'statuses': [], + self.assertEqual(decoded['introducers'], {u'statuses': []}) + actual_servers = decoded[u"servers"] + self.assertEquals(len(actual_servers), 2) + self.assertIn( + { + u"nodeid": u'other_nodeid', + u'available_space': 123456, + u'connection_status': u'summary', + u'last_received_data': 30, + u'nickname': u'other_nickname \u263b', + u'version': u'1.0', }, - u'servers': sorted([ - {u"nodeid": u'other_nodeid', - u'available_space': 123456, - u'connection_status': u'summary', - u'last_received_data': 30, - u'nickname': u'other_nickname \u263b', - u'version': u'1.0', - }, - {u"nodeid": u'disconnected_nodeid', - u'available_space': 123456, - u'connection_status': u'summary', - u'last_received_data': 35, - u'nickname': u'disconnected_nickname \u263b', - u'version': u'1.0', - }, - ], key=lambda o: sorted(o.items())), - } - self.assertEqual(expected, decoded) + actual_servers + ) + self.assertIn( + { + u"nodeid": u'disconnected_nodeid', + u'available_space': 123456, + u'connection_status': u'summary', + u'last_received_data': 35, + u'nickname': u'disconnected_nickname \u263b', + u'version': u'1.0', + }, + actual_servers + ) + d.addCallback(_check) return d From 0bf713c38ab36651e841ca8c84e23ecf104aea55 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 7 Jan 2022 10:12:21 -0500 Subject: [PATCH 35/55] News fragment. --- newsfragments/3856.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3856.minor diff --git a/newsfragments/3856.minor b/newsfragments/3856.minor new file mode 100644 index 000000000..e69de29bb From 7e3cb44ede60de3bed90470bf7f7803abac607b9 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 7 Jan 2022 10:13:29 -0500 Subject: [PATCH 36/55] Pin non-broken version of Paramiko. --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7e7a955c6..53057b808 100644 --- a/setup.py +++ b/setup.py @@ -409,7 +409,9 @@ setup(name="tahoe-lafs", # also set in __init__.py "html5lib", "junitxml", "tenacity", - "paramiko", + # Pin old version until + # https://github.com/paramiko/paramiko/issues/1961 is fixed. + "paramiko < 2.9", "pytest-timeout", # Does our OpenMetrics endpoint adhere to the spec: "prometheus-client == 0.11.0", From 11f2097591e8161416237ecb4676d1843478eb5d Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 10:58:58 -0700 Subject: [PATCH 37/55] docstring --- src/allmydata/test/web/test_root.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/allmydata/test/web/test_root.py b/src/allmydata/test/web/test_root.py index 44b91fa48..199c8e545 100644 --- a/src/allmydata/test/web/test_root.py +++ b/src/allmydata/test/web/test_root.py @@ -156,6 +156,11 @@ class RenderRoot(AsyncTestCase): @inlineCallbacks def test_root_json(self): """ + The 'welcome' / root page renders properly with ?t=json when some + servers show None for available_space while others show a + valid int + + See also https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3852 """ ann = { "anonymous-storage-FURL": "pb://w2hqnbaa25yw4qgcvghl5psa3srpfgw3@tcp:127.0.0.1:51309/vucto2z4fxment3vfxbqecblbf6zyp6x", From a49baf44b68eac81bb1538000c042ed537e57ef0 Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 10:59:13 -0700 Subject: [PATCH 38/55] actually-reproduce 3852 --- src/allmydata/test/web/test_root.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/allmydata/test/web/test_root.py b/src/allmydata/test/web/test_root.py index 199c8e545..8c46b809a 100644 --- a/src/allmydata/test/web/test_root.py +++ b/src/allmydata/test/web/test_root.py @@ -166,8 +166,13 @@ class RenderRoot(AsyncTestCase): "anonymous-storage-FURL": "pb://w2hqnbaa25yw4qgcvghl5psa3srpfgw3@tcp:127.0.0.1:51309/vucto2z4fxment3vfxbqecblbf6zyp6x", "permutation-seed-base32": "w2hqnbaa25yw4qgcvghl5psa3srpfgw3", } - srv = NativeStorageServer(b"server_id", ann, None, {}, EMPTY_CLIENT_CONFIG) - srv.get_connection_status = lambda: ConnectionStatus(False, "summary", {}, 0, 0) + srv0 = NativeStorageServer(b"server_id0", ann, None, {}, EMPTY_CLIENT_CONFIG) + srv0.get_connection_status = lambda: ConnectionStatus(False, "summary0", {}, 0, 0) + + srv1 = NativeStorageServer(b"server_id1", ann, None, {}, EMPTY_CLIENT_CONFIG) + srv1.get_connection_status = lambda: ConnectionStatus(False, "summary1", {}, 0, 0) + # arrange for this server to have some valid available space + srv1.get_available_space = lambda: 12345 class FakeClient(_Client): history = [] @@ -185,7 +190,8 @@ class RenderRoot(AsyncTestCase): tub_maker=None, node_config=EMPTY_CLIENT_CONFIG, ) - self.storage_broker.test_add_server(b"test-srv", srv) + self.storage_broker.test_add_server(b"test-srv0", srv0) + self.storage_broker.test_add_server(b"test-srv1", srv1) root = Root(FakeClient(), now_fn=time.time) @@ -217,12 +223,20 @@ class RenderRoot(AsyncTestCase): }, "servers": [ { - "connection_status": "summary", - "nodeid": "server_id", + "connection_status": "summary0", + "nodeid": "server_id0", "last_received_data": 0, "version": None, "available_space": None, "nickname": "" + }, + { + "connection_status": "summary1", + "nodeid": "server_id1", + "last_received_data": 0, + "version": None, + "available_space": 12345, + "nickname": "" } ] }) From e8f5023ae2e8b6404f3d7ad37db34fb28a3c4333 Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 10:59:34 -0700 Subject: [PATCH 39/55] its a bugfix --- newsfragments/3852.minor => 3852.bugfix | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename newsfragments/3852.minor => 3852.bugfix (100%) diff --git a/newsfragments/3852.minor b/3852.bugfix similarity index 100% rename from newsfragments/3852.minor rename to 3852.bugfix From 9d823aef67d7328c9b8ee2d2ae75703b5cd3b26a Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 11:05:35 -0700 Subject: [PATCH 40/55] newsfragment to correct spot --- 3852.bugfix => newsfragments/3852.bugfix | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename 3852.bugfix => newsfragments/3852.bugfix (100%) diff --git a/3852.bugfix b/newsfragments/3852.bugfix similarity index 100% rename from 3852.bugfix rename to newsfragments/3852.bugfix From 9644532916de535b738f1e85f5ce060c5e77c604 Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 11:28:55 -0700 Subject: [PATCH 41/55] don't depend on order --- src/allmydata/test/web/test_root.py | 49 ++++++++++++++--------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/allmydata/test/web/test_root.py b/src/allmydata/test/web/test_root.py index 8c46b809a..228b8e449 100644 --- a/src/allmydata/test/web/test_root.py +++ b/src/allmydata/test/web/test_root.py @@ -215,29 +215,28 @@ class RenderRoot(AsyncTestCase): yield root.render(req) raw_js = b"".join(lines).decode("utf8") - self.assertThat( - json.loads(raw_js), - Equals({ - "introducers": { - "statuses": [] - }, - "servers": [ - { - "connection_status": "summary0", - "nodeid": "server_id0", - "last_received_data": 0, - "version": None, - "available_space": None, - "nickname": "" - }, - { - "connection_status": "summary1", - "nodeid": "server_id1", - "last_received_data": 0, - "version": None, - "available_space": 12345, - "nickname": "" - } - ] - }) + js = json.loads(raw_js) + servers = js["servers"] + self.assertEquals(len(servers), 2) + self.assertIn( + { + "connection_status": "summary0", + "nodeid": "server_id0", + "last_received_data": 0, + "version": None, + "available_space": None, + "nickname": "" + }, + servers + ) + self.assertIn( + { + "connection_status": "summary1", + "nodeid": "server_id1", + "last_received_data": 0, + "version": None, + "available_space": 12345, + "nickname": "" + }, + servers ) From b91835a2007764fc924d05f484563b303100f8b5 Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 13:06:26 -0700 Subject: [PATCH 42/55] update NEWS.txt for release --- NEWS.rst | 14 ++++++++++++++ newsfragments/3848.minor | 0 newsfragments/3849.minor | 0 newsfragments/3850.minor | 0 newsfragments/3852.bugfix | 1 - newsfragments/3854.bugfix | 1 - newsfragments/3856.minor | 0 7 files changed, 14 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/3848.minor delete mode 100644 newsfragments/3849.minor delete mode 100644 newsfragments/3850.minor delete mode 100644 newsfragments/3852.bugfix delete mode 100644 newsfragments/3854.bugfix delete mode 100644 newsfragments/3856.minor diff --git a/NEWS.rst b/NEWS.rst index 15cb9459d..62d1587dd 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -5,6 +5,20 @@ User-Visible Changes in Tahoe-LAFS ================================== .. towncrier start line +Release 1.17.0.post55 (2022-01-07)Release 1.17.0.post55 (2022-01-07) +'''''''''''''''''''''''''''''''''' + +Bug Fixes +--------- + +- (`#3852 `_) +- Fixed regression on Python 3 where JSON HTTP POSTs failed to be processed. (`#3854 `_) + + +Misc/Other +---------- + +- `#3848 `_, `#3849 `_, `#3850 `_, `#3856 `_ Release 1.17.0 (2021-12-06) diff --git a/newsfragments/3848.minor b/newsfragments/3848.minor deleted file mode 100644 index e69de29bb..000000000 diff --git a/newsfragments/3849.minor b/newsfragments/3849.minor deleted file mode 100644 index e69de29bb..000000000 diff --git a/newsfragments/3850.minor b/newsfragments/3850.minor deleted file mode 100644 index e69de29bb..000000000 diff --git a/newsfragments/3852.bugfix b/newsfragments/3852.bugfix deleted file mode 100644 index 8b1378917..000000000 --- a/newsfragments/3852.bugfix +++ /dev/null @@ -1 +0,0 @@ - diff --git a/newsfragments/3854.bugfix b/newsfragments/3854.bugfix deleted file mode 100644 index d12e174f9..000000000 --- a/newsfragments/3854.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fixed regression on Python 3 where JSON HTTP POSTs failed to be processed. \ No newline at end of file diff --git a/newsfragments/3856.minor b/newsfragments/3856.minor deleted file mode 100644 index e69de29bb..000000000 From 22734dccba2cc95752de1c360830b728d4ac83b2 Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 13:13:44 -0700 Subject: [PATCH 43/55] fix text for 3852 --- NEWS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 62d1587dd..c2d405f8f 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -11,7 +11,7 @@ Release 1.17.0.post55 (2022-01-07)Release 1.17.0.post55 (2022-01-07) Bug Fixes --------- -- (`#3852 `_) +- Fixed regression on Python 3 causing the JSON version of the Welcome page to sometimes produce a 500 error (`#3852 `_) - Fixed regression on Python 3 where JSON HTTP POSTs failed to be processed. (`#3854 `_) From e9ece061f4a1a6373f39fe37291e1775df3a0391 Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 13:18:03 -0700 Subject: [PATCH 44/55] news --- newsfragments/3858.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3858.minor diff --git a/newsfragments/3858.minor b/newsfragments/3858.minor new file mode 100644 index 000000000..e69de29bb From f9ddd3b3bedf692ffdf598d9def96b3c79097602 Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 13:21:44 -0700 Subject: [PATCH 45/55] fix NEWS title --- NEWS.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index c2d405f8f..0f9194cc4 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -5,8 +5,8 @@ User-Visible Changes in Tahoe-LAFS ================================== .. towncrier start line -Release 1.17.0.post55 (2022-01-07)Release 1.17.0.post55 (2022-01-07) -'''''''''''''''''''''''''''''''''' +Release 1.17.1 (2022-01-07) +''''''''''''''''''''''''''' Bug Fixes --------- From b5251eb0a12eaa07411473583facd5c21cee729f Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 13:27:53 -0700 Subject: [PATCH 46/55] update relnotes --- relnotes.txt | 45 +++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/relnotes.txt b/relnotes.txt index dff4f192e..e9b298771 100644 --- a/relnotes.txt +++ b/relnotes.txt @@ -1,6 +1,6 @@ -ANNOUNCING Tahoe, the Least-Authority File Store, v1.17.0 +ANNOUNCING Tahoe, the Least-Authority File Store, v1.17.1 -The Tahoe-LAFS team is pleased to announce version 1.17.0 of +The Tahoe-LAFS team is pleased to announce version 1.17.1 of Tahoe-LAFS, an extremely reliable decentralized storage system. Get it with "pip install tahoe-lafs", or download a tarball here: @@ -15,19 +15,12 @@ unique security and fault-tolerance properties: https://tahoe-lafs.readthedocs.org/en/latest/about.html -The previous stable release of Tahoe-LAFS was v1.16.0, released on -October 19, 2021. +The previous stable release of Tahoe-LAFS was v1.17.0, released on +December 6, 2021. -This release fixes several security issues raised as part of an audit -by Cure53. We developed fixes for these issues in a private -repository. Shortly after this release, public tickets will be updated -with further information (along with, of course, all the code). +This release fixes two Python3-releated regressions and 4 minor bugs. -There is also OpenMetrics support now and several bug fixes. - -In all, 46 issues have been fixed since the last release. - -Please see ``NEWS.rst`` for a more complete list of changes. +Please see ``NEWS.rst`` [1] for a complete list of changes. WHAT IS IT GOOD FOR? @@ -66,12 +59,12 @@ to v1.0 (which was released March 25, 2008). Clients from this release can read files and directories produced by clients of all versions since v1.0. -Network connections are limited by the Introducer protocol in -use. If the Introducer is running v1.10 or v1.11, then servers -from this release (v1.12) can serve clients of all versions -back to v1.0 . If it is running v1.12, then they can only -serve clients back to v1.10. Clients from this release can use -servers back to v1.10, but not older servers. +Network connections are limited by the Introducer protocol in use. If +the Introducer is running v1.10 or v1.11, then servers from this +release can serve clients of all versions back to v1.0 . If it is +running v1.12 or higher, then they can only serve clients back to +v1.10. Clients from this release can use servers back to v1.10, but +not older servers. Except for the new optional MDMF format, we have not made any intentional compatibility changes. However we do not yet have @@ -79,7 +72,7 @@ the test infrastructure to continuously verify that all new versions are interoperable with previous versions. We intend to build such an infrastructure in the future. -This is the twenty-first release in the version 1 series. This +This is the twenty-second release in the version 1 series. This series of Tahoe-LAFS will be actively supported and maintained for the foreseeable future, and future versions of Tahoe-LAFS will retain the ability to read and write files compatible @@ -139,7 +132,7 @@ Of Fame" [13]. ACKNOWLEDGEMENTS -This is the eighteenth release of Tahoe-LAFS to be created +This is the nineteenth release of Tahoe-LAFS to be created solely as a labor of love by volunteers. Thank you very much to the team of "hackers in the public interest" who make Tahoe-LAFS possible. @@ -147,16 +140,16 @@ Tahoe-LAFS possible. meejah on behalf of the Tahoe-LAFS team -December 6, 2021 +January 7, 2022 Planet Earth -[1] https://github.com/tahoe-lafs/tahoe-lafs/blob/tahoe-lafs-1.17.0/NEWS.rst +[1] https://github.com/tahoe-lafs/tahoe-lafs/blob/tahoe-lafs-1.17.1/NEWS.rst [2] https://github.com/tahoe-lafs/tahoe-lafs/blob/master/docs/known_issues.rst [3] https://tahoe-lafs.org/trac/tahoe-lafs/wiki/RelatedProjects -[4] https://github.com/tahoe-lafs/tahoe-lafs/blob/tahoe-lafs-1.17.0/COPYING.GPL -[5] https://github.com/tahoe-lafs/tahoe-lafs/blob/tahoe-lafs-1.17.0/COPYING.TGPPL.rst -[6] https://tahoe-lafs.readthedocs.org/en/tahoe-lafs-1.17.0/INSTALL.html +[4] https://github.com/tahoe-lafs/tahoe-lafs/blob/tahoe-lafs-1.17.1/COPYING.GPL +[5] https://github.com/tahoe-lafs/tahoe-lafs/blob/tahoe-lafs-1.17.1/COPYING.TGPPL.rst +[6] https://tahoe-lafs.readthedocs.org/en/tahoe-lafs-1.17.1/INSTALL.html [7] https://lists.tahoe-lafs.org/mailman/listinfo/tahoe-dev [8] https://tahoe-lafs.org/trac/tahoe-lafs/roadmap [9] https://github.com/tahoe-lafs/tahoe-lafs/blob/master/CREDITS From c7664762365e1e14dd2c52374e3206ecd9b077a5 Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 13:28:27 -0700 Subject: [PATCH 47/55] nix --- nix/tahoe-lafs.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/tahoe-lafs.nix b/nix/tahoe-lafs.nix index 04d6c4163..2b41e676e 100644 --- a/nix/tahoe-lafs.nix +++ b/nix/tahoe-lafs.nix @@ -7,7 +7,7 @@ , html5lib, pyutil, distro, configparser, klein, cbor2 }: python.pkgs.buildPythonPackage rec { - # Most of the time this is not exactly the release version (eg 1.17.0). + # Most of the time this is not exactly the release version (eg 1.17.1). # Give it a `post` component to make it look newer than the release version # and we'll bump this up at the time of each release. # @@ -20,7 +20,7 @@ python.pkgs.buildPythonPackage rec { # is not a reproducable artifact (in the sense of "reproducable builds") so # it is excluded from the source tree by default. When it is included, the # package tends to be frequently spuriously rebuilt. - version = "1.17.0.post1"; + version = "1.17.1.post1"; name = "tahoe-lafs-${version}"; src = lib.cleanSourceWith { src = ../.; From aa81bfc937b1ee7bbe2b43e814cc7683eed1d29e Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 13:29:45 -0700 Subject: [PATCH 48/55] cleanup whitespace --- docs/Installation/install-tahoe.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/Installation/install-tahoe.rst b/docs/Installation/install-tahoe.rst index 2fe47f4a8..8ceca2e01 100644 --- a/docs/Installation/install-tahoe.rst +++ b/docs/Installation/install-tahoe.rst @@ -28,15 +28,15 @@ To install Tahoe-LAFS on Windows: 3. Open the installer by double-clicking it. Select the **Add Python to PATH** check-box, then click **Install Now**. 4. Start PowerShell and enter the following command to verify python installation:: - + python --version 5. Enter the following command to install Tahoe-LAFS:: - + pip install tahoe-lafs 6. Verify installation by checking for the version:: - + tahoe --version If you want to hack on Tahoe's source code, you can install Tahoe in a ``virtualenv`` on your Windows Machine. To learn more, see :doc:`install-on-windows`. @@ -56,13 +56,13 @@ If you are working on MacOS or a Linux distribution which does not have Tahoe-LA * **pip**: Most python installations already include `pip`. However, if your installation does not, see `pip installation `_. 2. Install Tahoe-LAFS using pip:: - + pip install tahoe-lafs 3. Verify installation by checking for the version:: - + tahoe --version -If you are looking to hack on the source code or run pre-release code, we recommend you install Tahoe-LAFS on a `virtualenv` instance. To learn more, see :doc:`install-on-linux`. +If you are looking to hack on the source code or run pre-release code, we recommend you install Tahoe-LAFS on a `virtualenv` instance. To learn more, see :doc:`install-on-linux`. You can always write to the `tahoe-dev mailing list `_ or chat on the `Libera.chat IRC `_ if you are not able to get Tahoe-LAFS up and running on your deployment. From f7477043c5025642ef0fbeb042310decb774bd01 Mon Sep 17 00:00:00 2001 From: meejah Date: Fri, 7 Jan 2022 13:29:52 -0700 Subject: [PATCH 49/55] unnecessary step --- docs/release-checklist.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/release-checklist.rst b/docs/release-checklist.rst index f943abb5d..2b954449e 100644 --- a/docs/release-checklist.rst +++ b/docs/release-checklist.rst @@ -70,7 +70,6 @@ Create Branch and Apply Updates - commit it - update "docs/known_issues.rst" if appropriate -- update "docs/Installation/install-tahoe.rst" references to the new release - Push the branch to github - Create a (draft) PR; this should trigger CI (note that github doesn't let you create a PR without some changes on the branch so From 852ebe90e5bd5b04b5a75d1850df87673a78955f Mon Sep 17 00:00:00 2001 From: meejah Date: Mon, 10 Jan 2022 11:48:55 -0700 Subject: [PATCH 50/55] clean clone --- docs/release-checklist.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-checklist.rst b/docs/release-checklist.rst index 2b954449e..edfe9e20f 100644 --- a/docs/release-checklist.rst +++ b/docs/release-checklist.rst @@ -106,6 +106,11 @@ they will need to evaluate which contributors' signatures they trust. - tox -e deprecations,upcoming-deprecations +- clone to a clean, local checkout (to avoid extra files being included in the release) + + - cd /tmp + - git clone /home/meejah/src/tahoe-lafs + - build tarballs - tox -e tarballs From d2ff2a7376f99f08fba22ee3c3b28cba535a0117 Mon Sep 17 00:00:00 2001 From: meejah Date: Mon, 10 Jan 2022 11:49:02 -0700 Subject: [PATCH 51/55] whitespace --- docs/release-checklist.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-checklist.rst b/docs/release-checklist.rst index edfe9e20f..3c984d122 100644 --- a/docs/release-checklist.rst +++ b/docs/release-checklist.rst @@ -158,7 +158,8 @@ need to be uploaded to https://tahoe-lafs.org in `~source/downloads` - secure-copy all release artifacts to the download area on the tahoe-lafs.org host machine. `~source/downloads` on there maps to https://tahoe-lafs.org/downloads/ on the Web. -- scp dist/*1.15.0* username@tahoe-lafs.org:/home/source/downloads + - scp dist/*1.15.0* username@tahoe-lafs.org:/home/source/downloads + - the following developers have access to do this: - exarkun From 1446c9c4adebda255276659dfef883f17770ca7f Mon Sep 17 00:00:00 2001 From: meejah Date: Mon, 10 Jan 2022 11:49:15 -0700 Subject: [PATCH 52/55] add 'push the tags' step --- docs/release-checklist.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-checklist.rst b/docs/release-checklist.rst index 3c984d122..7acca6bb3 100644 --- a/docs/release-checklist.rst +++ b/docs/release-checklist.rst @@ -166,6 +166,10 @@ need to be uploaded to https://tahoe-lafs.org in `~source/downloads` - meejah - warner +Push the signed tag to the main repository: + +- git push origin_push tahoe-lafs-1.17.1 + For the actual release, the tarball and signature files need to be uploaded to PyPI as well. From 8cd4e7a4b5069c3fb30c195934974755e8f0c53c Mon Sep 17 00:00:00 2001 From: meejah Date: Mon, 10 Jan 2022 11:49:31 -0700 Subject: [PATCH 53/55] news --- newsfragments/3859.minor | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 newsfragments/3859.minor diff --git a/newsfragments/3859.minor b/newsfragments/3859.minor new file mode 100644 index 000000000..e69de29bb From ea83b16d1171b789c2041ed1e67e2ffa6dec3ff4 Mon Sep 17 00:00:00 2001 From: meejah Date: Mon, 10 Jan 2022 14:17:50 -0700 Subject: [PATCH 54/55] most people say 'origin' --- docs/release-checklist.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release-checklist.rst b/docs/release-checklist.rst index 7acca6bb3..165aa8826 100644 --- a/docs/release-checklist.rst +++ b/docs/release-checklist.rst @@ -168,7 +168,7 @@ need to be uploaded to https://tahoe-lafs.org in `~source/downloads` Push the signed tag to the main repository: -- git push origin_push tahoe-lafs-1.17.1 +- git push origin tahoe-lafs-1.17.1 For the actual release, the tarball and signature files need to be uploaded to PyPI as well. From a753a71105a8865cb27c9a59258fe349d55ba06a Mon Sep 17 00:00:00 2001 From: meejah Date: Mon, 10 Jan 2022 14:22:57 -0700 Subject: [PATCH 55/55] please the Sphinx --- docs/release-checklist.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/release-checklist.rst b/docs/release-checklist.rst index 165aa8826..d2f1b3eb8 100644 --- a/docs/release-checklist.rst +++ b/docs/release-checklist.rst @@ -157,7 +157,8 @@ need to be uploaded to https://tahoe-lafs.org in `~source/downloads` - secure-copy all release artifacts to the download area on the tahoe-lafs.org host machine. `~source/downloads` on there maps to - https://tahoe-lafs.org/downloads/ on the Web. + https://tahoe-lafs.org/downloads/ on the Web: + - scp dist/*1.15.0* username@tahoe-lafs.org:/home/source/downloads - the following developers have access to do this: