From 2732c379411db85bd8256d598ada7fb003ba0796 Mon Sep 17 00:00:00 2001 From: meejah Date: Mon, 5 Sep 2016 16:34:17 -0600 Subject: [PATCH] Check for unknown config options The list of valid sections + config-items came from grep'ing the source for `.get_config` --- src/allmydata/client.py | 61 ++++++++++++++++++++++++++- src/allmydata/introducer/server.py | 8 +++- src/allmydata/node.py | 39 +++++++++++++++-- src/allmydata/test/test_configutil.py | 42 ++++++++++++++++++ src/allmydata/util/configutil.py | 33 +++++++++++++++ 5 files changed, 176 insertions(+), 7 deletions(-) diff --git a/src/allmydata/client.py b/src/allmydata/client.py index b0b82e39d..0ec1ad62c 100644 --- a/src/allmydata/client.py +++ b/src/allmydata/client.py @@ -17,7 +17,7 @@ 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, keyutil, idlib, - yamlutil) + yamlutil, configutil) from allmydata.util.encodingutil import (get_filesystem_encoding, from_utf8_or_none) from allmydata.util.fileutil import abspath_expanduser_unicode @@ -28,7 +28,7 @@ from allmydata.history import History from allmydata.interfaces import IStatsProducer, SDMF_VERSION, MDMF_VERSION from allmydata.nodemaker import NodeMaker from allmydata.blacklist import Blacklist -from allmydata.node import OldConfigOptionError +from allmydata.node import OldConfigOptionError, _common_config_sections KiB=1024 @@ -37,6 +37,62 @@ GiB=1024*MiB TiB=1024*GiB PiB=1024*TiB +def _valid_config_sections(): + cfg = _common_config_sections() + cfg.update({ + "client": ( + "helper.furl", + "introducer.furl", + "key_generator.furl", + "mutable.format", + "peers.preferred", + "shares.happy", + "shares.needed", + "shares.total", + "stats_gatherer.furl", + ), + "drop_upload": ( # deprecated already? + "enabled", + ), + "ftpd": ( + "accounts.file", + "accounts.url", + "enabled", + "port", + ), + "storage": ( + "debug_discard", + "enabled", + "expire.cutoff_date", + "expire.enabled", + "expire.immutable", + "expire.mode", + "expire.mode", + "expire.mutable", + "expire.override_lease_duration", + "readonly", + "reserved_space", + ), + "sftpd": ( + "accounts.file", + "accounts.url", + "enabled", + "host_privkey_file", + "host_pubkey_file", + "port", + ), + "helper": ( + "enabled", + ), + "magic_folder": ( + "download.umask", + "enabled", + "local.directory", + ), + }) + return cfg + + def _make_secret(): return base32.b2a(os.urandom(hashutil.CRYPTO_VAL_SIZE)) + "\n" @@ -123,6 +179,7 @@ class Client(node.Node, pollmixin.PollMixin): node.Node.__init__(self, basedir) # All tub.registerReference must happen *after* we upcall, since # that's what does tub.setLocation() + configutil.validate_config(self.config_fname, self.config, _valid_config_sections()) self._magic_folder = None self.started_timestamp = time.time() self.logSource="Client" diff --git a/src/allmydata/introducer/server.py b/src/allmydata/introducer/server.py index b92a88c08..e999fc098 100644 --- a/src/allmydata/introducer/server.py +++ b/src/allmydata/introducer/server.py @@ -5,13 +5,17 @@ from twisted.application import service from foolscap.api import Referenceable import allmydata from allmydata import node -from allmydata.util import log, rrefutil +from allmydata.util import log, rrefutil, configutil from allmydata.util.fileutil import abspath_expanduser_unicode from allmydata.introducer.interfaces import \ RIIntroducerPublisherAndSubscriberService_v2 from allmydata.introducer.common import unsign_from_foolscap, \ SubscriberDescriptor, AnnouncementDescriptor +def _valid_config_sections(): + return node._common_config_sections() + + class FurlFileConflictError(Exception): pass @@ -22,7 +26,7 @@ class IntroducerNode(node.Node): def __init__(self, basedir=u"."): node.Node.__init__(self, basedir) - self.read_config() + configutil.validate_config(self.config_fname, self.config, _valid_config_sections()) self.init_introducer() webport = self.get_config("node", "web.port", None) if webport: diff --git a/src/allmydata/node.py b/src/allmydata/node.py index d9065501f..a8a31eb00 100644 --- a/src/allmydata/node.py +++ b/src/allmydata/node.py @@ -29,6 +29,39 @@ def _import_i2p(): except ImportError: # pragma: no cover return None +def _common_config_sections(): + return { + "connections": ( + "tcp", + ), + "node": ( + "log_gatherer.furl", + "nickname", + "reveal-ip-address", + "tempdir", + "timeout.disconnect", + "timeout.keepalive", + "tub.location", + "tub.port", + "web.port", + "web.static", + ), + "i2p": ( + "enabled", + "i2p.configdir", + "i2p.executable", + "launch", + "sam.port", + ), + "tor": ( + "control.port", + "enabled", + "launch", + "socks.port", + "tor.executable", + ), + } + # Add our application versions to the data that Foolscap's LogPublisher # reports. for thing, things_version in get_package_versions().iteritems(): @@ -91,6 +124,7 @@ class Node(service.MultiService): def __init__(self, basedir=u"."): service.MultiService.__init__(self) self.basedir = abspath_expanduser_unicode(unicode(basedir)) + self.config_fname = os.path.join(self.basedir, "tahoe.cfg") self._portnumfile = os.path.join(self.basedir, self.PORTNUMFILE) fileutil.make_dirs(os.path.join(self.basedir, "private"), 0700) open(os.path.join(self.basedir, "private", "README"), "w").write(PRIV_README) @@ -159,11 +193,10 @@ class Node(service.MultiService): self.error_about_old_config_files() self.config = ConfigParser.SafeConfigParser() - tahoe_cfg = os.path.join(self.basedir, "tahoe.cfg") try: - self.config = configutil.get_config(tahoe_cfg) + self.config = configutil.get_config(self.config_fname) except EnvironmentError: - if os.path.exists(tahoe_cfg): + if os.path.exists(self.config_fname): raise def error_about_old_config_files(self): diff --git a/src/allmydata/test/test_configutil.py b/src/allmydata/test/test_configutil.py index ad0b807ad..b9dae5cf2 100644 --- a/src/allmydata/test/test_configutil.py +++ b/src/allmydata/test/test_configutil.py @@ -32,3 +32,45 @@ class ConfigUtilTests(CLITestMixin, GridTestMixin, unittest.TestCase): config = configutil.get_config(tahoe_cfg) self.failUnlessEqual(config.get("node", "descriptor"), descriptor) + + def test_config_validation_success(self): + d = self.mktemp() + os.mkdir(d) + fname = os.path.join(d, 'tahoe.cfg') + + with open(fname, 'w') as f: + f.write('[node]\nvalid = foo\n') + + config = configutil.get_config(fname) + # should succeed, no exceptions + configutil.validate_config(fname, config, dict(node=['valid'])) + + def test_config_validation_invalid_item(self): + d = self.mktemp() + os.mkdir(d) + fname = os.path.join(d, 'tahoe.cfg') + + with open(fname, 'w') as f: + f.write('[node]\nvalid = foo\ninvalid = foo\n') + + config = configutil.get_config(fname) + self.assertRaises( + configutil.UnknownConfigError, + configutil.validate_config, + fname, config, dict(node=['valid']), + ) + + def test_config_validation_invalid_section(self): + d = self.mktemp() + os.mkdir(d) + fname = os.path.join(d, 'tahoe.cfg') + + with open(fname, 'w') as f: + f.write('[node]\nvalid = foo\n[invalid]\n') + + config = configutil.get_config(fname) + self.assertRaises( + configutil.UnknownConfigError, + configutil.validate_config, + fname, config, dict(node=['valid']), + ) diff --git a/src/allmydata/util/configutil.py b/src/allmydata/util/configutil.py index 19f712dce..78894e301 100644 --- a/src/allmydata/util/configutil.py +++ b/src/allmydata/util/configutil.py @@ -2,6 +2,14 @@ from ConfigParser import SafeConfigParser +class UnknownConfigError(Exception): + """ + An unknown config item was found. + + This is possibly raised by validate_config() + """ + + def get_config(tahoe_cfg): config = SafeConfigParser() f = open(tahoe_cfg, "rb") @@ -27,3 +35,28 @@ def write_config(tahoe_cfg, config): config.write(f) finally: f.close() + +def validate_config(fname, cfg, valid_sections): + """ + raises UnknownConfigError if there are any unknown sections or config + values. + """ + for section in cfg.sections(): + try: + valid_in_section = valid_sections[section] + except KeyError: + raise UnknownConfigError( + "'{fname}' contains unknown section [{section}]".format( + fname=fname, + section=section, + ) + ) + for option in cfg.options(section): + if option not in valid_in_section: + raise UnknownConfigError( + "'{fname}' section [{section}] contains unknown option '{option}'".format( + fname=fname, + section=section, + option=option, + ) + )