mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-01-31 08:25:35 +00:00
Merge pull request #39 from tahoe-lafs/LFS-01-010.mutable
LFS-01-010 (mutables): Weak authentication for client leases Fixes: ticket:3841
This commit is contained in:
commit
131319971e
1
newsfragments/3841.security
Normal file
1
newsfragments/3841.security
Normal file
@ -0,0 +1 @@
|
||||
The storage server now keeps hashes of lease renew and cancel secrets for mutable share files instead of keeping the original secrets.
|
@ -230,8 +230,8 @@ def dump_mutable_share(options):
|
||||
print(" ownerid: %d" % lease.owner_num, file=out)
|
||||
when = format_expiration_time(lease.get_expiration_time())
|
||||
print(" expires in %s" % when, file=out)
|
||||
print(" renew_secret: %s" % str(base32.b2a(lease.renew_secret), "utf-8"), file=out)
|
||||
print(" cancel_secret: %s" % str(base32.b2a(lease.cancel_secret), "utf-8"), file=out)
|
||||
print(" renew_secret: %s" % lease.present_renew_secret(), file=out)
|
||||
print(" cancel_secret: %s" % lease.present_cancel_secret(), file=out)
|
||||
print(" secrets are for nodeid: %s" % idlib.nodeid_b2a(lease.nodeid), file=out)
|
||||
else:
|
||||
print("No leases.", file=out)
|
||||
|
@ -231,7 +231,7 @@ class ShareFile(object):
|
||||
offset = self._lease_offset + lease_number * self.LEASE_SIZE
|
||||
f.seek(offset)
|
||||
assert f.tell() == offset
|
||||
f.write(self._schema.serialize_lease(lease_info))
|
||||
f.write(self._schema.lease_serializer.serialize(lease_info))
|
||||
|
||||
def _read_num_leases(self, f):
|
||||
f.seek(0x08)
|
||||
@ -262,7 +262,7 @@ class ShareFile(object):
|
||||
for i in range(num_leases):
|
||||
data = f.read(self.LEASE_SIZE)
|
||||
if data:
|
||||
yield self._schema.unserialize_lease(data)
|
||||
yield self._schema.lease_serializer.unserialize(data)
|
||||
|
||||
def add_lease(self, lease_info):
|
||||
with open(self.home, 'rb+') as f:
|
||||
|
@ -13,84 +13,28 @@ if PY2:
|
||||
|
||||
import struct
|
||||
|
||||
try:
|
||||
from typing import Union
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import attr
|
||||
|
||||
from nacl.hash import blake2b
|
||||
from nacl.encoding import RawEncoder
|
||||
|
||||
from .lease import (
|
||||
LeaseInfo,
|
||||
HashedLeaseInfo,
|
||||
from .lease_schema import (
|
||||
v1_immutable,
|
||||
v2_immutable,
|
||||
)
|
||||
|
||||
def _header(version, max_size):
|
||||
# type: (int, int) -> bytes
|
||||
@attr.s(frozen=True)
|
||||
class _Schema(object):
|
||||
"""
|
||||
Construct the header for an immutable container.
|
||||
Implement encoding and decoding for multiple versions of the immutable
|
||||
container schema.
|
||||
|
||||
:param version: the container version to include the in header
|
||||
:param max_size: the maximum data size the container will hold
|
||||
:ivar int version: the version number of the schema this object supports
|
||||
|
||||
:return: some bytes to write at the beginning of the container
|
||||
:ivar lease_serializer: an object that is responsible for lease
|
||||
serialization and unserialization
|
||||
"""
|
||||
# The second field -- the four-byte share data length -- is no longer
|
||||
# used as of Tahoe v1.3.0, but we continue to write it in there in
|
||||
# case someone downgrades a storage server from >= Tahoe-1.3.0 to <
|
||||
# Tahoe-1.3.0, or moves a share file from one server to another,
|
||||
# etc. We do saturation -- a share data length larger than 2**32-1
|
||||
# (what can fit into the field) is marked as the largest length that
|
||||
# can fit into the field. That way, even if this does happen, the old
|
||||
# < v1.3.0 server will still allow clients to read the first part of
|
||||
# the share.
|
||||
return struct.pack(">LLL", version, min(2**32 - 1, max_size), 0)
|
||||
version = attr.ib()
|
||||
lease_serializer = attr.ib()
|
||||
|
||||
|
||||
class _V2(object):
|
||||
"""
|
||||
Implement encoding and decoding for v2 of the immutable container.
|
||||
"""
|
||||
version = 2
|
||||
|
||||
@classmethod
|
||||
def _hash_secret(cls, secret):
|
||||
# type: (bytes) -> bytes
|
||||
"""
|
||||
Hash a lease secret for storage.
|
||||
"""
|
||||
return blake2b(secret, digest_size=32, encoder=RawEncoder())
|
||||
|
||||
@classmethod
|
||||
def _hash_lease_info(cls, lease_info):
|
||||
# type: (LeaseInfo) -> HashedLeaseInfo
|
||||
"""
|
||||
Hash the cleartext lease info secrets into a ``HashedLeaseInfo``.
|
||||
"""
|
||||
if not isinstance(lease_info, LeaseInfo):
|
||||
# Provide a little safety against misuse, especially an attempt to
|
||||
# re-hash an already-hashed lease info which is represented as a
|
||||
# different type.
|
||||
raise TypeError(
|
||||
"Can only hash LeaseInfo, not {!r}".format(lease_info),
|
||||
)
|
||||
|
||||
# Hash the cleartext secrets in the lease info and wrap the result in
|
||||
# a new type.
|
||||
return HashedLeaseInfo(
|
||||
attr.assoc(
|
||||
lease_info,
|
||||
renew_secret=cls._hash_secret(lease_info.renew_secret),
|
||||
cancel_secret=cls._hash_secret(lease_info.cancel_secret),
|
||||
),
|
||||
cls._hash_secret,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def header(cls, max_size):
|
||||
def header(self, max_size):
|
||||
# type: (int) -> bytes
|
||||
"""
|
||||
Construct a container header.
|
||||
@ -99,78 +43,23 @@ class _V2(object):
|
||||
|
||||
:return: the header bytes
|
||||
"""
|
||||
return _header(cls.version, max_size)
|
||||
# The second field -- the four-byte share data length -- is no longer
|
||||
# used as of Tahoe v1.3.0, but we continue to write it in there in
|
||||
# case someone downgrades a storage server from >= Tahoe-1.3.0 to <
|
||||
# Tahoe-1.3.0, or moves a share file from one server to another,
|
||||
# etc. We do saturation -- a share data length larger than 2**32-1
|
||||
# (what can fit into the field) is marked as the largest length that
|
||||
# can fit into the field. That way, even if this does happen, the old
|
||||
# < v1.3.0 server will still allow clients to read the first part of
|
||||
# the share.
|
||||
return struct.pack(">LLL", self.version, min(2**32 - 1, max_size), 0)
|
||||
|
||||
@classmethod
|
||||
def serialize_lease(cls, lease):
|
||||
# type: (Union[LeaseInfo, HashedLeaseInfo]) -> bytes
|
||||
"""
|
||||
Serialize a lease to be written to a v2 container.
|
||||
|
||||
:param lease: the lease to serialize
|
||||
|
||||
:return: the serialized bytes
|
||||
"""
|
||||
if isinstance(lease, LeaseInfo):
|
||||
# v2 of the immutable schema stores lease secrets hashed. If
|
||||
# we're given a LeaseInfo then it holds plaintext secrets. Hash
|
||||
# them before trying to serialize.
|
||||
lease = cls._hash_lease_info(lease)
|
||||
if isinstance(lease, HashedLeaseInfo):
|
||||
return lease.to_immutable_data()
|
||||
raise ValueError(
|
||||
"ShareFile v2 schema cannot represent lease {!r}".format(
|
||||
lease,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def unserialize_lease(cls, data):
|
||||
# type: (bytes) -> HashedLeaseInfo
|
||||
"""
|
||||
Unserialize some bytes from a v2 container.
|
||||
|
||||
:param data: the bytes from the container
|
||||
|
||||
:return: the ``HashedLeaseInfo`` the bytes represent
|
||||
"""
|
||||
# In v2 of the immutable schema lease secrets are stored hashed. Wrap
|
||||
# a LeaseInfo in a HashedLeaseInfo so it can supply the correct
|
||||
# interpretation for those values.
|
||||
return HashedLeaseInfo(LeaseInfo.from_immutable_data(data), cls._hash_secret)
|
||||
|
||||
|
||||
class _V1(object):
|
||||
"""
|
||||
Implement encoding and decoding for v1 of the immutable container.
|
||||
"""
|
||||
version = 1
|
||||
|
||||
@classmethod
|
||||
def header(cls, max_size):
|
||||
return _header(cls.version, max_size)
|
||||
|
||||
@classmethod
|
||||
def serialize_lease(cls, lease):
|
||||
if isinstance(lease, LeaseInfo):
|
||||
return lease.to_immutable_data()
|
||||
raise ValueError(
|
||||
"ShareFile v1 schema only supports LeaseInfo, not {!r}".format(
|
||||
lease,
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def unserialize_lease(cls, data):
|
||||
# In v1 of the immutable schema lease secrets are stored plaintext.
|
||||
# So load the data into a plain LeaseInfo which works on plaintext
|
||||
# secrets.
|
||||
return LeaseInfo.from_immutable_data(data)
|
||||
|
||||
|
||||
ALL_SCHEMAS = {_V2, _V1}
|
||||
ALL_SCHEMA_VERSIONS = {schema.version for schema in ALL_SCHEMAS} # type: ignore
|
||||
NEWEST_SCHEMA_VERSION = max(ALL_SCHEMAS, key=lambda schema: schema.version) # type: ignore
|
||||
ALL_SCHEMAS = {
|
||||
_Schema(version=2, lease_serializer=v2_immutable),
|
||||
_Schema(version=1, lease_serializer=v1_immutable),
|
||||
}
|
||||
ALL_SCHEMA_VERSIONS = {schema.version for schema in ALL_SCHEMAS}
|
||||
NEWEST_SCHEMA_VERSION = max(ALL_SCHEMAS, key=lambda schema: schema.version)
|
||||
|
||||
def schema_from_version(version):
|
||||
# (int) -> Optional[type]
|
||||
|
@ -25,6 +25,7 @@ from twisted.python.components import (
|
||||
)
|
||||
|
||||
from allmydata.util.hashutil import timing_safe_compare
|
||||
from allmydata.util import base32
|
||||
|
||||
# struct format for representation of a lease in an immutable share
|
||||
IMMUTABLE_FORMAT = ">L32s32sL"
|
||||
@ -102,12 +103,24 @@ class ILeaseInfo(Interface):
|
||||
secret, ``False`` otherwise
|
||||
"""
|
||||
|
||||
def present_renew_secret():
|
||||
"""
|
||||
:return str: Text which could reasonably be shown to a person representing
|
||||
this lease's renew secret.
|
||||
"""
|
||||
|
||||
def is_cancel_secret(candidate_secret):
|
||||
"""
|
||||
:return bool: ``True`` if the given byte string is this lease's cancel
|
||||
secret, ``False`` otherwise
|
||||
"""
|
||||
|
||||
def present_cancel_secret():
|
||||
"""
|
||||
:return str: Text which could reasonably be shown to a person representing
|
||||
this lease's cancel secret.
|
||||
"""
|
||||
|
||||
|
||||
@implementer(ILeaseInfo)
|
||||
@attr.s(frozen=True)
|
||||
@ -173,6 +186,13 @@ class LeaseInfo(object):
|
||||
"""
|
||||
return timing_safe_compare(self.renew_secret, candidate_secret)
|
||||
|
||||
def present_renew_secret(self):
|
||||
# type: () -> str
|
||||
"""
|
||||
Return the renew secret, base32-encoded.
|
||||
"""
|
||||
return str(base32.b2a(self.renew_secret), "utf-8")
|
||||
|
||||
def is_cancel_secret(self, candidate_secret):
|
||||
# type: (bytes) -> bool
|
||||
"""
|
||||
@ -183,6 +203,13 @@ class LeaseInfo(object):
|
||||
"""
|
||||
return timing_safe_compare(self.cancel_secret, candidate_secret)
|
||||
|
||||
def present_cancel_secret(self):
|
||||
# type: () -> str
|
||||
"""
|
||||
Return the cancel secret, base32-encoded.
|
||||
"""
|
||||
return str(base32.b2a(self.cancel_secret), "utf-8")
|
||||
|
||||
def get_grant_renew_time_time(self):
|
||||
# hack, based upon fixed 31day expiration period
|
||||
return self._expiration_time - 31*24*60*60
|
||||
@ -203,7 +230,7 @@ class LeaseInfo(object):
|
||||
"cancel_secret",
|
||||
"expiration_time",
|
||||
]
|
||||
values = struct.unpack(">L32s32sL", data)
|
||||
values = struct.unpack(IMMUTABLE_FORMAT, data)
|
||||
return cls(nodeid=None, **dict(zip(names, values)))
|
||||
|
||||
def immutable_size(self):
|
||||
@ -247,7 +274,7 @@ class LeaseInfo(object):
|
||||
"cancel_secret",
|
||||
"nodeid",
|
||||
]
|
||||
values = struct.unpack(">LL32s32s20s", data)
|
||||
values = struct.unpack(MUTABLE_FORMAT, data)
|
||||
return cls(**dict(zip(names, values)))
|
||||
|
||||
|
||||
@ -264,14 +291,30 @@ class HashedLeaseInfo(proxyForInterface(ILeaseInfo, "_lease_info")): # type: ign
|
||||
# to `_lease_info`. Here we override a few of those methods to adjust
|
||||
# their behavior to make them suitable for use with hashed secrets.
|
||||
|
||||
def renew(self, new_expire_time):
|
||||
# Preserve the HashedLeaseInfo wrapper around the renewed LeaseInfo.
|
||||
return attr.assoc(
|
||||
self,
|
||||
_lease_info=super(HashedLeaseInfo, self).renew(new_expire_time),
|
||||
)
|
||||
|
||||
def is_renew_secret(self, candidate_secret):
|
||||
# type: (bytes) -> bool
|
||||
"""
|
||||
Hash the candidate secret and compare the result to the stored hashed
|
||||
secret.
|
||||
"""
|
||||
return super(HashedLeaseInfo, self).is_renew_secret(self._hash(candidate_secret))
|
||||
|
||||
def present_renew_secret(self):
|
||||
# type: () -> str
|
||||
"""
|
||||
Present the hash of the secret with a marker indicating it is a hash.
|
||||
"""
|
||||
return u"hash:" + super(HashedLeaseInfo, self).present_renew_secret()
|
||||
|
||||
def is_cancel_secret(self, candidate_secret):
|
||||
# type: (bytes) -> bool
|
||||
"""
|
||||
Hash the candidate secret and compare the result to the stored hashed
|
||||
secret.
|
||||
@ -292,10 +335,21 @@ class HashedLeaseInfo(proxyForInterface(ILeaseInfo, "_lease_info")): # type: ign
|
||||
|
||||
return super(HashedLeaseInfo, self).is_cancel_secret(hashed_candidate)
|
||||
|
||||
def present_cancel_secret(self):
|
||||
# type: () -> str
|
||||
"""
|
||||
Present the hash of the secret with a marker indicating it is a hash.
|
||||
"""
|
||||
return u"hash:" + super(HashedLeaseInfo, self).present_cancel_secret()
|
||||
|
||||
@property
|
||||
def owner_num(self):
|
||||
return self._lease_info.owner_num
|
||||
|
||||
@property
|
||||
def nodeid(self):
|
||||
return self._lease_info.nodeid
|
||||
|
||||
@property
|
||||
def cancel_secret(self):
|
||||
"""
|
||||
|
138
src/allmydata/storage/lease_schema.py
Normal file
138
src/allmydata/storage/lease_schema.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""
|
||||
Ported to Python 3.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2
|
||||
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
|
||||
|
||||
try:
|
||||
from typing import Union
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
import attr
|
||||
|
||||
from nacl.hash import blake2b
|
||||
from nacl.encoding import RawEncoder
|
||||
|
||||
from .lease import (
|
||||
LeaseInfo,
|
||||
HashedLeaseInfo,
|
||||
)
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class CleartextLeaseSerializer(object):
|
||||
"""
|
||||
Serialize and unserialize leases with cleartext secrets.
|
||||
"""
|
||||
_to_data = attr.ib()
|
||||
_from_data = attr.ib()
|
||||
|
||||
def serialize(self, lease):
|
||||
# type: (LeaseInfo) -> bytes
|
||||
"""
|
||||
Represent the given lease as bytes with cleartext secrets.
|
||||
"""
|
||||
if isinstance(lease, LeaseInfo):
|
||||
return self._to_data(lease)
|
||||
raise ValueError(
|
||||
"ShareFile v1 schema only supports LeaseInfo, not {!r}".format(
|
||||
lease,
|
||||
),
|
||||
)
|
||||
|
||||
def unserialize(self, data):
|
||||
# type: (bytes) -> LeaseInfo
|
||||
"""
|
||||
Load a lease with cleartext secrets from the given bytes representation.
|
||||
"""
|
||||
# In v1 of the immutable schema lease secrets are stored plaintext.
|
||||
# So load the data into a plain LeaseInfo which works on plaintext
|
||||
# secrets.
|
||||
return self._from_data(data)
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class HashedLeaseSerializer(object):
|
||||
_to_data = attr.ib()
|
||||
_from_data = attr.ib()
|
||||
|
||||
@classmethod
|
||||
def _hash_secret(cls, secret):
|
||||
# type: (bytes) -> bytes
|
||||
"""
|
||||
Hash a lease secret for storage.
|
||||
"""
|
||||
return blake2b(secret, digest_size=32, encoder=RawEncoder())
|
||||
|
||||
@classmethod
|
||||
def _hash_lease_info(cls, lease_info):
|
||||
# type: (LeaseInfo) -> HashedLeaseInfo
|
||||
"""
|
||||
Hash the cleartext lease info secrets into a ``HashedLeaseInfo``.
|
||||
"""
|
||||
if not isinstance(lease_info, LeaseInfo):
|
||||
# Provide a little safety against misuse, especially an attempt to
|
||||
# re-hash an already-hashed lease info which is represented as a
|
||||
# different type.
|
||||
raise TypeError(
|
||||
"Can only hash LeaseInfo, not {!r}".format(lease_info),
|
||||
)
|
||||
|
||||
# Hash the cleartext secrets in the lease info and wrap the result in
|
||||
# a new type.
|
||||
return HashedLeaseInfo(
|
||||
attr.assoc(
|
||||
lease_info,
|
||||
renew_secret=cls._hash_secret(lease_info.renew_secret),
|
||||
cancel_secret=cls._hash_secret(lease_info.cancel_secret),
|
||||
),
|
||||
cls._hash_secret,
|
||||
)
|
||||
|
||||
def serialize(self, lease):
|
||||
# type: (Union[LeaseInfo, HashedLeaseInfo]) -> bytes
|
||||
if isinstance(lease, LeaseInfo):
|
||||
# v2 of the immutable schema stores lease secrets hashed. If
|
||||
# we're given a LeaseInfo then it holds plaintext secrets. Hash
|
||||
# them before trying to serialize.
|
||||
lease = self._hash_lease_info(lease)
|
||||
if isinstance(lease, HashedLeaseInfo):
|
||||
return self._to_data(lease)
|
||||
raise ValueError(
|
||||
"ShareFile v2 schema cannot represent lease {!r}".format(
|
||||
lease,
|
||||
),
|
||||
)
|
||||
|
||||
def unserialize(self, data):
|
||||
# type: (bytes) -> HashedLeaseInfo
|
||||
# In v2 of the immutable schema lease secrets are stored hashed. Wrap
|
||||
# a LeaseInfo in a HashedLeaseInfo so it can supply the correct
|
||||
# interpretation for those values.
|
||||
return HashedLeaseInfo(self._from_data(data), self._hash_secret)
|
||||
|
||||
v1_immutable = CleartextLeaseSerializer(
|
||||
LeaseInfo.to_immutable_data,
|
||||
LeaseInfo.from_immutable_data,
|
||||
)
|
||||
|
||||
v2_immutable = HashedLeaseSerializer(
|
||||
HashedLeaseInfo.to_immutable_data,
|
||||
LeaseInfo.from_immutable_data,
|
||||
)
|
||||
|
||||
v1_mutable = CleartextLeaseSerializer(
|
||||
LeaseInfo.to_mutable_data,
|
||||
LeaseInfo.from_mutable_data,
|
||||
)
|
||||
|
||||
v2_mutable = HashedLeaseSerializer(
|
||||
HashedLeaseInfo.to_mutable_data,
|
||||
LeaseInfo.from_mutable_data,
|
||||
)
|
@ -24,7 +24,10 @@ from allmydata.storage.lease import LeaseInfo
|
||||
from allmydata.storage.common import UnknownMutableContainerVersionError, \
|
||||
DataTooLargeError
|
||||
from allmydata.mutable.layout import MAX_MUTABLE_SHARE_SIZE
|
||||
|
||||
from .mutable_schema import (
|
||||
NEWEST_SCHEMA_VERSION,
|
||||
schema_from_header,
|
||||
)
|
||||
|
||||
# the MutableShareFile is like the ShareFile, but used for mutable data. It
|
||||
# has a different layout. See docs/mutable.txt for more details.
|
||||
@ -64,9 +67,6 @@ class MutableShareFile(object):
|
||||
# our sharefiles share with a recognizable string, plus some random
|
||||
# binary data to reduce the chance that a regular text file will look
|
||||
# like a sharefile.
|
||||
MAGIC = b"Tahoe mutable container v1\n" + b"\x75\x09\x44\x03\x8e"
|
||||
assert len(MAGIC) == 32
|
||||
assert isinstance(MAGIC, bytes)
|
||||
MAX_SIZE = MAX_MUTABLE_SHARE_SIZE
|
||||
# TODO: decide upon a policy for max share size
|
||||
|
||||
@ -82,20 +82,19 @@ class MutableShareFile(object):
|
||||
:return: ``True`` if the bytes could belong to this container,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
return header.startswith(cls.MAGIC)
|
||||
return schema_from_header(header) is not None
|
||||
|
||||
def __init__(self, filename, parent=None):
|
||||
def __init__(self, filename, parent=None, schema=NEWEST_SCHEMA_VERSION):
|
||||
self.home = filename
|
||||
if os.path.exists(self.home):
|
||||
# we don't cache anything, just check the magic
|
||||
with open(self.home, 'rb') as f:
|
||||
data = f.read(self.HEADER_SIZE)
|
||||
(magic,
|
||||
write_enabler_nodeid, write_enabler,
|
||||
data_length, extra_least_offset) = \
|
||||
struct.unpack(">32s20s32sQQ", data)
|
||||
if not self.is_valid_header(data):
|
||||
raise UnknownMutableContainerVersionError(filename, magic)
|
||||
header = f.read(self.HEADER_SIZE)
|
||||
self._schema = schema_from_header(header)
|
||||
if self._schema is None:
|
||||
raise UnknownMutableContainerVersionError(filename, header)
|
||||
else:
|
||||
self._schema = schema
|
||||
self.parent = parent # for logging
|
||||
|
||||
def log(self, *args, **kwargs):
|
||||
@ -103,23 +102,8 @@ class MutableShareFile(object):
|
||||
|
||||
def create(self, my_nodeid, write_enabler):
|
||||
assert not os.path.exists(self.home)
|
||||
data_length = 0
|
||||
extra_lease_offset = (self.HEADER_SIZE
|
||||
+ 4 * self.LEASE_SIZE
|
||||
+ data_length)
|
||||
assert extra_lease_offset == self.DATA_OFFSET # true at creation
|
||||
num_extra_leases = 0
|
||||
with open(self.home, 'wb') as f:
|
||||
header = struct.pack(
|
||||
">32s20s32sQQ",
|
||||
self.MAGIC, my_nodeid, write_enabler,
|
||||
data_length, extra_lease_offset,
|
||||
)
|
||||
leases = (b"\x00" * self.LEASE_SIZE) * 4
|
||||
f.write(header + leases)
|
||||
# data goes here, empty after creation
|
||||
f.write(struct.pack(">L", num_extra_leases))
|
||||
# extra leases go here, none at creation
|
||||
f.write(self._schema.header(my_nodeid, write_enabler))
|
||||
|
||||
def unlink(self):
|
||||
os.unlink(self.home)
|
||||
@ -252,7 +236,7 @@ class MutableShareFile(object):
|
||||
+ (lease_number-4)*self.LEASE_SIZE)
|
||||
f.seek(offset)
|
||||
assert f.tell() == offset
|
||||
f.write(lease_info.to_mutable_data())
|
||||
f.write(self._schema.lease_serializer.serialize(lease_info))
|
||||
|
||||
def _read_lease_record(self, f, lease_number):
|
||||
# returns a LeaseInfo instance, or None
|
||||
@ -269,7 +253,7 @@ class MutableShareFile(object):
|
||||
f.seek(offset)
|
||||
assert f.tell() == offset
|
||||
data = f.read(self.LEASE_SIZE)
|
||||
lease_info = LeaseInfo.from_mutable_data(data)
|
||||
lease_info = self._schema.lease_serializer.unserialize(data)
|
||||
if lease_info.owner_num == 0:
|
||||
return None
|
||||
return lease_info
|
||||
|
144
src/allmydata/storage/mutable_schema.py
Normal file
144
src/allmydata/storage/mutable_schema.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""
|
||||
Ported to Python 3.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from future.utils import PY2
|
||||
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 struct
|
||||
|
||||
import attr
|
||||
|
||||
from ..util.hashutil import (
|
||||
tagged_hash,
|
||||
)
|
||||
from .lease import (
|
||||
LeaseInfo,
|
||||
)
|
||||
from .lease_schema import (
|
||||
v1_mutable,
|
||||
v2_mutable,
|
||||
)
|
||||
|
||||
def _magic(version):
|
||||
# type: (int) -> bytes
|
||||
"""
|
||||
Compute a "magic" header string for a container of the given version.
|
||||
|
||||
:param version: The version number of the container.
|
||||
"""
|
||||
# Make it easy for people to recognize
|
||||
human_readable = u"Tahoe mutable container v{:d}\n".format(version).encode("ascii")
|
||||
# But also keep the chance of accidental collision low
|
||||
if version == 1:
|
||||
# It's unclear where this byte sequence came from. It may have just
|
||||
# been random. In any case, preserve it since it is the magic marker
|
||||
# in all v1 share files.
|
||||
random_bytes = b"\x75\x09\x44\x03\x8e"
|
||||
else:
|
||||
# For future versions, use a reproducable scheme.
|
||||
random_bytes = tagged_hash(
|
||||
b"allmydata_mutable_container_header",
|
||||
human_readable,
|
||||
truncate_to=5,
|
||||
)
|
||||
magic = human_readable + random_bytes
|
||||
assert len(magic) == 32
|
||||
if version > 1:
|
||||
# The chance of collision is pretty low but let's just be sure about
|
||||
# it.
|
||||
assert magic != _magic(version - 1)
|
||||
|
||||
return magic
|
||||
|
||||
def _header(magic, extra_lease_offset, nodeid, write_enabler):
|
||||
# type: (bytes, int, bytes, bytes) -> bytes
|
||||
"""
|
||||
Construct a container header.
|
||||
|
||||
:param nodeid: A unique identifier for the node holding this
|
||||
container.
|
||||
|
||||
:param write_enabler: A secret shared with the client used to
|
||||
authorize changes to the contents of this container.
|
||||
"""
|
||||
fixed_header = struct.pack(
|
||||
">32s20s32sQQ",
|
||||
magic,
|
||||
nodeid,
|
||||
write_enabler,
|
||||
# data length, initially the container is empty
|
||||
0,
|
||||
extra_lease_offset,
|
||||
)
|
||||
blank_leases = b"\x00" * LeaseInfo().mutable_size() * 4
|
||||
extra_lease_count = struct.pack(">L", 0)
|
||||
|
||||
return b"".join([
|
||||
fixed_header,
|
||||
# share data will go in between the next two items eventually but
|
||||
# for now there is none.
|
||||
blank_leases,
|
||||
extra_lease_count,
|
||||
])
|
||||
|
||||
|
||||
_HEADER_FORMAT = ">32s20s32sQQ"
|
||||
|
||||
# This size excludes leases
|
||||
_HEADER_SIZE = struct.calcsize(_HEADER_FORMAT)
|
||||
|
||||
_EXTRA_LEASE_OFFSET = _HEADER_SIZE + 4 * LeaseInfo().mutable_size()
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class _Schema(object):
|
||||
"""
|
||||
Implement encoding and decoding for the mutable container.
|
||||
|
||||
:ivar int version: the version number of the schema this object supports
|
||||
|
||||
:ivar lease_serializer: an object that is responsible for lease
|
||||
serialization and unserialization
|
||||
"""
|
||||
version = attr.ib()
|
||||
lease_serializer = attr.ib()
|
||||
_magic = attr.ib()
|
||||
|
||||
@classmethod
|
||||
def for_version(cls, version, lease_serializer):
|
||||
return cls(version, lease_serializer, magic=_magic(version))
|
||||
|
||||
def magic_matches(self, candidate_magic):
|
||||
# type: (bytes) -> bool
|
||||
"""
|
||||
Return ``True`` if a candidate string matches the expected magic string
|
||||
from a mutable container header, ``False`` otherwise.
|
||||
"""
|
||||
return candidate_magic[:len(self._magic)] == self._magic
|
||||
|
||||
def header(self, nodeid, write_enabler):
|
||||
return _header(self._magic, _EXTRA_LEASE_OFFSET, nodeid, write_enabler)
|
||||
|
||||
ALL_SCHEMAS = {
|
||||
_Schema.for_version(version=2, lease_serializer=v2_mutable),
|
||||
_Schema.for_version(version=1, lease_serializer=v1_mutable),
|
||||
}
|
||||
ALL_SCHEMA_VERSIONS = {schema.version for schema in ALL_SCHEMAS}
|
||||
NEWEST_SCHEMA_VERSION = max(ALL_SCHEMAS, key=lambda schema: schema.version)
|
||||
|
||||
def schema_from_header(header):
|
||||
# (int) -> Optional[type]
|
||||
"""
|
||||
Find the schema object that corresponds to a certain version number.
|
||||
"""
|
||||
for schema in ALL_SCHEMAS:
|
||||
if schema.magic_matches(header):
|
||||
return schema
|
||||
return None
|
@ -42,9 +42,12 @@ from allmydata.util import fileutil, hashutil, base32
|
||||
from allmydata.storage.server import StorageServer, DEFAULT_RENEWAL_TIME
|
||||
from allmydata.storage.shares import get_share_file
|
||||
from allmydata.storage.mutable import MutableShareFile
|
||||
from allmydata.storage.mutable_schema import (
|
||||
ALL_SCHEMAS as ALL_MUTABLE_SCHEMAS,
|
||||
)
|
||||
from allmydata.storage.immutable import BucketWriter, BucketReader, ShareFile
|
||||
from allmydata.storage.immutable_schema import (
|
||||
ALL_SCHEMAS,
|
||||
ALL_SCHEMAS as ALL_IMMUTABLE_SCHEMAS,
|
||||
)
|
||||
from allmydata.storage.common import storage_index_to_dir, \
|
||||
UnknownMutableContainerVersionError, UnknownImmutableContainerVersionError, \
|
||||
@ -1361,14 +1364,25 @@ class MutableServer(unittest.TestCase):
|
||||
2: [b"2"*10]})
|
||||
|
||||
def compare_leases_without_timestamps(self, leases_a, leases_b):
|
||||
self.failUnlessEqual(len(leases_a), len(leases_b))
|
||||
for i in range(len(leases_a)):
|
||||
a = leases_a[i]
|
||||
b = leases_b[i]
|
||||
self.failUnlessEqual(a.owner_num, b.owner_num)
|
||||
self.failUnlessEqual(a.renew_secret, b.renew_secret)
|
||||
self.failUnlessEqual(a.cancel_secret, b.cancel_secret)
|
||||
self.failUnlessEqual(a.nodeid, b.nodeid)
|
||||
"""
|
||||
Assert that, except for expiration times, ``leases_a`` contains the same
|
||||
lease information as ``leases_b``.
|
||||
"""
|
||||
for a, b in zip(leases_a, leases_b):
|
||||
# The leases aren't always of the same type (though of course
|
||||
# corresponding elements in the two lists should be of the same
|
||||
# type as each other) so it's inconvenient to just reach in and
|
||||
# normalize the expiration timestamp. We don't want to call
|
||||
# `renew` on both objects to normalize the expiration timestamp in
|
||||
# case `renew` is broken and gives us back equal outputs from
|
||||
# non-equal inputs (expiration timestamp aside). It seems
|
||||
# reasonably safe to use `renew` to make _one_ of the timestamps
|
||||
# equal to the other though.
|
||||
self.assertEqual(
|
||||
a.renew(b.get_expiration_time()),
|
||||
b,
|
||||
)
|
||||
self.assertEqual(len(leases_a), len(leases_b))
|
||||
|
||||
def test_leases(self):
|
||||
ss = self.create("test_leases")
|
||||
@ -3134,7 +3148,7 @@ class Stats(unittest.TestCase):
|
||||
self.failUnless(output["get"]["99_0_percentile"] is None, output)
|
||||
self.failUnless(output["get"]["99_9_percentile"] is None, output)
|
||||
|
||||
immutable_schemas = strategies.sampled_from(list(ALL_SCHEMAS))
|
||||
immutable_schemas = strategies.sampled_from(list(ALL_IMMUTABLE_SCHEMAS))
|
||||
|
||||
class ShareFileTests(unittest.TestCase):
|
||||
"""Tests for allmydata.storage.immutable.ShareFile."""
|
||||
@ -3273,15 +3287,17 @@ class ShareFileTests(unittest.TestCase):
|
||||
(loaded_lease,) = sf.get_leases()
|
||||
self.assertTrue(loaded_lease.is_cancel_secret(cancel_secret))
|
||||
|
||||
mutable_schemas = strategies.sampled_from(list(ALL_MUTABLE_SCHEMAS))
|
||||
|
||||
class MutableShareFileTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for allmydata.storage.mutable.MutableShareFile.
|
||||
"""
|
||||
def get_sharefile(self):
|
||||
return MutableShareFile(self.mktemp())
|
||||
def get_sharefile(self, **kwargs):
|
||||
return MutableShareFile(self.mktemp(), **kwargs)
|
||||
|
||||
@given(
|
||||
schema=mutable_schemas,
|
||||
nodeid=strategies.just(b"x" * 20),
|
||||
write_enabler=strategies.just(b"y" * 32),
|
||||
datav=strategies.lists(
|
||||
@ -3292,12 +3308,12 @@ class MutableShareFileTests(unittest.TestCase):
|
||||
),
|
||||
new_length=offsets(),
|
||||
)
|
||||
def test_readv_reads_share_data(self, nodeid, write_enabler, datav, new_length):
|
||||
def test_readv_reads_share_data(self, schema, nodeid, write_enabler, datav, new_length):
|
||||
"""
|
||||
``MutableShareFile.readv`` returns bytes from the share data portion
|
||||
of the share file.
|
||||
"""
|
||||
sf = self.get_sharefile()
|
||||
sf = self.get_sharefile(schema=schema)
|
||||
sf.create(my_nodeid=nodeid, write_enabler=write_enabler)
|
||||
sf.writev(datav=datav, new_length=new_length)
|
||||
|
||||
@ -3332,12 +3348,13 @@ class MutableShareFileTests(unittest.TestCase):
|
||||
self.assertEqual(expected_data, read_data)
|
||||
|
||||
@given(
|
||||
schema=mutable_schemas,
|
||||
nodeid=strategies.just(b"x" * 20),
|
||||
write_enabler=strategies.just(b"y" * 32),
|
||||
readv=strategies.lists(strategies.tuples(offsets(), lengths()), min_size=1),
|
||||
random=strategies.randoms(),
|
||||
)
|
||||
def test_readv_rejects_negative_length(self, nodeid, write_enabler, readv, random):
|
||||
def test_readv_rejects_negative_length(self, schema, nodeid, write_enabler, readv, random):
|
||||
"""
|
||||
If a negative length is given to ``MutableShareFile.readv`` in a read
|
||||
vector then ``AssertionError`` is raised.
|
||||
@ -3376,7 +3393,7 @@ class MutableShareFileTests(unittest.TestCase):
|
||||
*broken_readv[readv_index]
|
||||
)
|
||||
|
||||
sf = self.get_sharefile()
|
||||
sf = self.get_sharefile(schema=schema)
|
||||
sf.create(my_nodeid=nodeid, write_enabler=write_enabler)
|
||||
|
||||
# A read with a broken read vector is an error.
|
||||
|
Loading…
x
Reference in New Issue
Block a user