create_node.py: use tor_provider to handle --listen=tor

This adds tor-related CLI arguments to "create-node" and
"create-introducer", to control exactly how we should be using Tor.

* --tor-launch
* --tor-executable=
* --tor-control-port=

I went with "--tor-launch" instead of "--launch-tor" for consistency. I
don't particularly like the grammatical flow of it, and it doesn't
actually put all the tor-related arguments next to each other in the
--help output (the flags are put in one block, then the parameters in
the next). But it seems slightly more consistent to start all the
tor-related argument names with a "--tor*" prefix.
This commit is contained in:
Brian Warner 2016-10-09 01:02:28 -04:00
parent a1741ce4dc
commit 6b9218ff22
2 changed files with 123 additions and 25 deletions

View File

@ -1,11 +1,11 @@
import os
from twisted.internet import defer
from twisted.internet import reactor, defer
from twisted.python.usage import UsageError
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
from allmydata.util import fileutil, iputil, tor_provider
dummy_tac = """
@ -33,10 +33,12 @@ WHERE_OPTS = [
TOR_OPTS = [
("tor-control-port", None, None,
"Tor's control port endpoint descriptor string (e.g. tcp:127.0.0.1:9051 or unix:/var/run/tor/control)"),
("tor-executable", None, None,
"The 'tor' executable to run (default is to search $PATH)."),
]
TOR_FLAG = [
("launch-tor", None, "Launch a tor instead of connecting to a tor control port."),
TOR_FLAGS = [
("tor-launch", None, "Launch a tor instead of connecting to a tor control port."),
]
def validate_where_options(o):
@ -77,6 +79,16 @@ def validate_where_options(o):
if 'tcp' not in listeners and o['hostname']:
raise UsageError("--listen= must be tcp to use --hostname")
def validate_tor_options(o):
use_tor = "tor" in o["listen"].split(",")
if not use_tor:
if o["tor-launch"]:
raise UsageError("--tor-launch requires --listen=tor")
if o["tor-control-port"]:
raise UsageError("--tor-control-port= requires --listen=tor")
if o["tor-launch"] and o["tor-control-port"]:
raise UsageError("use either --tor-launch or --tor-control-port=, not both")
class _CreateBaseOptions(BasedirOptions):
optFlags = [
("hide-ip", None, "prohibit any configuration that would reveal the node's IP address"),
@ -107,7 +119,7 @@ class CreateClientOptions(_CreateBaseOptions):
class CreateNodeOptions(CreateClientOptions):
optFlags = [
("no-storage", None, "Do not offer storage service to other nodes."),
] + TOR_FLAG
] + TOR_FLAGS
synopsis = "[options] [NODEDIR]"
description = "Create a full Tahoe-LAFS node (client+server)."
@ -116,17 +128,19 @@ class CreateNodeOptions(CreateClientOptions):
def parseArgs(self, basedir=None):
CreateClientOptions.parseArgs(self, basedir)
validate_where_options(self)
validate_tor_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_FLAG
] + TOR_FLAGS
optParameters = NoDefaultBasedirOptions.optParameters + WHERE_OPTS + TOR_OPTS
def parseArgs(self, basedir=None):
NoDefaultBasedirOptions.parseArgs(self, basedir)
validate_where_options(self)
validate_tor_options(self)
@defer.inlineCallbacks
def write_node_config(c, config):
@ -161,6 +175,8 @@ def write_node_config(c, config):
c.write("web.static = public_html\n")
listeners = config['listen'].split(",")
tor_config = {}
tub_ports = []
tub_locations = []
if listeners == ["none"]:
@ -168,15 +184,10 @@ def write_node_config(c, config):
c.write("tub.location = disabled\n")
else:
if "tor" in listeners:
key_file = "default.onion_key"
onion_port = 3456
c.write("[tor]\n")
c.write("onion.external_port = %s\n" % onion_port)
c.write("onion.private_key_file = %s\n" % key_file)
#tor_provider = TorProvider(tor_binary = )
# XXX fix me
tor_provider = TorProvider()
yield CreateOnion(tor_provider,key_file, onion_port)
(tor_config, tor_port, tor_location) = \
yield tor_provider.create_onion(reactor, 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")
@ -201,7 +212,13 @@ def write_node_config(c, config):
c.write("#ssh.port = 8022\n")
c.write("#ssh.authorized_keys_file = ~/.ssh/authorized_keys\n")
c.write("\n")
yield None
if tor_config:
c.write("[tor]\n")
for key, value in tor_config.items():
c.write("%s = %s\n" % (key, value))
c.write("\n")
def write_client_config(c, config):
c.write("[client]\n")

View File

@ -1,9 +1,11 @@
import os
import mock
from twisted.trial import unittest
from twisted.internet import defer
from twisted.internet import defer, reactor
from twisted.python import usage
from allmydata.util import configutil
from ..common_util import run_cli, parse_cli
from ...scripts import create_node
def read_config(basedir):
tahoe_cfg = os.path.join(basedir, "tahoe.cfg")
@ -154,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_tor(self):
basedir = self.mktemp()
d = run_cli("create-node", "--listen=tor", basedir)
e = yield self.assertFailure(d, NotImplementedError)
self.assertEqual(str(e), "--listen=tor is under development, "
"see ticket #2490 for details")
@defer.inlineCallbacks
def test_node_listen_i2p(self):
basedir = self.mktemp()
@ -201,6 +195,19 @@ class Config(unittest.TestCase):
self.assertIn("is not empty", err)
self.assertIn("To avoid clobbering anything, I am going to quit now", err)
@defer.inlineCallbacks
def test_node_slow_tor(self):
basedir = self.mktemp()
d = defer.Deferred()
with mock.patch("allmydata.util.tor_provider.create_onion",
return_value=d):
d2 = run_cli("create-node", "--listen=tor", 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,
@ -236,3 +243,77 @@ class Config(unittest.TestCase):
self.assertIn(basedir, err)
self.assertIn("is not empty", err)
self.assertIn("To avoid clobbering anything, I am going to quit now", err)
class Tor(unittest.TestCase):
def test_default(self):
basedir = self.mktemp()
tor_config = {"abc": "def"}
tor_port = "ghi"
tor_location = "jkl"
onion_d = defer.succeed( (tor_config, tor_port, tor_location) )
with mock.patch("allmydata.util.tor_provider.create_onion",
return_value=onion_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=tor", 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"], "tor")
cfg = read_config(basedir)
self.assertEqual(cfg.get("tor", "abc"), "def")
self.assertEqual(cfg.get("node", "tub.port"), "ghi")
self.assertEqual(cfg.get("node", "tub.location"), "jkl")
def test_launch(self):
basedir = self.mktemp()
tor_config = {"abc": "def"}
tor_port = "ghi"
tor_location = "jkl"
onion_d = defer.succeed( (tor_config, tor_port, tor_location) )
with mock.patch("allmydata.util.tor_provider.create_onion",
return_value=onion_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=tor", "--tor-launch",
basedir))
args = co.mock_calls[0][1]
self.assertEqual(args[1]["listen"], "tor")
self.assertEqual(args[1]["tor-launch"], True)
self.assertEqual(args[1]["tor-control-port"], None)
def test_control_port(self):
basedir = self.mktemp()
tor_config = {"abc": "def"}
tor_port = "ghi"
tor_location = "jkl"
onion_d = defer.succeed( (tor_config, tor_port, tor_location) )
with mock.patch("allmydata.util.tor_provider.create_onion",
return_value=onion_d) as co:
rc, out, err = self.successResultOf(
run_cli("create-node", "--listen=tor", "--tor-control-port=mno",
basedir))
args = co.mock_calls[0][1]
self.assertEqual(args[1]["listen"], "tor")
self.assertEqual(args[1]["tor-launch"], False)
self.assertEqual(args[1]["tor-control-port"], "mno")
def test_not_both(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--listen=tor",
"--tor-launch", "--tor-control-port=foo")
self.assertEqual(str(e), "use either --tor-launch or"
" --tor-control-port=, not both")
def test_launch_without_listen(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--listen=none", "--tor-launch")
self.assertEqual(str(e), "--tor-launch requires --listen=tor")
def test_control_port_without_listen(self):
e = self.assertRaises(usage.UsageError,
parse_cli,
"create-node", "--listen=none",
"--tor-control-port=foo")
self.assertEqual(str(e), "--tor-control-port= requires --listen=tor")