Implement i2p_provider and --listen=i2p

Closes ticket:2838
This commit is contained in:
str4d 2016-10-22 14:26:36 -05:00
parent f42b4f687b
commit 6071c2b6f8
No known key found for this signature in database
GPG Key ID: 6A6914DAFBEA00DA
8 changed files with 696 additions and 54 deletions

View File

@ -275,7 +275,7 @@ setup(name="tahoe-lafs", # also set in __init__.py
"foolscap[tor] >= 0.12.3",
"txtorcon >= 0.17.0", # in case pip's resolver doesn't work
"foolscap[i2p]",
"txi2p", # in case pip's resolver doesn't work
"txi2p >= 0.3.1", # in case pip's resolver doesn't work
"pytest",
"pytest-twisted",
],
@ -285,7 +285,7 @@ setup(name="tahoe-lafs", # also set in __init__.py
],
"i2p": [
"foolscap[i2p]",
"txi2p", # in case pip's resolver doesn't work
"txi2p >= 0.3.1", # in case pip's resolver doesn't work
],
},
package_data={"allmydata.web": ["*.xhtml",

View File

@ -1,7 +1,7 @@
import datetime, os.path, re, types, ConfigParser, tempfile
from base64 import b32decode, b32encode
from twisted.internet import reactor, endpoints
from twisted.internet import reactor
from twisted.python import log as twlog
from twisted.application import service
from foolscap.api import Tub, app_versions
@ -13,14 +13,7 @@ from allmydata.util.assertutil import _assert
from allmydata.util.fileutil import abspath_expanduser_unicode
from allmydata.util.encodingutil import get_filesystem_encoding, quote_output
from allmydata.util import configutil
from allmydata.util import tor_provider
def _import_i2p():
try:
from foolscap.connections import i2p
return i2p
except ImportError: # pragma: no cover
return None
from allmydata.util import i2p_provider, tor_provider
def _common_config_sections():
return {
@ -45,6 +38,9 @@ def _common_config_sections():
"i2p.executable",
"launch",
"sam.port",
"dest",
"dest.port",
"dest.private_key_file",
),
"tor": (
"control.port",
@ -140,6 +136,7 @@ class Node(service.MultiService):
self.logSource="Node"
self.setup_logging()
self.create_i2p_provider()
self.create_tor_provider()
self.init_connections()
self.set_tub_options()
@ -222,6 +219,11 @@ class Node(service.MultiService):
def check_privacy(self):
self._reveal_ip = self.get_config("node", "reveal-IP-address", True,
boolean=True)
def create_i2p_provider(self):
self._i2p_provider = i2p_provider.Provider(self.basedir, self, reactor)
self._i2p_provider.check_dest_config()
self._i2p_provider.setServiceParent(self)
def create_tor_provider(self):
self._tor_provider = tor_provider.Provider(self.basedir, self, reactor)
self._tor_provider.check_onion_config()
@ -236,32 +238,7 @@ class Node(service.MultiService):
return self._tor_provider.get_tor_handler()
def _make_i2p_handler(self):
enabled = self.get_config("i2p", "enabled", True, boolean=True)
if not enabled:
return None
i2p = _import_i2p()
if not i2p:
return None
samport = self.get_config("i2p", "sam.port", None)
launch = self.get_config("i2p", "launch", False, boolean=True)
configdir = self.get_config("i2p", "i2p.configdir", None)
if samport:
if launch:
raise ValueError("tahoe.cfg [i2p] must not set both "
"sam.port and launch")
ep = endpoints.clientFromString(reactor, samport)
return i2p.sam_endpoint(ep)
if launch:
executable = self.get_config("i2p", "i2p.executable", None)
return i2p.launch(i2p_configdir=configdir, i2p_binary=executable)
if configdir:
return i2p.local_i2p(configdir)
return i2p.default(reactor)
return self._i2p_provider.get_i2p_handler()
def init_connections(self):
# We store handlers for everything. None means we were unable to

View File

@ -5,7 +5,7 @@ from allmydata.scripts.common import BasedirOptions, NoDefaultBasedirOptions
from allmydata.scripts.default_nodedir import _default_nodedir
from allmydata.util.assertutil import precondition
from allmydata.util.encodingutil import listdir_unicode, argv_to_unicode, quote_local_unicode_path
from allmydata.util import fileutil, iputil, tor_provider
from allmydata.util import fileutil, i2p_provider, iputil, tor_provider
dummy_tac = """
@ -41,6 +41,17 @@ TOR_FLAGS = [
("tor-launch", None, "Launch a tor instead of connecting to a tor control port."),
]
I2P_OPTS = [
("i2p-sam-port", None, None,
"I2P's SAM API port endpoint descriptor string (e.g. tcp:127.0.0.1:7656)"),
("i2p-executable", None, None,
"(future) The 'i2prouter' executable to run (default is to search $PATH)."),
]
I2P_FLAGS = [
("i2p-launch", None, "(future) Launch an I2P router instead of connecting to a SAM API port."),
]
def validate_where_options(o):
if o['listen'] == "none":
# no other arguments are accepted
@ -89,6 +100,18 @@ def validate_tor_options(o):
if o["tor-launch"] and o["tor-control-port"]:
raise UsageError("use either --tor-launch or --tor-control-port=, not both")
def validate_i2p_options(o):
use_i2p = "i2p" in o["listen"].split(",")
if not use_i2p:
if o["i2p-launch"]:
raise UsageError("--i2p-launch requires --listen=i2p")
if o["i2p-sam-port"]:
raise UsageError("--i2p-sam-port= requires --listen=i2p")
if o["i2p-launch"] and o["i2p-sam-port"]:
raise UsageError("use either --i2p-launch or --i2p-sam-port=, not both")
if o["i2p-launch"]:
raise UsageError("--i2p-launch is under development")
class _CreateBaseOptions(BasedirOptions):
optFlags = [
("hide-ip", None, "prohibit any configuration that would reveal the node's IP address"),
@ -119,28 +142,30 @@ class CreateClientOptions(_CreateBaseOptions):
class CreateNodeOptions(CreateClientOptions):
optFlags = [
("no-storage", None, "Do not offer storage service to other nodes."),
] + TOR_FLAGS
] + TOR_FLAGS + I2P_FLAGS
synopsis = "[options] [NODEDIR]"
description = "Create a full Tahoe-LAFS node (client+server)."
optParameters = CreateClientOptions.optParameters + WHERE_OPTS + TOR_OPTS
optParameters = CreateClientOptions.optParameters + WHERE_OPTS + TOR_OPTS + I2P_OPTS
def parseArgs(self, basedir=None):
CreateClientOptions.parseArgs(self, basedir)
validate_where_options(self)
validate_tor_options(self)
validate_i2p_options(self)
class CreateIntroducerOptions(NoDefaultBasedirOptions):
subcommand_name = "create-introducer"
description = "Create a Tahoe-LAFS introducer."
optFlags = [
("hide-ip", None, "prohibit any configuration that would reveal the node's IP address"),
] + TOR_FLAGS
optParameters = NoDefaultBasedirOptions.optParameters + WHERE_OPTS + TOR_OPTS
] + TOR_FLAGS + I2P_FLAGS
optParameters = NoDefaultBasedirOptions.optParameters + WHERE_OPTS + TOR_OPTS + I2P_OPTS
def parseArgs(self, basedir=None):
NoDefaultBasedirOptions.parseArgs(self, basedir)
validate_where_options(self)
validate_tor_options(self)
validate_i2p_options(self)
@defer.inlineCallbacks
def write_node_config(c, config):
@ -177,6 +202,7 @@ def write_node_config(c, config):
listeners = config['listen'].split(",")
tor_config = {}
i2p_config = {}
tub_ports = []
tub_locations = []
if listeners == ["none"]:
@ -189,8 +215,10 @@ def write_node_config(c, config):
tub_ports.append(tor_port)
tub_locations.append(tor_location)
if "i2p" in listeners:
raise NotImplementedError("--listen=i2p is under development, "
"see ticket #2490 for details")
(i2p_config, i2p_port, i2p_location) = \
yield i2p_provider.create_dest(reactor, config)
tub_ports.append(i2p_port)
tub_locations.append(i2p_location)
if "tcp" in listeners:
if config["port"]: # --port/--location are a pair
tub_ports.append(config["port"].encode('utf-8'))
@ -219,6 +247,12 @@ def write_node_config(c, config):
c.write("%s = %s\n" % (key, value))
c.write("\n")
if i2p_config:
c.write("[i2p]\n")
for key, value in i2p_config.items():
c.write("%s = %s\n" % (key, value))
c.write("\n")
def write_client_config(c, config):
c.write("[client]\n")

View File

@ -156,14 +156,6 @@ class Config(unittest.TestCase):
basedir)
self.assertEqual(str(e), "--listen= must be none, or one/some of: tcp, tor, i2p")
@defer.inlineCallbacks
def test_node_listen_i2p(self):
basedir = self.mktemp()
d = run_cli("create-node", "--listen=i2p", basedir)
e = yield self.assertFailure(d, NotImplementedError)
self.assertEqual(str(e), "--listen=i2p is under development, "
"see ticket #2490 for details")
def test_node_listen_tor_hostname(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
@ -208,6 +200,19 @@ class Config(unittest.TestCase):
self.assertIn("Node created", out)
self.assertEqual(err, "")
@defer.inlineCallbacks
def test_node_slow_i2p(self):
basedir = self.mktemp()
d = defer.Deferred()
with mock.patch("allmydata.util.i2p_provider.create_dest",
return_value=d):
d2 = run_cli("create-node", "--listen=i2p", basedir)
d.callback(({}, "port", "location"))
rc, out, err = yield d2
self.assertEqual(rc, 0)
self.assertIn("Node created", out)
self.assertEqual(err, "")
def test_introducer_no_hostname(self):
basedir = self.mktemp()
e = self.assertRaises(usage.UsageError, parse_cli,
@ -317,3 +322,68 @@ class Tor(unittest.TestCase):
"create-node", "--listen=none",
"--tor-control-port=foo")
self.assertEqual(str(e), "--tor-control-port= requires --listen=tor")
class I2P(unittest.TestCase):
def test_default(self):
basedir = self.mktemp()
i2p_config = {"abc": "def"}
i2p_port = "ghi"
i2p_location = "jkl"
dest_d = defer.succeed( (i2p_config, i2p_port, i2p_location) )
with mock.patch("allmydata.util.i2p_provider.create_dest",
return_value=dest_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=i2p", basedir))
self.assertEqual(len(co.mock_calls), 1)
args = co.mock_calls[0][1]
self.assertIdentical(args[0], reactor)
self.assertIsInstance(args[1], create_node.CreateNodeOptions)
self.assertEqual(args[1]["listen"], "i2p")
cfg = read_config(basedir)
self.assertEqual(cfg.get("i2p", "abc"), "def")
self.assertEqual(cfg.get("node", "tub.port"), "ghi")
self.assertEqual(cfg.get("node", "tub.location"), "jkl")
def test_launch(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--listen=i2p", "--i2p-launch")
self.assertEqual(str(e), "--i2p-launch is under development")
def test_sam_port(self):
basedir = self.mktemp()
i2p_config = {"abc": "def"}
i2p_port = "ghi"
i2p_location = "jkl"
dest_d = defer.succeed( (i2p_config, i2p_port, i2p_location) )
with mock.patch("allmydata.util.i2p_provider.create_dest",
return_value=dest_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=i2p", "--i2p-sam-port=mno",
basedir))
args = co.mock_calls[0][1]
self.assertEqual(args[1]["listen"], "i2p")
self.assertEqual(args[1]["i2p-launch"], False)
self.assertEqual(args[1]["i2p-sam-port"], "mno")
def test_not_both(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--listen=i2p",
"--i2p-launch", "--i2p-sam-port=foo")
self.assertEqual(str(e), "use either --i2p-launch or"
" --i2p-sam-port=, not both")
def test_launch_without_listen(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--listen=none", "--i2p-launch")
self.assertEqual(str(e), "--i2p-launch requires --listen=i2p")
def test_sam_port_without_listen(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--listen=none",
"--i2p-sam-port=foo")
self.assertEqual(str(e), "--i2p-sam-port= requires --listen=i2p")

View File

@ -15,6 +15,7 @@ class FakeNode(Node):
self._reveal_ip = True
self.basedir = "BASEDIR"
self.services = []
self.create_i2p_provider()
self.create_tor_provider()
BASECONFIG = ("[client]\n"
@ -141,8 +142,9 @@ class I2P(unittest.TestCase):
self.assertEqual(h, None)
def test_unimportable(self):
n = FakeNode(BASECONFIG)
with mock.patch("allmydata.node._import_i2p", return_value=None):
with mock.patch("allmydata.util.i2p_provider._import_i2p",
return_value=None):
n = FakeNode(BASECONFIG)
h = n._make_i2p_handler()
self.assertEqual(h, None)

View File

@ -0,0 +1,356 @@
import os
from twisted.trial import unittest
from twisted.internet import defer, error
from twisted.python.usage import UsageError
from StringIO import StringIO
import mock
from ..util import i2p_provider
from ..scripts import create_node, runner
def mock_txi2p(txi2p):
return mock.patch("allmydata.util.i2p_provider._import_txi2p",
return_value=txi2p)
def mock_i2p(i2p):
return mock.patch("allmydata.util.i2p_provider._import_i2p",
return_value=i2p)
def make_cli_config(basedir, *argv):
parent = runner.Options()
cli_config = create_node.CreateNodeOptions()
cli_config.parent = parent
cli_config.parseOptions(argv)
cli_config["basedir"] = basedir
cli_config.stdout = StringIO()
return cli_config
class TryToConnect(unittest.TestCase):
def test_try(self):
reactor = object()
txi2p = mock.Mock()
d = defer.succeed(True)
txi2p.testAPI = mock.Mock(return_value=d)
ep = object()
stdout = StringIO()
with mock.patch("allmydata.util.i2p_provider.clientFromString",
return_value=ep) as cfs:
d = i2p_provider._try_to_connect(reactor, "desc", stdout, txi2p)
r = self.successResultOf(d)
self.assertTrue(r)
cfs.assert_called_with(reactor, "desc")
txi2p.testAPI.assert_called_with(reactor, 'SAM', ep)
def test_try_handled_error(self):
reactor = object()
txi2p = mock.Mock()
d = defer.fail(error.ConnectError("oops"))
txi2p.testAPI = mock.Mock(return_value=d)
ep = object()
stdout = StringIO()
with mock.patch("allmydata.util.i2p_provider.clientFromString",
return_value=ep) as cfs:
d = i2p_provider._try_to_connect(reactor, "desc", stdout, txi2p)
r = self.successResultOf(d)
self.assertIs(r, None)
cfs.assert_called_with(reactor, "desc")
txi2p.testAPI.assert_called_with(reactor, 'SAM', ep)
self.assertEqual(stdout.getvalue(),
"Unable to reach I2P SAM API at 'desc': "
"An error occurred while connecting: oops.\n")
def test_try_unhandled_error(self):
reactor = object()
txi2p = mock.Mock()
d = defer.fail(ValueError("oops"))
txi2p.testAPI = mock.Mock(return_value=d)
ep = object()
stdout = StringIO()
with mock.patch("allmydata.util.i2p_provider.clientFromString",
return_value=ep) as cfs:
d = i2p_provider._try_to_connect(reactor, "desc", stdout, txi2p)
f = self.failureResultOf(d)
self.assertIsInstance(f.value, ValueError)
self.assertEqual(str(f.value), "oops")
cfs.assert_called_with(reactor, "desc")
txi2p.testAPI.assert_called_with(reactor, 'SAM', ep)
self.assertEqual(stdout.getvalue(), "")
class ConnectToI2P(unittest.TestCase):
def _do_test_connect(self, endpoint, reachable):
reactor = object()
txi2p = object()
args = []
if endpoint:
args = ["--i2p-sam-port=%s" % endpoint]
cli_config = make_cli_config("basedir", "--listen=i2p", *args)
stdout = cli_config.stdout
expected_port = "tcp:127.0.0.1:7656"
if endpoint:
expected_port = endpoint
tried = []
def _try_to_connect(reactor, port, stdout, txi2p):
tried.append( (reactor, port, stdout, txi2p) )
if not reachable:
return defer.succeed(None)
if port == expected_port:
return defer.succeed(True)
return defer.succeed(None)
with mock.patch("allmydata.util.i2p_provider._try_to_connect",
_try_to_connect):
d = i2p_provider._connect_to_i2p(reactor, cli_config, txi2p)
if not reachable:
f = self.failureResultOf(d)
self.assertIsInstance(f.value, ValueError)
self.assertEqual(str(f.value),
"unable to reach any default I2P SAM port")
return
successful_port = self.successResultOf(d)
self.assertEqual(successful_port, expected_port)
expected = [(reactor, "tcp:127.0.0.1:7656", stdout, txi2p)]
if endpoint:
expected = [(reactor, endpoint, stdout, txi2p)]
self.assertEqual(tried, expected)
def test_connect(self):
return self._do_test_connect(None, True)
def test_connect_endpoint(self):
return self._do_test_connect("tcp:other:port", True)
def test_connect_unreachable(self):
return self._do_test_connect(None, False)
class CreateDest(unittest.TestCase):
def test_no_txi2p(self):
with mock.patch("allmydata.util.i2p_provider._import_txi2p",
return_value=None):
d = i2p_provider.create_dest("reactor", "cli_config")
f = self.failureResultOf(d)
self.assertIsInstance(f.value, ValueError)
self.assertEqual(str(f.value),
"Cannot create I2P Destination without txi2p. "
"Please 'pip install tahoe-lafs[i2p]' to fix this.")
def _do_test_launch(self, executable):
basedir = self.mktemp()
os.mkdir(basedir)
args = ["--listen=i2p", "--i2p-launch"]
if executable:
args.append("--i2p-executable=%s" % executable)
self.assertRaises(UsageError, make_cli_config, basedir, *args)
def test_launch(self):
return self._do_test_launch(None)
def test_launch_executable(self):
return self._do_test_launch("myi2p")
def test_sam_endpoint(self):
basedir = self.mktemp()
os.mkdir(basedir)
private_dir = os.path.join(basedir, "private")
os.mkdir(private_dir)
privkeyfile = os.path.abspath(os.path.join(private_dir, "i2p_dest.privkey"))
reactor = object()
cli_config = make_cli_config(basedir, "--listen=i2p")
connect_to_i2p = mock.Mock(return_value=defer.succeed("goodport"))
txi2p = mock.Mock()
ep = object()
dest = mock.Mock()
dest.host = "FOOBAR.b32.i2p"
txi2p.generateDestination = mock.Mock(return_value=defer.succeed(dest))
with mock_txi2p(txi2p):
with mock.patch("allmydata.util.i2p_provider._connect_to_i2p",
connect_to_i2p):
with mock.patch("allmydata.util.i2p_provider.clientFromString",
return_value=ep) as cfs:
d = i2p_provider.create_dest(reactor, cli_config)
tahoe_config_i2p, i2p_port, i2p_location = self.successResultOf(d)
connect_to_i2p.assert_called_with(reactor, cli_config, txi2p)
cfs.assert_called_with(reactor, "goodport")
txi2p.generateDestination.assert_called_with(reactor, privkeyfile, 'SAM', ep)
expected = {"sam.port": "goodport",
"dest": "true",
"dest.port": "3457",
"dest.private_key_file": os.path.join("private",
"i2p_dest.privkey"),
}
self.assertEqual(tahoe_config_i2p, expected)
self.assertEqual(i2p_port, "i2p:%s:3457:api=SAM:apiEndpoint=goodport" % privkeyfile)
self.assertEqual(i2p_location, "i2p:FOOBAR.b32.i2p:3457")
_None = object()
class FakeConfig(dict):
def get_config(self, section, option, default=_None, boolean=False):
if section != "i2p":
raise ValueError(section)
value = self.get(option, default)
if value is _None:
raise KeyError
return value
class Provider(unittest.TestCase):
def test_build(self):
i2p_provider.Provider("basedir", FakeConfig(), "reactor")
def test_handler_disabled(self):
p = i2p_provider.Provider("basedir", FakeConfig(enabled=False),
"reactor")
self.assertEqual(p.get_i2p_handler(), None)
def test_handler_no_i2p(self):
with mock_i2p(None):
p = i2p_provider.Provider("basedir", FakeConfig(), "reactor")
self.assertEqual(p.get_i2p_handler(), None)
def test_handler_sam_endpoint(self):
i2p = mock.Mock()
handler = object()
i2p.sam_endpoint = mock.Mock(return_value=handler)
ep = object()
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(**{"sam.port": "ep_desc"}),
reactor)
with mock.patch("allmydata.util.i2p_provider.clientFromString",
return_value=ep) as cfs:
h = p.get_i2p_handler()
cfs.assert_called_with(reactor, "ep_desc")
self.assertIs(h, handler)
i2p.sam_endpoint.assert_called_with(ep)
def test_handler_launch(self):
i2p = mock.Mock()
handler = object()
i2p.launch = mock.Mock(return_value=handler)
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir", FakeConfig(launch=True),
reactor)
h = p.get_i2p_handler()
self.assertIs(h, handler)
i2p.launch.assert_called_with(i2p_configdir=None, i2p_binary=None)
def test_handler_launch_configdir(self):
i2p = mock.Mock()
handler = object()
i2p.launch = mock.Mock(return_value=handler)
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(launch=True,
**{"i2p.configdir": "configdir"}),
reactor)
h = p.get_i2p_handler()
self.assertIs(h, handler)
i2p.launch.assert_called_with(i2p_configdir="configdir", i2p_binary=None)
def test_handler_launch_configdir_executable(self):
i2p = mock.Mock()
handler = object()
i2p.launch = mock.Mock(return_value=handler)
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(launch=True,
**{"i2p.configdir": "configdir",
"i2p.executable": "myi2p",
}),
reactor)
h = p.get_i2p_handler()
self.assertIs(h, handler)
i2p.launch.assert_called_with(i2p_configdir="configdir", i2p_binary="myi2p")
def test_handler_configdir(self):
i2p = mock.Mock()
handler = object()
i2p.local_i2p = mock.Mock(return_value=handler)
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir",
FakeConfig(**{"i2p.configdir": "configdir"}),
reactor)
h = p.get_i2p_handler()
i2p.local_i2p.assert_called_with("configdir")
self.assertIs(h, handler)
def test_handler_default(self):
i2p = mock.Mock()
handler = object()
i2p.default = mock.Mock(return_value=handler)
reactor = object()
with mock_i2p(i2p):
p = i2p_provider.Provider("basedir", FakeConfig(), reactor)
h = p.get_i2p_handler()
self.assertIs(h, handler)
i2p.default.assert_called_with(reactor)
class Provider_CheckI2PConfig(unittest.TestCase):
def test_default(self):
# default config doesn't start an I2P service, so it should be
# happy both with and without txi2p
p = i2p_provider.Provider("basedir", FakeConfig(), "reactor")
p.check_dest_config()
with mock_txi2p(None):
p = i2p_provider.Provider("basedir", FakeConfig(), "reactor")
p.check_dest_config()
def test_no_txi2p(self):
with mock_txi2p(None):
p = i2p_provider.Provider("basedir", FakeConfig(dest=True),
"reactor")
e = self.assertRaises(ValueError, p.check_dest_config)
self.assertEqual(str(e), "Cannot create I2P Destination without txi2p. "
"Please 'pip install tahoe-lafs[i2p]' to fix.")
def test_no_launch_no_control(self):
p = i2p_provider.Provider("basedir", FakeConfig(dest=True), "reactor")
e = self.assertRaises(ValueError, p.check_dest_config)
self.assertEqual(str(e), "[i2p] dest = true, but we have neither "
"sam.port= nor launch=true nor configdir=")
def test_missing_keys(self):
p = i2p_provider.Provider("basedir", FakeConfig(dest=True,
**{"sam.port": "x",
}), "reactor")
e = self.assertRaises(ValueError, p.check_dest_config)
self.assertEqual(str(e), "[i2p] dest = true, "
"but dest.port= is missing")
p = i2p_provider.Provider("basedir",
FakeConfig(dest=True,
**{"sam.port": "x",
"dest.port": "y",
}), "reactor")
e = self.assertRaises(ValueError, p.check_dest_config)
self.assertEqual(str(e), "[i2p] dest = true, "
"but dest.private_key_file= is missing")
def test_launch_not_implemented(self):
p = i2p_provider.Provider("basedir",
FakeConfig(dest=True, launch=True,
**{"dest.port": "x",
"dest.private_key_file": "y",
}), "reactor")
e = self.assertRaises(NotImplementedError, p.check_dest_config)
self.assertEqual(str(e), "[i2p] launch is under development.")
def test_ok(self):
p = i2p_provider.Provider("basedir",
FakeConfig(dest=True,
**{"sam.port": "x",
"dest.port": "y",
"dest.private_key_file": "z",
}), "reactor")
p.check_dest_config()

View File

@ -355,6 +355,7 @@ class MultiplePorts(unittest.TestCase):
n.read_config()
n.check_privacy()
n.services = []
n.create_i2p_provider()
n.create_tor_provider()
n.init_connections()
n.set_tub_options()

View File

@ -0,0 +1,202 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, with_statement
import os
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.internet.endpoints import clientFromString
from twisted.internet.error import ConnectionRefusedError, ConnectError
from twisted.application import service
def _import_i2p():
# this exists to be overridden by unit tests
try:
from foolscap.connections import i2p
return i2p
except ImportError: # pragma: no cover
return None
def _import_txi2p():
try:
import txi2p
return txi2p
except ImportError: # pragma: no cover
return None
def _try_to_connect(reactor, endpoint_desc, stdout, txi2p):
# yields True or None
ep = clientFromString(reactor, endpoint_desc)
d = txi2p.testAPI(reactor, 'SAM', ep)
def _failed(f):
# depending upon what's listening at that endpoint, we might get
# various errors. If this list is too short, we might expose an
# exception to the user (causing "tahoe create-node" to fail messily)
# when we're supposed to just try the next potential port instead.
# But I don't want to catch everything, because that may hide actual
# coding errors.
f.trap(ConnectionRefusedError, # nothing listening on TCP
ConnectError, # missing unix socket, or permission denied
#ValueError,
# connecting to e.g. an HTTP server causes an
# UnhandledException (around a ValueError) when the handshake
# fails to parse, but that's not something we can catch. The
# attempt hangs, so don't do that.
RuntimeError, # authentication failure
)
if stdout:
stdout.write("Unable to reach I2P SAM API at '%s': %s\n" %
(endpoint_desc, f.value))
return None
d.addErrback(_failed)
return d
@inlineCallbacks
def _connect_to_i2p(reactor, cli_config, txi2p):
# we assume i2p is already running
ports_to_try = ["tcp:127.0.0.1:7656"]
if cli_config["i2p-sam-port"]:
ports_to_try = [cli_config["i2p-sam-port"]]
for port in ports_to_try:
accessible = yield _try_to_connect(reactor, port, cli_config.stdout,
txi2p)
if accessible:
returnValue(port) ; break # helps editor
else:
raise ValueError("unable to reach any default I2P SAM port")
@inlineCallbacks
def create_dest(reactor, cli_config):
txi2p = _import_txi2p()
if not txi2p:
raise ValueError("Cannot create I2P Destination without txi2p. "
"Please 'pip install tahoe-lafs[i2p]' to fix this.")
tahoe_config_i2p = {} # written into tahoe.cfg:[i2p]
private_dir = os.path.abspath(os.path.join(cli_config["basedir"], "private"))
stdout = cli_config.stdout
if cli_config["i2p-launch"]:
raise NotImplementedError("--i2p-launch is under development.")
else:
print("connecting to I2P (to allocate .i2p address)..", file=stdout)
sam_port = yield _connect_to_i2p(reactor, cli_config, txi2p)
print("I2P connection established", file=stdout)
tahoe_config_i2p["sam.port"] = sam_port
external_port = 3457 # TODO: pick this randomly? there's no contention.
privkeyfile = os.path.join(private_dir, "i2p_dest.privkey")
sam_endpoint = clientFromString(reactor, sam_port)
print("allocating .i2p address...", file=stdout)
dest = yield txi2p.generateDestination(reactor, privkeyfile, 'SAM', sam_endpoint)
print(".i2p address allocated", file=stdout)
escaped_sam_port = sam_port.replace(':', '\:')
i2p_port = "i2p:%s:%d:api=SAM:apiEndpoint=%s" % \
(privkeyfile, external_port, escaped_sam_port)
i2p_location = "i2p:%s:%d" % (dest.host, external_port)
# in addition to the "how to launch/connect-to i2p" keys above, we also
# record information about the I2P service into tahoe.cfg.
# * "port" is the random "public Destination port" (integer), which
# (when combined with the .i2p address) should match "i2p_location"
# (which will be added to tub.location)
# * "private_key_file" points to the on-disk copy of the private key
# material (although we always write it to the same place)
tahoe_config_i2p["dest"] = "true"
tahoe_config_i2p["dest.port"] = str(external_port)
tahoe_config_i2p["dest.private_key_file"] = os.path.join("private",
"i2p_dest.privkey")
# tahoe_config_i2p: this is a dictionary of keys/values to add to the
# "[i2p]" section of tahoe.cfg, which tells the new node how to launch
# I2P in the right way.
# i2p_port: a server endpoint string, it will be added to tub.port=
# i2p_location: a foolscap connection hint, "i2p:B32_ADDR:PORT"
# We assume/require that the Node gives us the same data_directory=
# at both create-node and startup time. The data directory is not
# recorded in tahoe.cfg
returnValue((tahoe_config_i2p, i2p_port, i2p_location))
# we can always create a Provider. If foolscap.connections.i2p or txi2p
# are not installed, then get_i2p_handler() will return None. If tahoe.cfg
# wants to start an I2P Destination too, then check_dest_config() will throw
# a nice error, and startService will throw an ugly error.
class Provider(service.MultiService):
def __init__(self, basedir, node_for_config, reactor):
service.MultiService.__init__(self)
self._basedir = basedir
self._node_for_config = node_for_config
self._i2p = _import_i2p()
self._txi2p = _import_txi2p()
self._reactor = reactor
def _get_i2p_config(self, *args, **kwargs):
return self._node_for_config.get_config("i2p", *args, **kwargs)
def get_i2p_handler(self):
enabled = self._get_i2p_config("enabled", True, boolean=True)
if not enabled:
return None
if not self._i2p:
return None
sam_port = self._get_i2p_config("sam.port", None)
launch = self._get_i2p_config("launch", False, boolean=True)
configdir = self._get_i2p_config("i2p.configdir", None)
if sam_port:
if launch:
raise ValueError("tahoe.cfg [i2p] must not set both "
"sam.port and launch")
ep = clientFromString(self._reactor, sam_port)
return self._i2p.sam_endpoint(ep)
if launch:
executable = self._get_i2p_config("i2p.executable", None)
return self._i2p.launch(i2p_configdir=configdir, i2p_binary=executable)
if configdir:
return self._i2p.local_i2p(configdir)
return self._i2p.default(self._reactor)
def check_dest_config(self):
if self._get_i2p_config("dest", False, boolean=True):
if not self._txi2p:
raise ValueError("Cannot create I2P Destination without txi2p. "
"Please 'pip install tahoe-lafs[i2p]' to fix.")
# to start an I2P server, we either need an I2P SAM port, or
# we need to launch I2P
sam_port = self._get_i2p_config("sam.port", None)
launch = self._get_i2p_config("launch", False, boolean=True)
configdir = self._get_i2p_config("i2p.configdir", None)
if not sam_port and not launch and not configdir:
raise ValueError("[i2p] dest = true, but we have neither "
"sam.port= nor launch=true nor configdir=")
if sam_port and launch:
raise ValueError("tahoe.cfg [i2p] must not set both "
"sam.port and launch")
if launch:
raise NotImplementedError("[i2p] launch is under development.")
# check that all the expected Destination-specific keys are present
def require(name):
if not self._get_i2p_config("dest.%s" % name, None):
raise ValueError("[i2p] dest = true,"
" but dest.%s= is missing" % name)
require("port")
require("private_key_file")
def startService(self):
service.MultiService.startService(self)
# if we need to start I2P, now is the time
# TODO: implement i2p launching
@inlineCallbacks
def stopService(self):
# TODO: can we also stop i2p?
yield service.MultiService.stopService(self)