[Pkg-privacy-commits] [txtorcon] 01/04: Imported Upstream version 0.15.0

Iain R. Learmonth irl at moszumanska.debian.org
Wed Aug 3 13:31:17 UTC 2016


This is an automated email from the git hooks/post-receive script.

irl pushed a commit to branch master
in repository txtorcon.

commit b9d87db7fe2a2e635f90c1b6e587a8d0c4013106
Author: Iain R. Learmonth <irl at debian.org>
Date:   Wed Aug 3 14:06:56 2016 +0100

    Imported Upstream version 0.15.0
---
 INSTALL                                     |   4 +-
 Makefile                                    |   6 +-
 PKG-INFO                                    |  13 +-
 README.rst                                  |   6 +-
 dev-requirements.txt                        |   4 +-
 docs/conf.py                                |   2 +-
 docs/howtos.rst                             |  20 +-
 docs/index.rst                              |   2 +-
 docs/release-checklist.rst                  | 149 +++++++------
 docs/releases.rst                           |  59 ++++-
 docs/tutorial.rst                           | 262 ++++++++++++++++++++++
 docs/txtorcon-config.rst                    |   2 +
 docs/txtorcon-launching.rst                 |   5 +
 docs/txtorcon-protocol.rst                  |   8 +
 docs/walkthrough.rst                        | 329 +++++++++++++++-------------
 examples/add_hiddenservice_to_system_tor.py |  63 ++++++
 examples/connect.py                         |  27 +++
 examples/ephemeral_endpoint.py              |  49 ++---
 examples/launch_tor_data_dir.py             |  43 ++++
 examples/multiple-socks-ports.py            |   7 +-
 examples/redirect_streams.py                |   1 -
 examples/stem_relay_descriptor.py           |   2 +-
 examples/tunnel_tls_through_tor_client.py   |  30 +++
 requirements.txt                            |   2 +-
 scripts/asciinema-demo1.py                  |  72 ++++++
 setup.py                                    | 133 ++++++-----
 test/test_circuit.py                        |  13 +-
 test/test_endpoints.py                      | 153 ++++++++-----
 test/test_torconfig.py                      | 311 +++++++++++++++++++++++++-
 test/test_torcontrolprotocol.py             | 102 ++++++++-
 test/test_torinfo.py                        |   2 +-
 test/test_torstate.py                       |  55 +++++
 test/test_util.py                           |  66 +++++-
 test/test_util_imports.py                   |  32 +--
 txtorcon.egg-info/PKG-INFO                  |  13 +-
 txtorcon.egg-info/SOURCES.txt               |   7 +
 txtorcon.egg-info/pbr.json                  |   2 +-
 txtorcon.egg-info/requires.txt              |   6 +-
 txtorcon/__init__.py                        |  77 ++++---
 txtorcon/_metadata.py                       |   6 +
 txtorcon/circuit.py                         |  27 ++-
 txtorcon/endpoints.py                       | 143 ++++++------
 txtorcon/interface.py                       |  16 --
 txtorcon/torconfig.py                       | 227 +++++++++++++++++--
 txtorcon/torcontrolprotocol.py              | 170 +++++++++-----
 txtorcon/torstate.py                        |  49 +++--
 txtorcon/util.py                            |  52 +++--
 47 files changed, 2156 insertions(+), 673 deletions(-)

diff --git a/INSTALL b/INSTALL
index 8cc2cbe..20ed05f 100644
--- a/INSTALL
+++ b/INSTALL
@@ -4,7 +4,7 @@ See README for more information.
 To just install this as quickly as possible, using a Debian or Ubuntu
 system, run the following as root:
 
-   apt-get install python-setuptools python-twisted python-ipaddr python-geoip python-psutil graphviz
+   apt-get install python-setuptools python-twisted python-ipaddress python-geoip python-psutil graphviz
 
    python setup.py install
 
@@ -23,7 +23,7 @@ use virtualenv:
    virtualenv --never-download --extra-search-dir=/usr/lib/python2.7/dist-packages/ tmp/txtorcon_env
    cd tmp/txtorcon_env
    source bin/activate
