]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/client.py
client.py: create node key even when storage is disabled. Closes #1945.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / client.py
index d007b8b17734060f8998a4adc0ac9c1b3120023b..217a7b47448f145153095f87611c61483032480d 100644 (file)
@@ -1,12 +1,10 @@
 import os, stat, time, weakref
-from allmydata.interfaces import RIStorageServer
 from allmydata import node
 
 from zope.interface import implements
 from twisted.internet import reactor, defer
 from twisted.application import service
 from twisted.application.internet import TimerService
-from foolscap.api import Referenceable
 from pycryptopp.publickey import rsa
 
 import allmydata
@@ -16,14 +14,16 @@ from allmydata.immutable.upload import Uploader
 from allmydata.immutable.offloaded import Helper
 from allmydata.control import ControlServer
 from allmydata.introducer.client import IntroducerClient
-from allmydata.util import hashutil, base32, pollmixin, log
+from allmydata.util import hashutil, base32, pollmixin, log, keyutil, idlib
 from allmydata.util.encodingutil import get_filesystem_encoding
 from allmydata.util.abbreviate import parse_abbreviated_size
 from allmydata.util.time_format import parse_duration, parse_date
 from allmydata.stats import StatsProvider
 from allmydata.history import History
-from allmydata.interfaces import IStatsProducer, RIStubClient
+from allmydata.interfaces import IStatsProducer, SDMF_VERSION, MDMF_VERSION
 from allmydata.nodemaker import NodeMaker
+from allmydata.blacklist import Blacklist
+from allmydata.node import OldConfigOptionError
 
 
 KiB=1024
@@ -32,9 +32,6 @@ GiB=1024*MiB
 TiB=1024*GiB
 PiB=1024*TiB
 
-class StubClient(Referenceable):
-    implements(RIStubClient)
-
 def _make_secret():
     return base32.b2a(os.urandom(hashutil.CRYPTO_VAL_SIZE)) + "\n"
 
@@ -68,14 +65,14 @@ class KeyGenerator:
         mutable files which don't otherwise specify a size. This will affect
         all subsequent calls to generate() without a keysize= argument. The
         default size is 2048 bits. Test cases should call this method once
-        during setup, to cause me to create smaller (522 bit) keys, so the
-        unit tests run faster."""
+        during setup, to cause me to create smaller keys, so the unit tests
+        run faster."""
         self.default_keysize = keysize
 
     def generate(self, keysize=None):
         """I return a Deferred that fires with a (verifyingkey, signingkey)
-        pair. I accept a keysize in bits (522 bit keys are fast for testing,
-        2048 bit keys are standard). If you do not provide a keysize, I will
+        pair. I accept a keysize in bits (2048 bit keys are standard, smaller
+        keys are used for testing). If you do not provide a keysize, I will
         use my default, which is set by a call to set_default_keysize(). If
         set_default_keysize() has never been called, I will create 2048 bit
         keys."""
@@ -137,6 +134,7 @@ class Client(node.Node, pollmixin.PollMixin):
         self.init_introducer_client()
         self.init_stats_provider()
         self.init_secrets()
+        self.init_node_key()
         self.init_storage()
         self.init_control()
         self.helper = None
@@ -166,12 +164,24 @@ class Client(node.Node, pollmixin.PollMixin):
         if webport:
             self.init_web(webport) # strports string
 
+    def _sequencer(self):
+        seqnum_s = self.get_config_from_file("announcement-seqnum")
+        if not seqnum_s:
+            seqnum_s = "0"
+        seqnum = int(seqnum_s.strip())
+        seqnum += 1 # increment
+        self.write_config("announcement-seqnum", "%d\n" % seqnum)
+        nonce = _make_secret().strip()
+        return seqnum, nonce
+
     def init_introducer_client(self):
         self.introducer_furl = self.get_config("client", "introducer.furl")
         ic = IntroducerClient(self.tub, self.introducer_furl,
                               self.nickname,
                               str(allmydata.__full_version__),
-                              str(self.OLDEST_SUPPORTED_VERSION))
+                              str(self.OLDEST_SUPPORTED_VERSION),
+                              self.get_app_versions(),
+                              self._sequencer)
         self.introducer_client = ic
         # hold off on starting the IntroducerClient until our tub has been
         # started, so we'll have a useful address on our RemoteReference, so
