Skip to content
Commits on Source (7)
.PHONY: test html counts coverage sdist clean install doc integration diagrams
default: test
VERSION = 18.0.2
VERSION = 18.3.0
test:
PYTHONPATH=. trial --reporter=text test
......@@ -112,11 +112,11 @@ dist/txtorcon-${VERSION}-py2.py3-none-any.whl:
python setup.py bdist_wheel --universal
dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc: dist/txtorcon-${VERSION}-py2.py3-none-any.whl
gpg --verify dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc || gpg --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}-py2.py3-none-any.whl
gpg --verify dist/txtorcon-${VERSION}-py2.py3-none-any.whl.asc || gpg --pinentry loopback --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}-py2.py3-none-any.whl
dist/txtorcon-${VERSION}.tar.gz: sdist
dist/txtorcon-${VERSION}.tar.gz.asc: dist/txtorcon-${VERSION}.tar.gz
gpg --verify dist/txtorcon-${VERSION}.tar.gz.asc || gpg --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}.tar.gz
gpg --verify dist/txtorcon-${VERSION}.tar.gz.asc || gpg --pinentry loopback --no-version --detach-sign --armor --local-user meejah@meejah.ca dist/txtorcon-${VERSION}.tar.gz
release:
twine upload -r pypi -c "txtorcon v${VERSION} tarball" dist/txtorcon-${VERSION}.tar.gz dist/txtorcon-${VERSION}.tar.gz.asc
......
Metadata-Version: 2.1
Name: txtorcon
Version: 18.0.2
Version: 18.3.0
Summary:
Twisted-based Tor controller client, with state-tracking and
configuration abstractions.
......
txtorcon (18.3.0-1) unstable; urgency=medium
* New upstream version 18.3.0
* Refreshed patches
-- Iain R. Learmonth <irl@debian.org> Mon, 07 Jan 2019 12:09:47 +0000
txtorcon (18.0.2-2) unstable; urgency=medium
* No longer builds a Python 2 package (Closes: #905253)
......
......@@ -72,8 +72,8 @@ consults local documentation, which shouldn't happen.
- .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.svg
- :target: https://coveralls.io/r/meejah/txtorcon
-
- .. image:: http://codecov.io/github/meejah/txtorcon/coverage.svg?branch=master
- :target: http://codecov.io/github/meejah/txtorcon?branch=master
- .. image:: https://codecov.io/gh/meejah/txtorcon/branch/master/graphs/badge.svg?branch=master
- :target: https://codecov.io/github/meejah/txtorcon?branch=master
-
- .. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable
- :target: https://txtorcon.readthedocs.io/en/stable
......
......@@ -103,9 +103,10 @@ will fire with a :class:`.Tor` instance. If you need access to the
:class:`.TorControlProtocol` instance, it's available via the
``.protocol`` property (there is always exactly one of these per
:class:`.Tor` instance). Similarly, the current configuration is
available via ``.config``. You can change the configuration by
updating attributes on this class but it won't take effect until you
call :meth:`.TorConfig.save`.
available via ``.get_config`` (which returns a Deferred firing a
:class:`.TorConfig`). You can change the configuration by updating
attributes on this class but it won't take effect until you call
:meth:`.TorConfig.save`.
Launching a New Tor
......
......@@ -16,8 +16,8 @@ txtorcon
.. image:: https://coveralls.io/repos/meejah/txtorcon/badge.svg
:target: https://coveralls.io/r/meejah/txtorcon
.. image:: http://codecov.io/github/meejah/txtorcon/coverage.svg?branch=master
:target: http://codecov.io/github/meejah/txtorcon?branch=master
.. image:: https://codecov.io/gh/meejah/txtorcon/branch/master/graphs/badge.svg?branch=master
:target: https://codecov.io/github/meejah/txtorcon?branch=master
.. image:: https://readthedocs.org/projects/txtorcon/badge/?version=stable
:target: https://txtorcon.readthedocs.io/en/stable
......
......@@ -10,7 +10,7 @@ Release Checklist
* txtorcon/_metadata.py
* run all tests, on all configurations
* "tox"
* "detox"
* ensure long_description will render properly:
* python setup.py check -r -s
......
......@@ -21,6 +21,41 @@ unreleased
`git master <https://github.com/meejah/txtorcon>`_ *will likely become v19.0.0*
v18.3.0
-------
* `txtorcon-18.3.0.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-18.3.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/18.3.0>`_ (:download:`local-sig </../signatues/txtorcon-18.3.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-18.3.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v18.3.0.tar.gz>`_)
* add `singleHop={true,false}` for endpoint-strings as well
v18.2.0
-------
* `txtorcon-18.2.0.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-18.2.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/18.2.0>`_ (:download:`local-sig </../signatues/txtorcon-18.2.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-18.2.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v18.2.0.tar.gz>`_)
* add `privateKeyFile=` option to endpoint parser (ticket 313)
* use `privateKey=` option properly in endpoint parser
* support `NonAnonymous` mode for `ADD_ONION` via `single_hop=` kwarg
v18.1.0
-------
September 26, 2018
* `txtorcon-18.1.0.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-18.1.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/18.1.0>`_ (:download:`local-sig </../signatues/txtorcon-18.1.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-18.1.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v18.1.0.tar.gz>`_)
* better error-reporting (include REASON and REMOTE_REASON if
available) when circuit-builds fail (thanks `David Stainton
<https://github.com/david415>`_)
* more-robust detection of "do we have Python3" (thanks `Balint
Reczey <https://github.com/rbalint>`_)
* fix parsing of Unix-sockets for SOCKS
* better handling of concurrent Web agent requests before SOCKS ports
are known
* allow fowarding to ip:port pairs for Onion services when using the
"list of 2-tuples" method of specifying the remote vs local
connections.
v18.0.2
-------
......
#!/usr/bin/env python
# This shows how to leverage the endpoints API to get a new hidden
# service up and running quickly. You can pass along this API to your
# users by accepting endpoint strings as per Twisted recommendations.
#
# http://twistedmatrix.com/documents/current/core/howto/endpoints.html#maximizing-the-return-on-your-endpoint-investment
#
# note that only the progress-updates needs the "import txtorcon" --
# you do still need it installed so that Twisted finds the endpoint
# parser plugin but code without knowledge of txtorcon can still
# launch a Tor instance using it. cool!
from __future__ import print_function
from twisted.internet import defer, task, endpoints
from twisted.web import server, resource
import txtorcon
from txtorcon.util import default_control_port
from txtorcon.onion import AuthBasic
class Simple(resource.Resource):
"""
A really simple Web site.
"""
isLeaf = True
def render_GET(self, request):
return b"<html>Hello, world! I'm a single-hop hidden service!</html>"
@defer.inlineCallbacks
def main(reactor):
tor = yield txtorcon.connect(
reactor,
endpoints.TCP4ClientEndpoint(reactor, "localhost", 9251),
)
ep = endpoints.serverFromString(
reactor,
"onion:80:version=3:privateKeyFile=/home/mike/src/txtorcon/foodir/hs_ed25519_secret_key"
)
def on_progress(percent, tag, msg):
print('%03d: %s' % (percent, msg))
txtorcon.IProgressProvider(ep).add_progress_listener(on_progress)
print("Note: descriptor upload can take several minutes")
port = yield ep.listen(server.Site(Simple()))
print("Private key:\n{}".format(port.getHost().onion_key))
hs = port.onion_service
print("hs {}".format(hs))
print("{}".format(hs.hostname))
yield defer.Deferred() # wait forever
task.react(main)
#!/usr/bin/env python
# Here we use some very new Tor configuration options to set up a
# "single-hop" or "non-anonymous" onion service. These do NOT give the
# server location-privacy, so may be appropriate for certain kinds of
# services. Once you publish a service like this, there's no going
# back to location-hidden.
from __future__ import print_function
from twisted.internet import defer, task, endpoints
from twisted.web import server, resource
import txtorcon
from txtorcon.util import default_control_port
from txtorcon.onion import AuthBasic
class Simple(resource.Resource):
"""
A really simple Web site.
"""
isLeaf = True
def render_GET(self, request):
return b"<html>Hello, world! I'm a single-hop onion service!</html>"
@defer.inlineCallbacks
def main(reactor):
# For the "single_hop=True" below to work, the Tor we're
# connecting to must have the following options set:
# SocksPort 0
# HiddenServiceSingleHopMode 1
# HiddenServiceNonAnonymousMode 1
tor = yield txtorcon.connect(
reactor,
endpoints.TCP4ClientEndpoint(reactor, "localhost", 9351),
)
if False:
ep = tor.create_onion_endpoint(
80,
version=3,
single_hop=True,
)
else:
ep = endpoints.serverFromString(reactor, "onion:80:version=3:singleHop=true")
def on_progress(percent, tag, msg):
print('%03d: %s' % (percent, msg))
txtorcon.IProgressProvider(ep).add_progress_listener(on_progress)
port = yield ep.listen(server.Site(Simple()))
print("Private key:\n{}".format(port.getHost().onion_key))
hs = port.onion_service
print("Site on http://{}".format(hs.hostname))
yield defer.Deferred() # wait forever
task.react(main)
......@@ -5,6 +5,7 @@ import sys
from mock import patch
from mock import Mock, MagicMock
from unittest import skipIf
from binascii import b2a_base64
from zope.interface import implementer, directlyProvides
......@@ -547,6 +548,18 @@ class EndpointTests(unittest.TestCase):
ep._tor_progress_update(40, "FOO", "foo to bar")
return ep
def test_single_hop_non_ephemeral(self, ftb):
control_ep = Mock()
control_ep.connect = Mock(return_value=defer.succeed(None))
directlyProvides(control_ep, IStreamClientEndpoint)
with self.assertRaises(ValueError) as ctx:
TCPHiddenServiceEndpoint.system_tor(
self.reactor, control_ep, 1234,
ephemeral=False,
single_hop=True,
)
self.assertIn("single_hop=", str(ctx.exception))
def test_progress_updates_global_tor(self, ftb):
with patch('txtorcon.endpoints.get_global_tor_instance') as tor:
ep = TCPHiddenServiceEndpoint.global_tor(self.reactor, 1234)
......@@ -704,6 +717,172 @@ class EndpointTests(unittest.TestCase):
self.assertEqual(ep.local_port, 1234)
self.assertEqual(ep.hidden_service_dir, '/foo/bar')
def test_parse_via_plugin_key_from_file(self, ftb):
tmp = self.mktemp()
os.mkdir(tmp)
with open(os.path.join(tmp, 'some_data'), 'wb') as f:
f.write(b'ED25519-V3:deadbeefdeadbeef\n')
# make sure we have a valid thing from get_global_tor without
# actually launching tor
config = TorConfig()
config.post_bootstrap = defer.succeed(config)
from txtorcon import torconfig
torconfig._global_tor_config = None
get_global_tor(
self.reactor,
_tor_launcher=lambda react, config, progress_updates=None: defer.succeed(config)
)
ep = serverFromString(
self.reactor,
'onion:88:localPort=1234:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')),
)
self.assertEqual(ep.public_port, 88)
self.assertEqual(ep.local_port, 1234)
self.assertEqual(ep.private_key, "ED25519-V3:deadbeefdeadbeef")
def test_parse_via_plugin_key_from_v3_private_file(self, ftb):
tmp = self.mktemp()
os.mkdir(tmp)
with open(os.path.join(tmp, 'some_data'), 'wb') as f:
f.write(b'== ed25519v1-secret: type0 ==\x00\x00\x00H\x9e\xa6j\x0e\x98\x85\xa9\xec\xee@\x9d&\xe2\xbfe\xc9\x90\xb9\xcb\xb2g\xb0\xab\xe4\xd0\x14c\xb0\xb2\x9dX\xfa\xaa\xf8,di8\xec\xc6\x82t\xd0A\x16>u\xde\xc6&\x82\x03\x1app\x18c`T\xc3\xdc\x1a\xca')
# make sure we have a valid thing from get_global_tor without
# actually launching tor
config = TorConfig()
config.post_bootstrap = defer.succeed(config)
from txtorcon import torconfig
torconfig._global_tor_config = None
get_global_tor(
self.reactor,
_tor_launcher=lambda react, config, progress_updates=None: defer.succeed(config)
)
ep = serverFromString(
self.reactor,
'onion:88:localPort=1234:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')),
)
self.assertEqual(ep.public_port, 88)
self.assertEqual(ep.local_port, 1234)
self.assertTrue("\n" not in ep.private_key)
self.assertEqual(
ep.private_key,
u"ED25519-V3:" + b2a_base64(b"H\x9e\xa6j\x0e\x98\x85\xa9\xec\xee@\x9d&\xe2\xbfe\xc9\x90\xb9\xcb\xb2g\xb0\xab\xe4\xd0\x14c\xb0\xb2\x9dX\xfa\xaa\xf8,di8\xec\xc6\x82t\xd0A\x16>u\xde\xc6&\x82\x03\x1app\x18c`T\xc3\xdc\x1a\xca").decode('ascii').strip(),
)
def test_parse_via_plugin_key_from_v2_private_file(self, ftb):
tmp = self.mktemp()
os.mkdir(tmp)
with open(os.path.join(tmp, 'some_data'), 'w') as f:
f.write('-----BEGIN RSA PRIVATE KEY-----\nthekeyblob\n-----END RSA PRIVATE KEY-----\n')
# make sure we have a valid thing from get_global_tor without
# actually launching tor
config = TorConfig()
config.post_bootstrap = defer.succeed(config)
from txtorcon import torconfig
torconfig._global_tor_config = None
get_global_tor(
self.reactor,
_tor_launcher=lambda react, config, progress_updates=None: defer.succeed(config)
)
ep = serverFromString(
self.reactor,
'onion:88:localPort=1234:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')),
)
self.assertEqual(ep.public_port, 88)
self.assertEqual(ep.local_port, 1234)
self.assertEqual(
ep.private_key,
u"RSA1024:thekeyblob",
)
def test_parse_via_plugin_key_from_invalid_private_file(self, ftb):
tmp = self.mktemp()
os.mkdir(tmp)
with open(os.path.join(tmp, 'some_data'), 'w') as f:
f.write('nothing to see here\n')
# make sure we have a valid thing from get_global_tor without
# actually launching tor
config = TorConfig()
config.post_bootstrap = defer.succeed(config)
from txtorcon import torconfig
torconfig._global_tor_config = None
get_global_tor(
self.reactor,
_tor_launcher=lambda react, config, progress_updates=None: defer.succeed(config)
)
with self.assertRaises(ValueError):
serverFromString(
self.reactor,
'onion:88:localPort=1234:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')),
)
def test_parse_via_plugin_single_hop(self, ftb):
tmp = self.mktemp()
os.mkdir(tmp)
with open(os.path.join(tmp, 'some_data'), 'wb') as f:
f.write(b'ED25519-V3:deadbeefdeadbeef\n')
# make sure we have a valid thing from get_global_tor without
# actually launching tor
config = TorConfig()
config.post_bootstrap = defer.succeed(config)
from txtorcon import torconfig
torconfig._global_tor_config = None
get_global_tor(
self.reactor,
_tor_launcher=lambda react, config, progress_updates=None: defer.succeed(config)
)
ep = serverFromString(
self.reactor,
'onion:88:localPort=1234:singleHop=True:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')),
)
self.assertEqual(ep.public_port, 88)
self.assertEqual(ep.local_port, 1234)
self.assertEqual(ep.private_key, "ED25519-V3:deadbeefdeadbeef")
self.assertTrue(ep.single_hop)
def test_parse_via_plugin_single_hop_explicit_false(self, ftb):
tmp = self.mktemp()
os.mkdir(tmp)
with open(os.path.join(tmp, 'some_data'), 'wb') as f:
f.write(b'ED25519-V3:deadbeefdeadbeef\n')
# make sure we have a valid thing from get_global_tor without
# actually launching tor
config = TorConfig()
config.post_bootstrap = defer.succeed(config)
from txtorcon import torconfig
torconfig._global_tor_config = None
get_global_tor(
self.reactor,
_tor_launcher=lambda react, config, progress_updates=None: defer.succeed(config)
)
ep = serverFromString(
self.reactor,
'onion:88:localPort=1234:singleHop=false:privateKeyFile={}'.format(os.path.join(tmp, 'some_data')),
)
self.assertEqual(ep.public_port, 88)
self.assertEqual(ep.local_port, 1234)
self.assertEqual(ep.private_key, "ED25519-V3:deadbeefdeadbeef")
self.assertFalse(ep.single_hop)
def test_parse_via_plugin_single_hop_bogus(self, ftb):
with self.assertRaises(ValueError):
serverFromString(
self.reactor,
'onion:88:singleHop=yes_please',
)
def test_parse_via_plugin_key_and_keyfile(self, ftb):
with self.assertRaises(ValueError):
serverFromString(
self.reactor,
'onion:88:privateKeyFile=foo:privateKey=blarg'
)
def test_parse_via_plugin_key_and_dir(self, ftb):
with self.assertRaises(ValueError):
serverFromString(
......@@ -1681,6 +1860,26 @@ class TestSocksFactory(unittest.TestCase):
self.assertTrue(isinstance(ep, UNIXClientEndpoint))
self.assertEqual("/tmp/boom", ep._path)
@defer.inlineCallbacks
def test_unix_socket_bad(self):
reactor = Mock()
cp = Mock()
cp.get_conf = Mock(
return_value=defer.succeed({
'SocksPort': ['unix:bad worse wosrt']
})
)
the_error = Exception("a bad thing")
def boom(*args, **kw):
raise the_error
with patch('txtorcon.endpoints.available_tcp_port', lambda r: 1234):
with patch('txtorcon.torconfig.UNIXClientEndpoint', boom):
yield _create_socks_endpoint(reactor, cp)
errs = self.flushLoggedErrors()
self.assertEqual(errs[0].value, the_error)
@defer.inlineCallbacks
def test_nothing_exists(self):
reactor = Mock()
......
......@@ -264,6 +264,56 @@ class OnionServiceTest(unittest.TestCase):
self.assertEqual(u"ADD_ONION NEW:ED25519-V3 Port=80,127.0.0.1:80 Flags=Detach", cmd)
d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id))
def test_ephemeral_v3_ip_addr_tuple(self):
protocol = FakeControlProtocol([])
config = TorConfig(protocol)
# returns a Deferred we're ignoring
EphemeralOnionService.create(
Mock(),
config,
ports=[(80, "192.168.1.2:80")],
detach=True,
version=3,
)
cmd, d = protocol.commands[0]
self.assertEqual(u"ADD_ONION NEW:ED25519-V3 Port=80,192.168.1.2:80 Flags=Detach", cmd)
d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id))
def test_ephemeral_v3_non_anonymous(self):
protocol = FakeControlProtocol([])
config = TorConfig(protocol)
# returns a Deferred we're ignoring
EphemeralOnionService.create(
Mock(),
config,
ports=[(80, "192.168.1.2:80")],
version=3,
detach=True,
single_hop=True,
)
cmd, d = protocol.commands[0]
self.assertEqual(u"ADD_ONION NEW:ED25519-V3 Port=80,192.168.1.2:80 Flags=Detach,NonAnonymous", cmd)
d.callback("PrivateKey={}\nServiceID={}".format(_test_private_key_blob, _test_onion_id))
@defer.inlineCallbacks
def test_ephemeral_v3_ip_addr_tuple_non_local(self):
protocol = FakeControlProtocol([])
config = TorConfig(protocol)
# returns a Deferred we're ignoring
with self.assertRaises(ValueError):
yield EphemeralOnionService.create(
Mock(),
config,
ports=[(80, "hostname:80")],
detach=True,
version=3,
)
@defer.inlineCallbacks
def test_ephemeral_v3_wrong_key_type(self):
protocol = FakeControlProtocol([])
......
......@@ -28,8 +28,8 @@ from txtorcon.interface import ICircuitListener
from txtorcon.interface import IStreamListener
from txtorcon.interface import StreamListenerMixin
from txtorcon.interface import CircuitListenerMixin
from txtorcon.torstate import _extract_reason
from txtorcon.circuit import _get_circuit_attacher
from txtorcon.circuit import _extract_reason
try:
from .py3_torstate import TorStatePy3Tests # noqa
......@@ -1372,6 +1372,52 @@ s Fast Guard Running Stable Valid
d.addErrback(check_for_timeout_error)
return d
@defer.inlineCallbacks
def test_build_circuit_cancelled(self):
class FakeRouter:
def __init__(self, i):
self.id_hex = i
self.flags = []
path = []
for x in range(3):
path.append(FakeRouter("$%040d" % x))
# can't just check flags for guard status, need to know if
# it's in the running Tor's notion of Entry Guards
path[0].flags = ['guard']
class FakeCircuit:
close_called = False
def when_built(self):
return defer.Deferred()
def close(self):
self.close_called = True
return defer.succeed(None)
circ = FakeCircuit()
def _build(*args, **kw):
print("DING {} {}".format(args, kw))
return defer.succeed(circ)
self.state.build_circuit = _build
timeout = 10
clock = task.Clock()
# we want this circuit to get to BUILT, but *then* we call
# .cancel() on the deferred -- in which case, the circuit must
# be closed
d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=False)
clock.advance(1)
print("DING {}".format(self.state))
d.cancel()
with self.assertRaises(CircuitBuildTimedOutError):
yield d
self.assertTrue(circ.close_called)
def test_build_circuit_timeout_after_progress(self):
"""
Similar to above but we timeout after Tor has ack'd our
......@@ -1434,3 +1480,32 @@ s Fast Guard Running Stable Valid
# guard
self.assertEqual(len(self.flushWarnings()), 1)
return d
def test_build_circuit_failure(self):
class FakeRouter:
def __init__(self, i):
self.id_hex = i
self.flags = []
path = []
for x in range(3):
path.append(FakeRouter("$%040d" % x))
path[0].flags = ['guard']
timeout = 10
clock = task.Clock()
d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=True)
d.addCallback(self.circuit_callback)
self.assertEqual(self.transport.value(), b'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n')
self.send(b"250 EXTENDED 1234")
# we can't just .send(b'650 CIRC 1234 BUILT') this because we
# didn't fully hook up the protocol to the state, e.g. via
# post_bootstrap etc.
self.state.circuits[1234].update(['1234', 'FAILED', 'REASON=TIMEOUT'])
def check_reason(fail):
self.assertEqual(fail.value.reason, 'TIMEOUT')
d.addErrback(check_reason)
return d
Metadata-Version: 2.1
Name: txtorcon
Version: 18.0.2
Version: 18.3.0
Summary:
Twisted-based Tor controller client, with state-tracking and
configuration abstractions.
......
......@@ -76,6 +76,8 @@ examples/web_onion_service_aiohttp.py
examples/web_onion_service_endpoints.py
examples/web_onion_service_ephemeral.py
examples/web_onion_service_ephemeral_auth.py
examples/web_onion_service_ephemeral_keyfile.py
examples/web_onion_service_ephemeral_nonanon.py
examples/web_onion_service_ephemeral_unix.py
examples/web_onion_service_filesystem.py
examples/web_onion_service_prop224.py
......@@ -137,6 +139,5 @@ txtorcon/web.py
txtorcon.egg-info/PKG-INFO
txtorcon.egg-info/SOURCES.txt
txtorcon.egg-info/dependency_links.txt
txtorcon.egg-info/pbr.json
txtorcon.egg-info/requires.txt
txtorcon.egg-info/top_level.txt
\ No newline at end of file
{"is_release": false, "git_version": "0f966c2"}
\ No newline at end of file
__version__ = '18.0.2'
__version__ = '18.3.0'
__author__ = 'meejah'
__contact__ = 'meejah@meejah.ca'
__url__ = 'https://github.com/meejah/txtorcon'
......
......@@ -23,6 +23,24 @@ from txtorcon.util import find_keywords, maybe_ip_addr, SingleObserver
TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
def _extract_reason(kw):
"""
Internal helper. Extracts a reason (possibly both reasons!) from
the kwargs for a circuit failed or closed event.
"""
try:
# we "often" have a REASON
reason = kw['REASON']
try:
# ...and sometimes even have a REMOTE_REASON
reason = '{}, {}'.format(reason, kw['REMOTE_REASON'])
except KeyError:
pass # should still be the 'REASON' error if we had it
except KeyError:
reason = "unknown"
return reason
@implementer(IStreamAttacher)
class _CircuitAttacher(object):
"""
......@@ -500,7 +518,7 @@ class Circuit(object):
class CircuitBuildTimedOutError(Exception):
"""
"""
This exception is thrown when using `timed_circuit_build`
and the circuit build times-out.
"""
......@@ -530,11 +548,12 @@ def build_timeout_circuit(tor_state, reactor, path, timeout, using_guards=False)
d2 = timed_circuit[0].close()
else:
d2 = defer.succeed(None)
d2.addCallback(lambda ign: Failure(CircuitBuildTimedOutError("circuit build timed out")))
d2.addCallback(lambda _: Failure(CircuitBuildTimedOutError("circuit build timed out")))
return d2
d.addCallback(get_circuit)
d.addCallback(lambda circ: circ.when_built())
d.addErrback(trap_cancel)
reactor.callLater(timeout, d.cancel)
return d
......@@ -42,7 +42,7 @@ from .interface import ITor
try:
from .controller_py3 import _AsyncOnionAuthContext
HAVE_ASYNC = True
except SyntaxError:
except Exception:
HAVE_ASYNC = False
if sys.platform in ('linux', 'linux2', 'darwin'):
......@@ -755,7 +755,7 @@ class Tor(object):
auth=auth,
)
def create_onion_endpoint(self, port, private_key=None, version=None):
def create_onion_endpoint(self, port, private_key=None, version=None, single_hop=None):
"""
WARNING: API subject to change
......@@ -778,6 +778,11 @@ class Tor(object):
:param version: if not None, a specific version of service to
use; version=3 is Proposition 224 and version=2 is the
older 1024-bit key based implementation.
:param single_hop: if True, pass the `NonAnonymous` flag. Note
that Tor options `HiddenServiceSingleHopMode`,
`HiddenServiceNonAnonymousMode` must be set to `1` and there
must be no `SOCKSPort` configured for this to actually work.
"""
# note, we're just depending on this being The Ultimate
# Everything endpoint. Which seems fine, because "normal"
......@@ -791,6 +796,7 @@ class Tor(object):
private_key=private_key,
version=version,
auth=None,
single_hop=single_hop,
)
def create_filesystem_onion_endpoint(self, port, hs_dir, group_readable=False, version=None):
......@@ -939,7 +945,7 @@ class Tor(object):
# method names are kind of long (not-ideal)
@inlineCallbacks
def create_onion_service(self, ports, private_key=None, version=3, progress=None, await_all_uploads=False):
def create_onion_service(self, ports, private_key=None, version=3, progress=None, await_all_uploads=False, single_hop=None):
"""
Create a new Onion service
......@@ -975,6 +981,11 @@ class Tor(object):
until at least one upload of our Descriptor to a Directory
Authority has completed; if True we wait until all have
completed.
:param single_hop: if True, pass the `NonAnonymous` flag. Note
that Tor options `HiddenServiceSingleHopMode`,
`HiddenServiceNonAnonymousMode` must be set to `1` and there
must be no `SOCKSPort` configured for this to actually work.
"""
if version not in (2, 3):
raise ValueError(
......@@ -993,6 +1004,7 @@ class Tor(object):
version=version,
progress=progress,
await_all_uploads=await_all_uploads,
single_hop=single_hop,
)
returnValue(service)
......
......@@ -9,6 +9,7 @@ import shutil
import weakref
import tempfile
import functools
from binascii import b2a_base64
from txtorcon.util import available_tcp_port
from txtorcon.socks import TorSocksEndpoint
......@@ -17,6 +18,7 @@ from twisted.internet.interfaces import IStreamClientEndpointStringParserWithRea
from twisted.internet import defer, error
from twisted.python import log
from twisted.python.deprecate import deprecated
from twisted.python.failure import Failure
from twisted.internet.interfaces import IStreamServerEndpointStringParser
from twisted.internet.interfaces import IStreamServerEndpoint
from twisted.internet.interfaces import IStreamClientEndpoint
......@@ -222,7 +224,8 @@ class TCPHiddenServiceEndpoint(object):
ephemeral=None,
auth=None,
private_key=None,
version=None):
version=None,
single_hop=None):
"""
This returns a TCPHiddenServiceEndpoint connected to the
endpoint you specify in `control_endpoint`. After connecting, a
......@@ -248,6 +251,7 @@ class TCPHiddenServiceEndpoint(object):
private_key=private_key,
auth=auth,
version=version,
single_hop=single_hop,
)
@classmethod
......@@ -259,7 +263,8 @@ class TCPHiddenServiceEndpoint(object):
auth=None,
ephemeral=None,
private_key=None,
version=None):
version=None,
single_hop=None):
"""
This returns a TCPHiddenServiceEndpoint connected to a
txtorcon global Tor instance. The first time you call this, a
......@@ -306,6 +311,7 @@ class TCPHiddenServiceEndpoint(object):
ephemeral=ephemeral,
private_key=private_key,
version=version,
single_hop=single_hop,
)
progress.target = r._tor_progress_update
return r
......@@ -318,7 +324,8 @@ class TCPHiddenServiceEndpoint(object):
ephemeral=None,
private_key=None,
auth=None,
version=None):
version=None,
single_hop=None):
"""
This returns a TCPHiddenServiceEndpoint that's always
connected to its own freshly-launched Tor instance. All
......@@ -344,6 +351,7 @@ class TCPHiddenServiceEndpoint(object):
private_key=private_key,
auth=auth,
version=version,
single_hop=single_hop,
)
progress.target = r._tor_progress_update
return r
......@@ -356,7 +364,8 @@ class TCPHiddenServiceEndpoint(object):
ephemeral=None, # will be set to True, unless hsdir spec'd
private_key=None,
group_readable=False,
version=None):
version=None,
single_hop=None):
"""
:param reactor:
:api:`twisted.internet.interfaces.IReactorTCP` provider
......@@ -395,6 +404,11 @@ class TCPHiddenServiceEndpoint(object):
:param version:
Either None, 2 or 3 to specify a version 2 service or
Proposition 224 (version 3) service.
:param single_hop: if True, pass the `NonAnonymous` flag. Note
that Tor options `HiddenServiceSingleHopMode`,
`HiddenServiceNonAnonymousMode` must be set to `1` and there
must be no `SOCKSPort` configured for this to actually work.
"""
# this supports API backwards-compatibility -- if you didn't
......@@ -431,6 +445,11 @@ class TCPHiddenServiceEndpoint(object):
"'private_key' only understood for ephemeral services"
)
if single_hop and not ephemeral:
raise ValueError(
"'single_hop=' flag only makes sense for ephemeral onions"
)
self._reactor = reactor
self._config = defer.maybeDeferred(lambda: config)
self.public_port = public_port
......@@ -446,6 +465,7 @@ class TCPHiddenServiceEndpoint(object):
self.hiddenservice = None
self.group_readable = group_readable
self.version = version
self.single_hop = single_hop
self.retries = 0
if self.version is None:
......@@ -592,6 +612,7 @@ class TCPHiddenServiceEndpoint(object):
progress=self._descriptor_progress_update,
version=self.version,
auth=self.auth,
single_hop=self.single_hop,
)
else:
......@@ -603,6 +624,7 @@ class TCPHiddenServiceEndpoint(object):
detach=False,
progress=self._descriptor_progress_update,
version=self.version,
single_hop=self.single_hop,
)
else:
if self.auth is not None:
......@@ -798,6 +820,35 @@ class TorOnionListeningPort(object):
return self._service.directory
def _load_private_key_file(fname):
"""
Loads an onion-service private-key from the given file. This can
be either a 'key blog' as returned from a previous ADD_ONION call,
or a v3 or v2 file as created by Tor when using the
HiddenServiceDir directive.
In any case, a key-blob suitable for ADD_ONION use is returned.
"""
with open(fname, "rb") as f:
data = f.read()
if b"\x00\x00\x00" in data: # v3 private key file
blob = data[data.find(b"\x00\x00\x00") + 3:]
return u"ED25519-V3:{}".format(b2a_base64(blob.strip()).decode('ascii').strip())
if b"-----BEGIN RSA PRIVATE KEY-----" in data: # v2 RSA key
blob = "".join(data.decode('ascii').split('\n')[1:-2])
return u"RSA1024:{}".format(blob)
blob = data.decode('ascii').strip()
if ':' in blob:
kind, key = blob.split(':', 1)
if kind in ['ED25519-V3', 'RSA1024']:
return blob
raise ValueError(
"'{}' does not appear to contain v2 or v3 private key data".format(
fname,
)
)
@implementer(IStreamServerEndpointStringParser, IPlugin)
class TCPHiddenServiceEndpointParser(object):
"""
......@@ -821,6 +872,10 @@ class TCPHiddenServiceEndpointParser(object):
If ``hiddenServiceDir`` is not specified, one is created with
``tempfile.mkdtemp()``. The IStreamServerEndpoint returned will be
an instance of :class:`txtorcon.TCPHiddenServiceEndpoint`
If ``privateKey`` or ``privateKeyFile`` is specified, the service
will be "ephemeral" and Tor will receive the private key via the
ADD_ONION control-port command.
"""
prefix = "onion"
......@@ -830,16 +885,37 @@ class TCPHiddenServiceEndpointParser(object):
def parseStreamServer(self, reactor, public_port, localPort=None,
controlPort=None, hiddenServiceDir=None,
privateKey=None, version=None):
privateKey=None, privateKeyFile=None,
version=None, singleHop=None):
"""
:api:`twisted.internet.interfaces.IStreamServerEndpointStringParser`
"""
if privateKeyFile is not None:
if privateKey is not None:
raise ValueError(
"Can't specify both privateKey= and privateKeyFile="
)
privateKey = _load_private_key_file(privateKeyFile)
privateKeyFile = None
if hiddenServiceDir is not None and privateKey is not None:
raise ValueError(
"Only one of hiddenServiceDir and privateKey accepted"
"Only one of hiddenServiceDir and privateKey/privateKeyFile accepted"
)
if singleHop is not None:
if singleHop.strip().lower() in ['0', 'false']:
singleHop = False
elif singleHop.strip().lower() in ['1', 'true']:
singleHop = True
else:
raise ValueError(
"singleHop= param must be 'true' or 'false'"
)
else:
singleHop = False
if version is not None:
try:
version = int(version)
......@@ -879,6 +955,8 @@ class TCPHiddenServiceEndpointParser(object):
local_port=localPort,
ephemeral=ephemeral,
version=version,
private_key=privateKey,
single_hop=singleHop,
)
return TCPHiddenServiceEndpoint.global_tor(
......@@ -888,6 +966,8 @@ class TCPHiddenServiceEndpointParser(object):
control_port=controlPort,
ephemeral=ephemeral,
version=version,
private_key=privateKey,
single_hop=singleHop,
)
......@@ -922,7 +1002,7 @@ def _create_socks_endpoint(reactor, control_protocol, socks_config=None):
# could check platform? but why would you have unix ports on a
# platform that doesn't?
unix_ports = set([p.startswith('unix:') for p in socks_ports])
unix_ports = set([p for p in socks_ports if p.startswith('unix:')])
tcp_ports = set(socks_ports) - unix_ports
socks_endpoint = None
......@@ -932,7 +1012,10 @@ def _create_socks_endpoint(reactor, control_protocol, socks_config=None):
try:
socks_endpoint = _endpoint_from_socksport_line(reactor, p)
except Exception as e:
log.msg("clientFromString('{}') failed: {}".format(p, e))
log.err(
Failure(),
"failed to process SOCKS port '{}': {}".format(p, e)
)
# if we still don't have an endpoint, nothing worked (or there
# were no SOCKSPort lines at all) so we add config to tor
......