-   pip install Twisted ipaddr pygeoip     # this will download from internets:
+   pip install Twisted ipaddress pygeoip     # this will download from internets:
    export PYTHONPATH=../../build/lib.linux-x86_64-2.7/
 
 (Or you can type "make virtualenv" which creates tmp/txtorcon_env, up
diff --git a/Makefile b/Makefile
index 61f0a56..520bc72 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
 .PHONY: test html counts coverage sdist clean install doc integration
 default: test
-VERSION = 0.14.2
+VERSION = 0.15.0
 
 test:
 	trial --reporter=text test
@@ -40,7 +40,6 @@ coverage:
 
 htmlcoverage:
 	coverage run --source=txtorcon `which trial` test
-	coverage -a -d annotated_coverage
 	coverage report --show-missing
 	coverage html  # creates htmlcov/
 	sensible-browser htmlcov/index.html
@@ -65,6 +64,7 @@ pyflakescount:
 	pyflakes txtorcon/ examples/ | wc -l
 
 clean:
+	-rm twisted/plugins/dropin.cache
 	-rm -rf _trial_temp
 	-rm -rf build
 	-rm -rf dist
@@ -80,7 +80,7 @@ counts:
 	ohcount -s txtorcon/*.py
 
 test-release: dist
-	./test-release.sh $(shell pwd) ${VERSION}
+	./scripts/test-release.sh $(shell pwd) ${VERSION}
 
 dist: dist/txtorcon-${VERSION}-py2-none-any.whl dist/txtorcon-${VERSION}.tar.gz
 
diff --git a/PKG-INFO b/PKG-INFO
index 80d1067..112af9b 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,7 +1,10 @@
 Metadata-Version: 1.1
 Name: txtorcon
-Version: 0.14.2
-Summary: Twisted-based Tor controller client, with state-tracking and configuration abstractions.
+Version: 0.15.0
+Summary: 
+    Twisted-based Tor controller client, with state-tracking and
+    configuration abstractions.
+
 Home-page: https://github.com/meejah/txtorcon
 Author: meejah
 Author-email: meejah at meejah.ca
@@ -17,6 +20,9 @@ Description: README
         .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png
             :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:: http://api.flattr.com/button/flattr-badge-large.png
             :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
         
@@ -143,9 +149,6 @@ Description: README
            optional, in that we'll query Tor for the IP if the GeoIP database
            doesn't have an answer. It also does ASN lookups if you installed that MaxMind database.
         
-        -  `python-ipaddr <http://code.google.com/p/ipaddr-py/>`_: **optional**.
-           Google's IP address manipulation code.
-        
         -  development: `Sphinx <http://sphinx.pocoo.org/>`_ if you want to build the
            documentation. In that case you'll also need something called
            ``python-repoze.sphinx.autointerface`` (at least in Debian) to build
diff --git a/README.rst b/README.rst
index 7f55548..fca84b2 100644
--- a/README.rst
+++ b/README.rst
@@ -9,6 +9,9 @@ Documentation at https://txtorcon.readthedocs.org
 .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png
     :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:: http://api.flattr.com/button/flattr-badge-large.png
     :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
 
@@ -135,9 +138,6 @@ dependencies / requirements
    optional, in that we'll query Tor for the IP if the GeoIP database
    doesn't have an answer. It also does ASN lookups if you installed that MaxMind database.
 
--  `python-ipaddr <http://code.google.com/p/ipaddr-py/>`_: **optional**.
-   Google's IP address manipulation code.
-
 -  development: `Sphinx <http://sphinx.pocoo.org/>`_ if you want to build the
    documentation. In that case you'll also need something called
    ``python-repoze.sphinx.autointerface`` (at least in Debian) to build
diff --git a/dev-requirements.txt b/dev-requirements.txt
index 4fb2635..219160a 100644
--- a/dev-requirements.txt
+++ b/dev-requirements.txt
@@ -1,12 +1,14 @@
+tox
 coverage
 setuptools>=0.8.0
 Sphinx
 repoze.sphinx.autointerface>=0.4
 coveralls
+codecov
 wheel
 twine
 pyflakes
 pep8
 mock
-ipaddr
+ipaddress>=1.0.16
 GeoIP
diff --git a/docs/conf.py b/docs/conf.py
index 77e7d80..7f7817f 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -66,7 +66,7 @@ copyright = u'2012, meejah at meejah.ca'
 # built documents.
 #
 # The short X.Y version.
-from txtorcon import __version__
+from txtorcon._metadata import __version__
 version = __version__
 # The full version, including alpha/beta/rc tags.
 release = __version__
diff --git a/docs/howtos.rst b/docs/howtos.rst
index b5de964..fb723be 100644
--- a/docs/howtos.rst
+++ b/docs/howtos.rst
@@ -26,32 +26,34 @@ Install txtorcon On Debian
 Thanks to work by `Lunar
 <http://qa.debian.org/developer.php?login=lunar@debian.org>`_,
 txtorcon is usually rapidly packaged into Debian. This means that it
-gets into `jessie
-<https://packages.debian.org/jessie/python-txtorcon>`_ fairly quickly,
-and then arrives in `wheezy-backports
-<https://packages.debian.org/wheezy-backports/python-txtorcon>`_ a
+gets into `stretch
+<https://packages.debian.org/stretch/python-txtorcon>`_ fairly quickly,
+and then arrives in `jessie-backports
+<https://packages.debian.org/jessie-backports/python-txtorcon>`_ a
 couple weeks after that. You can see the current status on the `Debian
 QA Page for txtorcon <http://packages.qa.debian.org/t/txtorcon.html>`_
 
-If you're using ``jessie`` (testing), simply:
+If you're using ``stretch`` (testing), simply:
 
 .. code-block:: shell-session
 
    $ apt-get install python-txtorcon
 
-For wheezy users, you'll need to enabled the ``wheezy-backports``
-repository to Apt. There are `instructions on the Debian wiki
+If you're using wheezy, it should "just work".  For jessie users,
+you'll probably want to enabled the ``jessie-backports`` repository to
+Apt. There are `instructions on the Debian wiki
 <https://wiki.debian.org/Backports#Adding_the_repository>`_ If you're
 in a hurry, you could try this:
 
 .. code-block:: shell-session
 
-   # echo "deb http://ftp.debian.org/debian wheezy-backports main contrib non-free" >> /etc/apt/sources.list
+   # echo "deb http://ftp.debian.org/debian jessie-backports main contrib non-free" >> /etc/apt/sources.list
    # apt-get update
-   # apt-get install -t wheezy-backports python-txtorcon
+   # apt-get install -t jessie-backports python-txtorcon
 
 .. _howto-endpoint:
 
+
 Endpoints Enable Tor With Any Twisted Service
 ---------------------------------------------
 
diff --git a/docs/index.rst b/docs/index.rst
index cf456cc..daede1d 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -118,7 +118,7 @@ to Lunar::
 
     echo "deb http://ftp.ca.debian.org/debian/ wheezy-backports main" >> /etc/apt/sources.list
     apt-get update
-    apt-get install python-txtorcon 
+    apt-get install python-txtorcon
 
 It also `appears txtorcon is in Gentoo
 <http://packages.gentoo.org/package/net-libs/txtorcon>`_ but I don't
diff --git a/docs/release-checklist.rst b/docs/release-checklist.rst
index 03872fe..1a1f4ef 100644
--- a/docs/release-checklist.rst
+++ b/docs/release-checklist.rst
@@ -1,102 +1,101 @@
 Release Checklist
 =================
 
- * double-check version updated, sadly in a few places:
-    * Makefile
-    * setup.py
-    * txtorcon/__init__.py
+* double-check version updated, sadly in a few places:
+   * Makefile
+   * txtorcon/_metadata.py
 
- * run all tests, on all configurations
-    * "tox"
+* run all tests, on all configurations
+   * "tox"
 
- * "make pep8" should run cleanly (ideally)
+* "make pep8" should run cleanly (ideally)
 
- * update docs/releases.rst to reflect upcoming reality
-    * blindly make links to the signatures
-    * update heading, date
+* update docs/releases.rst to reflect upcoming reality
+   * blindly make links to the signatures
+   * update heading, date
 
- * "make dist" and "make dist-sig" (if on signing machine)
-    * creates:
-      dist/txtorcon-X.Y.Z.tar.gz.asc
-      dist/txtorcon-X.Y.Z-py2-none-any.whl.asc
-    * add the signatures to "signatues/"
-    * add ALL FOUR files to dist/ (OR fix twine commands)
+* "make dist" and "make dist-sig" (if on signing machine)
+   * creates:
+     dist/txtorcon-X.Y.Z.tar.gz.asc
+     dist/txtorcon-X.Y.Z-py2-none-any.whl.asc
+   * add the signatures to "signatues/"
+   * add ALL FOUR files to dist/ (OR fix twine commands)
 
- * (if not on signing machine) do "make dist"
-   * scp dist/txtorcon-X.Y.Z.tar.gz dist/txtorcon-X.Y.Z-py2-none-any.whl signingmachine:
-   * sign both, with .asc detached signatures (see Makefile for command)
-   * copy signatures back to build machine, in dist/
-   * double-check that they validate
+* (if not on signing machine) do "make dist"
+  * scp dist/txtorcon-X.Y.Z.tar.gz dist/txtorcon-X.Y.Z-py2-none-any.whl signingmachine:
+  * sign both, with .asc detached signatures (see Makefile for command)
+  * copy signatures back to build machine, in dist/
+  * double-check that they validate
 
- * generate sha256sum for each:
-      sha256sum dist/txtorcon-X.Y.Z.tar.gz dist/txtorcon-X.Y.Z-py2-none-any.whl
+* generate sha256sum for each:
+     sha256sum dist/txtorcon-X.Y.Z.tar.gz dist/txtorcon-X.Y.Z-py2-none-any.whl
 
- * copy signature files to <root of dist>/signatures and commit them
-   along with the above changes for versions, etc.
+* copy signature files to <root of dist>/signatures and commit them
+  along with the above changes for versions, etc.
 
- * draft email to tor-dev (and probably twisted-python):
-    * example: https://lists.torproject.org/pipermail/tor-dev/2014-January/006111.html
-    * example: https://lists.torproject.org/pipermail/tor-dev/2014-June/007006.html
-    * copy-paste release notes, un-rst-format them
-    * include above sha256sums
-    * clear-sign the announcement
-    * gpg --armor --clearsign -u meejah at meejah.ca path/to/release-announcement-X-Y-Z
-    * Example boilerplate:
+* draft email to tor-dev (and probably twisted-python):
+   * example: https://lists.torproject.org/pipermail/tor-dev/2014-January/006111.html
+   * example: https://lists.torproject.org/pipermail/tor-dev/2014-June/007006.html
+   * copy-paste release notes, un-rst-format them
+   * include above sha256sums
+   * clear-sign the announcement
+   * gpg --armor --clearsign -u meejah at meejah.ca path/to/release-announcement-X-Y-Z
+   * Example boilerplate:
 
-            I'm [adjective] to announce txtorcon 0.10.0. This adds
-            several amazing features, including levitation. Full list
-            of improvements:
+           I'm [adjective] to announce txtorcon 0.10.0. This adds
+           several amazing features, including levitation. Full list
+           of improvements:
 
-               * take from releases.rst
-               * ...but un-rST them
+              * take from releases.rst
+              * ...but un-rST them
 
-            You can download the release from PyPI or GitHub (or of
-            course "pip install txtorcon"):
+           You can download the release from PyPI or GitHub (or of
+           course "pip install txtorcon"):
 
-               https://pypi.python.org/pypi/txtorcon/0.10.0
-               https://github.com/meejah/txtorcon/releases/tag/v0.10.0
+              https://pypi.python.org/pypi/txtorcon/0.10.0
+              https://github.com/meejah/txtorcon/releases/tag/v0.10.0
 
-            Releases are also available from the hidden service:
+           Releases are also available from the hidden service:
 
-               http://timaq4ygg2iegci7.onion/txtorcon-0.12.0.tar.gz
-               http://timaq4ygg2iegci7.onion/txtorcon-0.12.0.tar.gz.asc
+              http://timaq4ygg2iegci7.onion/txtorcon-0.12.0.tar.gz
+              http://timaq4ygg2iegci7.onion/txtorcon-0.12.0.tar.gz.asc
 
-            You can verify the sha256sum of both by running the following 4 lines
-            in a shell wherever you have the files downloaded:
+           You can verify the sha256sum of both by running the following 4 lines
+           in a shell wherever you have the files downloaded:
 
-            cat <<EOF | sha256sum --check
-            910ff3216035de0a779cfc167c0545266ff1f26687b163fc4655f298aca52d74  txtorcon-0.10.0-py2-none-any.whl
-            c93f3d0f21d53c6b4c1521fc8d9dc2c9aff4a9f60497becea207d1738fa78279  txtorcon-0.10.0.tar.gz
-            EOF
+           cat <<EOF | sha256sum --check
+           910ff3216035de0a779cfc167c0545266ff1f26687b163fc4655f298aca52d74  txtorcon-0.10.0-py2-none-any.whl
+           c93f3d0f21d53c6b4c1521fc8d9dc2c9aff4a9f60497becea207d1738fa78279  txtorcon-0.10.0.tar.gz
+           EOF
 
-            thanks,
-            meejah
+           thanks,
+           meejah
 
- * copy release announcement to signing machine, update code
+* copy release announcement to signing machine, update code
 
- * create signed tag
-    * git tag -s -u meejah at meejah.ca -F path/to/release-announcement-X-Y-Z vX.Y.Z
+* create signed tag
+   * git tag -s -u meejah at meejah.ca -F path/to/release-announcement-X-Y-Z vX.Y.Z
 
- * copy dist/* files + signatures to hidden-service machine
- * copy them to the HTML build directory! (docs/_build/html/)
+* copy dist/* files + signatures to hidden-service machine
+* copy them to the HTML build directory! (docs/_build/html/)
 
- * git pull and build docs there
-    * FIXME: why aren't all the dist files copied as part of doc build (only .tar.gz)
+* git pull and build docs there
+   * FIXME: why aren't all the dist files copied as part of doc build (only .tar.gz)
 
- * download both distributions + signatures from hidden-service
-    * verify sigs
-    * verify sha256sums versus announcement text
-    * verify tag (git tag -v v0.9.2) on machine other than signing-machine
+* download both distributions + signatures from hidden-service
+   * verify sigs
+   * verify sha256sums versus announcement text
+   * verify tag (git tag -v v0.9.2) on machine other than signing-machine
 
- * upload release
-    * to PyPI: "make release" (which uses twine so this isn't the same step as "sign the release")
-       * make sure BOTH the .tar.gz and .tar.gz.asc (ditto for .whl) are in the dist/ directory first!!)
-       * ls dist/txtorcon-${VERSION}*
-       * note this depends on a ~/.pypirc file with [server-login] section containing "username:" and "password:"
-    * git push --tags github master
-    * to github: use web-upload interface to upload the 4 files (both dists, both signature)
+* upload release
+   * to PyPI: "make release" (which uses twine so this isn't the same step as "sign the release")
+      * make sure BOTH the .tar.gz and .tar.gz.asc (ditto for .whl) are in the dist/ directory first!!)
+      * ls dist/txtorcon-${VERSION}*
+      * note this depends on a ~/.pypirc file with [server-login] section containing "username:" and "password:"
+   * git push --tags github master
+   * to github: use web-upload interface to upload the 4 files (both dists, both signature)
 
- * make announcement
-    * post to tor-dev@ the clear-signed release announcement
-    * post to twisted-python@ the clear-signed release announcement
-    * tell #tor-dev??
+* make announcement
+   * post to tor-dev@ the clear-signed release announcement
+   * post to twisted-python@ the clear-signed release announcement
+   * tell #tor-dev??
diff --git a/docs/releases.rst b/docs/releases.rst
index 63f6689..280f82b 100644
--- a/docs/releases.rst
+++ b/docs/releases.rst
@@ -6,29 +6,66 @@ in master your project depends upon, let me know and I'll do a
 release. Starting with v0.8.0 versions are following `semantic
 versioning <http://semver.org/>`_.
 
+release-1.x
+-----------
+
+A branch is in development fixing, updating and/or breaking some APIs
+and adding a new object to represent the running tor instance and a
+completel documentation re-write. This is not ready for release quite
+yet, but feedback appreciated, especially on the documentation!
+Rendered docs on `txtorcon.readthedocs <http://txtorcon.readthedocs.io/en/release-1.x/>`_.
+
+
 unreleased
 ----------
 
-`git master <https://github.com/meejah/txtorcon>`_ *will likely become v0.15.0*
+`git master <https://github.com/meejah/txtorcon>`_ *will likely become v0.16.0*
 
 
-v0.14.1
+v0.15.0
 -------
 
-*October 25, 2015*
-
- * `txtorcon-0.14.1.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-0.14.1.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/0.14.1>`_ (:download:`local-sig </../signatues/txtorcon-0.14.1.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-0.14.1.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v0.14.1.tar.gz>`_)
- * subtle bug with ``.is_built`` on Circuit; changing the API (but
-   with backwards-compatibility until 0.15.0 at least)
+*July 26, 2016*
+
+ * `txtorcon-0.15.0.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-0.15.0.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/0.15.0>`_ (:download:`local-sig </../signatues/txtorcon-0.15.0.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-0.15.0.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v0.15.0.tar.gz>`_)
+ * added support for NULL control-port-authentication which is often
+   appropriate when used with a UNIX domain socket
+ * switched to `ipaddress
+   <https://docs.python.org/3/library/ipaddress.html>`_ instead of
+   Google's ``ipaddr``; the API should be the same from a user
+   perspective but **packagers and tutorials** will want to change
+   their instructions slightly (``pip install ipaddress`` or ``apt-get
+   install python-ipaddress`` are the new ways).
+ * support the new ADD_ONION and DEL_ONION "ephemeral hidden services"
+   commands in TorConfig
+ * a first stealth-authentication implementation (for "normal" hidden
+   services, not ephemeral)
+ * bug-fix from `david415 <https://github.com/david415>`_ to raise
+   ConnectionRefusedError instead of StopIteration when running out of
+   SOCKS ports.
+ * new feature from `david415 <https://github.com/david415>`_ adding a
+   ``build_timeout_circuit`` method which provides a Deferred that
+   callbacks only when the circuit is completely built and errbacks if
+   the provided timeout expires. This is useful because
+   :doc:`TorState.build_circuit` callbacks as soon as a Circuit
+   instance can be provided (and then you'd use
+   :doc:`Circuit.when_built` to find out when it's done building).
+ * new feature from `coffeemakr <https://github.com/coffeemakr>`_
+   falling back to password authentication if cookie authentication
+   isn't available (or fails, e.g. because the file isn't readable).
+ * both TorState and TorConfig now have a ``.from_protocol`` class-method.
+ * spec-compliant string-un-escaping from `coffeemakr <https://github.com/coffeemakr>`_
+ * a proposed new API: :meth:`txtorcon.connect`
+ * fix `issue 176 <https://github.com/meejah/txtorcon/issues/176>`_
 
 
-v0.14.2
+v0.14.1
 -------
 
-*December 2, 2015*
+*October 25, 2015*
 
- * `txtorcon-0.14.2.tar.gz <http://timaq4ygg2iegci7.onion/txtorcon-0.14.2.tar.gz>`_ (`PyPI <https://pypi.python.org/pypi/txtorcon/0.14.2>`_ (:download:`local-sig </../signatues/txtorcon-0.14.2.tar.gz.asc>` or `github-sig <https://github.com/meejah/txtorcon/blob/master/signatues/txtorcon-0.14.2.tar.gz.asc?raw=true>`_) (`source <https://github.com/meejah/txtorcon/archive/v0.14.2.tar.gz>`_)
- * compatibility for Twisted 15.5.0 (released on 0.14.x for `OONI <http://ooni.io/>`_)
+ * subtle bug with ``.is_built`` on Circuit; changing the API (but
+   with backwards-compatibility until 0.15.0 at least)
 
 
 v0.14.0
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
new file mode 100644
index 0000000..5ef0528
--- /dev/null
+++ b/docs/tutorial.rst
@@ -0,0 +1,262 @@
+txtorcon Tutorial
+=================
+
+.. note::
+
+    **NOT COMPLETE** this is a work-in-progress against an in-development API.
+
+This builds up, step-by-step, a tiny program that uses the main
+features of txtorcon. We will:
+
+  - copy-pasta some Twisted boilerplate;
+  - launch our own Tor instance (or connect to one);
+  - show all the circuits;
+  - add a simple Onion service;
+  - change our Tor's configuration;
+  - shut down the Tor instance;
+
+We concentrate on the highest-level API, the :class:`txtorcon.Tor`
+class. There are lower-level APIs available for everything that's done
+here, but this is the easiest. To see the other available APIs you can
+use the API-docs, some of the HOWTOs and the many :ref:`examples`.
+
+
+Before We Begin
+---------------
+
+All the file-listings quoted below are in ``./tutorial/*.py`` for
+convenience.
+
+If you do not yet have a development environment with txtorcon
+installed, please see :ref:`installing`. You can prove your
+environment is working by opening a file called `tutorial0.py` with
+your favourite editor and typing:
+
+Listing :download:`tutorial0.py <../tutorial/tutorial0.py>`:
+
+.. literalinclude:: ../tutorial/tutorial0.py
+
+
+Then, run ``python tutorial0.py`` in a terminal. If everything is set
+up properly, your terminal should show something like this:
+
+.. code-block:: shell-session
+
+    $ python ./tutorial/tutorial0.py
+    it worked!
+    $
+
+If you are having problems, the best thing is to contact me on IRC;
+see :ref:`getting help`.
+
+
+Twisted's main
+--------------
+
+If you do not already know how to do event-based programming in
+Twisted, you should learn that first. Start with `Twisted's narrative
+documentation
+<http://twistedmatrix.com/documents/current/core/howto/index.html>`_
+or the very fine `Twisted tutorial
+<http://krondo.com/an-introduction-to-asynchronous-programming-and-twisted/>`_
+by Dave Peticolas.
+
+We use the `task <https://twistedmatrix.com/documents/current/api/twisted.internet.task.html>`_ and `endpoints <https://twistedmatrix.com/documents/current/api/twisted.internet.endpoints.html>`_ APIs from Twisted and
+`@inlineCallbacks <http://twistedmatrix.com/documents/current/core/howto/defer-intro.html#inline-callbacks-using-yield>`_ extensively; if you're used to calling
+``reactor.run`` you'll want to read about `task.react
+<http://twistedmatrix.com/documents/current/api/twisted.internet.task.html#react>`_.
+
+In any case, we need some Twisted boilerplate to get a running
+"client"-style program; if any of this looks confusing please see the
+documentation linked above.
+
+Listing :download:`tutorial1.py <../tutorial/tutorial1.py>`:
+
+.. literalinclude:: ../tutorial/tutorial1.py
+
+
+Default Values
+--------------
+
+txtorcon strives to have sane, safe defaults. For all APIs if you're
+in doubt about whether to pass an optional argument, it is most likely
+that you can simply allow the default value to be assigned. If you
+think you've found a case where this *isn't* true, please file a bug!
+
+
+Launching (or Connecting to) Tor
+--------------------------------
+
+Obviously, txtorcon depends on having a running tor daemon to talk to;
+if you're having trouble with installing and running tor itself, see
+`Tor's documentation
+<https://www.torproject.org/docs/documentation.html.en>`_.
+
+"A running tor" is abstracted in txtorcon with instances of the
+:class:`txtorcon.Tor` class. This can be either a Tor that we
+ourselves launched, or one that was already running that we connect
+to. From there, we can ask this instance to create other useful
+txtorcon objects.
+
+.. note::
+
+    Tor allows multiple control connections, and txtorcon can
+    instantiate multiple "Tor" class instances talking to the same
+    underlying tor daemon. This complicates a few things, but for the
+    most part will work "as expected" from the outside. See the
+    :class:`txtorcon.Tor` API documentation for how shutdown is
+    handled. See :class:`txtorcon.TorState` for a caveat about what
+    "up to date" means.
+
+For simplicity, we will launch our own Tor instance.  So, building on
+the "Twisted boilerplate" from above, we do the simplest possible
+launching (don't panic if this doesn't immediately work; we add more
+information + debugging :ref:`immediately below`):
+
+Listing :download:`tutorial2.py <../tutorial/tutorial2.py>`:
+
+.. literalinclude:: ../tutorial/tutorial2.py
+
+The launching may take a while (e.g. several minutes) depending upon
+your connection. Although the above looks simple, there's a lot going
+on under the hood. Before explaining that, lets at least add three
+very useful things:
+
+  - progress message updates (1);
+  - an explicit ``DataDirectory`` for Tor (2);
+  - Tor's raw stdout output (useful for debugging) (3).
+
+
+.. _immediately below:
+
+Listing :download:`tutorial3.py <../tutorial/tutorial3.py>`:
+
+.. literalinclude:: ../tutorial/tutorial3.py
+
+Now we can at least see what Tor is doing while it launches! Giving an
+explicit ``DataDirectory`` means that txtorcon will no longer delete
+the whole thing when we exit; this saves the Tor directory authorities
+some bandwidth and makes our startup time much shorter. Try running
+the above twice in a row to illustrate this.
+
+You can pass any file-like object to ``stdout=`` and ``stderr=``. See
+:func:`txtorcon.launch_tor` for all options.
+
+Now that we know how to create our :class:`txtorcon.Tor` instance
+we'll want to do something with it. "Something" can be broadly
+categorized as "server-side" or "client-side". Either use may or may
+not include monitoring some aspects of Tor's current state.
+
+In Tor, "service-side" things are called "Onion Services" (or before
+that "Hidden Services"). In Twisted, a server-side endpoint (see
+`Twisted docs
+<http://twistedmatrix.com/documents/current/core/howto/endpoints.html>`_)
+must implement IServerStreamEndpoint_ and a client-side one implements
+IClientStreamEndpoint_.
+
+.. _IServerStreamEndpoint: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamServerEndpoint.html
+.. _IClientStreamEndpoint: https://twistedmatrix.com/documents/current/api/twisted.internet.interfaces.IStreamClientEndpoint.html
+
+First, however, we'll look at listening to and changing Tor's state.
+
+
+Protocol, State and Configuration
+---------------------------------
+
+Each :class:`txtorcon.Tor` instance contains one instance that
+implements :class:`txtorcon.ITorControlProtocol` (this is currently
+always a :class:`txtorcon.TorControlProtocol` but the interface
+declares the public API).
+
+If you want to queue tor control commands directly or add listeners
+for Tor events directly, you can use this class. However, this is
+pretty "low level" stuff and you should be able to use one of
+:class:`txtorcon.TorState` or :class:`txtorcon.TorConfig`.
+
+These instances are created with :meth:`txtorcon.Tor.create_state` or
+:meth:`txtorcon.Tor.create_config`. It is also possible to create a
+``TorConfig`` instance without a protocol connection -- used when
+launching a fresh Tor instance, for example. (Note that when used in
+this manner, the very same TorConfig instance as passed to ``launch``
+will be returned by ``create_config``).
+
+So, here's an example which shows all current circuits and streams in
+Tor, utilizing :class:`txtorcon.TorState`. Note that this provides
+"live" Tor state -- ``TorState`` subscribes to certain events and
+updates structures accordingly.
+
+Listing :download:`tutorial4.py <../tutorial/tutorial4.py>`:
+
+.. literalinclude:: ../tutorial/tutorial4.py
+
+Some things to notice:
+
+ - all of the "accessing data" usage of the API is attribute-access
+   style;
+ - "doing things" to data is a method call, often returning Deferred;
+ - all the data is "live", so we can keep printing things in a loop
+   and they'll represent current Tor state;
+ - no need to yield for each bit of data/state required.
+
+If you're really interested in when circuits appear or disappear (or
+streams), it's much better to use the "listener" API. That means
+implementing a :class:`txtorcon.IStreamListener` (or
+subclassing :class:`txtorcon.StreamListenerMixin`). There
+are similar ones for circuits:
+:class:`txtorcon.ICircuitListener` and
+:class:`txtorcon.CircuitListenerMixin`.
+
+You can add instances implementing these interfaces with
+:meth:`txtorcon.TorState.add_circuit_listener` or
+:meth:`txtorcon.TorState.add_stream_listener`.
+
+Modifying the example to listen for stream events:
+
+Listing :download:`tutorial5.py <../tutorial/tutorial5.py>`:
+
+.. literalinclude:: ../tutorial/tutorial5.py
+
+To make some streams through this launched Tor instance, we need to
+speak SOCKS5 to port 9999, for example with ``curl``::
+
+    curl --socks5-hostname localhost:9999 https://check.torproject.org/api/ip
+
+You'll note that the ``tutorial5.py`` code causes every second stream
+to be immediately closed, which should manifest as an error on the
+``curl`` side for ever other request.
+
+
+Server-Side Endpoints
+---------------------
+
+To create an object that implements IServerStreamEndpoint_, you call :meth:`txtorcon.Tor.create_onion_endpoint`. This creates a "disk-based" onion service, where the configuration (i.e. public/private keys) are in a ``HiddenServiceDir`` someone on the filesystem. This is in contrast to :meth:`txtorcon.Tor.create_ephemeral_onion_endpoint` which instead uses the ``ADD_ONION`` API from Tor to create an onion service whose keys are only ever communicated over the Tor control protocol.
+
+In either case, when you call ``.listen()`` on the endpoint, the
+appropriate things will happen such that the Tor we're talking to has
+a new hidden-service added to it. You get back a
+:class:`txtorcon.TorOnionListeningPort` when that Deferred fires
+(shhhh! you're only supposed to know it's a Twisted IListeningPort_!)
+which gives a :class:`txtorcon.TorOnionAddress` back from
+``.getHost``. This is how you gain access to the public-key hash which
+makes up the ``timaq4ygg2iegci7.onion`` address.
+
+It sounds more complex than in practice, where it works naturally with
+things like Twisted's ``Site`` for Web.
+
+Listing :download:`tutorial6.py <../tutorial/tutorial6.py>`:
+
+.. literalinclude:: ../tutorial/tutorial6.py
+
+One thing that happens after the ``.listen()`` call is that a
+"descrpitor" is uploaded to the Tor network, which can take a bit of
+time. For details of how the hidden-service system works overall, see
+Tor's `hidden service protocol
+<https://www.torproject.org/docs/hidden-services.html.en>`_ page.
+
+
+
+**Connecting, instead**: all of this tutorial and any use for the
+:class:`txtorcon.Tor` APIs will work the same with a "launched" or
+"connected to" tor -- so if you prefer, you should be able to follow
+along by calling :meth:`txtorcon.connect` instead of
+:meth:`txtorcon.launch`; see the API documentation for details.
diff --git a/docs/txtorcon-config.rst b/docs/txtorcon-config.rst
index fd85035..64428fe 100644
--- a/docs/txtorcon-config.rst
+++ b/docs/txtorcon-config.rst
@@ -1,10 +1,12 @@
 Configuration Classes
 =====================
 
+
 TorConfig
 ---------
 .. autoclass:: txtorcon.TorConfig
 
+
 HiddenService
 -------------
 .. autoclass:: txtorcon.HiddenService
diff --git a/docs/txtorcon-launching.rst b/docs/txtorcon-launching.rst
index 4db2c23..c3f9a20 100644
--- a/docs/txtorcon-launching.rst
+++ b/docs/txtorcon-launching.rst
@@ -1,3 +1,5 @@
+.. _launching_tor:
+
 Launching Tor
 =============
 
@@ -8,15 +10,18 @@ Launching Tor
   need configuration control, you can do that too and you're in the
   right spot.
 
+
 get_global_tor
 --------------
 
 .. autofunction:: txtorcon.get_global_tor
 
+
 TorProcessProtocol
 ------------------
 .. autoclass:: txtorcon.TorProcessProtocol
 
+
 launch_tor
 ----------
 .. autofunction:: txtorcon.launch_tor
diff --git a/docs/txtorcon-protocol.rst b/docs/txtorcon-protocol.rst
index c7f1f79..aed6176 100644
--- a/docs/txtorcon-protocol.rst
+++ b/docs/txtorcon-protocol.rst
@@ -1,18 +1,26 @@
 Protocol and Helper Classes
 ===========================
 
+connect
+-------
+.. autofunction:: txtorcon.connect
+
+
 TorControlProtocol
 ------------------
 .. autoclass:: txtorcon.TorControlProtocol
 
+
 TorProtocolFactory
 ------------------
 .. autoclass:: txtorcon.TorProtocolFactory
 
+
 TorProcessProtocol
 ------------------
 .. autoclass:: txtorcon.TorProcessProtocol
 
+
 TCPHiddenServiceEndpoint
 ------------------------
 .. autoclass:: txtorcon.TCPHiddenServiceEndpoint
diff --git a/docs/walkthrough.rst b/docs/walkthrough.rst
index 55bf5b4..cdf3db0 100644
--- a/docs/walkthrough.rst
+++ b/docs/walkthrough.rst
@@ -19,8 +19,8 @@ small program. We will:
 
  * connect to a running Tor;
  * launch our own Tor;
- * change the configuration; 
- * get some information from Tor; 
+ * change the configuration;
+ * get some information from Tor;
  * listen for events;
  * and send a NEWNYM_ signal.
 
@@ -32,20 +32,14 @@ Install txtorcon in a virtualenv
 First we need to be able to ``import txtorcon`` in a Python shell. We
 will accomplish that in a virtualenv_.
 
-.. note:: If you're using Debian or Ubuntu, ``pip install txtorcon`` may just work. 
+.. note:: If you're using Debian or Ubuntu, ``pip install python-txtorcon`` may just work.
 
-For the virtualenv, first get the code::
+To try the latest released version of txtorcon in a virtualenv_ is
+similar to other Python packages::
 
-   git clone https://github.com/meejah/txtorcon
-   cd txtorcon
-
-Now, we can use the Makefile there to create ourselves a virtualenv,
-activate it and install all the pre-requisites::
-
-   make venv
-   . venv/bin/activate
-   pip install -r requirements.txt
-   pip install -r dev-requirements.txt  # optional
+   virtualenv /tmp/txtorcon-venv
+   /tmp/txtorcon-venv/bin/pip install txtorcon
+   source /tmp/txtorcon-venv/bin/activate
 
 You should now be able to run "import txtorcon" in a python shell, for
 example::
@@ -53,8 +47,9 @@ example::
    python -c "import txtorcon"
 
 The above should produce no output. If you got an exception, or
-something else went wrong, read up on virtualenv or try a global
-install with ``python setup.py install``
+something else went wrong, read up on virtualenv or ask "meejah" in
+#tor-dev for help.
+
 
 Connect to a Running Tor
 ------------------------
@@ -87,34 +82,40 @@ The code to do this would look something like:
 
 .. sourcecode:: python
 
-   from twisted.internet import reactor
-   from twisted.internet.endpoints import TCP4ClientEndpoint
-   import txtorcon
+    from __future__ import print_function
+    from twisted.internet.task import react
+    from twisted.internet.defer import inlineCallbacks
+    from twisted.internet.endpoints import TCP4ClientEndpoint
+    import txtorcon
+
+    @inlineCallbacks
+    def main(reactor):
+        # change the port to 9151 for Tor Browser Bundle
+        connection = TCP4ClientEndpoint(reactor, "localhost", 9051)
 
-      def example(state):
-	  """
-	  This callback gets called after we've connected and loaded all the
-	  current Tor state. state is a TorState instance.
-	  """
-	  print "Fully bootstrapped state:", state
-	  print "   with bootstrapped protocol:", state.protocol
-	  reactor.stop()
+        state = yield txtorcon.build_tor_connection(connection)
+        print("Connected to tor {state.protocol.version}".format(state=state))
+        print("Current circuits:")
+        for circ in state.circuits.values():
+            path = '->'.join([r.name for r in circ.path])
+            print("  {circ.id}: {circ.state}, {path}".format(circ=circ, path=path))
 
-      ## change the port to 9151 for Tor Browser Bundle
-      connection = TCP4ClientEndpoint(reactor, "localhost", 9051)
+        # can also do "low level" things with the protocol
+        proto = state.protocol
+        answer = yield proto.queue_command("GETINFO version")
+        print("GETINFO version: {answer}".format(answer=answer))
 
-      d = txtorcon.build_tor_connection(connection)
-      d.addCallback(example)
+    react(main)
 
-      ## this will only return after reactor.stop() is called
-      reactor.run()
+If all is well, you should see some output like this::
 
-If all is well, you should see two lines get printed out and then the
-script will exit::
+    python walkthrough/0_connection.py
+    Connected to tor 0.2.5.12 (git-3731dd5c3071dcba)
+    Current circuits:
+      16929: BUILT, someguard->ecrehd->aTomicRelayFR1
+      16930: BUILT, someguard->Ferguson->NLNode1EddaiSu
+    GETINFO version: version=0.2.5.12 (git-3731dd5c3071dcba)
 
-   python 0_connection.py 
-   Fully bootstrapped state: <txtorcon.torstate.TorState object at 0x21cf710>
-      with bootstrapped protocol: <txtorcon.torcontrolprotocol.TorControlProtocol instance at 0x21c81b8>
 
 Launch Our Own Tor
 ------------------
@@ -128,11 +129,11 @@ uses some Tor commands to link the controller to the Tor instance, so
 that if the connection is lost Tor will shut itself down.
 
 The main difference between connecting and launching is that you have
-to provide a configuration to launch a Tor with. This is provided via
-a :class:`TorConfig<txtorcon.TorConfig>` instance. This class is a
+to provide a configuration to launch Tor with. This is provided via a
+:class:`TorConfig<txtorcon.TorConfig>` instance. This class is a
 little "magic" in order to provide a nice API, and so you simply set
-configuration options as members. A minimal configuration to launch a Tor might
-be::
+configuration options as members. A minimal configuration to launch a
+Tor might be::
 
    config = txtorcon.TorConfig()
    config.ORPort = 0
@@ -154,55 +155,63 @@ Check out the :meth:`txtorcon.launch_tor` documentation. You'll likely want
 to provide a ``progress_updates`` listener to provide interesting
 information to your user. Here's a full example::
 
-   import os
-   from twisted.internet import reactor, defer
-   from twisted.internet.endpoints import TCP4ClientEndpoint
-   import txtorcon
-
-   @defer.inlineCallbacks
-   def launched(process_proto):
-       """
-       This callback gets called after Tor considers itself fully
-       bootstrapped -- it has created a circuit. We get the
-       TorProcessProtocol object, which has the TorControlProtocol
-       instance as .tor_protocol
-       """
-
-       protocol = process_proto.tor_protocol
-       print "Tor has launched.\nProtocol:", protocol
-       info = yield protocol.get_info('traffic/read', 'traffic/written')
-       print info
-       reactor.stop()
-
-   def error(failure):
-       print "There was an error", failure.getErrorMessage()
-       reactor.stop()
-
-   def progress(percent, tag, summary):
-       ticks = int((percent/100.0) * 10.0)
-       prog = (ticks * '#') + ((10 - ticks) * '.')
-       print '%s %s' % (prog, summary)
-
-   config = txtorcon.TorConfig()
-   config.ORPort = 0
-   config.SocksPort = 9999
-   try:
-       os.mkdir('tor-data')
-   except OSError:
-       pass
-   config.DataDirectory = './tor-data'
-
-   d = txtorcon.launch_tor(config, reactor, progress_updates=progress)
-   d.addCallback(launched).addErrback(error)
-
-   ## this will only return after reactor.stop() is called
-   reactor.run()
-
-If you've never seen the ``defer.inlineCallbacks`` decorator, then you
+    #!/usr/bin/env python
+
+    from __future__ import print_function
+    import os
+    from twisted.internet.defer import inlineCallbacks
+    from twisted.internet.task import react
+    from twisted.internet.endpoints import TCP4ClientEndpoint
+    import txtorcon
+
+    def progress(percent, tag, summary):
+        """
+        Progress update from tor; we print a cheezy progress bar and the
+        message.
+        """
+        ticks = int((percent/100.0) * 10.0)
+        prog = (ticks * '#') + ((10 - ticks) * '.')
+        print('{} {}'.format(prog, summary))
+
+    @inlineCallbacks
+    def main(reactor):
+        config = txtorcon.TorConfig()
+        config.ORPort = 0
+        config.SocksPort = 9998
+        try:
+            os.mkdir('tor-data')
+        except OSError:
+            pass
+        config.DataDirectory = './tor-data'
+
+        try:
+            process = yield txtorcon.launch_tor(
+                config, reactor, progress_updates=progress
+            )
+        except Exception as e:
+            print("Error launching tor:", e)
+            return
+
+        protocol = process.tor_protocol
+        print("Tor has launched.")
+        print("Protocol:", protocol)
+        info = yield protocol.get_info('traffic/read', 'traffic/written')
+        print(info)
+
+        # explicitly stop tor by either disconnecting our protocol or the
+        # Twisted IProcessProtocol (or just exit our program)
+        print("Killing our tor, PID={pid}".format(pid=process.transport.pid))
+        yield process.transport.signalProcess('TERM')
+
+    react(main)
+
+If you've never seen the ``inlineCallbacks`` decorator, then you
 should `read up on it
 <https://twistedmatrix.com/documents/current/api/twisted.internet.defer.html#inlineCallbacks>`_.
 Once we get the Tor instance launched, we just make two GETINFO_ calls
-and then exit (which will cause the underlying Tor to also exit).
+and then explicitly kill it. You can also simply exit, which will
+cause the underlying Tor to also exit.
+
 
 Putting It All Together
 -----------------------
@@ -220,100 +229,106 @@ and then continuously monitor two events: circuit events via
 
 First, we add a simple implementation of :class:`txtorcon.ICircuitListener`::
 
-   class MyCircuitListener(object):
-       implements(txtorcon.ICircuitListener)
-       def circuit_new(self, circuit):
-	   print "new", circuit
+    @implementer(txtorcon.ICircuitListener)
+    class MyCircuitListener(object):
 
-       def circuit_launched(self, circuit):
-	   print "launched", circuit
+        def circuit_new(self, circuit):
+            print("\n\nnew", circuit)
 
-       def circuit_extend(self, circuit, router):
-	   print "extend", circuit
+        def circuit_launched(self, circuit):
+            print("\n\nlaunched", circuit)
 
-       def circuit_built(self, circuit):
-	   print "built", circuit
+        def circuit_extend(self, circuit, router):
+            print("\n\nextend", circuit)
 
-       def circuit_closed(self, circuit, **kw):
-	   print "closed", circuit, kw
+        def circuit_built(self, circuit):
+            print("\n\nbuilt", circuit)
 
-       def circuit_failed(self, circuit, **kw):
-	   print "failed", circuit, kw
+        def circuit_closed(self, circuit, **kw):
+            print("\n\nclosed", circuit, kw)
+
+        def circuit_failed(self, circuit, **kw):
+            print("\n\nfailed", circuit, kw)
 
 Next, to illustrate setting up TorState from a TorControlProtocol
-directly, we add a ``main()`` method that uses ``inlineCallbacks`` to do a
-few things sequentially after startup. First we use
-``TorControlProtocol.signal`` to send a NEWNYM_ request. After that we
-create a ``TorState`` instance, print out all existing circuits and set
-up listeners for circuit events (an instance of ``MyCircuitListener``)
-and INFO messages (via our own method).
+directly we first make a "bare" protocol connection, and then use a
+TorState classmethod (with the protocol instance) to query Tor's state
+(this instance also adds listeners to stay updated).
+
+Then we use ``TorControlProtocol.signal`` to send a NEWNYM_
+request. After that we create a ``TorState`` instance, print out all
+existing circuits and set up listeners for circuit events (an instance
+of ``MyCircuitListener``) and INFO messages (via our own method).
+
+Note there is a :class:`txtorcon.CircuitListenerMixin`_ class -- and
+similar interfaces for :class:`txtorcon.Stream`_ as well -- which
+makes it easier to write a listener subclass.
 
 Here is the full listing::
 
-   from twisted.internet import reactor, defer
-   from twisted.internet.endpoints import TCP4ClientEndpoint
-   from zope.interface import implements
-   import txtorcon
+    from __future__ import print_function
+    from twisted.internet.task import react
+    from twisted.internet.defer import inlineCallbacks, Deferred
+    from twisted.internet.endpoints import TCP4ClientEndpoint
+    from zope.interface import implementer
+    import txtorcon
 
-   ## change the port to 9151 for Tor Browser Bundle
-   connection = TCP4ClientEndpoint(reactor, "localhost", 9051)
 
-   def error(failure):
-       print "Error:", failure.getErrorMessage()
-       reactor.stop()
+    @implementer(txtorcon.ICircuitListener)
+    class MyCircuitListener(object):
 
-   class MyCircuitListener(object):
-       implements(txtorcon.ICircuitListener)
-       def circuit_new(self, circuit):
-	   print "new", circuit
+        def circuit_new(self, circuit):
+            print("new", circuit)
 
-       def circuit_launched(self, circuit):
-	   print "launched", circuit
+        def circuit_launched(self, circuit):
+            print("launched", circuit)
 
-       def circuit_extend(self, circuit, router):
-	   print "extend", circuit
+        def circuit_extend(self, circuit, router):
+            print("extend", circuit)
 
-       def circuit_built(self, circuit):
-	   print "built", circuit
+        def circuit_built(self, circuit):
+            print("built", circuit)
 
-       def circuit_closed(self, circuit, **kw):
-	   print "closed", circuit, kw
+        def circuit_closed(self, circuit, **kw):
+            print("closed", circuit, kw)
 
-       def circuit_failed(self, circuit, **kw):
-	   print "failed", circuit, kw
+        def circuit_failed(self, circuit, **kw):
+            print("failed", circuit, kw)
 
 
-   @defer.inlineCallbacks
-   def main(connection):
-       version = yield connection.get_info('version', 'events/names')
-       print "Connected to Tor.", version['version']
-       print version['events/names']
+    @inlineCallbacks
+    def main(reactor):
+        # change the port to 9151 for Tor Browser Bundle
+        tor_ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
+        connection = yield txtorcon.build_tor_connection(tor_ep, build_state=False)
+        version = yield connection.get_info('version', 'events/names')
+        print("Connected to Tor {version}".format(**version))
+        print("Events:", version['events/names'])
 
-       print "Issuing NEWNYM."
-       yield connection.signal('NEWNYM')
-       print "OK."
+        print("Building state.")
+        state = yield txtorcon.TorState.from_protocol(connection)
 
-       print "Building state."
-       state = txtorcon.TorState(connection)
-       yield state.post_bootstrap
-       print "State initialized."
-       print "Existing circuits:"
-       for c in state.circuits.values():
-	   print ' ', c
+        print("listening for circuit events")
+        state.add_circuit_listener(MyCircuitListener())
 
-       print "listening for circuit events"
-       state.add_circuit_listener(MyCircuitListener())
+        print("Issuing NEWNYM.")
+        yield connection.signal('NEWNYM')
+        print("OK.")
 
-       print "listening for INFO events"
-       def print_info(i):
-	   print "INFO:", i
-       connection.add_event_listener('INFO', print_info)
+        print("Existing circuits:")
+        for c in state.circuits.values():
+            print(' ', c)
 
-       ## since we don't call reactor.stop(), we keep running
+        print("listening for INFO events")
+        def print_info(i):
+            print("INFO:", i)
+        connection.add_event_listener('INFO', print_info)
 
-   d = txtorcon.build_tor_connection(connection, build_state=False)
-   d.addCallback(main).addErrback(error)
+        done = Deferred()
+        yield done  # never callback()s so infinite loop
 
-   ## this will only return after reactor.stop() is called
-   reactor.run()
+    react(main)
 
+If your Tor instance has been dormant for a while, try something like
+``torsocks curl https://www.torprojec.org`` in another termainl so you
+can see some more logging and circuit events.
diff --git a/examples/add_hiddenservice_to_system_tor.py b/examples/add_hiddenservice_to_system_tor.py
new file mode 100755
index 0000000..94dba17
--- /dev/null
+++ b/examples/add_hiddenservice_to_system_tor.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+
+# This connects to the system Tor (by default on control port 9151)
+# and adds a new hidden service configuration to it.
+
+import os
+import functools
+import shutil
+
+from twisted.internet import reactor, defer
+from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint
+from twisted.web import server, resource
+from twisted.internet.task import react
+
+import txtorcon
+
+
+class Simple(resource.Resource):
+    isLeaf = True
+
+    def render_GET(self, request):
+        return "<html>Hello, world! I'm a hidden service!</html>"
+
+
+ at defer.inlineCallbacks
+def main(reactor):
+    ep = TCP4ClientEndpoint(reactor, "localhost", 9251)
+    tor_protocol = yield txtorcon.build_tor_connection(ep, build_state=False)
+    print "Connected to Tor"
+
+    hs_public_port = 80
+    hs_port = yield txtorcon.util.available_tcp_port(reactor)
+    hs_string = '%s 127.0.0.1:%d' % (hs_public_port, hs_port)
+    print "Adding ephemeral service", hs_string
+    print "(this can take some time; please be patient)"
+    hs = txtorcon.EphemeralHiddenService([hs_string])
+    yield hs.add_to_tor(tor_protocol)
+    print "Added ephemeral HS to Tor:", hs.hostname
+
+    print "Starting site"
+    site = server.Site(Simple())
+    hs_endpoint = TCP4ServerEndpoint(reactor, hs_port, interface='127.0.0.1')
+    yield hs_endpoint.listen(site)
+
+    # in 5 seconds, remove the hidden service -- obviously this is
+    # where you'd do your "real work" or whatever.
+    d = defer.Deferred()
+
+    @defer.inlineCallbacks
+    def remove():
+        print "Removing the hiddenservice. Private key was"
+        print hs.private_key
+        yield hs.remove_from_tor(tor_protocol)
+        d.callback(None)
+    if False:
+        reactor.callLater(5, remove)
+        print "waiting 5 seconds"
+    else:
+        print "waiting forever"
+    yield d
+
+
+react(main)
diff --git a/examples/connect.py b/examples/connect.py
new file mode 100644
index 0000000..50f3265
--- /dev/null
+++ b/examples/connect.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python
+
+from __future__ import print_function
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.endpoints import TCP4ClientEndpoint
+import txtorcon
+
+ at inlineCallbacks
+def main(reactor):
+    #ep = TCP4ClientEndpoint(reactor, "localhost", 9251)
+    ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
+    # XXX nicer/better thing than "build_tor_connection" switching
+    # between returning two different objects depending on kwarg?
+    tor_protocol = yield txtorcon.connect(ep)
+    print(
+        "Connected to Tor {version}".format(
+            version=tor_protocol.version,
+        )
+    )
+    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))
+
+
+react(main)
diff --git a/examples/ephemeral_endpoint.py b/examples/ephemeral_endpoint.py
index 96fca4e..a5b75b0 100644
--- a/examples/ephemeral_endpoint.py
+++ b/examples/ephemeral_endpoint.py
@@ -3,16 +3,14 @@
 # This connects to the system Tor (by default on control port 9151)
 # and adds a new hidden service configuration to it.
 
-import os
-import functools
-import shutil
-
-from twisted.internet import reactor, defer
-from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint
+from twisted.internet import defer
+from twisted.internet.endpoints import TCP4ClientEndpoint
 from twisted.web import server, resource
 from twisted.internet.task import react
 
 import txtorcon
+from txtorcon.util import default_control_port
+
 
 # Ephemeral endpoints. You own all the bits.
 securely_stored_keyblob = '''RSA1024:MIICWwIBAAKBgQCsEuaUlvU651/lEl986XfX4QylkQCLhA9Nc19LTTt38oDeHRl3i5VgNsfsXyLrnk2iWapOsc3nmvxMt9vhFFanDB9p/rZTonERnTAw50M7PP4H4E8MDkPm6yZJSES7TPEI9u7WfSdq/HsNk4bsQU9Q3Vndy+hPZtPeGl+rs+3MawIDAQABAoGAGHPDIoBlLs6sWOAIg7almh7X7jsxyaGljwsDEq9R8RSb7XRTJyLFwltmg5dtXfAr9hMp2W745J2olrpV26FJQs4LFQBFawUwytvSV9IanpOew02yjvUQ0zqQUUbuR8rNHhzxJrvfJLDEzCmB8RBb1fE6BcUdv5t8xCu0/BwJdCkCQQDaL1ZJQ4aVHLcqru3IqiAwLsnA62aMNUPOO7twJ4YArX7Q6ZscqOPp8eLLoRzCYpMODcBX7kAOmuHxW8X3AKq [...]
@@ -27,35 +25,36 @@ class Simple(resource.Resource):
 
 @defer.inlineCallbacks
 def main(reactor):
-    ep = txtorcon.TCPEphemeralHiddenServiceEndpoint.system_tor(
+    ep = txtorcon.TCPHiddenServiceEndpoint.system_tor(
         reactor,
         public_port=8080,
-        control_endpoint=TCP4ClientEndpoint(reactor, 'localhost', 9251),
+        control_endpoint=TCP4ClientEndpoint(reactor, 'localhost', default_control_port()),
         private_key=securely_stored_keyblob,
+        ephemeral=True
     )
 
+    def on_progress(percent, tag, msg):
+        print('%03d: %s' % (percent, msg))
+    txtorcon.IProgressProvider(ep).add_progress_listener(on_progress)
+
     print "Starting site"
     port = yield ep.listen(server.Site(Simple()))
-    addr = port.address
+    host = port.getHost()
 
-    print "Site started. Available at http://{}:{}".format(addr.onion_uri, addr.onion_port)
-    print "Private key:\n{}".format(port.private_key)
+    print "Site started. Available at http://{}:{}".format(host.onion_uri, host.onion_port)
+    print "Private key:\n{}".format(host.onion_key)
 
-    # in 5 seconds, remove the hidden service -- obviously this is
-    # where you'd do your "real work" or whatever.
-    d = defer.Deferred()
+    # XXX how to get the HiddenService instance? or the
+    # IOnionBlahFoo-implementing thing, I mean
+    print("port", dir(port))
+    print("port", type(port))
+    print("port", port.__provides__)
+    print("host", dir(host))
+
+    print("ports", host.public_ports)
 
-    @defer.inlineCallbacks
-    def remove():
-        print "Removing the hiddenservice. Private key was"
-        print hs.onion_private_key
-        yield hs.remove_from_tor(tor_protocol)
-        d.callback(None)
-    if False:
-        reactor.callLater(5, remove)
-        print "waiting 5 seconds"
-    else:
-        print "waiting forever"
+    # wait forever; obviously you could do other work here
+    d = defer.Deferred()
     yield d
 
 
diff --git a/examples/launch_tor_data_dir.py b/examples/launch_tor_data_dir.py
new file mode 100644
index 0000000..651837c
--- /dev/null
+++ b/examples/launch_tor_data_dir.py
@@ -0,0 +1,43 @@
+#!/usr/bin/env python
+
+# Launch a slave Tor by first making a TorConfig object.
+
+from sys import stdout
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+import txtorcon
+
+
+ at inlineCallbacks
+def main(reactor):
+    config = txtorcon.TorConfig()
+    config.OrPort = 1234
+    config.SocksPort = 9999
+    config.DataDirectory = 'boomblam'
+    try:
+        yield txtorcon.launch_tor(config, reactor, stdout=stdout)
+
+    except RuntimeError as e:
+        print "Error:", e
+        return
+
+    proto = config.protocol
+    print "Connected to Tor version", proto.version
+
+    state = yield txtorcon.TorState.from_protocol(proto)
+    print "This Tor has PID", state.tor_pid
+    print "This Tor has the following %d Circuits:" % len(state.circuits)
+    for c in state.circuits.values():
+        print c
+
+    print "Changing our config (SOCKSPort=9876)"
+    config.SOCKSPort = 9876
+    yield config.save()
+
+    print "Querying to see it changed:"
+    socksport = yield proto.get_conf("SOCKSPort")
+    print "SOCKSPort", socksport
+
+
+if __name__ == '__main__':
+    react(main)
diff --git a/examples/multiple-socks-ports.py b/examples/multiple-socks-ports.py
index a2ef55f..7d12daa 100644
--- a/examples/multiple-socks-ports.py
+++ b/examples/multiple-socks-ports.py
@@ -3,13 +3,8 @@
 # This connects to the system Tor (by default on control port 9151)
 # and adds a new hidden service configuration to it.
 
-import os
-import functools
-import shutil
-
 from twisted.internet import reactor, defer
-from twisted.internet.endpoints import TCP4ClientEndpoint, TCP4ServerEndpoint
-from twisted.web import server, resource
+from twisted.internet.endpoints import TCP4ClientEndpoint
 from twisted.internet.task import react
 
 import txtorcon
diff --git a/examples/redirect_streams.py b/examples/redirect_streams.py
index 94e7e60..97040e1 100644
--- a/examples/redirect_streams.py
+++ b/examples/redirect_streams.py
@@ -1,6 +1,5 @@
 #!/usr/bin/env python
 
-from twisted.python import log
 from twisted.internet import reactor, defer
 from zope.interface import implements
 
diff --git a/examples/stem_relay_descriptor.py b/examples/stem_relay_descriptor.py
index fd300a7..76fd702 100755
--- a/examples/stem_relay_descriptor.py
+++ b/examples/stem_relay_descriptor.py
@@ -27,7 +27,7 @@ def main(reactor):
     descriptor_info = descriptor_info['desc/name/' + or_nickname]
     try:
         from stem.descriptor.server_descriptor import RelayDescriptor
-        relay_info = RelayDescriptor(descriptor_info) 
+        relay_info = RelayDescriptor(descriptor_info)
         print "The relay's fingerprint is:", relay_info.fingerprint
         print "Time in UTC when the descriptor was made:", relay_info.published
     except ImportError as e:
diff --git a/examples/tunnel_tls_through_tor_client.py b/examples/tunnel_tls_through_tor_client.py
new file mode 100644
index 0000000..942fa85
--- /dev/null
+++ b/examples/tunnel_tls_through_tor_client.py
@@ -0,0 +1,30 @@
+
+from twisted.internet.task import react
+from twisted.internet.defer import inlineCallbacks
+from twisted.internet.endpoints import TCP4ClientEndpoint
+from twisted.python.filepath import FilePath
+from twisted.internet import ssl, task, protocol, endpoints, defer
+from twisted.python.modules import getModule
+from txsocksx.tls import TLSWrapClientEndpoint
+import txtorcon
+
+import other_proto
+
+
+ at defer.inlineCallbacks
+def main(reactor):
+    factory = protocol.Factory.forProtocol(other_proto.DecisionProtocol)
+    ca_data = FilePath(b'ca_cert.pem').getContent()
+    client_data = FilePath(b'a.client.pem').getContent()
+    ca_cert = ssl.Certificate.loadPEM(ca_data)
+    client_key = ssl.PrivateCertificate.loadPEM(client_data)
+    options = ssl.optionsForClientTLS(u'the-authority', ca_cert, client_key)
+    exampleEndpoint = txtorcon.TorClientEndpoint(ip, 8966, socks_hostname="127.0.0.1")
+    tlsEndpoint = TLSWrapClientEndpoint(options, exampleEndpoint)
+    deferred = yield tlsEndpoint.connect(factory)
+    done = defer.Deferred()
+    deferred.connectionLost = lambda reason: done.callback(None)
+    yield done
+
+
+task.react(main)
diff --git a/requirements.txt b/requirements.txt
index 58935e7..4fc8215 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,6 +2,6 @@
 ## hmm, travis-ci doesn't like this since we need a GeoIP-dev package
 ##GeoIP>=1.2.9
 Twisted>=11.1.0
-ipaddr>=2.1.10
+ipaddress>=1.0.16
 zope.interface>=3.6.1
 txsocksx>=1.13.0
diff --git a/scripts/asciinema-demo1.py b/scripts/asciinema-demo1.py
new file mode 100755
index 0000000..79d7944
--- /dev/null
+++ b/scripts/asciinema-demo1.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+
+# this is a hack-tacular script to pass to asciinema like:
+#    asciinema -c ./asciinema-demo1.py rec
+# to script the show. as it were.
+
+import os
+import sys
+import time
+import random
+import colors
+import subprocess
+
+prompt = 'user at machine:~/src$ '
+
+def interkey_interval():
+    "in milliseconds"
+#    return 0  # faster debugging
+    return (random.lognormvariate(0.0, 0.5) * 30.0) / 1000.0
+    return (float(random.randrange(10,50)) / 1000.0)
+
+def type_it_out(line):
+    for c in line:
+        sys.stdout.write(c)
+        sys.stdout.flush()
+        time.sleep(interkey_interval())
+
+def do_commands(lines):
+    for line in lines:
+        if callable(line):
+            line()
+            continue
+        really_run = True
+        if line.startswith('!'):
+            really_run = False
+            line = line[1:]
+        sys.stdout.write(colors.blue(prompt))
+        type_it_out(line)
+        time.sleep(0.5)
+        print
+        if really_run:
+            # XXX nice to have time-limit?
+            os.system(colors.strip_color(line))
+
+def change_prompt(p):
+    global prompt
+    prompt = p
+
+commands = []
+commands.append(colors.bold('export TMPDIR=/dev/shm'))
+commands.append(colors.red('# see http://txtorcon.readthedocs.org'))
+commands.append('git clone https://github.com/meejah/txtorcon')
+commands.append(colors.bold('virtualenv venv'))
+commands.append('!' + colors.bold('source ./venv/bin/activate'))
+commands.append(lambda: change_prompt('(venv)user at machine:~/src/txtorcon$ '))
+commands.append(lambda: sys.path.insert(0, './venv/bin'))
+commands.append(colors.bold(colors.white('pip install --editable ./txtorcon')))
+commands.append('!' + colors.white('cd txtorcon'))
+commands.append(lambda: os.chdir('./txtorcon'))
+commands.append(lambda: change_prompt('user at machine:~/src/txtorcon$ '))
+commands.append('make coverage')
+#commands.append('python examples/add_hiddenservice_to_system_tor.py')
+commands.append(colors.red('# okay, lets try one of the examles'))
+commands.append('ls examples/')
+commands.append('python examples/dump_config.py | head')
+commands.append('python examples/hello_darkweb.py')
+commands.append(colors.red('# thanks for watching'))
+commands.append(colors.red('# https://github.com/meejah/txtorcon'))
+commands.append(colors.red('# https://txtorcon.readthedocs.org'))
+
+if __name__ == '__main__':
+    do_commands(commands)
diff --git a/setup.py b/setup.py
index e19437e..33c051c 100644
--- a/setup.py
+++ b/setup.py
@@ -3,71 +3,84 @@
 from __future__ import absolute_import
 from __future__ import print_function
 
-try:
-    import pypissh
-except:
-    print("WARNING: not using PyPi over SSH!")
-import sys
-import os
-import shutil
-import re
+from os.path import join
+from os import listdir
 from setuptools import setup
 
-# can't just naively import these from txtorcon, as that will only
-# work if you already installed the dependencies :(
-__version__ = '0.14.2'
-__author__ = 'meejah'
-__contact__ = 'meejah at meejah.ca'
-__url__ = 'https://github.com/meejah/txtorcon'
-__license__ = 'MIT'
-__copyright__ = 'Copyright 2012-2015'
+# Hmmmph.
+# So we get all the meta-information in one place (yay!) but we call
+# exec to get it (boo!). Note that we can't "from txtorcon._metadata
+# import *" here because that won't work when setup is being run by
+# pip (outside of Git checkout etc)
+with open('txtorcon/_metadata.py') as f:
+    exec(
+        compile(f.read(), '_metadata.py', 'exec'),
+        globals(),
+        locals(),
+    )
 
+description = '''
+    Twisted-based Tor controller client, with state-tracking and
+    configuration abstractions.
+'''
 
-setup(name = 'txtorcon',
-      version = __version__,
-      description = 'Twisted-based Tor controller client, with state-tracking and configuration abstractions.',
-      long_description = open('README.rst', 'r').read(),
-      keywords = ['python', 'twisted', 'tor', 'tor controller'],
-      install_requires = open('requirements.txt').readlines(),
-      # "pip install -e .[dev]" will install development requirements
-      extras_require=dict(
-          dev=open('dev-requirements.txt').readlines(),
-      ),
-      classifiers = ['Framework :: Twisted',
-                     'Development Status :: 4 - Beta',
-                     'Intended Audience :: Developers',
-                     'License :: OSI Approved :: MIT License',
-                     'Natural Language :: English',
-                     'Operating System :: POSIX :: Linux',
-                     'Operating System :: Unix',
-                     'Programming Language :: Python',
-                     'Programming Language :: Python :: 2',
-                     'Programming Language :: Python :: 2.6',
-                     'Programming Language :: Python :: 2.7',
-                     'Topic :: Software Development :: Libraries :: Python Modules',
-                     'Topic :: Internet :: Proxy Servers',
-                     'Topic :: Internet',
-                     'Topic :: Security'],
-      author = __author__,
-      author_email = __contact__,
-      url = __url__,
-      license = __license__,
-      packages  = ["txtorcon", "twisted.plugins"],
-#      scripts = ['examples/attach_streams_by_country.py'],
+sphinx_rst_files = [x for x in listdir('docs') if x[-3:] == 'rst']
+sphinx_docs = [join('docs', x) for x in sphinx_rst_files]
+sphinx_docs += [join('docs/_static', x) for x in listdir('docs/_static')]
+examples = [x for x in listdir('examples') if x[-3:] == '.py']
 
-      ## I'm a little unclear if I'm doing this "properly", especially
-      ## the documentation etc. Do we really want "share/txtorcon" for
-      ## the first member of the tuple? Why does it seem I need to
-      ## duplicate this in MANIFEST.in?
+setup(
+    name='txtorcon',
+    version=__version__,
+    description=description,
+    long_description=open('README.rst', 'r').read(),
+    keywords=['python', 'twisted', 'tor', 'tor controller'],
+    install_requires=open('requirements.txt').readlines(),
+    # "pip install -e .[dev]" will install development requirements
+    extras_require=dict(
+        dev=open('dev-requirements.txt').readlines(),
+    ),
+    classifiers=[
+        'Framework :: Twisted',
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: MIT License',
+        'Natural Language :: English',
+        'Operating System :: POSIX :: Linux',
+        'Operating System :: Unix',
+        'Programming Language :: Python',
+        'Programming Language :: Python :: 2',
+        'Programming Language :: Python :: 2.6',
+        'Programming Language :: Python :: 2.7',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+        'Topic :: Internet :: Proxy Servers',
+        'Topic :: Internet',
+        'Topic :: Security',
+    ],
+    author=__author__,
+    author_email=__contact__,
+    url=__url__,
+    license=__license__,
+    packages=["txtorcon", "twisted.plugins"],
 
-      data_files = [('share/txtorcon', ['INSTALL', 'README.rst', 'TODO', 'meejah.asc']),
+    # I'm a little unclear if I'm doing this "properly", especially
+    # the documentation etc. Do we really want "share/txtorcon" for
+    # the first member of the tuple? Why does it seem I need to
+    # duplicate this in MANIFEST.in?
 
-                    ## this includes the Sphinx source for the
-                    ## docs. The "map+filter" construct grabs all .rst
-                    ## files and re-maps the path
-                    ('share/txtorcon', ['docs/apilinks_sphinxext.py', 'docs/conf.py', 'docs/Makefile'] + [os.path.join('docs', x) for x in [x for x in os.listdir('docs') if x[-3:] == 'rst']] + [os.path.join('docs/_static', x) for x in os.listdir('docs/_static')]),
+    data_files=[
+        ('share/txtorcon', ['INSTALL', 'README.rst', 'TODO', 'meejah.asc']),
 
-                    ## include all the examples
-                    ('share/txtorcon/examples', [os.path.join('examples', x) for x in [x for x in os.listdir('examples') if x[-3:] == '.py']])
-                    ]
-      )
+        # this includes the Sphinx source for the
+        # docs. The "map+filter" construct grabs all .rst
+        # files and re-maps the path
+        ('share/txtorcon', [
+            'docs/apilinks_sphinxext.py',
+            'docs/conf.py',
+            'docs/Makefile',
+        ] + sphinx_docs),
+
+        # include all the examples
+        ('share/txtorcon/examples', [join('examples', x) for x in examples])
+    ],
+)
diff --git a/test/test_circuit.py b/test/test_circuit.py
index 3ec9ece..782e0c6 100644
--- a/test/test_circuit.py
+++ b/test/test_circuit.py
@@ -1,26 +1,32 @@
 import datetime
 import time
 from twisted.trial import unittest
-from twisted.internet import defer
+from twisted.internet import defer, task
 from twisted.python.failure import Failure
 from zope.interface import implements
 
 from txtorcon import Circuit
+from txtorcon import build_timeout_circuit
+
 from txtorcon import Stream
 from txtorcon import TorControlProtocol
 from txtorcon import TorState
 from txtorcon import Router
+from txtorcon.router import hexIdFromHash
 from txtorcon.interface import IRouterContainer
 from txtorcon.interface import ICircuitListener
 from txtorcon.interface import ICircuitContainer
 from txtorcon.interface import CircuitListenerMixin
 from txtorcon.interface import ITorControlProtocol
 
+from mock import Mock
+
 
 class FakeTorController(object):
     implements(IRouterContainer, ICircuitListener, ICircuitContainer, ITorControlProtocol)
 
     post_bootstrap = defer.Deferred()
+    queue_command = Mock()
 
     def __init__(self):
         self.routers = {}
@@ -70,7 +76,8 @@ class FakeRouter:
 
     def __init__(self, hsh, nm):
         self.name = nm
-        self.hash = hsh
+        self.id_hash = hsh
+        self.id_hex = hexIdFromHash(self.id_hash)
         self.location = FakeLocation()
 
 examples = ['CIRC 365 LAUNCHED PURPOSE=GENERAL',
@@ -205,7 +212,7 @@ class CircuitTests(unittest.TestCase):
                 )
                 for (r, p) in zip(ex.split()[3].split(','), circuit.path):
                     d = r.split('=')[0]
-                    self.assertEqual(d, p.hash)
+                    self.assertEqual(d, p.id_hash)
 
     def test_extend_messages(self):
         tor = FakeTorController()
diff --git a/test/test_endpoints.py b/test/test_endpoints.py
index a101597..ab0c68a 100644
--- a/test/test_endpoints.py
+++ b/test/test_endpoints.py
@@ -3,7 +3,7 @@ import shutil
 import tempfile
 
 from mock import patch
-from mock import Mock
+from mock import Mock, MagicMock
 
 from zope.interface import implements
 
@@ -39,9 +39,6 @@ from txtorcon.endpoints import default_tcp4_endpoint_generator
 import util
 
 
-connectionRefusedFailure = Failure(ConnectionRefusedError())
-
-
 class EndpointTests(unittest.TestCase):
 
     def setUp(self):
@@ -254,19 +251,24 @@ class EndpointTests(unittest.TestCase):
 
     @defer.inlineCallbacks
     def test_explicit_data_dir(self):
-        config = TorConfig(self.protocol)
-        ep = TCPHiddenServiceEndpoint(self.reactor, config, 123, '/dev/null')
+        d = tempfile.mkdtemp()
+        try:
+            with open(os.path.join(d, 'hostname'), 'w') as f:
+                f.write('public')
 
-        # make sure listen() correctly configures our hidden-serivce
-        # with the explicit directory we passed in above
-        d = ep.listen(NoOpProtocolFactory())
+            config = TorConfig(self.protocol)
+            ep = TCPHiddenServiceEndpoint(self.reactor, config, 123, d)
 
-        def foo(fail):
-            print "ERROR", fail
-        d.addErrback(foo)
-        port = yield d
-        self.assertEqual(1, len(config.HiddenServices))
-        self.assertEqual(config.HiddenServices[0].dir, '/dev/null')
+            # make sure listen() correctly configures our hidden-serivce
+            # with the explicit directory we passed in above
+            port = yield ep.listen(NoOpProtocolFactory())
+
+            self.assertEqual(1, len(config.HiddenServices))
+            self.assertEqual(config.HiddenServices[0].dir, d)
+            self.assertEqual(config.HiddenServices[0].hostname, 'public')
+
+        finally:
+            shutil.rmtree(d, ignore_errors=True)
 
     def test_failure(self):
         self.reactor.failures = 1
@@ -361,6 +363,32 @@ class EndpointTests(unittest.TestCase):
         finally:
             os.chdir(orig)
 
+    @defer.inlineCallbacks
+    def test_stealth_auth(self):
+        '''
+        make sure we produce a HiddenService instance with stealth-auth
+        lines if we had authentication specified in the first place.
+        '''
+
+        config = TorConfig(self.protocol)
+        ep = TCPHiddenServiceEndpoint(self.reactor, config, 123, '/dev/null',
+                                      stealth_auth=['alice', 'bob'])
+
+        # make sure listen() correctly configures our hidden-serivce
+        # with the explicit directory we passed in above
+        d = ep.listen(NoOpProtocolFactory())
+
+        def foo(fail):
+            print "ERROR", fail
+        d.addErrback(foo)
+        port = yield d
+        self.assertEqual(1, len(config.HiddenServices))
+        self.assertEqual(config.HiddenServices[0].dir, '/dev/null')
+        self.assertEqual(config.HiddenServices[0].authorize_client[0], 'stealth alice,bob')
+        self.assertEqual(None, ep.onion_uri)
+        config.HiddenServices[0].hostname = 'oh my'
+        self.assertEqual('oh my', ep.onion_uri)
+
 
 class EndpointLaunchTests(unittest.TestCase):
 
@@ -540,12 +568,12 @@ class FakeTorSocksEndpoint(object):
         self.transport = None
 
         self.failure = kw.get('failure', None)
-        self.acceptPort = kw.get('acceptPort', None)
+        self.accept_port = kw.get('accept_port', None)
 
     def connect(self, fac):
         self.factory = fac
-        if self.acceptPort:
-            if self.port != self.acceptPort:
+        if self.accept_port:
+            if self.port != self.accept_port:
                 return defer.fail(self.failure)
         else:
             if self.failure:
@@ -564,10 +592,10 @@ class TestTorClientEndpoint(unittest.TestCase):
         This test is equivalent to txsocksx's
         TestSOCKS4ClientEndpoint.test_clientConnectionFailed
         """
-        def FailTorSocksEndpointGenerator(*args, **kw):
-            kw['failure'] = connectionRefusedFailure
+        def fail_tor_socks_endpoint_generator(*args, **kw):
+            kw['failure'] = Failure(ConnectionRefusedError())
             return FakeTorSocksEndpoint(*args, **kw)
-        endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=FailTorSocksEndpointGenerator)
+        endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=fail_tor_socks_endpoint_generator)
         d = endpoint.connect(None)
         return self.assertFailure(d, ConnectionRefusedError)
 
@@ -575,18 +603,18 @@ class TestTorClientEndpoint(unittest.TestCase):
         """
         Same as above, but with a username/password.
         """
-        def FailTorSocksEndpointGenerator(*args, **kw):
-            kw['failure'] = connectionRefusedFailure
+        def fail_tor_socks_endpoint_generator(*args, **kw):
+            kw['failure'] = Failure(ConnectionRefusedError())
             return FakeTorSocksEndpoint(*args, **kw)
         endpoint = TorClientEndpoint(
             'invalid host', 0,
             socks_username='billy', socks_password='s333cure',
-            _proxy_endpoint_generator=FailTorSocksEndpointGenerator)
+            _proxy_endpoint_generator=fail_tor_socks_endpoint_generator)
         d = endpoint.connect(None)
         return self.assertFailure(d, ConnectionRefusedError)
 
     def test_default_generator(self):
-        # just ensuring the default generator doesn't blow updoesn't blow up
+        # just ensuring the default generator doesn't blow up
         default_tcp4_endpoint_generator(None, 'foo.bar', 1234)
 
     def test_no_host(self):
@@ -616,28 +644,49 @@ class TestTorClientEndpoint(unittest.TestCase):
         """
         This test is equivalent to txsocksx's TestSOCKS5ClientEndpoint.test_defaultFactory
         """
-        def TorSocksEndpointGenerator(*args, **kw):
+        endpoints = []
+
+        def tor_socks_endpoint_generator(*args, **kw):
+            endpoints.append(FakeTorSocksEndpoint(*args, **kw))
+            return endpoints[-1]
+        endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator)
+        endpoint.connect(Mock)
+        self.assertEqual(1, len(endpoints))
+        self.assertEqual(endpoints[0].transport.value(), '\x05\x01\x00')
+
+    @patch('txtorcon.endpoints.SOCKS5ClientEndpoint')
+    @defer.inlineCallbacks
+    def test_success(self, socks5_factory):
+        ep = MagicMock()
+        gold_proto = object()
+        ep.connect = MagicMock(return_value=gold_proto)
+        socks5_factory.return_value = ep
+
+        def tor_socks_endpoint_generator(*args, **kw):
             return FakeTorSocksEndpoint(*args, **kw)
-        endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator)
-        endpoint.connect(None)
-        self.assertEqual(endpoint.tor_socks_endpoint.transport.value(), '\x05\x01\x00')
+
+        endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator)
+        other_proto = yield endpoint.connect(MagicMock())
+        self.assertEqual(other_proto, gold_proto)
 
     def test_good_port_retry(self):
         """
         This tests that our Tor client endpoint retry logic works correctly.
-        We create a proxy endpoint that fires a connectionRefusedFailure
+        We create a proxy endpoint that fires a ConnectionRefusedError
         unless the connecting port matches. We attempt to connect with the
         proxy endpoint for each port that the Tor client endpoint will try.
         """
         success_ports = TorClientEndpoint.socks_ports_to_try
+        endpoints = []
         for port in success_ports:
-            def TorSocksEndpointGenerator(*args, **kw):
-                kw['acceptPort'] = port
-                kw['failure'] = connectionRefusedFailure
-                return FakeTorSocksEndpoint(*args, **kw)
-            endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator)
+            def tor_socks_endpoint_generator(*args, **kw):
+                kw['accept_port'] = port
+                kw['failure'] = Failure(ConnectionRefusedError())
+                endpoints.append(FakeTorSocksEndpoint(*args, **kw))
+                return endpoints[-1]
+            endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator)
             endpoint.connect(None)
-            self.assertEqual(endpoint.tor_socks_endpoint.transport.value(), '\x05\x01\x00')
+            self.assertEqual(endpoints[-1].transport.value(), '\x05\x01\x00')
 
     def test_bad_port_retry(self):
         """
@@ -645,11 +694,11 @@ class TestTorClientEndpoint(unittest.TestCase):
         """
         fail_ports = [1984, 666]
         for port in fail_ports:
-            def TorSocksEndpointGenerator(*args, **kw):
-                kw['acceptPort'] = port
-                kw['failure'] = connectionRefusedFailure
+            def tor_socks_endpoint_generator(*args, **kw):
+                kw['accept_port'] = port
+                kw['failure'] = Failure(ConnectionRefusedError())
                 return FakeTorSocksEndpoint(*args, **kw)
-            endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator)
+            endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator)
             d = endpoint.connect(None)
             return self.assertFailure(d, ConnectionRefusedError)
 
@@ -658,13 +707,17 @@ class TestTorClientEndpoint(unittest.TestCase):
         This tests that if a SOCKS port is specified, we *only* attempt to
         connect to that SOCKS port.
         """
-        def TorSocksEndpointGenerator(*args, **kw):
-            kw['acceptPort'] = 6669
-            kw['failure'] = connectionRefusedFailure
-            return FakeTorSocksEndpoint(*args, **kw)
-        endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator, socks_port=6669)
+        endpoints = []
+
+        def tor_socks_endpoint_generator(*args, **kw):
+            kw['accept_port'] = 6669
+            kw['failure'] = Failure(ConnectionRefusedError())
+            endpoints.append(FakeTorSocksEndpoint(*args, **kw))
+            return endpoints[-1]
+        endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator, socks_port=6669)
         endpoint.connect(None)
-        self.assertEqual(endpoint.tor_socks_endpoint.transport.value(), '\x05\x01\x00')
+        self.assertEqual(1, len(endpoints))
+        self.assertEqual(endpoints[-1].transport.value(), '\x05\x01\x00')
 
     def test_bad_no_guess_socks_port(self):
         """
@@ -672,10 +725,10 @@ class TestTorClientEndpoint(unittest.TestCase):
         specified SOCKS port... even if there is a valid SOCKS port listening on
         the socks_ports_to_try list.
         """
-        def TorSocksEndpointGenerator(*args, **kw):
-            kw['acceptPort'] = 9050
-            kw['failure'] = connectionRefusedFailure
+        def tor_socks_endpoint_generator(*args, **kw):
+            kw['accept_port'] = 9050
+            kw['failure'] = Failure(ConnectionRefusedError())
             return FakeTorSocksEndpoint(*args, **kw)
-        endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=TorSocksEndpointGenerator, socks_port=6669)
+        endpoint = TorClientEndpoint('', 0, _proxy_endpoint_generator=tor_socks_endpoint_generator, socks_port=6669)
         d = endpoint.connect(None)
         self.assertFailure(d, ConnectionRefusedError)
diff --git a/test/test_torconfig.py b/test/test_torconfig.py
index df329fe..b0edea0 100644
--- a/test/test_torconfig.py
+++ b/test/test_torconfig.py
@@ -166,6 +166,47 @@ class ConfigTests(unittest.TestCase):
         self.assertTrue(conf.foo is False)
         self.assertTrue(conf.bar is True)
 
+    def test_save_boolean(self):
+        self.protocol.answers.append('config/names=\nfoo Boolean\nbar Boolean')
+        self.protocol.answers.append({'foo': '0'})
+        self.protocol.answers.append({'bar': '1'})
+
+        conf = TorConfig(self.protocol)
+
+        # save some boolean value
+        conf.foo = True
+        conf.bar = False
+        conf.save()
+        self.assertEqual(set(self.protocol.sets),
+                         set([('foo', 1), ('bar', 0)]))
+
+    def test_read_boolean_after_save(self):
+        self.protocol.answers.append('config/names=\nfoo Boolean\nbar Boolean')
+        self.protocol.answers.append({'foo': '0'})
+        self.protocol.answers.append({'bar': '1'})
+
+        conf = TorConfig(self.protocol)
+
+        # save some boolean value
+        conf.foo = True
+        conf.bar = False
+        conf.save()
+        self.assertTrue(conf.foo is True, msg="foo not True: %s" % conf.foo)
+        self.assertTrue(conf.bar is False, msg="bar not False: %s" % conf.bar)
+
+    def test_save_boolean_with_strange_values(self):
+        self.protocol.answers.append('config/names=\nfoo Boolean\nbar Boolean')
+        self.protocol.answers.append({'foo': '0'})
+        self.protocol.answers.append({'bar': '1'})
+
+        conf = TorConfig(self.protocol)
+        # save some non-boolean value
+        conf.foo = "Something True"
+        conf.bar = 0
+        conf.save()
+        self.assertEqual(set(self.protocol.sets),
+                         set([('foo', 1), ('bar', 0)]))
+
     def test_boolean_auto_parser(self):
         self.protocol.answers.append(
             'config/names=\nfoo Boolean+Auto\nbar Boolean+Auto\nbaz Boolean+Auto'
@@ -179,6 +220,48 @@ class ConfigTests(unittest.TestCase):
         self.assertTrue(conf.bar is 1)
         self.assertTrue(conf.baz is -1)
 
+    def test_save_boolean_auto(self):
+        self.protocol.answers.append(
+            'config/names=\nfoo Boolean+Auto\nbar Boolean+Auto\nbaz Boolean+Auto\nqux Boolean+Auto'
+        )
+        self.protocol.answers.append({'foo': '1'})
+        self.protocol.answers.append({'bar': '1'})
+        self.protocol.answers.append({'baz': '1'})
+        self.protocol.answers.append({'qux': '1'})
+
+        conf = TorConfig(self.protocol)
+        conf.foo = 1
+        conf.bar = 0
+        conf.baz = True
+        conf.qux = -1
+        conf.save()
+        self.assertEqual(set(self.protocol.sets),
+                         set([('foo', 1),
+                              ('bar', 0),
+                              ('baz', 1),
+                              ('qux', 'auto')]))
+        self.assertTrue(conf.foo is 1)
+        self.assertTrue(conf.bar is 0)
+        self.assertTrue(conf.baz is 1)
+        self.assertTrue(conf.qux is -1)
+
+    def test_save_invalid_boolean_auto(self):
+        self.protocol.answers.append(
+            'config/names=\nfoo Boolean+Auto'
+        )
+        self.protocol.answers.append({'foo': '1'})
+
+        conf = TorConfig(self.protocol)
+        for value in ('auto', 'True', 'False', None):
+            try:
+                conf.foo = value
+            except (ValueError, TypeError):
+                pass
+            else:
+                self.fail("Invalid value '%s' allowed" % value)
+            conf.save()
+            self.assertEqual(self.protocol.sets, [])
+
     def test_string_parser(self):
         self.protocol.answers.append('config/names=\nfoo String')
         self.protocol.answers.append({'foo': 'bar'})
@@ -191,6 +274,39 @@ class ConfigTests(unittest.TestCase):
         conf = TorConfig(self.protocol)
         self.assertEqual(conf.foo, 123)
 
+    def test_int_validator(self):
+        self.protocol.answers.append('config/names=\nfoo Integer')
+        self.protocol.answers.append({'foo': '123'})
+        conf = TorConfig(self.protocol)
+
+        conf.foo = 2.33
+        conf.save()
+        self.assertEqual(conf.foo, 2)
+
+        conf.foo = '1'
+        conf.save()
+        self.assertEqual(conf.foo, 1)
+
+        conf.foo = '-100'
+        conf.save()
+        self.assertEqual(conf.foo, -100)
+
+        conf.foo = 0
+        conf.save()
+        self.assertEqual(conf.foo, 0)
+
+        conf.foo = '0'
+        conf.save()
+        self.assertEqual(conf.foo, 0)
+
+        for value in ('no', 'Not a value', None):
+            try:
+                conf.foo = value
+            except (ValueError, TypeError):
+                pass
+            else:
+                self.fail("No excpetion thrown")
+
     def test_int_parser_error(self):
         self.protocol.answers.append('config/names=\nfoo Integer')
         self.protocol.answers.append({'foo': '123foo'})
@@ -603,6 +719,41 @@ class EventTests(unittest.TestCase):
         self.assertEqual(config.Foo, 'bar')
         self.assertEqual(config.Bar, DEFAULT_VALUE)
 
+    def test_conf_changed_parsed(self):
+        '''
+        Create a configuration which holds boolean types. These types
+        have to be parsed as booleans.
+        '''
+        protocol = FakeControlProtocol([])
+        protocol.answers.append('config/names=\nFoo Boolean\nBar Boolean')
+        protocol.answers.append({'Foo': '0'})
+        protocol.answers.append({'Bar': '1'})
+
+        config = TorConfig(protocol)
+        # Initial value is not tested here
+        protocol.events['CONF_CHANGED']('Foo=1\nBar=0')
+
+        msg = "Foo is not True: %r" % config.Foo
+        self.assertTrue(config.Foo is True, msg=msg)
+
+        msg = "Foo is not False: %r" % config.Bar
+        self.assertTrue(config.Bar is False, msg=msg)
+
+    def test_conf_changed_invalid_values(self):
+        protocol = FakeControlProtocol([])
+        protocol.answers.append('config/names=\nFoo Integer\nBar Integer')
+        protocol.answers.append({'Foo': '0'})
+        protocol.answers.append({'Bar': '1'})
+
+        config = TorConfig(protocol)
+        # Initial value is not tested here
+        try:
+            protocol.events['CONF_CHANGED']('Foo=INVALID\nBar=VALUES')
+        except (ValueError, TypeError):
+            pass
+        else:
+            self.fail("No excpetion thrown")
+
 
 class CreateTorrcTests(unittest.TestCase):
 
@@ -709,6 +860,48 @@ HiddenServiceAuthorizeClient Dependant''')
         finally:
             shutil.rmtree(d, ignore_errors=True)
 
+    def test_single_client(self):
+        # FIXME test without crapping on filesystem
+        self.protocol.answers.append('HiddenServiceDir=/fake/path\n')
+        d = tempfile.mkdtemp()
+
+        try:
+            with open(os.path.join(d, 'hostname'), 'w') as f:
+                f.write('gobledegook\n')
+
+            conf = TorConfig(self.protocol)
+            hs = HiddenService(conf, d, [])
+
+            self.assertEqual(1, len(hs.clients))
+            self.assertEqual('default', hs.clients[0][0])
+            self.assertEqual('gobledegook', hs.clients[0][1])
+
+        finally:
+            shutil.rmtree(d, ignore_errors=True)
+
+    def test_stealth_clients(self):
+        # FIXME test without crapping on filesystem
+        self.protocol.answers.append('HiddenServiceDir=/fake/path\n')
+        d = tempfile.mkdtemp()
+
+        try:
+            with open(os.path.join(d, 'hostname'), 'w') as f:
+                f.write('oniona cookiea\n')
+                f.write('onionb cookieb\n')
+
+            conf = TorConfig(self.protocol)
+            hs = HiddenService(conf, d, [])
+
+            self.assertEqual(2, len(hs.clients))
+            self.assertEqual('oniona', hs.clients[0][0])
+            self.assertEqual('cookiea', hs.clients[0][1])
+            self.assertEqual('onionb', hs.clients[1][0])
+            self.assertEqual('cookieb', hs.clients[1][1])
+            self.assertRaises(RuntimeError, getattr, hs, 'hostname')
+
+        finally:
+            shutil.rmtree(d, ignore_errors=True)
+
     def test_modify_hidden_service(self):
         self.protocol.answers.append('HiddenServiceDir=/fake/path\nHiddenServicePort=80 127.0.0.1:1234\n')
 
@@ -1545,11 +1738,11 @@ class HiddenServiceAuthTests(unittest.TestCase):
         self.assertEqual(3, len(clients))
         self.assertEqual('bar', clients[0].name)
         self.assertEqual('O4rQyZ+IJr2PNHUdeXi0nA', clients[0].cookie)
-        self.assertEqual('MIICXQIBAAKBgQC1R/bPGTWnpGJpNCfT1KIfFq1QEGHz4enKSEKUDkz1CSEPOMGSbV37dfqTuI4klsFvdUsR3NpYXLin9xRWvw1viKwAN0y8cv5totl4qMxO5i+zcfVhbJiNvVv2EjfEyQaZfAy2PUfp/tAPYZMsyfps2DptWyNR', clients[0].key)
+        self.assertEqual('RSA1024:MIICXQIBAAKBgQC1R/bPGTWnpGJpNCfT1KIfFq1QEGHz4enKSEKUDkz1CSEPOMGSbV37dfqTuI4klsFvdUsR3NpYXLin9xRWvw1viKwAN0y8cv5totl4qMxO5i+zcfVhbJiNvVv2EjfEyQaZfAy2PUfp/tAPYZMsyfps2DptWyNR', clients[0].key)
 
         self.assertEqual('foo', clients[1].name)
         self.assertEqual('btlj4+RsWEkxigmlszInhQ', clients[1].cookie)
-        self.assertEqual(clients[1].key, 'MIICXgIBAAKBgQDdLdHU1fbABtFutOFtpdWQdv/9qG1OAc0r1TfaBtkPSNcLezcxSThalIEnRFfejy0suOHmsqspruvn0FEflIEQvFWeXAPvXg==')
+        self.assertEqual(clients[1].key, 'RSA1024:MIICXgIBAAKBgQDdLdHU1fbABtFutOFtpdWQdv/9qG1OAc0r1TfaBtkPSNcLezcxSThalIEnRFfejy0suOHmsqspruvn0FEflIEQvFWeXAPvXg==')
 
         self.assertEqual('quux', clients[2].name)
         self.assertEqual('asdlkjasdlfkjalsdkfffj', clients[2].cookie)
@@ -1562,3 +1755,117 @@ class HiddenServiceAuthTests(unittest.TestCase):
             RuntimeError,
             parse_client_keys, data
         )
+
+
+class EphemeralHiddenServiceTest(unittest.TestCase):
+    def test_defaults(self):
+        eph = torconfig.EphemeralHiddenService("80 localhost:80")
+        self.assertEqual(eph._ports, ["80,localhost:80"])
+
+    def test_wrong_blob(self):
+        try:
+            eph = torconfig.EphemeralHiddenService("80 localhost:80", "foo")
+            self.fail("should get exception")
+        except RuntimeError as e:
+            pass
+
+    def test_add(self):
+        eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80")
+        proto = Mock()
+        proto.queue_command = Mock(return_value="PrivateKey=blam\nServiceID=ohai")
+        eph.add_to_tor(proto)
+
+        self.assertEqual("blam", eph.private_key)
+        self.assertEqual("ohai.onion", eph.hostname)
+
+    def test_descriptor_wait(self):
+        eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80")
+        proto = Mock()
+        proto.queue_command = Mock(return_value=defer.succeed("PrivateKey=blam\nServiceID=ohai\n"))
+
+        eph.add_to_tor(proto)
+
+        # get the event-listener callback that torconfig code added;
+        # the last call [-1] was to add_event_listener; we want the
+        # [1] arg of that
+        cb = proto.method_calls[-1][1][1]
+
+        # Tor doesn't actually provide the .onion, but we can test it anyway
+        cb('UPLOADED ohai UNKNOWN somehsdir')
+        cb('UPLOADED UNKNOWN UNKNOWN somehsdir')
+
+        self.assertEqual("blam", eph.private_key)
+        self.assertEqual("ohai.onion", eph.hostname)
+
+    def test_remove(self):
+        eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80")
+        eph.hostname = 'foo.onion'
+        proto = Mock()
+        proto.queue_command = Mock(return_value="OK")
+
+        eph.remove_from_tor(proto)
+
+    @defer.inlineCallbacks
+    def test_remove_error(self):
+        eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80")
+        eph.hostname = 'foo.onion'
+        proto = Mock()
+        proto.queue_command = Mock(return_value="it's not ok")
+
+        try:
+            yield eph.remove_from_tor(proto)
+            self.fail("should have gotten exception")
+        except RuntimeError as e:
+            pass
+
+    def test_failed_upload(self):
+        eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80")
+        proto = Mock()
+        proto.queue_command = Mock(return_value=defer.succeed("PrivateKey=seekrit\nServiceID=42\n"))
+
+        d = eph.add_to_tor(proto)
+
+        # get the event-listener callback that torconfig code added;
+        # the last call [-1] was to add_event_listener; we want the
+        # [1] arg of that
+        cb = proto.method_calls[-1][1][1]
+
+        # Tor leads with UPLOAD events for each attempt; we queue 2 of
+        # these...
+        cb('UPLOAD 42 UNKNOWN hsdir0')
+        cb('UPLOAD 42 UNKNOWN hsdir1')
+
+        # ...but fail them both
+        cb('FAILED 42 UNKNOWN hsdir1 REASON=UPLOAD_REJECTED')
+        cb('FAILED 42 UNKNOWN hsdir0 REASON=UPLOAD_REJECTED')
+
+        self.assertEqual("seekrit", eph.private_key)
+        self.assertEqual("42.onion", eph.hostname)
+        self.assertTrue(d.called)
+        d.addErrback(lambda e: self.assertTrue('Failed to upload' in str(e)))
+
+    def test_single_failed_upload(self):
+        eph = torconfig.EphemeralHiddenService("80 127.0.0.1:80")
+        proto = Mock()
+        proto.queue_command = Mock(return_value=defer.succeed("PrivateKey=seekrit\nServiceID=42\n"))
+
+        d = eph.add_to_tor(proto)
+
+        # get the event-listener callback that torconfig code added;
+        # the last call [-1] was to add_event_listener; we want the
+        # [1] arg of that
+        cb = proto.method_calls[-1][1][1]
+
+        # Tor leads with UPLOAD events for each attempt; we queue 2 of
+        # these...
+        cb('UPLOAD 42 UNKNOWN hsdir0')
+        cb('UPLOAD 42 UNKNOWN hsdir1')
+
+        # ...then fail one
+        cb('FAILED 42 UNKNOWN hsdir1 REASON=UPLOAD_REJECTED')
+        # ...and succeed on the last.
+        cb('UPLOADED 42 UNKNOWN hsdir0')
+
+        self.assertEqual("seekrit", eph.private_key)
+        self.assertEqual("42.onion", eph.hostname)
+        self.assertTrue(d.called)
diff --git a/test/test_torcontrolprotocol.py b/test/test_torcontrolprotocol.py
index 9b9e5fa..9da82a6 100644
--- a/test/test_torcontrolprotocol.py
+++ b/test/test_torcontrolprotocol.py
@@ -104,6 +104,17 @@ class AuthenticationTests(unittest.TestCase):
 
         self.assertEqual(self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex"))
 
+    def test_authenticate_null(self):
+        self.protocol.makeConnection(self.transport)
+        self.assertEqual(self.transport.value(), 'PROTOCOLINFO 1\r\n')
+        self.transport.clear()
+        self.send('250-PROTOCOLINFO 1')
+        self.send('250-AUTH METHODS=NULL')
+        self.send('250-VERSION Tor="0.2.2.34"')
+        self.send('250 OK')
+
+        self.assertEqual(self.transport.value(), 'AUTHENTICATE\r\n')
+
     def test_authenticate_password_deferred(self):
         d = defer.Deferred()
         self.protocol.password_function = lambda: d
@@ -335,6 +346,68 @@ OK''' % cookietmp.name)
             )
             self.assertTrue('AUTHENTICATE ' in self.transport.value())
 
+    def test_authenticate_cookie_without_reading(self):
+        server_nonce = str(bytearray([0] * 32))
+        server_hash = str(bytearray([0] * 32))
+        try:
+            self.protocol._safecookie_authchallenge(
+                '250 AUTHCHALLENGE SERVERHASH=%s SERVERNONCE=%s' %
+                (base64.b16encode(server_hash), base64.b16encode(server_nonce))
+            )
+            self.assertTrue(False)
+        except RuntimeError, e:
+            self.assertTrue('not read' in str(e))
+
+    def test_authenticate_unexisting_cookie_file(self):
+        unexisting_file = __file__ + "-unexisting"
+        try:
+            self.protocol._do_authenticate('''PROTOCOLINFO 1
+AUTH METHODS=COOKIE COOKIEFILE="%s"
+VERSION Tor="0.2.2.35"
+OK''' % unexisting_file)
+            self.assertTrue(False)
+        except RuntimeError:
+            pass
+
+    def test_authenticate_unexisting_safecookie_file(self):
+        unexisting_file = __file__ + "-unexisting"
+        try:
+            self.protocol._do_authenticate('''PROTOCOLINFO 1
+AUTH METHODS=SAFECOOKIE COOKIEFILE="%s"
+VERSION Tor="0.2.2.35"
+OK''' % unexisting_file)
+            self.assertTrue(False)
+        except RuntimeError:
+            pass
+
+    def test_authenticate_dont_send_cookiefile(self):
+        try:
+            self.protocol._do_authenticate('''PROTOCOLINFO 1
+AUTH METHODS=SAFECOOKIE
+VERSION Tor="0.2.2.35"
+OK''')
+            self.assertTrue(False)
+        except RuntimeError:
+            pass
+
+    def test_authenticate_password_when_cookie_unavailable(self):
+        unexisting_file = __file__ + "-unexisting"
+        self.protocol.password_function = lambda: 'foo'
+        self.protocol._do_authenticate('''PROTOCOLINFO 1
+AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="%s"
+VERSION Tor="0.2.2.35"
+OK''' % unexisting_file)
+        self.assertEqual(self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex"))
+
+    def test_authenticate_password_when_safecookie_unavailable(self):
+        unexisting_file = __file__ + "-unexisting"
+        self.protocol.password_function = lambda: 'foo'
+        self.protocol._do_authenticate('''PROTOCOLINFO 1
+AUTH METHODS=SAFECOOKIE,HASHEDPASSWORD COOKIEFILE="%s"
+VERSION Tor="0.2.2.35"
+OK''' % unexisting_file)
+        self.assertEqual(self.transport.value(), 'AUTHENTICATE %s\r\n' % "foo".encode("hex"))
+
     def test_authenticate_safecookie_wrong_hash(self):
         cookiedata = str(bytearray([0] * 32))
         server_nonce = str(bytearray([0] * 32))
@@ -342,7 +415,7 @@ OK''' % cookietmp.name)
 
         # pretend we already did PROTOCOLINFO and read the cookie
         # file
-        self.protocol.cookie_data = cookiedata
+        self.protocol._cookie_data = cookiedata
         self.protocol.client_nonce = server_nonce  # all 0's anyway
         try:
             self.protocol._safecookie_authchallenge(
@@ -441,6 +514,18 @@ OK''' % cookietmp.name)
         self.send("250 OK")
         return d
 
+    def test_multiline_plus_embedded_equals(self):
+        """
+        """
+
+        d = self.protocol.get_info("FOO")
+        d.addCallback(CallbackChecker({"FOO": "\na="}))
+        self.send("250+FOO=")
+        self.send("a=")
+        self.send(".")
+        self.send("250 OK")
+        return d
+
     def incremental_check(self, expected, actual):
         if '=' in actual:
             return
@@ -648,6 +733,21 @@ iO3EUE0AEYah2W9gdz8t+i3Dtr0zgqLS841GC/TyDKCm+MKmN8d098qnwK0NGF9q
             self.send(line)
         return d
 
+    def test_getinfo_multiline(self):
+        descriptor_info = """250+desc/name/moria1=
+router moria1 128.31.0.34 9101 0 9131
+platform Tor 0.2.5.0-alpha-dev on Linux
+.
+250 OK"""
+        d = self.protocol.get_info("desc/name/moria1")
+        gold = "\nrouter moria1 128.31.0.34 9101 0 9131\nplatform Tor 0.2.5.0-alpha-dev on Linux"
+        d.addCallback(CallbackChecker({'desc/name/moria1': gold}))
+        d.addErrback(self.fail)
+
+        for line in descriptor_info.split('\n'):
+            self.send(line)
+        return d
+
     def test_addevent(self):
         self.protocol._set_valid_events('FOO BAR')
 
diff --git a/test/test_torinfo.py b/test/test_torinfo.py
index a6a3019..8060444 100644
--- a/test/test_torinfo.py
+++ b/test/test_torinfo.py
@@ -73,7 +73,7 @@ class ProtocolIntegrationTests(unittest.TestCase):
 
         # answer all the requests generated by TorControlProtocol
         # boostrapping etc.
-        self.send('250-AUTH METHODS=PASSWORD')
+        self.send('250-AUTH METHODS=HASHEDPASSWORD')
         self.send('250 OK')
 
         # response to AUTHENTICATE
diff --git a/test/test_torstate.py b/test/test_torstate.py
index d04202c..a00edd0 100644
--- a/test/test_torstate.py
+++ b/test/test_torstate.py
@@ -18,6 +18,8 @@ from txtorcon import Stream
 from txtorcon import Circuit
 from txtorcon import build_tor_connection
 from txtorcon import build_local_tor_connection
+from txtorcon import build_timeout_circuit
+from txtorcon import CircuitBuildTimedOutError
 from txtorcon.interface import ITorControlProtocol
 from txtorcon.interface import IStreamAttacher
 from txtorcon.interface import ICircuitListener
@@ -1142,3 +1144,56 @@ s Fast Guard Running Stable Valid
     def test_listener_mixins(self):
         self.assertTrue(verifyClass(IStreamListener, StreamListenerMixin))
         self.assertTrue(verifyClass(ICircuitListener, CircuitListenerMixin))
+
+    def test_build_circuit_timedout(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']
+
+        # FIXME TODO we should verify we get a circuit_new event for
+        # this circuit
+        timeout = 10
+        clock = task.Clock()
+
+        d = build_timeout_circuit(self.state, clock, path, timeout, using_guards=True)
+        clock.advance(10)
+
+        def check_for_timeout_error(f):
+            self.assertTrue(isinstance(f.type(), CircuitBuildTimedOutError))
+        d.addErrback(check_for_timeout_error)
+        return d
+
+    def test_build_circuit_not_timedout(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(), 'EXTENDCIRCUIT 0 0000000000000000000000000000000000000000,0000000000000000000000000000000000000001,0000000000000000000000000000000000000002\r\n')
+        self.send('250 EXTENDED 1234')
+        # we can't just .send('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', 'BUILT'])
+        # should have gotten a warning about this not being an entry
+        # guard
+        self.assertEqual(len(self.flushWarnings()), 1)
+        return d
diff --git a/test/test_util.py b/test/test_util.py
index 579c0c5..4d74b15 100644
--- a/test/test_util.py
+++ b/test/test_util.py
@@ -11,6 +11,7 @@ from txtorcon.util import find_keywords
 from txtorcon.util import ip_from_int
 from txtorcon.util import find_tor_binary
 from txtorcon.util import maybe_ip_addr
+from txtorcon.util import unescape_quoted_string
 
 import os
 import tempfile
@@ -267,13 +268,72 @@ class TestFindTor(unittest.TestCase):
 
 class TestIpAddr(unittest.TestCase):
 
-    @patch('txtorcon.util.ipaddr')
+    @patch('txtorcon.util.ipaddress')
     def test_create_ipaddr(self, ipaddr):
         ip = maybe_ip_addr('1.2.3.4')
 
-    @patch('txtorcon.util.ipaddr')
+    @patch('txtorcon.util.ipaddress')
     def test_create_ipaddr(self, ipaddr):
         def foo(blam):
             raise ValueError('testing')
-        ipaddr.IPAddress.side_effect = foo
+        ipaddr.ip_address.side_effect = foo
         ip = maybe_ip_addr('1.2.3.4')
+
+
+class TestUnescapeQuotedString(unittest.TestCase):
+    '''
+    Test cases for the function unescape_quoted_string.
+    '''
+    def test_valid_string_unescaping(self):
+        unescapeable = {
+            '\\\\': '\\',         # \\     -> \
+            r'\"': r'"',          # \"     -> "
+            r'\\\"': r'\"',       # \\\"   -> \"
+            r'\\\\\"': r'\\"',    # \\\\\" -> \\"
+            '\\"\\\\': '"\\',     # \"\\   -> "\
+            "\\'": "'",           # \'     -> '
+            "\\\\\\'": "\\'",     # \\\'   -> \
+            r'some\"text': 'some"text',
+            'some\\word': 'someword',
+            '\\delete\\ al\\l un\\used \\backslashes': 'delete all unused backslashes',
+            '\\n\\r\\t': '\n\r\t',
+            '\\x00 \\x0123': 'x00 x0123',
+            '\\\\x00 \\\\x00': '\\x00 \\x00',
+            '\\\\\\x00  \\\\\\x00': '\\x00  \\x00'
+        }
+
+        for escaped, correct_unescaped in unescapeable.items():
+            escaped = '"{}"'.format(escaped)
+            unescaped = unescape_quoted_string(escaped)
+            msg = "Wrong unescape: {escaped} -> {unescaped} instead of {correct}"
+            msg = msg.format(unescaped=unescaped, escaped=escaped,
+                             correct=correct_unescaped)
+            self.assertEqual(unescaped, correct_unescaped, msg=msg)
+
+    def test_string_unescape_octals(self):
+        '''
+        Octal numbers can be escaped by a backslash:
+        \0 is interpreted as a byte with the value 0
+        '''
+        for number in range(1000):
+            escaped = '\\{}'.format(number)
+            result = unescape_quoted_string('"{}"'.format(escaped))
+
+            expected = escaped.decode('string-escape')
+            if expected[0] == '\\' and len(expected) > 1:
+                expected = expected[1:]
+
+            msg = "Number not decoded correctly: {escaped} -> {result} instead of {expected}"
+            msg = msg.format(escaped=escaped, result=repr(result), expected=repr(expected))
+            self.assertEquals(result, expected, msg=msg)
+
+    def test_invalid_string_unescaping(self):
+        invalid_escaped = [
+            '"""',       # "     - unescaped quote
+            '"\\"',      # \     - unescaped backslash
+            '"\\\\\\"',  # \\\   - uneven backslashes
+            '"\\\\""',   # \\"   - quotes not escaped
+        ]
+
+        for invalid_string in invalid_escaped:
+            self.assertRaises(ValueError, unescape_quoted_string, invalid_string)
diff --git a/test/test_util_imports.py b/test/test_util_imports.py
index 77a2df1..ab64c78 100644
--- a/test/test_util_imports.py
+++ b/test/test_util_imports.py
@@ -7,7 +7,7 @@ from unittest import skipIf
 
 
 def fake_import(orig, name, *args, **kw):
-    if name in ['GeoIP', 'ipaddr']:
+    if name in ['GeoIP']:
         raise ImportError('testing!')
     return orig(*((name,) + args), **kw)
 
@@ -44,33 +44,3 @@ class TestImports(unittest.TestCase):
 
         finally:
             __import__ = orig
-
-    @skipIf('pypy' in sys.version.lower(), "Doesn't work in PYPY")
-    def test_no_ipaddr(self):
-        """
-        make sure the code we run if there's no ipaddr installed
-        doesn't do anything horrific
-        """
-
-        global __import__
-        orig = __import__
-        try:
-            # attempt to ensure we've unimportted txtorcon.util
-            del sys.modules['txtorcon.util']
-            import gc
-            gc.collect()
-
-            # replace global import with our test import, which will
-            # throw on GeoIP import no matter what
-            global __builtins__
-            __builtins__['__import__'] = functools.partial(fake_import, orig)
-
-            # now ensure we set up all the databases as "None" when we
-            # import w/o the GeoIP thing available.
-            import txtorcon.util
-            self.assertEqual(None, txtorcon.util.city)
-            self.assertEqual(None, txtorcon.util.asn)
-            self.assertEqual(None, txtorcon.util.country)
-
-        finally:
-            __import__ = orig
diff --git a/txtorcon.egg-info/PKG-INFO b/txtorcon.egg-info/PKG-INFO
index 80d1067..112af9b 100644
--- a/txtorcon.egg-info/PKG-INFO
+++ b/txtorcon.egg-info/PKG-INFO
@@ -1,7 +1,10 @@
 Metadata-Version: 1.1
 Name: txtorcon
-Version: 0.14.2
-Summary: Twisted-based Tor controller client, with state-tracking and configuration abstractions.
+Version: 0.15.0
+Summary: 
+    Twisted-based Tor controller client, with state-tracking and
+    configuration abstractions.
+
 Home-page: https://github.com/meejah/txtorcon
 Author: meejah
 Author-email: meejah at meejah.ca
@@ -17,6 +20,9 @@ Description: README
         .. image:: https://coveralls.io/repos/meejah/txtorcon/badge.png
             :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:: http://api.flattr.com/button/flattr-badge-large.png
             :target: http://flattr.com/thing/1689502/meejahtxtorcon-on-GitHub
         
@@ -143,9 +149,6 @@ Description: README
            optional, in that we'll query Tor for the IP if the GeoIP database
            doesn't have an answer. It also does ASN lookups if you installed that MaxMind database.
         
-        -  `python-ipaddr <http://code.google.com/p/ipaddr-py/>`_: **optional**.
-           Google's IP address manipulation code.
-        
         -  development: `Sphinx <http://sphinx.pocoo.org/>`_ if you want to build the
            documentation. In that case you'll also need something called
            ``python-repoze.sphinx.autointerface`` (at least in Debian) to build
diff --git a/txtorcon.egg-info/SOURCES.txt b/txtorcon.egg-info/SOURCES.txt
index 61cc154..e478445 100644
--- a/txtorcon.egg-info/SOURCES.txt
+++ b/txtorcon.egg-info/SOURCES.txt
@@ -18,6 +18,7 @@ docs/index.rst
 docs/introduction.rst
 docs/release-checklist.rst
 docs/releases.rst
+docs/tutorial.rst
 docs/txtorcon-config.rst
 docs/txtorcon-endpoints.rst
 docs/txtorcon-interface.rst
@@ -42,9 +43,11 @@ docs/_themes/alabaster/support.py
 docs/_themes/alabaster/theme.conf
 docs/_themes/alabaster/static/alabaster.css_t
 docs/_themes/alabaster/static/pygments.css
+examples/add_hiddenservice_to_system_tor.py
 examples/attach_streams_by_country.py
 examples/circuit_failure_rates.py
 examples/circuit_for_next_stream.py
+examples/connect.py
 examples/disallow_streams_by_port.py
 examples/dump_config.py
 examples/ephemeral_endpoint.py
@@ -53,6 +56,7 @@ examples/hidden-service-systemd.service
 examples/hidden_echo.py
 examples/launch_tor.py
 examples/launch_tor2web.py
+examples/launch_tor_data_dir.py
 examples/launch_tor_endpoint.py
 examples/launch_tor_endpoint2.py
 examples/launch_tor_with_hiddenservice.py
@@ -68,9 +72,11 @@ examples/stream_circuit_logger.py
 examples/systemd.service
 examples/tor_info.py
 examples/torflow_path_selection.py
+examples/tunnel_tls_through_tor_client.py
 examples/txtorcon.tac
 examples/webui_server.py
 scripts/asciinema-demo0.py
+scripts/asciinema-demo1.py
 test/__init__.py
 test/profile_startup.py
 test/test_addrmap.py
@@ -89,6 +95,7 @@ test/test_util_imports.py
 test/util.py
 twisted/plugins/txtorcon_endpoint_parser.py
 txtorcon/__init__.py
+txtorcon/_metadata.py
 txtorcon/addrmap.py
 txtorcon/circuit.py
 txtorcon/endpoints.py
diff --git a/txtorcon.egg-info/pbr.json b/txtorcon.egg-info/pbr.json
index e18909d..de08980 100644
--- a/txtorcon.egg-info/pbr.json
+++ b/txtorcon.egg-info/pbr.json
@@ -1 +1 @@
-{"is_release": false, "git_version": "1ae3aad"}
\ No newline at end of file
+{"is_release": false, "git_version": "0f966c2"}
\ No newline at end of file
diff --git a/txtorcon.egg-info/requires.txt b/txtorcon.egg-info/requires.txt
index e974b69..7d85bda 100644
--- a/txtorcon.egg-info/requires.txt
+++ b/txtorcon.egg-info/requires.txt
@@ -1,18 +1,20 @@
 Twisted>=11.1.0
-ipaddr>=2.1.10
+ipaddress>=1.0.16
 zope.interface>=3.6.1
 txsocksx>=1.13.0
 
 [dev]
+tox
 coverage
 setuptools>=0.8.0
 Sphinx
 repoze.sphinx.autointerface>=0.4
 coveralls
+codecov
 wheel
 twine
 pyflakes
 pep8
 mock
-ipaddr
+ipaddress>=1.0.16
 GeoIP
diff --git a/txtorcon/__init__.py b/txtorcon/__init__.py
index 60d0f8c..a2387a8 100644
--- a/txtorcon/__init__.py
+++ b/txtorcon/__init__.py
@@ -5,19 +5,15 @@ from __future__ import print_function
 from __future__ import unicode_literals
 from __future__ import with_statement
 
-# for now, this needs to be changed in setup.py also until I find a
-# better solution
-__version__ = '0.14.2'
-__author__ = 'meejah'
-__contact__ = 'meejah at meejah.ca'
-__url__ = 'https://github.com/meejah/txtorcon'
-__license__ = 'MIT'
-__copyright__ = 'Copyright 2012-2015'
-
+from txtorcon._metadata import __version__, __author__, __contact__
+from txtorcon._metadata import __license__, __copyright__, __url__
 
 from txtorcon.router import Router
 from txtorcon.circuit import Circuit
+from txtorcon.circuit import build_timeout_circuit
+from txtorcon.circuit import CircuitBuildTimedOutError
 from txtorcon.stream import Stream
+from txtorcon.torcontrolprotocol import connect
 from txtorcon.torcontrolprotocol import TorControlProtocol
 from txtorcon.torcontrolprotocol import TorProtocolError
 from txtorcon.torcontrolprotocol import TorProtocolFactory
@@ -27,6 +23,7 @@ from txtorcon.torstate import build_tor_connection
 from txtorcon.torstate import build_local_tor_connection
 from txtorcon.torconfig import TorConfig
 from txtorcon.torconfig import HiddenService
+from txtorcon.torconfig import EphemeralHiddenService
 from txtorcon.torconfig import TorProcessProtocol
 from txtorcon.torconfig import launch_tor
 from txtorcon.torconfig import TorNotFound
@@ -38,33 +35,45 @@ from txtorcon.endpoints import TCPHiddenServiceEndpoint
 from txtorcon.endpoints import TCPHiddenServiceEndpointParser
 from txtorcon.endpoints import TorClientEndpoint
 from txtorcon.endpoints import TorClientEndpointStringParser
-from txtorcon.endpoints import IHiddenService
-from txtorcon.endpoints import IProgressProvider
+from txtorcon.endpoints import IHiddenService, IProgressProvider
+
 from txtorcon.endpoints import get_global_tor
 from . import util
 from . import interface
-from txtorcon.interface import *
+from txtorcon.interface import (
+    ITorControlProtocol,
+    IStreamListener, IStreamAttacher, StreamListenerMixin,
+    ICircuitContainer, ICircuitListener, CircuitListenerMixin,
+    IRouterContainer, IAddrListener,
+)
+
+__all__ = [
+    "Router",
+    "Circuit",
+    "Stream",
+    "connect",
+    "TorControlProtocol", "TorProtocolError", "TorProtocolFactory",
+    "TorState", "DEFAULT_VALUE",
+    "TorInfo",
+    "build_tor_connection", "build_local_tor_connection", "launch_tor",
+    "TorNotFound", "TorConfig", "HiddenService", "EphemeralHiddenService",
+    "TorProcessProtocol",
+    "TorInfo",
+    "TCPHiddenServiceEndpoint", "TCPHiddenServiceEndpointParser",
+    "TorClientEndpoint", "TorClientEndpointStringParser",
+    "IHiddenService", "IProgressProvider",
+    "TorOnionAddress", "TorOnionListeningPort",
+    "get_global_tor",
+    "build_timeout_circuit",
+    "CircuitBuildTimedOutError",
 
-__all__ = ["Router",
-           "Circuit",
-           "Stream",
-           "TorControlProtocol", "TorProtocolError", "TorProtocolFactory",
-           "TorState", "DEFAULT_VALUE",
-           "TorInfo",
-           "build_tor_connection", "build_local_tor_connection", "launch_tor",
-           "TorNotFound", "TorConfig", "HiddenService", "TorProcessProtocol",
-           "TorInfo",
-           "TCPHiddenServiceEndpoint", "TCPHiddenServiceEndpointParser",
-           "TorClientEndpoint", "TorClientEndpointStringParser",
-           "IHiddenService", "IProgressProvider",
-           "TorOnionAddress", "TorOnionListeningPort",
-           "get_global_tor",
+    "AddrMap",
+    "util", "interface",
+    "ITorControlProtocol",
+    "IStreamListener", "IStreamAttacher", "StreamListenerMixin",
+    "ICircuitContainer", "ICircuitListener", "CircuitListenerMixin",
+    "IRouterContainer", "IAddrListener", "IProgressProvider",
 
-           "AddrMap",
-           "util", "interface",
-           "ITorControlProtocol",
-           "IStreamListener", "IStreamAttacher", "StreamListenerMixin",
-           "ICircuitContainer", "ICircuitListener", "CircuitListenerMixin",
-           "IRouterContainer", "IAddrListener", "IProgressProvider",
-           "IHiddenService",
-           ]
+    "__version__", "__author__", "__contact__",
+    "__license__", "__copyright__", "__url__",
+]
diff --git a/txtorcon/_metadata.py b/txtorcon/_metadata.py
new file mode 100644
index 0000000..476c613
--- /dev/null
+++ b/txtorcon/_metadata.py
@@ -0,0 +1,6 @@
+__version__ = '0.15.0'
+__author__ = 'meejah'
+__contact__ = 'meejah at meejah.ca'
+__url__ = 'https://github.com/meejah/txtorcon'
+__license__ = 'MIT'
+__copyright__ = 'Copyright 2012-2015'
diff --git a/txtorcon/circuit.py b/txtorcon/circuit.py
index 21a6db8..5166fc1 100644
--- a/txtorcon/circuit.py
+++ b/txtorcon/circuit.py
@@ -8,10 +8,10 @@ from __future__ import with_statement
 import time
 import datetime
 
+from twisted.python.failure import Failure
 from twisted.python import log
 from twisted.internet import defer
 from .interface import IRouterContainer
-
 from txtorcon.util import find_keywords
 
 # look like "2014-01-25T02:12:14.593772"
@@ -275,3 +275,28 @@ class Circuit(object):
         path = ' '.join([x.ip for x in self.path])
         return "<Circuit %d %s [%s] for %s>" % (self.id, self.state, path,
                                                 self.purpose)
+
+
+class CircuitBuildTimedOutError(Exception):
+    """
+    This exception is thrown when using `timed_circuit_build`
+    and the circuit build times-out.
+    """
+
+
+def build_timeout_circuit(tor_state, reactor, path, timeout, using_guards=False):
+    """
+    returns a deferred which fires when the
+    circuit build succeeds or fails to build.
+    CircuitBuildTimedOutError will be raised unless we
+    receive a circuit build result within the `timeout` duration.
+    """
+    d = tor_state.build_circuit(path, using_guards)
+    reactor.callLater(timeout, d.cancel)
+
+    def trap_cancel(f):
+        f.trap(defer.CancelledError)
+        return Failure(CircuitBuildTimedOutError("circuit build timed out"))
+    d.addCallback(lambda circuit: circuit.when_built())
+    d.addErrback(trap_cancel)
+    return d
diff --git a/txtorcon/endpoints.py b/txtorcon/endpoints.py
index 8042e69..a901555 100644
--- a/txtorcon/endpoints.py
+++ b/txtorcon/endpoints.py
@@ -213,7 +213,7 @@ class TCPHiddenServiceEndpoint(object):
 
     @classmethod
     def global_tor(cls, reactor, public_port, hidden_service_dir=None,
-                   local_port=None, control_port=None):
+                   local_port=None, control_port=None, stealth_auth=None):
         """
         This returns a TCPHiddenServiceEndpoint connected to a
         txtorcon global Tor instance. The first time you call this, a
@@ -232,17 +232,27 @@ class TCPHiddenServiceEndpoint(object):
 
         All keyword options have defaults (e.g. random ports, or
         tempdirs).
+
+        :param stealth_auth:
+            None, or a list of strings -- one for each stealth
+            authenticator you require.
         """
 
         def progress(*args):
             progress.target(*args)
-        config = get_global_tor(reactor,
-                                control_port=control_port,
-                                progress_updates=progress)
-        # config is a Deferred here, but endpoint resolves in listen()
-        r = TCPHiddenServiceEndpoint(reactor, config, public_port,
-                                     hidden_service_dir=hidden_service_dir,
-                                     local_port=local_port)
+        config = get_global_tor(
+            reactor,
+            control_port=control_port,
+            progress_updates=progress
+        )
+        # config is a Deferred here, but endpoint resolves it in
+        # the listen() call
+        r = TCPHiddenServiceEndpoint(
+            reactor, config, public_port,
+            hidden_service_dir=hidden_service_dir,
+            local_port=local_port,
+            stealth_auth=stealth_auth,
+        )
         progress.target = r._tor_progress_update
         return r
 
@@ -274,7 +284,8 @@ class TCPHiddenServiceEndpoint(object):
         return r
 
     def __init__(self, reactor, config, public_port,
-                 hidden_service_dir=None, local_port=None):
+                 hidden_service_dir=None, local_port=None,
+                 stealth_auth=None):
         """
         :param reactor:
             :api:`twisted.internet.interfaces.IReactorTCP` provider
@@ -300,6 +311,10 @@ class TCPHiddenServiceEndpoint(object):
             not provided, one is created with temp.mkstemp() AND
             DELETED when the reactor shuts down.
 
+        :param stealth_auth:
+            A list of strings, one name for each stealth authenticator
+            you want. Like: ``['alice', 'bob']``
+
         :param endpoint_generator:
             A callable that generates a new instance of something that
             implements IServerEndpoint (by default TCP4ServerEndpoint)
@@ -309,6 +324,7 @@ class TCPHiddenServiceEndpoint(object):
         self.config = defer.maybeDeferred(lambda: config)
         self.public_port = public_port
         self.local_port = local_port
+        self.stealth_auth = stealth_auth
 
         self.hidden_service_dir = hidden_service_dir
         self.tcp_listening_port = None
@@ -421,11 +437,17 @@ class TCPHiddenServiceEndpoint(object):
                 info_callback.callback(None)
         self.config.protocol.add_event_listener('INFO', info_event)
 
-        if self.hidden_service_dir not in [hs.dir for hs in self.config.HiddenServices]:
+        hs_dirs = [hs.dir for hs in self.config.HiddenServices]
+        if self.hidden_service_dir not in hs_dirs:
+            authlines = []
+            if self.stealth_auth:
+                # like "stealth name0,name1"
+                authlines = ['stealth ' + ','.join(self.stealth_auth)]
             self.hiddenservice = HiddenService(
                 self.config, self.hidden_service_dir,
                 ['%d 127.0.0.1:%d' % (self.public_port, self.local_port)],
-                group_readable=1)
+                group_readable=1, auth=authlines,
+            )
             self.config.HiddenServices.append(self.hiddenservice)
         yield self.config.save()
 
@@ -436,15 +458,29 @@ class TCPHiddenServiceEndpoint(object):
         self._tor_progress_update(100.0, 'wait_descriptor',
                                   'At least one descriptor uploaded.')
 
-        log.msg(
-            'Started hidden service "%s" on port %d' %
-            (self.onion_uri, self.public_port))
-        log.msg('Keys are in "%s".' % (self.hidden_service_dir,))
-        defer.returnValue(TorOnionListeningPort(self.tcp_listening_port,
-                                                self.hidden_service_dir,
-                                                self.onion_uri,
-                                                self.public_port,
-                                                self.config))
+        # FIXME XXX need to work out what happens here on stealth-auth'd
+        # things. maybe we need a separate StealthHiddenService
+        # vs. HiddenService ?!
+        # XXX that is, self.onion_uri isn't always avaialble :/
+
+        uri = None
+        if self.hiddenservice is not None:
+            log.msg('Started hidden service port %d' % self.public_port)
+            for client in self.hiddenservice.clients:
+                # XXX FIXME just taking the first one on multi-client services
+                if uri is None:
+                    uri = client[1]
+                log.msg('  listening on %s.onion' % client[1])
+
+        defer.returnValue(
+            TorOnionListeningPort(
+                self.tcp_listening_port,
+                self.hidden_service_dir,
+                uri,
+                self.public_port,
+                self.config,
+            )
+        )
 
 
 @implementer(IAddress)
@@ -638,10 +674,10 @@ class TorClientEndpoint(object):
             raise ValueError('host and port must be specified')
 
         self.host = host
-        self.port = port
+        self.port = int(port)
         self._proxy_endpoint_generator = _proxy_endpoint_generator
         self.socks_hostname = socks_hostname
-        self.socks_port = socks_port
+        self.socks_port = int(socks_port) if socks_port is not None else None
         self.socks_username = socks_username
         self.socks_password = socks_password
 
@@ -649,52 +685,37 @@ class TorClientEndpoint(object):
             self._socks_port_iter = iter(self.socks_ports_to_try)
             self._socks_guessing_enabled = True
         else:
+            self._socks_port_iter = [socks_port]
             self._socks_guessing_enabled = False
 
+    @defer.inlineCallbacks
     def connect(self, protocolfactory):
-        self.protocolfactory = protocolfactory
-
-        if self._socks_guessing_enabled:
-            self.socks_port = self._socks_port_iter.next()
+        last_error = None
+        for socks_port in self._socks_port_iter:
+            self.socks_port = socks_port
+            tor_ep = self._proxy_endpoint_generator(
+                reactor,
+                self.socks_hostname,
+                self.socks_port,
+            )
 
-        d = self._try_connect()
-        return d
+            args = (self.host, self.port, tor_ep)
+            kwargs = dict()
+            if self.socks_username is not None and self.socks_password is not None:
+                kwargs['methods'] = dict(
+                    login=(self.socks_username, self.socks_password),
+                )
 
-    def _try_connect(self):
-        self.tor_socks_endpoint = self._proxy_endpoint_generator(
-            reactor,
-            self.socks_hostname,
-            self.socks_port
-        )
+            socks_ep = SOCKS5ClientEndpoint(*args, **kwargs)
 
-        if self.socks_username is None or self.socks_password is None:
-            ep = SOCKS5ClientEndpoint(
-                self.host,
-                self.port,
-                self.tor_socks_endpoint
-            )
-        else:
-            ep = SOCKS5ClientEndpoint(
-                self.host,
-                self.port,
-                self.tor_socks_endpoint,
-                methods=dict(login=(self.socks_username, self.socks_password))
-            )
-
-        d = ep.connect(self.protocolfactory)
-        if self._socks_guessing_enabled:
-            d.addErrback(self._retry_socks_port)
-        return d
+            try:
+                proto = yield socks_ep.connect(protocolfactory)
+                defer.returnValue(proto)
 
-    def _retry_socks_port(self, failure):
-        failure.trap(error.ConnectError)
-        try:
-            self.socks_port = self._socks_port_iter.next()
-        except StopIteration:
-            return failure
-        d = self._try_connect()
-        d.addErrback(self._retry_socks_port)
-        return d
+            except error.ConnectError as e0:
+                last_error = e0
+        if last_error is not None:
+            raise last_error
 
 
 @implementer(IPlugin, IStreamClientEndpointStringParserWithReactor)
diff --git a/txtorcon/interface.py b/txtorcon/interface.py
index 393d49d..17592ec 100644
--- a/txtorcon/interface.py
+++ b/txtorcon/interface.py
@@ -299,22 +299,6 @@ class ITorControlProtocol(Interface):
         ICircuitListener and wait for circuit_closed()
         """
 
-    def add_circuit_listener(icircuitlistener):
-        """
-        Add an implementor of :class:`txtorcon.interface.ICircuitListener`
-        which will be added to all new circuits as well as all
-        existing ones (you won't, however, get circuit_new calls for
-        the existing ones)
-        """
-
-    def add_stream_listener(istreamlistener):
-        """
-        Add an implementor of :class:`txtorcon.interface.IStreamListener`
-        which will be added to all new circuits as well as all
-        existing ones (you won't, however, get stream_new calls for
-        the existing ones)
-        """
-
     def add_event_listener(evt, callback):
         """
         Add a listener to an Event object. This may be called multiple
diff --git a/txtorcon/torconfig.py b/txtorcon/torconfig.py
index 03a34bf..faf9e86 100644
--- a/txtorcon/torconfig.py
+++ b/txtorcon/torconfig.py
@@ -6,24 +6,24 @@ from __future__ import with_statement
 
 import os
 import sys
+import types
 import functools
 import tempfile
 import warnings
 from io import StringIO
 import shlex
-if sys.platform in ('linux2', 'darwin'):
-    import pwd
 
 from twisted.python import log
-from twisted.python.failure import Failure
 from twisted.internet import defer, error, protocol
 from twisted.internet.interfaces import IReactorTime
 from twisted.internet.endpoints import TCP4ClientEndpoint
 
-from txtorcon.torcontrolprotocol import parse_keywords, TorProtocolFactory
+from txtorcon.torcontrolprotocol import parse_keywords, TorProtocolFactory, DEFAULT_VALUE
 from txtorcon.util import delete_file_or_tree, find_keywords, find_tor_binary
 from txtorcon.log import txtorlog
 from txtorcon.interface import ITorControlProtocol
+if sys.platform in ('linux2', 'darwin'):
+    import pwd
 
 
 class TorNotFound(RuntimeError):
@@ -111,7 +111,6 @@ class TorProcessProtocol(protocol.ProcessProtocol):
         self.kill_on_stderr = kill_on_stderr
         self.stderr = stderr
         self.stdout = stdout
-        self.collected_stdout = StringIO()
 
         self._setup_complete = False
         self._did_timeout = False
@@ -195,7 +194,8 @@ class TorProcessProtocol(protocol.ProcessProtocol):
                     "Tor was killed (%s)." % status.value.signal)
         else:
             err = RuntimeError(
-                "Tor exited with error-code %d" % status.value.exitCode)
+                "Tor exited with error-code %d" % status.value.exitCode
+            )
 
         log.err(err)
         if self.connected_cb:
@@ -465,7 +465,7 @@ def launch_tor(config, reactor,
             tor_binary,
             args=args,
             env={'HOME': data_directory},
-            path=data_directory
+            path=data_directory if os.path.exists(data_directory) else None,
         )
         # FIXME? don't need rest of the args: uid, gid, usePTY, childFDs)
         transport.closeStdin()
@@ -500,11 +500,17 @@ class TorConfigType(object):
 
 
 class Boolean(TorConfigType):
+    "Boolean values are stored as 0 or 1."
     def parse(self, s):
         if int(s):
             return True
         return False
 
+    def validate(self, s, instance, name):
+        if s:
+            return 1
+        return 0
+
 
 class Boolean_Auto(TorConfigType):
     """
@@ -520,11 +526,24 @@ class Boolean_Auto(TorConfigType):
             return 1
         return 0
 
+    def validate(self, s, instance, name):
+        # FIXME: Is 'auto' an allowed value? (currently not)
+        s = int(s)
+        if s < 0:
+            return 'auto'
+        elif s:
+            return 1
+        else:
+            return 0
+
 
 class Integer(TorConfigType):
     def parse(self, s):
         return int(s)
 
+    def validate(self, s, instance, name):
+        return int(s)
+
 
 class SignedInteger(Integer):
     pass
@@ -741,9 +760,52 @@ class HiddenService(object):
         self.__dict__[name] = value
 
     def __getattr__(self, name):
-        if name in ('hostname', 'private_key'):
+        '''
+        FIXME can't we just move this to @property decorated methods
+        instead?
+        '''
+
+        # For stealth authentication, the .onion is per-client. So in
+        # that case, we really have no choice here -- we can't have
+        # "a" hostname. So we just barf; it's an error to access to
+        # hostname this way. Instead, use .clients.{hostname, cookie}
+
+        if name == 'private_key':
+            with open(os.path.join(self.dir, name)) as f:
+                data = f.read().strip()
+            self.__dict__[name] = data
+
+        elif name == 'clients':
+            clients = []
+            try:
+                with open(os.path.join(self.dir, 'hostname')) as f:
+                    for line in f.readlines():
+                        args = line.split()
+                        # XXX should be a dict?
+                        if len(args) > 1:
+                            # tag, onion-uri?
+                            clients.append((args[0], args[1]))
+                        else:
+                            clients.append(('default', args[0]))
+            except IOError:
+                pass
+            self.__dict__[name] = clients
+
+        elif name == 'hostname':
             with open(os.path.join(self.dir, name)) as f:
-                self.__dict__[name] = f.read().strip()
+                data = f.read().strip()
+            host = None
+            for line in data.split('\n'):
+                h = line.split(' ')[0]
+                if host is None:
+                    host = h
+                elif h != host:
+                    raise RuntimeError(
+                        ".hostname accessed on stealth-auth'd hidden-service "
+                        "with multiple onion addresses."
+                    )
+            self.__dict__[name] = h
+
         elif name == 'client_keys':
             fname = os.path.join(self.dir, name)
             keys = []
@@ -762,8 +824,8 @@ class HiddenService(object):
         if self.conf._supports['HiddenServiceDirGroupReadable'] \
            and self.group_readable:
             rtn.append(('HiddenServiceDirGroupReadable', str(1)))
-        for x in self.ports:
-            rtn.append(('HiddenServicePort', str(x)))
+        for port in self.ports:
+            rtn.append(('HiddenServicePort', str(port)))
         if self.version:
             rtn.append(('HiddenServiceVersion', str(self.version)))
         for authline in self.authorize_client:
@@ -771,8 +833,121 @@ class HiddenService(object):
         return rtn
 
 
+class EphemeralHiddenService(object):
+    '''
+    This uses the ephemeral hidden-service APIs (in comparison to
+    torrc or SETCONF). This means your hidden-service private-key is
+    never in a file. It also means that when the process exits, that
+    HS goes away. See documentation for ADD_ONION in torspec:
+    https://gitweb.torproject.org/torspec.git/tree/control-spec.txt#n1295
+    '''
+
+    # XXX the "ports" stuff is still kind of an awkward API, especialy
+    # making the actual list public (since it'll have
+    # "80,127.0.0.1:80" instead of with a space
+
+    # XXX descriptor upload stuff needs more features from Tor (the
+    # actual uploaded key; the event always says UNKNOWN)
+
+    # XXX "auth" is unused (also, no Tor support I don't think?)
+
+    def __init__(self, ports, key_blob_or_type='NEW:BEST', auth=[], ver=2):
+        if not isinstance(ports, types.ListType):
+            ports = [ports]
+        # for "normal" HSes the port-config bit looks like "80
+        # 127.0.0.1:1234" whereas this one wants a comma, so we leave
+        # the public API the same and fix up the space. Or of course
+        # you can just use the "real" comma-syntax if you wanted.
+        self._ports = map(lambda x: x.replace(' ', ','), ports)
+        self._key_blob = key_blob_or_type
+        self.auth = auth  # FIXME ununsed
+        # FIXME nicer than assert, plz
+        assert ' ' not in self._key_blob
+        assert isinstance(ports, types.ListType)
+        if not key_blob_or_type.startswith('NEW:') \
+           and (len(key_blob_or_type) > 825 or len(key_blob_or_type) < 820):
+            raise RuntimeError('Wrong size key-blob')
+
+    @defer.inlineCallbacks
+    def add_to_tor(self, protocol):
+        '''
+        Returns a Deferred which fires with 'self' after at least one
+        descriptor has been uploaded. Errback if no descriptor upload
+        succeeds.
+        '''
+        ports = ' '.join(map(lambda x: 'Port=' + x.strip(), self._ports))
+        cmd = 'ADD_ONION %s %s' % (self._key_blob, ports)
+        ans = yield protocol.queue_command(cmd)
+        ans = find_keywords(ans.split('\n'))
+        self.hostname = ans['ServiceID'] + '.onion'
+        if self._key_blob == 'NEW:BEST':
+            self.private_key = ans['PrivateKey']
+
+        log.msg('Created hidden-service at', self.hostname)
+
+        # Now we want to wait for the descriptor uploads. This doesn't
+        # quite work, as the UPLOADED events always say "UNKNOWN" for
+        # the HSAddress so we can't correlate it to *this* onion for
+        # sure :/ "yet", though. Yawning says on IRC this is coming.
+
+        # XXX Hmm, still UPLOADED always says UNKNOWN, but the UPLOAD
+        # events do say the address -- so we save all those, and
+        # correlate to the target nodes. Not sure if this will really
+        # even work, but better than nothing.
+
+        uploaded = defer.Deferred()
+        attempted_uploads = set()
+        confirmed_uploads = set()
+        failed_uploads = set()
+
+        def hs_desc(evt):
+            """
+            From control-spec:
+            "650" SP "HS_DESC" SP Action SP HSAddress SP AuthType SP HsDir
+            [SP DescriptorID] [SP "REASON=" Reason] [SP "REPLICA=" Replica]
+            """
+
+            args = evt.split()
+            subtype = args[0]
+            if subtype == 'UPLOAD':
+                if args[1] == self.hostname[:-6]:
+                    attempted_uploads.add(args[3])
+
+            elif subtype == 'UPLOADED':
+                # we only need ONE successful upload to happen for the
+                # HS to be reachable. (addr is args[1])
+                if args[3] in attempted_uploads:
+                    confirmed_uploads.add(args[3])
+                    log.msg("Uploaded '{}' to '{}'".format(self.hostname, args[3]))
+                    uploaded.callback(self)
+
+            elif subtype == 'FAILED':
+                if args[1] == self.hostname[:-6]:
+                    failed_uploads.add(args[3])
+                    if failed_uploads == attempted_uploads:
+                        msg = "Failed to upload '{}' to: {}".format(
+                            self.hostname,
+                            ', '.join(failed_uploads),
+                        )
+                        uploaded.errback(RuntimeError(msg))
+
+        log.msg("Created '{}', waiting for descriptor uploads.".format(self.hostname))
+        yield protocol.add_event_listener('HS_DESC', hs_desc)
+        yield uploaded
+        yield protocol.remove_event_listener('HS_DESC', hs_desc)
+
+    @defer.inlineCallbacks
+    def remove_from_tor(self, protocol):
+        '''
+        Returns a Deferred which fires with None
+        '''
+        r = yield protocol.queue_command('DEL_ONION %s' % self.hostname[:-6])
+        if r.strip() != 'OK':
+            raise RuntimeError('Failed to remove hidden service: "%s".' % r)
+
+
 def parse_rsa_blob(lines):
-    return ''.join(lines[1:-1])
+    return 'RSA1024:' + ''.join(lines[1:-1])
 
 
 def parse_client_keys(stream):
@@ -908,6 +1083,18 @@ class TorConfig(object):
 
     """
 
+    @classmethod
+    @defer.inlineCallbacks
+    def from_protocol(cls, proto):
+        """
+        This creates and returns a ready-to-go TorConfig instance from the
+        given protocol, which should be an instance of
+        TorControlProtocol.
+        """
+        cfg = TorConfig(control=proto)
+        yield cfg.post_bootstrap
+        defer.returnValue(cfg)
+
     def __init__(self, control=None):
         self.config = {}
         '''Current configuration, by keys.'''
@@ -1081,7 +1268,10 @@ class TorConfig(object):
         for (k, v) in conf.items():
             # v will be txtorcon.DEFAULT_VALUE already from
             # parse_keywords if it was unspecified
-            self.config[self._find_real_name(k)] = v
+            real_name = self._find_real_name(k)
+            if real_name in self.parsers:
+                v = self.parsers[real_name].parse(v)
+            self.config[real_name] = v
 
     def bootstrap(self, arg=None):
         '''
@@ -1150,8 +1340,10 @@ class TorConfig(object):
 
             if isinstance(value, list):
                 for x in value:
-                    args.append(key)
-                    args.append(str(x))
+                    # FIXME XXX
+                    if x is not DEFAULT_VALUE:
+                        args.append(key)
+                        args.append(str(x))
 
             else:
                 args.append(key)
@@ -1159,7 +1351,10 @@ class TorConfig(object):
 
             # FIXME in future we should wait for CONF_CHANGED and
             # update then, right?
-            self.config[self._find_real_name(key)] = value
+            real_name = self._find_real_name(key)
+            if not isinstance(value, list) and real_name in self.parsers:
+                value = self.parsers[real_name].parse(value)
+            self.config[real_name] = value
 
         # FIXME might want to re-think this, but currently there's no
         # way to put things into a config and get them out again
diff --git a/txtorcon/torcontrolprotocol.py b/txtorcon/torcontrolprotocol.py
index 410e14d..08c49ba 100644
--- a/txtorcon/torcontrolprotocol.py
+++ b/txtorcon/torcontrolprotocol.py
@@ -12,7 +12,7 @@ from twisted.protocols.basic import LineOnlyReceiver
 
 from zope.interface import implementer
 
-from txtorcon.util import hmac_sha256, compare_via_hash
+from txtorcon.util import hmac_sha256, compare_via_hash, unescape_quoted_string
 from txtorcon.log import txtorlog
 
 from txtorcon.interface import ITorControlProtocol
@@ -25,6 +25,39 @@ import base64
 DEFAULT_VALUE = 'DEFAULT'
 
 
+ at defer.inlineCallbacks
+def connect(endpoint, password_function=lambda: None):
+    """
+    (experimental; details may change) This connects to the given
+    endpoint, presuming it is a Tor control connection and gives back
+    a TorControlProtocol instance. This does *not* reconnect if the
+    connection is dropped. This is the preferred entry point to
+    control a running Tor; if you need to *start* a Tor see
+    :ref:`launching_tor`.
+
+    See :meth:`txtorcon.TorState.from_protocol` to create a valid
+    :class:`txtorcon.TorState` instance from the connection.
+
+    :param endpoint: A Twisted IEndpoint, in practice either a
+        TCP4ClientEndpoint or a UnixClientEndpoint. By default, Tor
+        uses "tcp:localhost:9051" or "unix:/var/run/tor/control".
+
+    :param password_function: This is only consulted if the Tor to
+        which we connect does not have cookie authentication enabled. In
+        that case, this function is called to request a password. It may
+        return a Deferred.
+
+    :returns: A Deferred that fires with a ready-to-use
+        TorControlProtocol instance is returned, or Failure if a
+        connection can't be established.
+    """
+
+    factory = TorProtocolFactory(password_function=password_function)
+    proto = yield endpoint.connect(factory)
+    yield proto.post_bootstrap
+    defer.returnValue(proto)
+
+
 class TorProtocolError(RuntimeError):
     """
     Happens on 500-level responses in the protocol, almost certainly
@@ -58,8 +91,9 @@ class TorProtocolFactory(object):
         Builds protocols to talk to a Tor client on the specified
         address. For example::
 
-        TCP4ClientEndpoint(reactor, "localhost", 9051).connect(TorProtocolFactory())
-        reactor.run()
+            ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
+            ep.connect(TorProtocolFactory())
+            reactor.run()
 
         By default, COOKIE authentication is used if
         available.
@@ -120,7 +154,7 @@ def unquote(word):
     return word
 
 
-def parse_keywords(lines, multiline_values=True):
+def parse_keywords(lines, multiline_values=True, key_hints=None):
     """
     Utility method to parse name=value pairs (GETINFO etc). Takes a
     string with newline-separated lines and expects at most one = sign
@@ -142,7 +176,11 @@ def parse_keywords(lines, multiline_values=True):
         if line.strip() == 'OK':
             continue
 
-        if '=' in line and ' ' not in line.split('=', 1)[0]:
+        sp = line.split('=', 1)
+        found_key = ('=' in line and ' ' not in sp[0])
+        if found_key and key_hints and sp[0] not in key_hints:
+            found_key = False
+        if found_key:
             if key:
                 if key in rtn:
                     if isinstance(rtn[key], list):
@@ -188,7 +226,7 @@ class TorControlProtocol(LineOnlyReceiver):
     :meth:`txtorcon.TorState.build_circuit` allows you to build custom
     circuits.
 
-   :meth:`txtorcon.TorControlProtocol.add_event_listener` can be used
+    :meth:`txtorcon.TorControlProtocol.add_event_listener` can be used
     to listen for specific events.
 
     To see how circuit and stream listeners are used, see
@@ -209,6 +247,9 @@ class TorControlProtocol(LineOnlyReceiver):
         authentication to Tor (default is to use COOKIE, however). May
         return Deferred."""
 
+        self._cookie_data = None
+        """Data read from cookie file used to authenticate."""
+
         self.version = None
         """Version of Tor we've connected to."""
 
@@ -244,7 +285,8 @@ class TorControlProtocol(LineOnlyReceiver):
             def setup(proto):
                 proto.post_bootstrap.addCallback(setup_complete)
 
-            TCP4ClientEndpoint(reactor, "localhost", 9051).connect(TorProtocolFactory())
+            ep = TCP4ClientEndpoint(reactor, "localhost", 9051)
+            ep.connect(TorProtocolFactory())
             d.addCallback(setup)
 
         See the helper method :func:`txtorcon.build_tor_connection`.
@@ -344,7 +386,6 @@ class TorControlProtocol(LineOnlyReceiver):
     # The following methods are the main TorController API and
     # probably the most interesting for users.
 
-    @defer.inlineCallbacks
     def get_info(self, *args):
         """
         Uses GETINFO to obtain informatoin from Tor.
@@ -362,16 +403,9 @@ class TorControlProtocol(LineOnlyReceiver):
             the keys you asked for. If you want to avoid the parsing
             into a dict, you can use get_info_raw instead.
         """
-        lines = yield self.get_info_raw(*args)
-        rtn = {}
-        key = None
-        for line in lines.split('\n'):
-            if line.split('=', 1)[0] in args:
-                key = line.split('=', 1)[0]
-                rtn[key] = line.split('=', 1)[1]
-            else:
-                rtn[key] = rtn[key] + '\n' + line
-        defer.returnValue(rtn)
+        d = self.get_info_raw(*args)
+        d.addCallback(parse_keywords, key_hints=args)
+        return d
 
     def get_conf(self, *args):
         """
@@ -618,7 +652,8 @@ class TorControlProtocol(LineOnlyReceiver):
         """
         Callback on AUTHCHALLENGE SAFECOOKIE
         """
-
+        if self._cookie_data is None:
+            raise RuntimeError("Cookie data not read.")
         kw = parse_keywords(reply.replace(' ', '\n'))
 
         server_hash = base64.b16decode(kw['SERVERHASH'])
@@ -626,7 +661,7 @@ class TorControlProtocol(LineOnlyReceiver):
         # FIXME put string in global. or something.
         expected_server_hash = hmac_sha256(
             "Tor safe cookie authentication server-to-controller hash",
-            self.cookie_data + self.client_nonce + server_nonce
+            self._cookie_data + self.client_nonce + server_nonce
         )
 
         if not compare_via_hash(expected_server_hash, server_hash):
@@ -638,18 +673,31 @@ class TorControlProtocol(LineOnlyReceiver):
 
         client_hash = hmac_sha256(
             "Tor safe cookie authentication controller-to-server hash",
-            self.cookie_data + self.client_nonce + server_nonce
+            self._cookie_data + self.client_nonce + server_nonce
         )
         client_hash_hex = base64.b16encode(client_hash)
         return self.queue_command('AUTHENTICATE %s' % client_hash_hex)
 
+    def _read_cookie(self, cookiefile):
+        """
+        Open and read a cookie file
+        :param cookie: Path to the cookie file
+        """
+        self._cookie_data = None
+        self._cookie_data = open(cookiefile, 'rb').read()
+        if len(self._cookie_data) != 32:
+            raise RuntimeError(
+                "Expected authentication cookie to be 32 bytes, got %d" %
+                len(self._cookie_data)
+            )
+
     def _do_authenticate(self, protoinfo):
         """
         Callback on PROTOCOLINFO to actually authenticate once we know
         what's supported.
         """
-
         methods = None
+        cookie_auth = False
         for line in protoinfo.split('\n'):
             if line[:5] == 'AUTH ':
                 kw = parse_keywords(line[5:].replace(' ', '\n'))
@@ -659,48 +707,56 @@ class TorControlProtocol(LineOnlyReceiver):
                 "Didn't find AUTH line in PROTOCOLINFO response."
             )
 
-        if 'SAFECOOKIE' in methods:
-            cookie = re.search('COOKIEFILE="(.*)"', protoinfo).group(1)
-            self.cookie_data = open(cookie, 'r').read()
-            if len(self.cookie_data) != 32:
-                raise RuntimeError(
-                    "Expected authentication cookie to be 32 bytes, got %d" %
-                    len(self.cookie_data)
-                )
-            txtorlog.msg("Using SAFECOOKIE authentication", cookie,
-                         len(self.cookie_data), "bytes")
-            self.client_nonce = os.urandom(32)
-
-            cmd = 'AUTHCHALLENGE SAFECOOKIE ' + \
-                  base64.b16encode(self.client_nonce)
-            d = self.queue_command(cmd)
-            d.addCallback(self._safecookie_authchallenge)
-            d.addCallback(self._bootstrap)
+        if 'SAFECOOKIE' in methods or 'COOKIE' in methods:
+            cookiefile_match = re.search(r'COOKIEFILE=("(?:[^"\\]|\\.)*")',
+                                         protoinfo)
+            if cookiefile_match:
+                cookiefile = cookiefile_match.group(1)
+                cookiefile = unescape_quoted_string(cookiefile)
+                try:
+                    self._read_cookie(cookiefile)
+                except IOError as why:
+                    txtorlog.msg("Reading COOKIEFILE failed: " + str(why))
+                    cookie_auth = False
+                else:
+                    cookie_auth = True
+            else:
+                txtorlog.msg("Didn't get COOKIEFILE")
+
+        if cookie_auth:
+            if 'SAFECOOKIE' in methods:
+                txtorlog.msg("Using SAFECOOKIE authentication", cookiefile,
+                             len(self._cookie_data), "bytes")
+                self.client_nonce = os.urandom(32)
+
+                cmd = 'AUTHCHALLENGE SAFECOOKIE ' + \
+                      base64.b16encode(self.client_nonce)
+                d = self.queue_command(cmd)
+                d.addCallback(self._safecookie_authchallenge)
+                d.addCallback(self._bootstrap)
+                d.addErrback(self._auth_failed)
+                return
+
+            elif 'COOKIE' in methods:
+                txtorlog.msg("Using COOKIE authentication",
+                             cookiefile, len(self._cookie_data), "bytes")
+                d = self.authenticate(self._cookie_data)
+                d.addCallback(self._bootstrap)
+                d.addErrback(self._auth_failed)
+                return
+
+        if self.password_function and 'HASHEDPASSWORD' in methods:
+            d = defer.maybeDeferred(self.password_function)
+            d.addCallback(self._do_password_authentication)
             d.addErrback(self._auth_failed)
             return
 
-        elif 'COOKIE' in methods:
-            cookie = re.search('COOKIEFILE="(.*)"', protoinfo).group(1)
-            with open(cookie, 'r') as cookiefile:
-                data = cookiefile.read()
-            if len(data) != 32:
-                raise RuntimeError(
-                    "Expected authentication cookie to be 32 "
-                    "bytes, got %d instead." % len(data)
-                )
-            txtorlog.msg("Using COOKIE authentication",
-                         cookie, len(data), "bytes")
-            d = self.authenticate(data)
+        if 'NULL' in methods:
+            d = self.queue_command('AUTHENTICATE')
             d.addCallback(self._bootstrap)
             d.addErrback(self._auth_failed)
             return
 
-        if self.password_function:
-            d = defer.maybeDeferred(self.password_function)
-            d.addCallback(self._do_password_authentication)
-            d.addErrback(self._auth_failed)
-            return
-
         raise RuntimeError(
             "The Tor I connected to doesn't support SAFECOOKIE nor COOKIE"
             " authentication and I have no password_function specified."
diff --git a/txtorcon/torstate.py b/txtorcon/torstate.py
index 394cdb6..59d6d4b 100644
--- a/txtorcon/torstate.py
+++ b/txtorcon/torstate.py
@@ -5,13 +5,11 @@ from __future__ import print_function
 from __future__ import with_statement
 
 import collections
-import datetime
 import os
 import stat
 import types
 import warnings
 
-from twisted.python import log
 from twisted.internet import defer
 from twisted.internet.endpoints import TCP4ClientEndpoint
 from twisted.internet.endpoints import UNIXClientEndpoint
@@ -227,19 +225,40 @@ class TorState(object):
         self.stream_listeners = []
 
         self.addrmap = AddrMap()
-        self.circuits = {}               # keys on id (integer)
-        self.streams = {}                # keys on id (integer)
+        #: keys on id (integer)
+        self.circuits = {}
 
-        self.all_routers = set()         # list of unique routers
-        self.routers = {}                # keys by hexid (string) and by unique names
-        self.routers_by_name = {}        # keys on name, value always list (many duplicate "Unnamed" routers, for example)
-        self.routers_by_hash = {}        # keys by hexid (string)
-        self.guards = {}                 # potentially-usable as entry guards, I think? (any router with 'Guard' flag)
-        self.entry_guards = {}           # from GETINFO entry-guards, our current entry guards
-        self.unusable_entry_guards = []  # list of entry guards we didn't parse out
-        self.authorities = {}            # keys by name
+        #: keys on id (integer)
+        self.streams = {}
 
-        self.cleanup = None              # see set_attacher
+        #: list of unique routers
+        self.all_routers = set()
+
+        #: keys by hexid (string) and by unique names
+        self.routers = {}
+
+        #: keys on name, value always list (many duplicate "Unnamed"
+        #: routers, for example)
+        self.routers_by_name = {}
+
+        #: keys by hexid (string)
+        self.routers_by_hash = {}
+
+        #: potentially-usable as entry guards, I think? (any router
+        #: with 'Guard' flag)
+        self.guards = {}
+
+        #: from GETINFO entry-guards, our current entry guards
+        self.entry_guards = {}
+
+        #: list of entry guards we didn't parse out
+        self.unusable_entry_guards = []
+
+        #: keys by name
+        self.authorities = {}
+
+        #: see set_attacher
+        self.cleanup = None
 
         class die(object):
             __name__ = 'die'  # FIXME? just to ease spagetti.py:82's pain
@@ -458,6 +477,7 @@ class TorState(object):
                                                        self.undo_attacher)
         return d
 
+    # noqa
     stream_close_reasons = {
         'REASON_MISC': 1,               # (catch-all for unlisted reasons)
         'REASON_RESOLVEFAILED': 2,      # (couldn't look up hostname)
@@ -731,7 +751,8 @@ class TorState(object):
         if circ_id not in self.circuits:
             c = self.circuit_factory(self)
             c.listen(self)
-            [c.listen(x) for x in self.circuit_listeners]
+            for listener in self.circuit_listeners:
+                c.listen(listener)
 
         else:
             c = self.circuits[circ_id]
diff --git a/txtorcon/util.py b/txtorcon/util.py
index ba84149..fd34758 100644
--- a/txtorcon/util.py
+++ b/txtorcon/util.py
@@ -11,7 +11,9 @@ import hashlib
 import shutil
 import socket
 import subprocess
+import ipaddress
 import struct
+import re
 
 from twisted.internet import defer
 from twisted.internet.interfaces import IProtocolFactory
@@ -61,16 +63,10 @@ def maybe_create_db(path):
     except IOError:
         return None
 
-city, asn, country = list(map(maybe_create_db,
-                         ("/usr/share/GeoIP/GeoLiteCity.dat",
-                          "/usr/share/GeoIP/GeoIPASNum.dat",
-                          "/usr/share/GeoIP/GeoIP.dat")))
 
-try:
-    import ipaddr as _ipaddr
-    ipaddr = _ipaddr
-except ImportError:
-    ipaddr = None
+city = maybe_create_db("/usr/share/GeoIP/GeoLiteCity.dat")
+asn = maybe_create_db("/usr/share/GeoIP/GeoIPASNum.dat")
+country = maybe_create_db("/usr/share/GeoIP/GeoIP.dat")
 
 
 def is_executable(path):
@@ -127,10 +123,9 @@ def maybe_ip_addr(addr):
     TODO consider explicitly checking for .exit or .onion at the end?
     """
 
-    if ipaddr is not None:
-        try:
-            return ipaddr.IPAddress(addr)
-        except ValueError:
+    try:
+        return ipaddress.ip_address(addr)
+    except ValueError:
             pass
     return str(addr)
 
@@ -145,6 +140,8 @@ def find_keywords(args, key_filter=lambda x: not x.startswith("$")):
     with "$hash=name" looks like a keyword argument (but it isn't). If you
     don't want this, override the "key_filter" argument to this method.
 
+    :param args: a list of strings, each with one key=value pair
+
     :return:
         a dict of key->value (both strings) of all name=value type
         keywords found in args.
@@ -290,3 +287,32 @@ def available_tcp_port(reactor):
     address = port.getHost()
     yield port.stopListening()
     defer.returnValue(address.port)
+
+
+def unescape_quoted_string(string):
+    r'''
+    This function implementes the recommended functionality described in the
+    tor control-spec to be compatible with older tor versions:
+
+      * Read \\n \\t \\r and \\0 ... \\377 as C escapes.
+      * Treat a backslash followed by any other character as that character.
+
+    Except the legacy support for the escape sequences above this function
+    implements parsing of QuotedString using qcontent from
+
+    QuotedString = DQUOTE *qcontent DQUOTE
+
+    :param string: The escaped quoted string.
+    :returns: The unescaped string.
+    :raises ValueError: If the string is in a invalid form
+                        (e.g. a single backslash)
+    '''
+    match = re.match(r'''^"((?:[^"\\]|\\.)*)"$''', string)
+    if not match:
+        raise ValueError("Invalid quoted string", string)
+    string = match.group(1)
+    # remove backslash before all characters which should not be
+    # handeled as escape codes by string.decode('string-escape').
+    # This is needed so e.g. '\x00' is not unescaped as '\0'
+    string = re.sub(r'((?:^|[^\\])(?:\\\\)*)\\([^ntr0-7\\])', r'\1\2', string)
+    return string.decode('string-escape')

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-privacy/packages/txtorcon.git



More information about the Pkg-privacy-commits mailing list