Skip to content
Commits on Source (4)
.PHONY: test html counts coverage sdist clean install doc integration diagrams
default: test
VERSION = 18.3.0
VERSION = 19.0.0
test:
PYTHONPATH=. trial --reporter=text test
......
Metadata-Version: 2.1
Name: txtorcon
Version: 18.3.0
Version: 19.0.0
Summary:
Twisted-based Tor controller client, with state-tracking and
configuration abstractions.
......@@ -44,10 +44,6 @@ Description:
:target: https://txtorcon.readthedocs.io/en/latest
:alt: ReadTheDocs
.. image:: http://api.flattr.com/button/flattr-badge-large.png
:target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
:alt: flattr
.. image:: https://landscape.io/github/meejah/txtorcon/master/landscape.svg?style=flat
:target: https://landscape.io/github/meejah/txtorcon/master
:alt: Code Health
......
......@@ -31,10 +31,6 @@
:target: https://txtorcon.readthedocs.io/en/latest
:alt: ReadTheDocs
.. image:: http://api.flattr.com/button/flattr-badge-large.png
:target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
:alt: flattr
.. image:: https://landscape.io/github/meejah/txtorcon/master/landscape.svg?style=flat
:target: https://landscape.io/github/meejah/txtorcon/master
:alt: Code Health
......
txtorcon (19.0.0-1) experimental; urgency=medium
* New upstream version 19.0.0
* d/patches:
- Refreshed remove-privacy-infringing-buttons.patch
-- Iain R. Learmonth <irl@debian.org> Thu, 13 Jun 2019 22:23:49 +0100
txtorcon (18.3.0-1) unstable; urgency=medium
* New upstream version 18.3.0
......
......@@ -11,7 +11,7 @@ consults local documentation, which shouldn't happen.
--- a/README.rst
+++ b/README.rst
@@ -11,35 +11,6 @@
@@ -11,31 +11,6 @@
......@@ -35,10 +35,6 @@ consults local documentation, which shouldn't happen.
- :target: https://txtorcon.readthedocs.io/en/latest
- :alt: ReadTheDocs
-
-.. image:: http://api.flattr.com/button/flattr-badge-large.png
- :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
- :alt: flattr
-
-.. image:: https://landscape.io/github/meejah/txtorcon/master/landscape.svg?style=flat
- :target: https://landscape.io/github/meejah/txtorcon/master
- :alt: Code Health
......
......@@ -23,7 +23,7 @@ Release Checklist
* update heading, date
* on both signing-machine and build-machine shells:
* export VERSION=18.0.0
* export VERSION=19.0.0
* (if on signing machine) "make dist" and "make dist-sigs"
* creates:
......
......@@ -15,11 +15,22 @@ that will be "19.2.1".
See also :ref:`api_stability`.
unreleased
----------
`git master <https://github.com/meejah/txtorcon>`_ *will likely become v19.0.0*
`git master <https://github.com/meejah/txtorcon>`_ *will likely become v19.1.0*
v19.0.0
-------
January 15, 2019
* `txtorcon-19.0.0.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-19.0.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/19.0.0>`_ (:download:`local-sig </../signatues/txtorcon-19.0.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-19.0.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v19.0.0.tar.gz>`_)
* add :func:`TorControlProtocol.when_disconnected` (will replace `.on_disconnect`)
* add `detach=` kwarg to :func:`Tor.create_onion_service`
* add `purpose=` kwarg to :func:`TorState.build_circuit`
v18.3.0
-------
......
......@@ -16,9 +16,11 @@ def main(reactor):
tor = yield txtorcon.connect(reactor, ep)
print("Connected to Tor {version}".format(version=tor.protocol.version))
state = yield tor.create_state()
# or:
# state = yield txtorcon.TorState.from_protocol(tor.protocol)
print("Tor state created. Circuits:")
for circuit in state.circuits.values():
print(" {circuit.id}: {circuit.path}".format(circuit=circuit))
d = tor.protocol.when_disconnected()
def its_gone(value):
print("Connection gone")
d.addCallback(its_gone)
tor.protocol.transport.loseConnection()
yield d
......@@ -3,7 +3,7 @@ import six
import functools
from os.path import join
from mock import Mock, patch
from six.moves import StringIO
from io import BytesIO
from twisted.internet.interfaces import IReactorCore
from twisted.internet.interfaces import IListeningPort
......@@ -51,7 +51,7 @@ class FakeProcessTransport(proto_helpers.StringTransportWithDisconnection):
)
def closeStdin(self):
self.process_protocol.outReceived(b"Bootstrap")
self.process_protocol.outReceived(b"Opening Control listener")
return
......@@ -570,12 +570,12 @@ class LaunchTorTests(unittest.TestCase):
return proto.post_bootstrap
def on_protocol(proto):
proto.errReceived('Something went horribly wrong!\n')
proto.errReceived(b'Something went horribly wrong!\n')
trans = FakeProcessTransport()
trans.protocol = Mock()
fakeout = StringIO()
fakeerr = StringIO()
fakeout = BytesIO()
fakeerr = BytesIO()
creator = functools.partial(connector, Mock(), Mock())
try:
yield launch(
......@@ -587,8 +587,8 @@ class LaunchTorTests(unittest.TestCase):
)
self.fail() # should't get callback
except RuntimeError as e:
self.assertEqual('', fakeout.getvalue())
self.assertEqual('Something went horribly wrong!\n', fakeerr.getvalue())
self.assertEqual(b'', fakeout.getvalue())
self.assertEqual(b'Something went horribly wrong!\n', fakeerr.getvalue())
self.assertTrue(
'Something went horribly wrong!' in str(e)
)
......@@ -599,7 +599,7 @@ class LaunchTorTests(unittest.TestCase):
trans = FakeProcessTransport()
def on_protocol(proto):
proto.outReceived(b'Bootstrapped 100%\n')
proto.outReceived(b'Opening Control listener\n')
reactor = FakeReactor(self, trans, on_protocol, [1, 2, 3])
fails = ['one']
......
......@@ -215,6 +215,38 @@ class DisconnectionTests(unittest.TestCase):
self.protocol.connectionLost(f)
self.assertTrue(it_was_called.yes)
def test_when_disconnect(self):
"""
see that we get our callback for when_disconnected if the
transport goes away
"""
def it_was_called(arg):
it_was_called.yes = True
return None
it_was_called.yes = False
d = self.protocol.when_disconnected()
d.addCallback(it_was_called)
f = failure.Failure(error.ConnectionDone("It's all over"))
self.protocol.connectionLost(f)
self.assertTrue(it_was_called.yes)
def test_when_disconnect_error(self):
"""
see that we get our errback for when_disconnected if the
transport goes away
"""
def it_was_called(arg):
it_was_called.yes = True
return None
it_was_called.yes = False
d = self.protocol.when_disconnected()
d.addErrback(it_was_called)
f = failure.Failure(RuntimeError("sadness"))
self.protocol.connectionLost(f)
self.assertTrue(it_was_called.yes)
def test_disconnect_errback(self):
"""
see that we get our callback on_disconnect if the transport
......
......@@ -1399,7 +1399,6 @@ s Fast Guard Running Stable Valid
circ = FakeCircuit()
def _build(*args, **kw):
print("DING {} {}".format(args, kw))
return defer.succeed(circ)
self.state.build_circuit = _build
......@@ -1411,7 +1410,6 @@ s Fast Guard Running Stable Valid
# 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):
......@@ -1509,3 +1507,30 @@ s Fast Guard Running Stable Valid
d.addErrback(check_reason)
return d
def test_build_circuit_with_purpose(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']
d = self.state.build_circuit(path, using_guards=True, purpose="general")
d.addCallback(self.circuit_callback)
self.assertEqual(self.transport.value(), b'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002 purpose=general\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.3.0
Version: 19.0.0
Summary:
Twisted-based Tor controller client, with state-tracking and
configuration abstractions.
......@@ -44,10 +44,6 @@ Description:
:target: https://txtorcon.readthedocs.io/en/latest
:alt: ReadTheDocs
.. image:: http://api.flattr.com/button/flattr-badge-large.png
:target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
:alt: flattr
.. image:: https://landscape.io/github/meejah/txtorcon/master/landscape.svg?style=flat
:target: https://landscape.io/github/meejah/txtorcon/master
:alt: Code Health
......
__version__ = '18.3.0'
__version__ = '19.0.0'
__author__ = 'meejah'
__contact__ = 'meejah@meejah.ca'
__url__ = 'https://github.com/meejah/txtorcon'
__license__ = 'MIT'
__copyright__ = 'Copyright 2012-2018'
__copyright__ = 'Copyright 2012-2019'
......@@ -945,7 +945,9 @@ 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, single_hop=None):
def create_onion_service(self, ports, private_key=None, version=3,
progress=None, await_all_uploads=False,
single_hop=None, detach=None):
"""
Create a new Onion service
......@@ -986,6 +988,12 @@ class Tor(object):
that Tor options `HiddenServiceSingleHopMode`,
`HiddenServiceNonAnonymousMode` must be set to `1` and there
must be no `SOCKSPort` configured for this to actually work.
:param detach: if True, the created service won't be tied to
this control connection and will still be active when this
control-connection goes away (this means the service will
appear in `GETINFO onions/detached` to all other
controllers)
"""
if version not in (2, 3):
raise ValueError(
......@@ -1005,6 +1013,7 @@ class Tor(object):
progress=progress,
await_all_uploads=await_all_uploads,
single_hop=single_hop,
detach=detach,
)
returnValue(service)
......@@ -1234,7 +1243,7 @@ class TorProcessProtocol(protocol.ProcessProtocol):
# tor_connection_failed)
txtorlog.msg(data)
if not self.attempted_connect and self.connection_creator \
and b'Bootstrap' in data:
and b'Opening Control listener' in data:
self.attempted_connect = True
# hmmm, we don't "do" anything with this Deferred?
# (should it be connected to the when_connected
......@@ -1273,7 +1282,8 @@ class TorProcessProtocol(protocol.ProcessProtocol):
if self.kill_on_stderr:
self.transport.loseConnection()
raise RuntimeError(
"Received stderr output from slave Tor process: " + data)
"Received stderr output from slave Tor process: " + data.decode('utf8')
)
def cleanup(self):
"""
......
......@@ -1118,7 +1118,6 @@ class FilesystemAuthenticatedOnionService(object):
config.HiddenServices.append(fhs)
def translate_progress(pct, tag, description):
print("OHAI {} {}".format(pct, tag))
# XXX fixme actually translate..
if progress:
progress(pct, tag, description)
......
......@@ -1104,7 +1104,7 @@ class TorConfig(object):
# then...?)
try:
ephemeral = yield self.protocol.get_info('onions/current')
except Exception as e:
except Exception:
self.config['EphemeralOnionServices'] = []
else:
onions = []
......
......@@ -23,7 +23,7 @@ from txtorcon.log import txtorlog
from txtorcon.interface import ITorControlProtocol
from .spaghetti import FSM, State, Transition
from .util import maybe_coroutine
from .util import maybe_coroutine, SingleObserver
DEFAULT_VALUE = 'DEFAULT'
......@@ -271,6 +271,11 @@ class TorControlProtocol(LineOnlyReceiver):
there was an error, the errback is called instead.
"""
self._when_disconnected = SingleObserver()
"""
Private. See :func:`.when_disconnected`
"""
self.post_bootstrap = defer.Deferred()
"""
This Deferred is triggered when we're done setting up
......@@ -648,6 +653,13 @@ class TorControlProtocol(LineOnlyReceiver):
self._maybe_issue_command()
return d
def when_disconnected(self):
"""
:returns: a Deferred that fires when (if) we disconnect from our
Tor process.
"""
return self._when_disconnected.when_fired()
# the remaining methods are internal API implementations,
# callbacks and state-tracking methods -- you shouldn't have any
# need to call them.
......@@ -672,11 +684,17 @@ class TorControlProtocol(LineOnlyReceiver):
def connectionLost(self, reason):
"Protocol API"
txtorlog.msg('connection terminated: ' + str(reason))
if reason.check(ConnectionDone):
self._when_disconnected.fire(self)
else:
self._when_disconnected.fire(reason)
# ...and this is why we don't do on_disconnect = Deferred() :(
# and instead should have had on_disconnect() method that
# returned a new Deferred to each caller..(we're checking if
# this Deferred has any callbacks because if it doesn't we'll
# generate an "Unhandled error in Deferred")
# XXX (deprecate this)
if not self.on_disconnect.called and self.on_disconnect.callbacks:
if reason.check(ConnectionDone):
self.on_disconnect.callback(self)
......
......@@ -545,7 +545,7 @@ class TorState(object):
circ.update([str(circ_id), 'EXTENDED'])
return circ
def build_circuit(self, routers=None, using_guards=True):
def build_circuit(self, routers=None, using_guards=True, purpose=None):
"""
Builds a circuit consisting of exactly the routers specified,
in order. This issues an EXTENDCIRCUIT call to Tor with all
......@@ -586,6 +586,9 @@ class TorState(object):
cmd += router.decode('utf8')
else:
cmd += router.id_hex[1:]
if purpose is not None:
cmd += " purpose={}".format(purpose)
d = self.protocol.queue_command(cmd)
d.addCallback(self._find_circuit_after_extend)
return d
......