From 883dd9795bcf4f309222f4f62038537c24d0791f Mon Sep 17 00:00:00 2001
From: Daira Hopwood <daira@jacaranda.org>
Date: Mon, 5 May 2014 22:55:50 +0100
Subject: [PATCH] Error if a .furl config entry contains an unescaped '#'.
 fixes #2128

Author: Andrew Miller <amiller@dappervision.com>
Signed-off-by: Daira Hopwood <daira@jacaranda.org>
---
 src/allmydata/node.py             | 23 ++++++++++++++++++++++-
 src/allmydata/test/test_client.py | 27 ++++++++++++++++++++++++++-
 src/allmydata/test/test_node.py   | 11 +++++++++++
 3 files changed, 59 insertions(+), 2 deletions(-)

diff --git a/src/allmydata/node.py b/src/allmydata/node.py
index 8873e5c7..47c8ac3b 100644
--- a/src/allmydata/node.py
+++ b/src/allmydata/node.py
@@ -55,6 +55,11 @@ class OldConfigError(Exception):
 class OldConfigOptionError(Exception):
     pass
 
+class UnescapedHashError(Exception):
+    def __str__(self):
+        return ("The configuration entry %s contained an unescaped '#' character."
+                % quote_output(self.args[0]))
+
 
 class Node(service.MultiService):
     # this implements common functionality of both Client nodes and Introducer
@@ -101,11 +106,27 @@ class Node(service.MultiService):
         test_name = tempfile.mktemp()
         _assert(os.path.dirname(test_name) == tempdir, test_name, tempdir)
 
+    @staticmethod
+    def _contains_unescaped_hash(item):
+        characters = iter(item)
+        for c in characters:
+            if c == '\\':
+                characters.next()
+            elif c == '#':
+                return True
+
+        return False
+
     def get_config(self, section, option, default=_None, boolean=False):
         try:
             if boolean:
                 return self.config.getboolean(section, option)
-            return self.config.get(section, option)
+
+            item = self.config.get(section, option)
+            if option.endswith(".furl") and self._contains_unescaped_hash(item):
+                raise UnescapedHashError(item)
+
+            return item
         except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
             if default is _None:
                 fn = os.path.join(self.basedir, u"tahoe.cfg")
diff --git a/src/allmydata/test/test_client.py b/src/allmydata/test/test_client.py
index 796971b2..df3691f3 100644
--- a/src/allmydata/test/test_client.py
+++ b/src/allmydata/test/test_client.py
@@ -3,7 +3,7 @@ from twisted.trial import unittest
 from twisted.application import service
 
 import allmydata
-from allmydata.node import OldConfigError, OldConfigOptionError, MissingConfigEntry
+from allmydata.node import Node, OldConfigError, OldConfigOptionError, MissingConfigEntry, UnescapedHashError
 from allmydata import client
 from allmydata.storage_client import StorageFarmBroker
 from allmydata.util import base32, fileutil
@@ -30,6 +30,31 @@ class Basic(testutil.ReallyEqualMixin, unittest.TestCase):
                            BASECONFIG)
         client.Client(basedir)
 
+    def test_comment(self):
+        dummy = "pb://wl74cyahejagspqgy4x5ukrvfnevlknt@127.0.0.1:58889/bogus"
+
+        should_fail = [r"test#test", r"#testtest", r"test\\#test"]
+        should_not_fail = [r"test\#test", r"test\\\#test", r"testtest"]
+
+        basedir = "test_client.Basic.test_comment"
+        os.mkdir(basedir)
+
+        def write_config(shouldfail, s):
+            config = ("[client]\n"
+                      "introducer.furl = %s\n" % s)
+            fileutil.write(os.path.join(basedir, "tahoe.cfg"), config)
+
+        for s in should_fail:
+            self.failUnless(Node._contains_unescaped_hash(s))
+            write_config(s)
+            self.failUnlessRaises(UnescapedHashError, client.Client, basedir)
+
+        for s in should_not_fail:
+            self.failIf(Node._contains_unescaped_hash(s))
+            write_config(s)
+            client.Client(basedir)
+
+
     @mock.patch('twisted.python.log.msg')
     def test_error_on_old_config_files(self, mock_log_msg):
         basedir = "test_client.Basic.test_error_on_old_config_files"
diff --git a/src/allmydata/test/test_node.py b/src/allmydata/test/test_node.py
index 72d6ef8c..88303f2a 100644
--- a/src/allmydata/test/test_node.py
+++ b/src/allmydata/test/test_node.py
@@ -87,6 +87,17 @@ class TestCase(testutil.SignalMixin, unittest.TestCase):
                                                        u"\u2621"))
         return d
 
+    def test_tahoe_cfg_hash_in_name(self):
+        basedir = "test_node/test_cfg_hash_in_name"
+        nickname = "Hash#Bang!" # a clever nickname containing a hash
+        fileutil.make_dirs(basedir)
+        f = open(os.path.join(basedir, 'tahoe.cfg'), 'wt')
+        f.write("[node]\n")
+        f.write("nickname = %s\n" % (nickname,))
+        f.close()
+        n = TestNode(basedir)
+        self.failUnless(n.nickname == nickname)
+
     def test_private_config(self):
         basedir = "test_node/test_private_config"
         privdir = os.path.join(basedir, "private")
-- 
2.45.2