mirror of
https://github.com/tahoe-lafs/tahoe-lafs.git
synced 2025-01-02 03:06:41 +00:00
update code/test to use new ed25512 module
This commit is contained in:
parent
3a5a0fb572
commit
9e31bfe2f4
@ -12,14 +12,14 @@ from twisted.python.failure import Failure
|
|||||||
from pycryptopp.publickey import rsa
|
from pycryptopp.publickey import rsa
|
||||||
|
|
||||||
import allmydata
|
import allmydata
|
||||||
|
from allmydata.crypto.ed25519 import SigningKey
|
||||||
from allmydata.storage.server import StorageServer
|
from allmydata.storage.server import StorageServer
|
||||||
from allmydata import storage_client
|
from allmydata import storage_client
|
||||||
from allmydata.immutable.upload import Uploader
|
from allmydata.immutable.upload import Uploader
|
||||||
from allmydata.immutable.offloaded import Helper
|
from allmydata.immutable.offloaded import Helper
|
||||||
from allmydata.control import ControlServer
|
from allmydata.control import ControlServer
|
||||||
from allmydata.introducer.client import IntroducerClient
|
from allmydata.introducer.client import IntroducerClient
|
||||||
from allmydata.util import (hashutil, base32, pollmixin, log, keyutil, idlib,
|
from allmydata.util import (hashutil, base32, pollmixin, log, idlib, yamlutil)
|
||||||
yamlutil)
|
|
||||||
from allmydata.util.encodingutil import (get_filesystem_encoding,
|
from allmydata.util.encodingutil import (get_filesystem_encoding,
|
||||||
from_utf8_or_none)
|
from_utf8_or_none)
|
||||||
from allmydata.util.abbreviate import parse_abbreviated_size
|
from allmydata.util.abbreviate import parse_abbreviated_size
|
||||||
@ -480,12 +480,14 @@ class _Client(node.Node, pollmixin.PollMixin):
|
|||||||
# we only create the key once. On all subsequent runs, we re-use the
|
# we only create the key once. On all subsequent runs, we re-use the
|
||||||
# existing key
|
# existing key
|
||||||
def _make_key():
|
def _make_key():
|
||||||
sk_vs,vk_vs = keyutil.make_keypair()
|
priv_key = SigningKey.generate()
|
||||||
return sk_vs+"\n"
|
return priv_key.encoded_key() + "\n"
|
||||||
sk_vs = self.config.get_or_create_private_config("node.privkey", _make_key)
|
|
||||||
sk,vk_vs = keyutil.parse_privkey(sk_vs.strip())
|
priv_key_str = self.config.get_or_create_private_config("node.privkey", _make_key)
|
||||||
self.config.write_config_file("node.pubkey", vk_vs+"\n")
|
priv_key = SigningKey.parse_encoded_key(priv_key_str)
|
||||||
self._node_key = sk
|
pub_key_str = priv_key.public_key().encoded_key()
|
||||||
|
self.config.write_config_file("node.pubkey", pub_key_str + "\n")
|
||||||
|
self._node_key = priv_key
|
||||||
|
|
||||||
def get_long_nodeid(self):
|
def get_long_nodeid(self):
|
||||||
# this matches what IServer.get_longname() says about us elsewhere
|
# this matches what IServer.get_longname() says about us elsewhere
|
||||||
|
@ -1 +1,8 @@
|
|||||||
|
class BadPrefixError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def remove_prefix(s_bytes, prefix):
|
||||||
|
if not s_bytes.startswith(prefix):
|
||||||
|
raise BadPrefixError("did not see expected '%s' prefix" % (prefix,))
|
||||||
|
return s_bytes[len(prefix):]
|
||||||
|
@ -28,22 +28,13 @@ from cryptography.exceptions import InvalidSignature
|
|||||||
BadSignatureError = InvalidSignature
|
BadSignatureError = InvalidSignature
|
||||||
del InvalidSignature
|
del InvalidSignature
|
||||||
|
|
||||||
|
from allmydata.crypto import remove_prefix
|
||||||
from allmydata.util.base32 import a2b, b2a
|
from allmydata.util.base32 import a2b, b2a
|
||||||
|
|
||||||
_PRIV_PREFIX = 'priv-v0-'
|
_PRIV_PREFIX = 'priv-v0-'
|
||||||
_PUB_PREFIX = 'pub-v0-'
|
_PUB_PREFIX = 'pub-v0-'
|
||||||
|
|
||||||
|
|
||||||
class BadPrefixError(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def _remove_prefix(s_bytes, prefix):
|
|
||||||
if not s_bytes.startswith(prefix):
|
|
||||||
raise BadPrefixError("did not see expected '%s' prefix" % (prefix,))
|
|
||||||
return s_bytes[len(prefix):]
|
|
||||||
|
|
||||||
|
|
||||||
class SigningKey:
|
class SigningKey:
|
||||||
|
|
||||||
def __init__(self, priv_key):
|
def __init__(self, priv_key):
|
||||||
@ -79,11 +70,11 @@ class SigningKey:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def parse_encoded_key(cls, priv_str):
|
def parse_encoded_key(cls, priv_str):
|
||||||
global _PRIV_PREFIX
|
global _PRIV_PREFIX
|
||||||
return cls.from_private_bytes(a2b(_remove_prefix(priv_str, _PRIV_PREFIX)))
|
return cls.from_private_bytes(a2b(remove_prefix(priv_str, _PRIV_PREFIX)))
|
||||||
|
|
||||||
def encoded_key(self):
|
def encoded_key(self):
|
||||||
global _PRIV_PREFIX
|
global _PRIV_PREFIX
|
||||||
return _PRIV_PREFIX + a2b(self.private_bytes())
|
return _PRIV_PREFIX + b2a(self.private_bytes())
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, type(self)):
|
if isinstance(other, type(self)):
|
||||||
@ -123,11 +114,11 @@ class VerifyingKey:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def parse_encoded_key(cls, pub_str):
|
def parse_encoded_key(cls, pub_str):
|
||||||
global _PUB_PREFIX
|
global _PUB_PREFIX
|
||||||
return cls.from_public_bytes(a2b(_remove_prefix(pub_str, _PUB_PREFIX)))
|
return cls.from_public_bytes(a2b(remove_prefix(pub_str, _PUB_PREFIX)))
|
||||||
|
|
||||||
def encoded_key(self):
|
def encoded_key(self):
|
||||||
global _PUB_PREFIX
|
global _PUB_PREFIX
|
||||||
return _PUB_PREFIX + a2b(self.public_bytes())
|
return _PUB_PREFIX + b2a(self.public_bytes())
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if isinstance(other, type(self)):
|
if isinstance(other, type(self)):
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import time
|
import time
|
||||||
from zope.interface import implementer
|
from zope.interface import implementer
|
||||||
from twisted.application import service
|
from twisted.application import service
|
||||||
@ -10,7 +9,7 @@ from allmydata.introducer.common import sign_to_foolscap, unsign_from_foolscap,\
|
|||||||
get_tubid_string_from_ann
|
get_tubid_string_from_ann
|
||||||
from allmydata.util import log, yamlutil, connection_status
|
from allmydata.util import log, yamlutil, connection_status
|
||||||
from allmydata.util.rrefutil import add_version_to_remote_reference
|
from allmydata.util.rrefutil import add_version_to_remote_reference
|
||||||
from allmydata.util.keyutil import BadSignatureError
|
from allmydata.crypto.ed25519 import BadSignatureError
|
||||||
from allmydata.util.assertutil import precondition
|
from allmydata.util.assertutil import precondition
|
||||||
|
|
||||||
class InvalidCacheError(Exception):
|
class InvalidCacheError(Exception):
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
from allmydata.util import keyutil, base32, rrefutil
|
from allmydata import crypto
|
||||||
|
from allmydata.crypto.ed25519 import VerifyingKey
|
||||||
|
from allmydata.util import base32, rrefutil
|
||||||
|
|
||||||
|
|
||||||
def get_tubid_string_from_ann(ann):
|
def get_tubid_string_from_ann(ann):
|
||||||
return get_tubid_string(str(ann.get("anonymous-storage-FURL")
|
return get_tubid_string(str(ann.get("anonymous-storage-FURL")
|
||||||
@ -23,9 +25,11 @@ def sign_to_foolscap(ann, sk):
|
|||||||
ann_t = (msg, sig, "v0-"+base32.b2a(vk_bytes))
|
ann_t = (msg, sig, "v0-"+base32.b2a(vk_bytes))
|
||||||
return ann_t
|
return ann_t
|
||||||
|
|
||||||
|
|
||||||
class UnknownKeyError(Exception):
|
class UnknownKeyError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def unsign_from_foolscap(ann_t):
|
def unsign_from_foolscap(ann_t):
|
||||||
(msg, sig_vs, claimed_key_vs) = ann_t
|
(msg, sig_vs, claimed_key_vs) = ann_t
|
||||||
if not sig_vs or not claimed_key_vs:
|
if not sig_vs or not claimed_key_vs:
|
||||||
@ -34,8 +38,9 @@ def unsign_from_foolscap(ann_t):
|
|||||||
raise UnknownKeyError("only v0- signatures recognized")
|
raise UnknownKeyError("only v0- signatures recognized")
|
||||||
if not claimed_key_vs.startswith("v0-"):
|
if not claimed_key_vs.startswith("v0-"):
|
||||||
raise UnknownKeyError("only v0- keys recognized")
|
raise UnknownKeyError("only v0- keys recognized")
|
||||||
claimed_key = keyutil.parse_pubkey("pub-"+claimed_key_vs)
|
|
||||||
sig_bytes = base32.a2b(keyutil.remove_prefix(sig_vs, "v0-"))
|
claimed_key = VerifyingKey.parse_encoded_key("pub-" + claimed_key_vs)
|
||||||
|
sig_bytes = base32.a2b(crypto.remove_prefix(sig_vs, "v0-"))
|
||||||
claimed_key.verify(sig_bytes, msg)
|
claimed_key.verify(sig_bytes, msg)
|
||||||
key_vs = claimed_key_vs
|
key_vs = claimed_key_vs
|
||||||
ann = json.loads(msg.decode("utf-8"))
|
ann = json.loads(msg.decode("utf-8"))
|
||||||
|
@ -14,11 +14,11 @@ Generate a public/private keypair, dumped to stdout as two lines of ASCII..
|
|||||||
return t
|
return t
|
||||||
|
|
||||||
def print_keypair(options):
|
def print_keypair(options):
|
||||||
from allmydata.util.keyutil import make_keypair
|
from allmydata.crypto.ed25519 import SigningKey
|
||||||
out = options.stdout
|
out = options.stdout
|
||||||
privkey_vs, pubkey_vs = make_keypair()
|
priv_key = SigningKey.generate()
|
||||||
print("private:", privkey_vs, file=out)
|
print("private:", priv_key.encoded_key(), file=out)
|
||||||
print("public:", pubkey_vs, file=out)
|
print("public:", priv_key.public_key().encoded_key(), file=out)
|
||||||
|
|
||||||
class DerivePubkeyOptions(BaseOptions):
|
class DerivePubkeyOptions(BaseOptions):
|
||||||
def parseArgs(self, privkey):
|
def parseArgs(self, privkey):
|
||||||
@ -38,11 +38,11 @@ generate-keypair, derive the public key and print it to stdout.
|
|||||||
|
|
||||||
def derive_pubkey(options):
|
def derive_pubkey(options):
|
||||||
out = options.stdout
|
out = options.stdout
|
||||||
from allmydata.util import keyutil
|
from allmydata.crypto.ed25519 import SigningKey
|
||||||
privkey_vs = options.privkey
|
privkey_vs = options.privkey
|
||||||
sk, pubkey_vs = keyutil.parse_privkey(privkey_vs)
|
priv_key = SigningKey.parse_encoded_key(privkey_vs)
|
||||||
print("private:", privkey_vs, file=out)
|
print("private:", priv_key.encoded_key(), file=out)
|
||||||
print("public:", pubkey_vs, file=out)
|
print("public:", priv_key.public_key().encoded_key(), file=out)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
class AdminCommand(BaseOptions):
|
class AdminCommand(BaseOptions):
|
||||||
|
@ -10,8 +10,9 @@ from twisted.internet import task
|
|||||||
from twisted.python.filepath import FilePath
|
from twisted.python.filepath import FilePath
|
||||||
|
|
||||||
import allmydata
|
import allmydata
|
||||||
|
from allmydata import crypto
|
||||||
from allmydata.crypto import ed25519
|
from allmydata.crypto import ed25519
|
||||||
from allmydata.util import fileutil, hashutil, base32, keyutil
|
from allmydata.util import fileutil, hashutil, base32
|
||||||
from allmydata.util.namespace import Namespace
|
from allmydata.util.namespace import Namespace
|
||||||
from allmydata import uri
|
from allmydata import uri
|
||||||
from allmydata.immutable import upload
|
from allmydata.immutable import upload
|
||||||
@ -34,10 +35,10 @@ from allmydata.scripts.common import DEFAULT_ALIAS, get_aliases, get_alias, \
|
|||||||
DefaultAliasMarker
|
DefaultAliasMarker
|
||||||
|
|
||||||
from allmydata.scripts import cli, debug, runner
|
from allmydata.scripts import cli, debug, runner
|
||||||
from ..common_util import (ReallyEqualMixin, skip_if_cannot_represent_filename,
|
from allmydata.test.common_util import (ReallyEqualMixin, skip_if_cannot_represent_filename,
|
||||||
run_cli)
|
run_cli)
|
||||||
from ..no_network import GridTestMixin
|
from allmydata.test.no_network import GridTestMixin
|
||||||
from .common import CLITestMixin, parse_options
|
from allmydata.test.cli.common import CLITestMixin, parse_options
|
||||||
from twisted.python import usage
|
from twisted.python import usage
|
||||||
|
|
||||||
from allmydata.util.encodingutil import listdir_unicode, get_io_encoding
|
from allmydata.util.encodingutil import listdir_unicode, get_io_encoding
|
||||||
@ -733,16 +734,18 @@ class Admin(unittest.TestCase):
|
|||||||
self.failUnlessEqual(pubkey_bits[0], vk_header, lines[1])
|
self.failUnlessEqual(pubkey_bits[0], vk_header, lines[1])
|
||||||
self.failUnless(privkey_bits[1].startswith("priv-v0-"), lines[0])
|
self.failUnless(privkey_bits[1].startswith("priv-v0-"), lines[0])
|
||||||
self.failUnless(pubkey_bits[1].startswith("pub-v0-"), lines[1])
|
self.failUnless(pubkey_bits[1].startswith("pub-v0-"), lines[1])
|
||||||
sk_bytes = base32.a2b(keyutil.remove_prefix(privkey_bits[1], "priv-v0-"))
|
sk_bytes = base32.a2b(crypto.remove_prefix(privkey_bits[1], "priv-v0-"))
|
||||||
sk = ed25519.SigningKey.from_private_bytes(sk_bytes)
|
sk = ed25519.SigningKey.from_private_bytes(sk_bytes)
|
||||||
vk_bytes = base32.a2b(keyutil.remove_prefix(pubkey_bits[1], "pub-v0-"))
|
vk_bytes = base32.a2b(crypto.remove_prefix(pubkey_bits[1], "pub-v0-"))
|
||||||
self.failUnlessEqual(sk.public_key().public_bytes(), vk_bytes)
|
self.failUnlessEqual(sk.public_key().public_bytes(), vk_bytes)
|
||||||
d.addCallback(_done)
|
d.addCallback(_done)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def test_derive_pubkey(self):
|
def test_derive_pubkey(self):
|
||||||
priv1,pub1 = keyutil.make_keypair()
|
priv_key = ed25519.SigningKey.generate()
|
||||||
d = run_cli("admin", "derive-pubkey", priv1)
|
priv_key_str = priv_key.encoded_key()
|
||||||
|
pub_key_str = priv_key.public_key().encoded_key()
|
||||||
|
d = run_cli("admin", "derive-pubkey", priv_key_str)
|
||||||
def _done(args):
|
def _done(args):
|
||||||
(rc, stdout, stderr) = args
|
(rc, stdout, stderr) = args
|
||||||
lines = stdout.split("\n")
|
lines = stdout.split("\n")
|
||||||
@ -752,8 +755,8 @@ class Admin(unittest.TestCase):
|
|||||||
vk_header = "public: pub-v0-"
|
vk_header = "public: pub-v0-"
|
||||||
self.failUnless(privkey_line.startswith(sk_header), privkey_line)
|
self.failUnless(privkey_line.startswith(sk_header), privkey_line)
|
||||||
self.failUnless(pubkey_line.startswith(vk_header), pubkey_line)
|
self.failUnless(pubkey_line.startswith(vk_header), pubkey_line)
|
||||||
pub2 = pubkey_line[len(vk_header):]
|
pub_key_str2 = pubkey_line[len(vk_header):]
|
||||||
self.failUnlessEqual("pub-v0-"+pub2, pub1)
|
self.failUnlessEqual("pub-v0-" + pub_key_str2, pub_key_str)
|
||||||
d.addCallback(_done)
|
d.addCallback(_done)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@ -174,3 +174,31 @@ class TestRegression(unittest.TestCase):
|
|||||||
self.failUnlessEqual(new_sig, sig)
|
self.failUnlessEqual(new_sig, sig)
|
||||||
|
|
||||||
pub_key.verify(new_sig, test_data)
|
pub_key.verify(new_sig, test_data)
|
||||||
|
|
||||||
|
|
||||||
|
class TestEd25519(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_keys(self):
|
||||||
|
priv_key = SigningKey.generate()
|
||||||
|
priv_key_str = priv_key.encoded_key()
|
||||||
|
|
||||||
|
self.assertIsInstance(priv_key_str, six.string_types)
|
||||||
|
self.assertIsInstance(priv_key.private_bytes(), six.binary_type)
|
||||||
|
|
||||||
|
priv_key2 = SigningKey.parse_encoded_key(priv_key_str)
|
||||||
|
|
||||||
|
self.failUnlessEqual(priv_key, priv_key2)
|
||||||
|
|
||||||
|
pub_key = priv_key.public_key()
|
||||||
|
pub_key2 = priv_key2.public_key()
|
||||||
|
|
||||||
|
self.failUnlessEqual(pub_key, pub_key2)
|
||||||
|
|
||||||
|
pub_key_str = pub_key.encoded_key()
|
||||||
|
|
||||||
|
self.assertIsInstance(pub_key_str, six.string_types)
|
||||||
|
self.assertIsInstance(pub_key.public_bytes(), six.binary_type)
|
||||||
|
|
||||||
|
pub_key2 = VerifyingKey.parse_encoded_key(pub_key_str)
|
||||||
|
|
||||||
|
self.failUnlessEqual(pub_key, pub_key2)
|
||||||
|
@ -14,6 +14,9 @@ from twisted.python.filepath import FilePath
|
|||||||
|
|
||||||
from foolscap.api import Tub, Referenceable, fireEventually, flushEventualQueue
|
from foolscap.api import Tub, Referenceable, fireEventually, flushEventualQueue
|
||||||
from twisted.application import service
|
from twisted.application import service
|
||||||
|
from allmydata import crypto
|
||||||
|
from allmydata.crypto import ed25519
|
||||||
|
from allmydata.crypto.ed25519 import SigningKey
|
||||||
from allmydata.interfaces import InsufficientVersionError
|
from allmydata.interfaces import InsufficientVersionError
|
||||||
from allmydata.introducer.client import IntroducerClient
|
from allmydata.introducer.client import IntroducerClient
|
||||||
from allmydata.introducer.server import IntroducerService, FurlFileConflictError
|
from allmydata.introducer.server import IntroducerService, FurlFileConflictError
|
||||||
@ -31,12 +34,12 @@ from allmydata.client import (
|
|||||||
create_client,
|
create_client,
|
||||||
create_introducer_clients,
|
create_introducer_clients,
|
||||||
)
|
)
|
||||||
from allmydata.util import pollmixin, keyutil, idlib, fileutil, yamlutil
|
from allmydata.util import pollmixin, idlib, fileutil, yamlutil
|
||||||
from allmydata.util.iputil import (
|
from allmydata.util.iputil import (
|
||||||
listenOnUnused,
|
listenOnUnused,
|
||||||
)
|
)
|
||||||
import allmydata.test.common_util as testutil
|
import allmydata.test.common_util as testutil
|
||||||
from .common import (
|
from allmydata.test.common import (
|
||||||
SyncTestCase,
|
SyncTestCase,
|
||||||
AsyncTestCase,
|
AsyncTestCase,
|
||||||
AsyncBrokenTestCase,
|
AsyncBrokenTestCase,
|
||||||
@ -200,21 +203,21 @@ class Client(AsyncTestCase):
|
|||||||
furl1a = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:7777/gydnp"
|
furl1a = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:7777/gydnp"
|
||||||
furl2 = "pb://ttwwooyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/ttwwoo"
|
furl2 = "pb://ttwwooyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/ttwwoo"
|
||||||
|
|
||||||
privkey_s, pubkey_vs = keyutil.make_keypair()
|
priv_key = SigningKey.generate()
|
||||||
privkey, _ignored = keyutil.parse_privkey(privkey_s)
|
pub_key_str = priv_key.public_key().encoded_key()
|
||||||
pubkey_s = keyutil.remove_prefix(pubkey_vs, "pub-")
|
pubkey_s = crypto.remove_prefix(pub_key_str, "pub-")
|
||||||
|
|
||||||
# ann1: ic1, furl1
|
# ann1: ic1, furl1
|
||||||
# ann1a: ic1, furl1a (same SturdyRef, different connection hints)
|
# ann1a: ic1, furl1a (same SturdyRef, different connection hints)
|
||||||
# ann1b: ic2, furl1
|
# ann1b: ic2, furl1
|
||||||
# ann2: ic2, furl2
|
# ann2: ic2, furl2
|
||||||
|
|
||||||
self.ann1 = make_ann_t(ic1, furl1, privkey, seqnum=10)
|
self.ann1 = make_ann_t(ic1, furl1, priv_key, seqnum=10)
|
||||||
self.ann1old = make_ann_t(ic1, furl1, privkey, seqnum=9)
|
self.ann1old = make_ann_t(ic1, furl1, priv_key, seqnum=9)
|
||||||
self.ann1noseqnum = make_ann_t(ic1, furl1, privkey, seqnum=None)
|
self.ann1noseqnum = make_ann_t(ic1, furl1, priv_key, seqnum=None)
|
||||||
self.ann1b = make_ann_t(ic2, furl1, privkey, seqnum=11)
|
self.ann1b = make_ann_t(ic2, furl1, priv_key, seqnum=11)
|
||||||
self.ann1a = make_ann_t(ic1, furl1a, privkey, seqnum=12)
|
self.ann1a = make_ann_t(ic1, furl1a, priv_key, seqnum=12)
|
||||||
self.ann2 = make_ann_t(ic2, furl2, privkey, seqnum=13)
|
self.ann2 = make_ann_t(ic2, furl2, priv_key, seqnum=13)
|
||||||
|
|
||||||
ic1.remote_announce_v2([self.ann1]) # queues eventual-send
|
ic1.remote_announce_v2([self.ann1]) # queues eventual-send
|
||||||
d = fireEventually()
|
d = fireEventually()
|
||||||
@ -298,14 +301,13 @@ class Server(AsyncTestCase):
|
|||||||
FilePath(self.mktemp()))
|
FilePath(self.mktemp()))
|
||||||
furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp"
|
furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:36106/gydnp"
|
||||||
|
|
||||||
privkey_s, _ = keyutil.make_keypair()
|
priv_key = SigningKey.generate()
|
||||||
privkey, _ = keyutil.parse_privkey(privkey_s)
|
|
||||||
|
|
||||||
ann1 = make_ann_t(ic1, furl1, privkey, seqnum=10)
|
ann1 = make_ann_t(ic1, furl1, priv_key, seqnum=10)
|
||||||
ann1_old = make_ann_t(ic1, furl1, privkey, seqnum=9)
|
ann1_old = make_ann_t(ic1, furl1, priv_key, seqnum=9)
|
||||||
ann1_new = make_ann_t(ic1, furl1, privkey, seqnum=11)
|
ann1_new = make_ann_t(ic1, furl1, priv_key, seqnum=11)
|
||||||
ann1_noseqnum = make_ann_t(ic1, furl1, privkey, seqnum=None)
|
ann1_noseqnum = make_ann_t(ic1, furl1, priv_key, seqnum=None)
|
||||||
ann1_badseqnum = make_ann_t(ic1, furl1, privkey, seqnum="not an int")
|
ann1_badseqnum = make_ann_t(ic1, furl1, priv_key, seqnum="not an int")
|
||||||
|
|
||||||
i.remote_publish_v2(ann1, None)
|
i.remote_publish_v2(ann1, None)
|
||||||
all = i.get_announcements()
|
all = i.get_announcements()
|
||||||
@ -396,22 +398,24 @@ class Queue(SystemTestMixin, AsyncTestCase):
|
|||||||
u"nickname", "version", "oldest", {}, fakeseq,
|
u"nickname", "version", "oldest", {}, fakeseq,
|
||||||
FilePath(self.mktemp()))
|
FilePath(self.mktemp()))
|
||||||
furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short")
|
furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short")
|
||||||
sk_s, vk_s = keyutil.make_keypair()
|
priv_key = SigningKey.generate()
|
||||||
sk, _ignored = keyutil.parse_privkey(sk_s)
|
|
||||||
|
|
||||||
d = introducer.disownServiceParent()
|
d = introducer.disownServiceParent()
|
||||||
|
|
||||||
def _offline(ign):
|
def _offline(ign):
|
||||||
# now that the introducer server is offline, create a client and
|
# now that the introducer server is offline, create a client and
|
||||||
# publish some messages
|
# publish some messages
|
||||||
c.setServiceParent(self.parent) # this starts the reconnector
|
c.setServiceParent(self.parent) # this starts the reconnector
|
||||||
c.publish("storage", make_ann(furl1), sk)
|
c.publish("storage", make_ann(furl1), priv_key)
|
||||||
|
|
||||||
introducer.setServiceParent(self.parent) # restart the server
|
introducer.setServiceParent(self.parent) # restart the server
|
||||||
# now wait for the messages to be delivered
|
# now wait for the messages to be delivered
|
||||||
def _got_announcement():
|
def _got_announcement():
|
||||||
return bool(introducer.get_announcements())
|
return bool(introducer.get_announcements())
|
||||||
return self.poll(_got_announcement)
|
return self.poll(_got_announcement)
|
||||||
|
|
||||||
d.addCallback(_offline)
|
d.addCallback(_offline)
|
||||||
|
|
||||||
def _done(ign):
|
def _done(ign):
|
||||||
v = introducer.get_announcements()[0]
|
v = introducer.get_announcements()[0]
|
||||||
furl = v.announcement["anonymous-storage-FURL"]
|
furl = v.announcement["anonymous-storage-FURL"]
|
||||||
@ -427,6 +431,7 @@ class Queue(SystemTestMixin, AsyncTestCase):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
return self.poll(_idle)
|
return self.poll(_idle)
|
||||||
|
|
||||||
d.addCallback(_wait_until_idle)
|
d.addCallback(_wait_until_idle)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
@ -482,16 +487,16 @@ class SystemTest(SystemTestMixin, AsyncTestCase):
|
|||||||
expected_announcements[i] += 1 # all expect a 'storage' announcement
|
expected_announcements[i] += 1 # all expect a 'storage' announcement
|
||||||
|
|
||||||
node_furl = tub.registerReference(Referenceable())
|
node_furl = tub.registerReference(Referenceable())
|
||||||
privkey_s, pubkey_s = keyutil.make_keypair()
|
priv_key = SigningKey.generate()
|
||||||
privkey, _ignored = keyutil.parse_privkey(privkey_s)
|
pub_key_str = priv_key.public_key().encoded_key()
|
||||||
privkeys[i] = privkey
|
privkeys[i] = priv_key
|
||||||
pubkeys[i] = pubkey_s
|
pubkeys[i] = pub_key_str
|
||||||
|
|
||||||
if i < NUM_STORAGE:
|
if i < NUM_STORAGE:
|
||||||
# sign all announcements
|
# sign all announcements
|
||||||
c.publish("storage", make_ann(node_furl), privkey)
|
c.publish("storage", make_ann(node_furl), priv_key)
|
||||||
assert pubkey_s.startswith("pub-")
|
assert pub_key_str.startswith("pub-")
|
||||||
printable_serverids[i] = pubkey_s[len("pub-"):]
|
printable_serverids[i] = pub_key_str[len("pub-"):]
|
||||||
publishing_clients.append(c)
|
publishing_clients.append(c)
|
||||||
else:
|
else:
|
||||||
# the last one does not publish anything
|
# the last one does not publish anything
|
||||||
@ -500,13 +505,12 @@ class SystemTest(SystemTestMixin, AsyncTestCase):
|
|||||||
if i == 2:
|
if i == 2:
|
||||||
# also publish something that nobody cares about
|
# also publish something that nobody cares about
|
||||||
boring_furl = tub.registerReference(Referenceable())
|
boring_furl = tub.registerReference(Referenceable())
|
||||||
c.publish("boring", make_ann(boring_furl), privkey)
|
c.publish("boring", make_ann(boring_furl), priv_key)
|
||||||
|
|
||||||
c.setServiceParent(self.parent)
|
c.setServiceParent(self.parent)
|
||||||
clients.append(c)
|
clients.append(c)
|
||||||
tubs[c] = tub
|
tubs[c] = tub
|
||||||
|
|
||||||
|
|
||||||
def _wait_for_connected(ign):
|
def _wait_for_connected(ign):
|
||||||
def _connected():
|
def _connected():
|
||||||
for c in clients:
|
for c in clients:
|
||||||
@ -746,6 +750,7 @@ class ClientInfo(AsyncTestCase):
|
|||||||
self.failUnlessEqual(s0.nickname, NICKNAME % u"v2")
|
self.failUnlessEqual(s0.nickname, NICKNAME % u"v2")
|
||||||
self.failUnlessEqual(s0.version, "my_version")
|
self.failUnlessEqual(s0.version, "my_version")
|
||||||
|
|
||||||
|
|
||||||
class Announcements(AsyncTestCase):
|
class Announcements(AsyncTestCase):
|
||||||
def test_client_v2_signed(self):
|
def test_client_v2_signed(self):
|
||||||
introducer = IntroducerService()
|
introducer = IntroducerService()
|
||||||
@ -755,10 +760,12 @@ class Announcements(AsyncTestCase):
|
|||||||
"my_version", "oldest", app_versions,
|
"my_version", "oldest", app_versions,
|
||||||
fakeseq, FilePath(self.mktemp()))
|
fakeseq, FilePath(self.mktemp()))
|
||||||
furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
|
furl1 = "pb://62ubehyunnyhzs7r6vdonnm2hpi52w6y@127.0.0.1:0/swissnum"
|
||||||
sk_s, vk_s = keyutil.make_keypair()
|
|
||||||
sk, _ignored = keyutil.parse_privkey(sk_s)
|
priv_key = SigningKey.generate()
|
||||||
pks = keyutil.remove_prefix(vk_s, "pub-")
|
pub_key = priv_key.public_key()
|
||||||
ann_t0 = make_ann_t(client_v2, furl1, sk, 10)
|
pks = crypto.remove_prefix(pub_key.encoded_key(), "pub-")
|
||||||
|
|
||||||
|
ann_t0 = make_ann_t(client_v2, furl1, priv_key, 10)
|
||||||
canary0 = Referenceable()
|
canary0 = Referenceable()
|
||||||
introducer.remote_publish_v2(ann_t0, canary0)
|
introducer.remote_publish_v2(ann_t0, canary0)
|
||||||
a = introducer.get_announcements()
|
a = introducer.get_announcements()
|
||||||
@ -786,20 +793,19 @@ class Announcements(AsyncTestCase):
|
|||||||
# during startup (although the announcement will wait in a queue
|
# during startup (although the announcement will wait in a queue
|
||||||
# until the introducer connection is established). To avoid getting
|
# until the introducer connection is established). To avoid getting
|
||||||
# confused by this, disable storage.
|
# confused by this, disable storage.
|
||||||
f = open(os.path.join(basedir, "tahoe.cfg"), "w")
|
with open(os.path.join(basedir, "tahoe.cfg"), "w") as f:
|
||||||
f.write("[client]\n")
|
f.write("[client]\n")
|
||||||
f.write("introducer.furl = nope\n")
|
f.write("introducer.furl = nope\n")
|
||||||
f.write("[storage]\n")
|
f.write("[storage]\n")
|
||||||
f.write("enabled = false\n")
|
f.write("enabled = false\n")
|
||||||
f.close()
|
|
||||||
|
|
||||||
c = yield create_client(basedir)
|
c = yield create_client(basedir)
|
||||||
ic = c.introducer_clients[0]
|
ic = c.introducer_clients[0]
|
||||||
sk_s, vk_s = keyutil.make_keypair()
|
priv_key = SigningKey.generate()
|
||||||
sk, _ignored = keyutil.parse_privkey(sk_s)
|
pub_key = priv_key.public_key()
|
||||||
pub1 = keyutil.remove_prefix(vk_s, "pub-")
|
pub_key_str = crypto.remove_prefix(pub_key.encoded_key(), "pub-")
|
||||||
furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short")
|
furl1 = "pb://onug64tu@127.0.0.1:123/short" # base32("short")
|
||||||
ann_t = make_ann_t(ic, furl1, sk, 1)
|
ann_t = make_ann_t(ic, furl1, priv_key, 1)
|
||||||
|
|
||||||
ic.got_announcements([ann_t])
|
ic.got_announcements([ann_t])
|
||||||
yield flushEventualQueue()
|
yield flushEventualQueue()
|
||||||
@ -807,7 +813,7 @@ class Announcements(AsyncTestCase):
|
|||||||
# check the cache for the announcement
|
# check the cache for the announcement
|
||||||
announcements = self._load_cache(cache_filepath)
|
announcements = self._load_cache(cache_filepath)
|
||||||
self.failUnlessEqual(len(announcements), 1)
|
self.failUnlessEqual(len(announcements), 1)
|
||||||
self.failUnlessEqual(announcements[0]['key_s'], pub1)
|
self.failUnlessEqual(announcements[0]['key_s'], pub_key_str)
|
||||||
ann = announcements[0]["ann"]
|
ann = announcements[0]["ann"]
|
||||||
self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
|
self.failUnlessEqual(ann["anonymous-storage-FURL"], furl1)
|
||||||
self.failUnlessEqual(ann["seqnum"], 1)
|
self.failUnlessEqual(ann["seqnum"], 1)
|
||||||
@ -815,29 +821,29 @@ class Announcements(AsyncTestCase):
|
|||||||
# a new announcement that replaces the first should replace the
|
# a new announcement that replaces the first should replace the
|
||||||
# cached entry, not duplicate it
|
# cached entry, not duplicate it
|
||||||
furl2 = furl1 + "er"
|
furl2 = furl1 + "er"
|
||||||
ann_t2 = make_ann_t(ic, furl2, sk, 2)
|
ann_t2 = make_ann_t(ic, furl2, priv_key, 2)
|
||||||
ic.got_announcements([ann_t2])
|
ic.got_announcements([ann_t2])
|
||||||
yield flushEventualQueue()
|
yield flushEventualQueue()
|
||||||
announcements = self._load_cache(cache_filepath)
|
announcements = self._load_cache(cache_filepath)
|
||||||
self.failUnlessEqual(len(announcements), 1)
|
self.failUnlessEqual(len(announcements), 1)
|
||||||
self.failUnlessEqual(announcements[0]['key_s'], pub1)
|
self.failUnlessEqual(announcements[0]['key_s'], pub_key_str)
|
||||||
ann = announcements[0]["ann"]
|
ann = announcements[0]["ann"]
|
||||||
self.failUnlessEqual(ann["anonymous-storage-FURL"], furl2)
|
self.failUnlessEqual(ann["anonymous-storage-FURL"], furl2)
|
||||||
self.failUnlessEqual(ann["seqnum"], 2)
|
self.failUnlessEqual(ann["seqnum"], 2)
|
||||||
|
|
||||||
# but a third announcement with a different key should add to the
|
# but a third announcement with a different key should add to the
|
||||||
# cache
|
# cache
|
||||||
sk_s2, vk_s2 = keyutil.make_keypair()
|
priv_key2 = SigningKey.generate()
|
||||||
sk2, _ignored = keyutil.parse_privkey(sk_s2)
|
pub_key2 = priv_key2.public_key()
|
||||||
pub2 = keyutil.remove_prefix(vk_s2, "pub-")
|
pub_key_str2 = crypto.remove_prefix(pub_key2.encoded_key(), "pub-")
|
||||||
furl3 = "pb://onug64tu@127.0.0.1:456/short"
|
furl3 = "pb://onug64tu@127.0.0.1:456/short"
|
||||||
ann_t3 = make_ann_t(ic, furl3, sk2, 1)
|
ann_t3 = make_ann_t(ic, furl3, priv_key2, 1)
|
||||||
ic.got_announcements([ann_t3])
|
ic.got_announcements([ann_t3])
|
||||||
yield flushEventualQueue()
|
yield flushEventualQueue()
|
||||||
|
|
||||||
announcements = self._load_cache(cache_filepath)
|
announcements = self._load_cache(cache_filepath)
|
||||||
self.failUnlessEqual(len(announcements), 2)
|
self.failUnlessEqual(len(announcements), 2)
|
||||||
self.failUnlessEqual(set([pub1, pub2]),
|
self.failUnlessEqual(set([pub_key_str, pub_key_str2]),
|
||||||
set([a["key_s"] for a in announcements]))
|
set([a["key_s"] for a in announcements]))
|
||||||
self.failUnlessEqual(set([furl2, furl3]),
|
self.failUnlessEqual(set([furl2, furl3]),
|
||||||
set([a["ann"]["anonymous-storage-FURL"]
|
set([a["ann"]["anonymous-storage-FURL"]
|
||||||
@ -855,17 +861,17 @@ class Announcements(AsyncTestCase):
|
|||||||
ic2._load_announcements() # normally happens when connection fails
|
ic2._load_announcements() # normally happens when connection fails
|
||||||
yield flushEventualQueue()
|
yield flushEventualQueue()
|
||||||
|
|
||||||
self.failUnless(pub1 in announcements)
|
self.failUnless(pub_key_str in announcements)
|
||||||
self.failUnlessEqual(announcements[pub1]["anonymous-storage-FURL"],
|
self.failUnlessEqual(announcements[pub_key_str]["anonymous-storage-FURL"],
|
||||||
furl2)
|
furl2)
|
||||||
self.failUnlessEqual(announcements[pub2]["anonymous-storage-FURL"],
|
self.failUnlessEqual(announcements[pub_key_str2]["anonymous-storage-FURL"],
|
||||||
furl3)
|
furl3)
|
||||||
|
|
||||||
c2 = yield create_client(basedir)
|
c2 = yield create_client(basedir)
|
||||||
c2.introducer_clients[0]._load_announcements()
|
c2.introducer_clients[0]._load_announcements()
|
||||||
yield flushEventualQueue()
|
yield flushEventualQueue()
|
||||||
self.assertEqual(c2.storage_broker.get_all_serverids(),
|
self.assertEqual(c2.storage_broker.get_all_serverids(),
|
||||||
frozenset([pub1, pub2]))
|
frozenset([pub_key_str, pub_key_str2]))
|
||||||
|
|
||||||
class ClientSeqnums(AsyncBrokenTestCase):
|
class ClientSeqnums(AsyncBrokenTestCase):
|
||||||
|
|
||||||
@ -978,11 +984,11 @@ class DecodeFurl(SyncTestCase):
|
|||||||
self.failUnlessEqual(nodeid, "\x9fM\xf2\x19\xcckU0\xbf\x03\r\x10\x99\xfb&\x9b-\xc7A\x1d")
|
self.failUnlessEqual(nodeid, "\x9fM\xf2\x19\xcckU0\xbf\x03\r\x10\x99\xfb&\x9b-\xc7A\x1d")
|
||||||
|
|
||||||
class Signatures(SyncTestCase):
|
class Signatures(SyncTestCase):
|
||||||
|
|
||||||
def test_sign(self):
|
def test_sign(self):
|
||||||
ann = {"key1": "value1"}
|
ann = {"key1": "value1"}
|
||||||
sk_s,vk_s = keyutil.make_keypair()
|
priv_key = SigningKey.generate()
|
||||||
sk,ignored = keyutil.parse_privkey(sk_s)
|
ann_t = sign_to_foolscap(ann, priv_key)
|
||||||
ann_t = sign_to_foolscap(ann, sk)
|
|
||||||
(msg, sig, key) = ann_t
|
(msg, sig, key) = ann_t
|
||||||
self.failUnlessEqual(type(msg), type("".encode("utf-8"))) # bytes
|
self.failUnlessEqual(type(msg), type("".encode("utf-8"))) # bytes
|
||||||
self.failUnlessEqual(json.loads(msg.decode("utf-8")), ann)
|
self.failUnlessEqual(json.loads(msg.decode("utf-8")), ann)
|
||||||
@ -990,7 +996,7 @@ class Signatures(SyncTestCase):
|
|||||||
self.failUnless(key.startswith("v0-"))
|
self.failUnless(key.startswith("v0-"))
|
||||||
(ann2,key2) = unsign_from_foolscap(ann_t)
|
(ann2,key2) = unsign_from_foolscap(ann_t)
|
||||||
self.failUnlessEqual(ann2, ann)
|
self.failUnlessEqual(ann2, ann)
|
||||||
self.failUnlessEqual("pub-"+key2, vk_s)
|
self.failUnlessEqual("pub-" + key2, priv_key.public_key().encoded_key())
|
||||||
|
|
||||||
# not signed
|
# not signed
|
||||||
self.failUnlessRaises(UnknownKeyError,
|
self.failUnlessRaises(UnknownKeyError,
|
||||||
@ -1000,14 +1006,14 @@ class Signatures(SyncTestCase):
|
|||||||
# bad signature
|
# bad signature
|
||||||
bad_ann = {"key1": "value2"}
|
bad_ann = {"key1": "value2"}
|
||||||
bad_msg = json.dumps(bad_ann).encode("utf-8")
|
bad_msg = json.dumps(bad_ann).encode("utf-8")
|
||||||
self.failUnlessRaises(keyutil.BadSignatureError,
|
self.failUnlessRaises(ed25519.BadSignatureError,
|
||||||
unsign_from_foolscap, (bad_msg,sig,key))
|
unsign_from_foolscap, (bad_msg, sig, key))
|
||||||
|
|
||||||
# unrecognized signatures
|
# unrecognized signatures
|
||||||
self.failUnlessRaises(UnknownKeyError,
|
self.failUnlessRaises(UnknownKeyError,
|
||||||
unsign_from_foolscap, (bad_msg,"v999-sig",key))
|
unsign_from_foolscap, (bad_msg, "v999-sig", key))
|
||||||
self.failUnlessRaises(UnknownKeyError,
|
self.failUnlessRaises(UnknownKeyError,
|
||||||
unsign_from_foolscap, (bad_msg,sig,"v999-key"))
|
unsign_from_foolscap, (bad_msg, sig, "v999-key"))
|
||||||
|
|
||||||
|
|
||||||
# add tests of StorageFarmBroker: if it receives duplicate announcements, it
|
# add tests of StorageFarmBroker: if it receives duplicate announcements, it
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
import time, os.path, platform, stat, re, json, struct, shutil
|
import time, os.path, platform, stat, re, json, struct, shutil
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# from the Python Standard Library
|
# from the Python Standard Library
|
||||||
|
import six
|
||||||
import string
|
import string
|
||||||
|
|
||||||
from allmydata.util.assertutil import precondition
|
from allmydata.util.assertutil import precondition
|
||||||
@ -179,13 +180,13 @@ def init_s5():
|
|||||||
s5 = init_s5()
|
s5 = init_s5()
|
||||||
|
|
||||||
def could_be_base32_encoded(s, s8=s8, tr=string.translate, identitytranstable=identitytranstable, chars=chars):
|
def could_be_base32_encoded(s, s8=s8, tr=string.translate, identitytranstable=identitytranstable, chars=chars):
|
||||||
precondition(isinstance(s, str), s)
|
precondition(isinstance(s, six.binary_type), s)
|
||||||
if s == '':
|
if s == '':
|
||||||
return True
|
return True
|
||||||
return s8[len(s)%8][ord(s[-1])] and not tr(s, identitytranstable, chars)
|
return s8[len(s)%8][ord(s[-1])] and not tr(s, identitytranstable, chars)
|
||||||
|
|
||||||
def could_be_base32_encoded_l(s, lengthinbits, s5=s5, tr=string.translate, identitytranstable=identitytranstable, chars=chars):
|
def could_be_base32_encoded_l(s, lengthinbits, s5=s5, tr=string.translate, identitytranstable=identitytranstable, chars=chars):
|
||||||
precondition(isinstance(s, str), s)
|
precondition(isinstance(s, six.binary_type), s)
|
||||||
if s == '':
|
if s == '':
|
||||||
return True
|
return True
|
||||||
assert lengthinbits%5 < len(s5), lengthinbits
|
assert lengthinbits%5 < len(s5), lengthinbits
|
||||||
@ -201,7 +202,7 @@ def a2b(cs):
|
|||||||
@param cs the base-32 encoded data (a string)
|
@param cs the base-32 encoded data (a string)
|
||||||
"""
|
"""
|
||||||
precondition(could_be_base32_encoded(cs), "cs is required to be possibly base32 encoded data.", cs=cs)
|
precondition(could_be_base32_encoded(cs), "cs is required to be possibly base32 encoded data.", cs=cs)
|
||||||
precondition(isinstance(cs, str), cs)
|
precondition(isinstance(cs, six.binary_type), cs)
|
||||||
|
|
||||||
return a2b_l(cs, num_octets_that_encode_to_this_many_quintets(len(cs))*8)
|
return a2b_l(cs, num_octets_that_encode_to_this_many_quintets(len(cs))*8)
|
||||||
|
|
||||||
@ -226,7 +227,7 @@ def a2b_l(cs, lengthinbits):
|
|||||||
@return the data encoded in cs
|
@return the data encoded in cs
|
||||||
"""
|
"""
|
||||||
precondition(could_be_base32_encoded_l(cs, lengthinbits), "cs is required to be possibly base32 encoded data.", cs=cs, lengthinbits=lengthinbits)
|
precondition(could_be_base32_encoded_l(cs, lengthinbits), "cs is required to be possibly base32 encoded data.", cs=cs, lengthinbits=lengthinbits)
|
||||||
precondition(isinstance(cs, str), cs)
|
precondition(isinstance(cs, six.binary_type), cs)
|
||||||
if cs == '':
|
if cs == '':
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -9,11 +9,13 @@ from allmydata.web.common import (
|
|||||||
from allmydata.util.abbreviate import abbreviate_space
|
from allmydata.util.abbreviate import abbreviate_space
|
||||||
from allmydata.util import time_format, idlib
|
from allmydata.util import time_format, idlib
|
||||||
|
|
||||||
|
|
||||||
def remove_prefix(s, prefix):
|
def remove_prefix(s, prefix):
|
||||||
if not s.startswith(prefix):
|
if not s.startswith(prefix):
|
||||||
return None
|
return None
|
||||||
return s[len(prefix):]
|
return s[len(prefix):]
|
||||||
|
|
||||||
|
|
||||||
class StorageStatus(MultiFormatPage):
|
class StorageStatus(MultiFormatPage):
|
||||||
docFactory = getxmlfile("storage_status.xhtml")
|
docFactory = getxmlfile("storage_status.xhtml")
|
||||||
# the default 'data' argument is the StorageServer instance
|
# the default 'data' argument is the StorageServer instance
|
||||||
|
Loading…
Reference in New Issue
Block a user