@@ -200,6 +210,46 @@ class Client(node.Node, pollmixin.PollMixin):
         self.convergence = base32.a2b(convergence_s)
         self._secret_holder = SecretHolder(lease_secret, self.convergence)
 
+    def init_node_key(self):
+        # we only create the key once. On all subsequent runs, we re-use the
+        # existing key
+        def _make_key():
+            sk_vs,vk_vs = keyutil.make_keypair()
+            return sk_vs+"\n"
+        sk_vs = self.get_or_create_private_config("node.privkey", _make_key)
+        sk,vk_vs = keyutil.parse_privkey(sk_vs.strip())
+        self.write_config("node.pubkey", vk_vs+"\n")
+        self._node_key = sk
+
+    def get_long_nodeid(self):
+        # this matches what IServer.get_longname() says about us elsewhere
+        vk_bytes = self._node_key.get_verifying_key_bytes()
+        return "v0-"+base32.b2a(vk_bytes)
+
+    def get_long_tubid(self):
+        return idlib.nodeid_b2a(self.nodeid)
+
+    def _init_permutation_seed(self, ss):
+        seed = self.get_config_from_file("permutation-seed")
+        if not seed:
+            have_shares = ss.have_shares()
+            if have_shares:
+                # if the server has shares but not a recorded
+                # permutation-seed, then it has been around since pre-#466
+                # days, and the clients who uploaded those shares used our
+                # TubID as a permutation-seed. We should keep using that same
+                # seed to keep the shares in the same place in the permuted
+                # ring, so those clients don't have to perform excessive
+                # searches.
+                seed = base32.b2a(self.nodeid)
+            else:
+                # otherwise, we're free to use the more natural seed of our
+                # pubkey-based serverid
+                vk_bytes = self._node_key.get_verifying_key_bytes()
+                seed = base32.b2a(vk_bytes)
+            self.write_config("permutation-seed", seed+"\n")
+        return seed.strip()
+
     def init_storage(self):
         # should we run a storage server (and publish it for others to use)?
         if not self.get_config("storage", "enabled", True, boolean=True):
@@ -209,12 +259,12 @@ class Client(node.Node, pollmixin.PollMixin):
         storedir = os.path.join(self.basedir, self.STOREDIR)
 
         data = self.get_config("storage", "reserved_space", None)
-        reserved = None
         try:
             reserved = parse_abbreviated_size(data)
         except ValueError:
             log.msg("[storage]reserved_space= contains unparseable value %s"
                     % data)
+            raise
         if reserved is None:
             reserved = 0
         discard = self.get_config("storage", "debug_discard", False,
@@ -259,14 +309,19 @@ class Client(node.Node, pollmixin.PollMixin):
         def _publish(res):
             furl_file = os.path.join(self.basedir, "private", "storage.furl").encode(get_filesystem_encoding())
             furl = self.tub.registerReference(ss, furlFile=furl_file)
-            ri_name = RIStorageServer.__remote_name__
-            self.introducer_client.publish(furl, "storage", ri_name)
+            ann = {"anonymous-storage-FURL": furl,
+                   "permutation-seed-base32": self._init_permutation_seed(ss),
+                   }
+            self.introducer_client.publish("storage", ann, self._node_key)
         d.addCallback(_publish)
         d.addErrback(log.err, facility="tahoe.init",
                      level=log.BAD, umid="aLGBKw")
 
     def init_client(self):
         helper_furl = self.get_config("client", "helper.furl", None)
+        if helper_furl in ("None", ""):
+            helper_furl = None
+
         DEP = self.DEFAULT_ENCODING_PARAMETERS
         DEP["k"] = int(self.get_config("client", "shares.needed", DEP["k"]))
         DEP["n"] = int(self.get_config("client", "shares.total", DEP["n"]))
@@ -276,8 +331,9 @@ class Client(node.Node, pollmixin.PollMixin):
         self.history = History(self.stats_provider)
         self.terminator = Terminator()
         self.terminator.setServiceParent(self)
-        self.add_service(Uploader(helper_furl, self.stats_provider))
-        self.init_stub_client()
+        self.add_service(Uploader(helper_furl, self.stats_provider,
+                                  self.history))
+        self.init_blacklist()
         self.init_nodemaker()
 
     def init_client_storage_broker(self):
@@ -316,28 +372,25 @@ class Client(node.Node, pollmixin.PollMixin):
     def get_storage_broker(self):
         return self.storage_broker
 
-    def init_stub_client(self):
-        def _publish(res):
-            # we publish an empty object so that the introducer can count how
-            # many clients are connected and see what versions they're
-            # running.
-            sc = StubClient()
-            furl = self.tub.registerReference(sc)
-            ri_name = RIStubClient.__remote_name__
-            self.introducer_client.publish(furl, "stub_client", ri_name)
-        d = self.when_tub_ready()
-        d.addCallback(_publish)
-        d.addErrback(log.err, facility="tahoe.init",
-                     level=log.BAD, umid="OEHq3g")
+    def init_blacklist(self):
+        fn = os.path.join(self.basedir, "access.blacklist")
+        self.blacklist = Blacklist(fn)
 
     def init_nodemaker(self):
+        default = self.get_config("client", "mutable.format", default="SDMF")
+        if default.upper() == "MDMF":
+            self.mutable_file_default = MDMF_VERSION
+        else:
+            self.mutable_file_default = SDMF_VERSION
         self.nodemaker = NodeMaker(self.storage_broker,
                                    self._secret_holder,
                                    self.get_history(),
                                    self.getServiceNamed("uploader"),
                                    self.terminator,
                                    self.get_encoding_parameters(),
-                                   self._key_generator)
+                                   self.mutable_file_default,
+                                   self._key_generator,
+                                   self.blacklist)
 
     def get_history(self):
         return self.history
@@ -424,19 +477,20 @@ class Client(node.Node, pollmixin.PollMixin):
 
     def init_drop_uploader(self):
         if self.get_config("drop_upload", "enabled", False, boolean=True):
-            upload_dircap = self.get_config("drop_upload", "upload.dircap", None)
-            local_dir_utf8 = self.get_config("drop_upload", "local.directory", None)
-
-            if upload_dircap and local_dir_utf8:
-                try:
-                    from allmydata.frontends import drop_upload
-                    s = drop_upload.DropUploader(self, upload_dircap, local_dir_utf8)
-                    s.setServiceParent(self)
-                    s.startService()
-                except Exception, e:
-                    self.log("couldn't start drop-uploader: %r", args=(e,))
-            else:
-                self.log("couldn't start drop-uploader: upload.dircap or local.directory not specified")
+            if self.get_config("drop_upload", "upload.dircap", None):
+                raise OldConfigOptionError("The [drop_upload]upload.dircap option is no longer supported; please "
+                                           "put the cap in a 'private/drop_upload_dircap' file, and delete this option.")
+
+            upload_dircap = self.get_or_create_private_config("drop_upload_dircap")
+            local_dir_utf8 = self.get_config("drop_upload", "local.directory")
+
+            try:
+                from allmydata.frontends import drop_upload
+                s = drop_upload.DropUploader(self, upload_dircap, local_dir_utf8)
+                s.setServiceParent(self)
+                s.startService()
+            except Exception, e:
+                self.log("couldn't start drop-uploader: %r", args=(e,))
 
     def _check_hotline(self, hotline_file):
         if os.path.exists(hotline_file):
@@ -485,16 +539,17 @@ class Client(node.Node, pollmixin.PollMixin):
         # may get an opaque node if there were any problems.
         return self.nodemaker.create_from_cap(write_uri, read_uri, deep_immutable=deep_immutable, name=name)
 
-    def create_dirnode(self, initial_children={}):
-        d = self.nodemaker.create_new_mutable_directory(initial_children)
+    def create_dirnode(self, initial_children={}, version=None):
+        d = self.nodemaker.create_new_mutable_directory(initial_children, version=version)
         return d
 
     def create_immutable_dirnode(self, children, convergence=None):
         return self.nodemaker.create_immutable_directory(children, convergence)
 
-    def create_mutable_file(self, contents=None, keysize=None):
-        return self.nodemaker.create_mutable_file(contents, keysize)
+    def create_mutable_file(self, contents=None, keysize=None, version=None):
+        return self.nodemaker.create_mutable_file(contents, keysize,
+                                                  version=version)
 
     def upload(self, uploadable):
         uploader = self.getServiceNamed("uploader")
-        return uploader.upload(uploadable, history=self.get_history())
+        return uploader.upload(uploadable)