{"action":"created","comment":{"url":"https://api.github.com/repos/warner/foolscap/pulls/comments/72480363","id":72480363,"diff_hunk":"@@ -0,0 +1,167 @@\n+import os, re\n+from twisted.internet.interfaces import IStreamClientEndpoint\n+from twisted.internet.defer import inlineCallbacks, returnValue, succeed\n+import ipaddress\n+from .. import observer\n+\n+from zope.interface import implementer\n+from ..ipb import IConnectionHintHandler, InvalidHintError\n+from ..util import allocate_tcp_port\n+import txtorcon\n+from .tcp import DOTTED_QUAD_RESTR, DNS_NAME_RESTR\n+\n+def is_non_public_numeric_address(host):\n+    # for numeric hostnames, skip RFC1918 addresses, since no Tor exit\n+    # node will be able to reach those. Likewise ignore IPv6 addresses.\n+    try:\n+        a = ipaddress.ip_address(host.decode(\"ascii\")) # wants unicode\n+    except ValueError:\n+        return False # non-numeric, let Tor try it\n+    if a.version != 4:\n+        return True # IPv6 gets ignored\n+    if (a.is_loopback or a.is_multicast or a.is_private or a.is_reserved\n+        or a.is_unspecified):\n+        return True # too weird, don't connect\n+    return False\n+\n+HINT_RE = re.compile(r\"^[^:]*:(%s|%s):(\\d+){1,5}$\" % (DOTTED_QUAD_RESTR,\n+                                                      DNS_NAME_RESTR))\n+\n+@implementer(IConnectionHintHandler)\n+class _Common:\n+    # subclasses must:\n+    #  define _connect()\n+    #  set self._socks_hostname, self._socks_port\n+\n+    def __init__(self):\n+        self._connected = False\n+        self._when_connected = observer.OneShotObserverList()\n+\n+    def _maybe_connect(self, reactor):\n+        if not self._connected:\n+            self._connected = True\n+            # connect\n+            d = self._connect()\n+            d.addBoth(self._when_connected.fire)\n+        return self._when_connected.whenFired()\n+\n+    @inlineCallbacks\n+    def hint_to_endpoint(self, hint, reactor):\n+        # Return (endpoint, hostname), where \"hostname\" is what we pass to the\n+        # HTTP \"Host:\" header so a dumb HTTP server can be used to redirect us.\n+        mo = HINT_RE.search(hint)\n+        if not mo:\n+            raise InvalidHintError(\"unrecognized TCP/Tor hint\")\n+        host, port = mo.group(1), int(mo.group(2))\n+        if is_non_public_numeric_address(host):\n+            raise InvalidHintError(\"ignoring non-Tor-able ipaddr %s\" % host)\n+        yield self._maybe_connect(reactor)\n+        # txsocksx doesn't like unicode: it concatenates some binary protocol\n+        # bytes with the hostname when talking to the SOCKS server, so the\n+        # py2 automatic unicode promotion blows up\n+        host = host.encode(\"ascii\")\n+        ep = txtorcon.TorClientEndpoint(host, port,\n+                                        socks_hostname=self._socks_hostname,\n+                                        socks_port=self._socks_port)\n+        returnValue( (ep, host) )\n+\n+\n+# note: TorClientEndpoint imports 'reactor' itself, doesn't provide override\n+\n+class _SocksTor(_Common):\n+    def __init__(self, hostname=None, port=None):\n+        _Common.__init__(self)\n+        self._connnected = True # no need to call _connect()\n+        self._socks_hostname = hostname\n+        self._socks_port = port # None means to use defaults: 9050, then 9150\n+    def _connect(self):\n+        return succeed(None)\n+\n+def default_socks():\n+    # TorClientEndpoint knows how to cycle through a built-in set of socks\n+    # ports, but it doesn't know to set the hostname to localhost\n+    return _SocksTor(\"127.0.0.1\")\n+\n+def socks_on_port(port):\n+    return _SocksTor(\"127.0.0.1\", port)\n+\n+\n+class _LaunchedTor(_Common):\n+    def __init__(self, reactor, data_directory=None, tor_binary=None):\n+        _Common.__init__(self)\n+        self._reactor = reactor\n+        self._data_directory = data_directory\n+        self._tor_binary = tor_binary\n+\n+    @inlineCallbacks\n+    def _connect(self):\n+        # create a new Tor\n+        config = self.config = txtorcon.TorConfig()\n+        if self._data_directory:\n+            # The default is for launch_tor to create a tempdir itself, and\n+            # delete it when done. We only need to set a DataDirectory if we\n+            # want it to be persistent. This saves some startup time, because\n+            # we cache the descriptors from last time. On one of my hosts,\n+            # this reduces connect from 20s to 15s.\n+            if not os.path.exists(self._data_directory):\n+                # tor will mkdir this, but txtorcon wants to chdir to it\n+                # before spawning the tor process, so (for now) we need to\n+                # mkdir it ourselves. TODO: txtorcon should take\n+                # responsibility for this.\n+                os.mkdir(self._data_directory)\n+            config.DataDirectory = self._data_directory\n+\n+        #config.ControlPort = allocate_tcp_port() # defaults to 9052\n+        config.SocksPort = allocate_tcp_port()\n+        self._socks_hostname = \"127.0.0.1\"\n+        self._socks_port = config.SocksPort\n+\n+        #print \"launching tor\"\n+        tpp = yield txtorcon.launch_tor(config, self._reactor,\n+                                        tor_binary=self._tor_binary,\n+                                        )\n+        #print \"launched\"\n+        # gives a TorProcessProtocol with .tor_protocol\n+        self._tor_protocol = tpp.tor_protocol\n+        returnValue(True)\n+\n+def launch_tor(reactor, data_directory=None, tor_binary=None):\n+    \"\"\"Return a handler which launches a new Tor process (once).\n+    - data_directory: a persistent directory where Tor can cache its\n+      descriptors. This allows subsequent invocations to start faster. If\n+      None, the process will use an ephemeral tempdir, deleting it when Tor\n+      exits.\n+    - tor_binary: the path to the Tor executable we should use. If None,\n+      search $PATH.\n+    \"\"\"\n+    return _LaunchedTor(reactor, data_directory, tor_binary)\n+\n+\n+@implementer(IConnectionHintHandler)\n+class _ConnectedTor(_Common):\n+    def __init__(self, reactor, tor_control_endpoint):\n+        _Common.__init__(self)\n+        self._reactor = reactor\n+        assert IStreamClientEndpoint.providedBy(tor_control_endpoint)\n+        self._tor_control_endpoint = tor_control_endpoint\n+\n+    @inlineCallbacks\n+    def _connect(self):\n+        ep = self._tor_control_endpoint\n+        tproto = yield txtorcon.build_tor_connection(ep, build_state=False)\n+        config = yield txtorcon.TorConfig.from_protocol(tproto)\n+        ports = list(config.SocksPort)\n+        port = ports[0]\n+        if port == \"DEFAULT\":\n+            port = \"9050\"","path":"src/foolscap/connections/tor.py","position":156,"original_position":156,"commit_id":"c77c03ef0e3e994719a11e0bd640e7036181c891","original_commit_id":"c77c03ef0e3e994719a11e0bd640e7036181c891","user":{"login":"meejah","id":145599,"avatar_url":"https://avatars.githubusercontent.com/u/145599?v=3","gravatar_id":"","url":"https://api.github.com/users/meejah","html_url":"https://github.com/meejah","followers_url":"https://api.github.com/users/meejah/followers","following_url":"https://api.github.com/users/meejah/following{/other_user}","gists_url":"https://api.github.com/users/meejah/gists{/gist_id}","starred_url":"https://api.github.com/users/meejah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/meejah/subscriptions","organizations_url":"https://api.github.com/users/meejah/orgs","repos_url":"https://api.github.com/users/meejah/repos","events_url":"https://api.github.com/users/meejah/events{/privacy}","received_events_url":"https://api.github.com/users/meejah/received_events","type":"User","site_admin":false},"body":"Might be slightly safer to use `if port is txtorcon.DEFAULT_VALUE`","created_at":"2016-07-27T17:07:44Z","updated_at":"2016-07-27T17:07:44Z","html_url":"https://github.com/warner/foolscap/pull/27#discussion_r72480363","pull_request_url":"https://api.github.com/repos/warner/foolscap/pulls/27","_links":{"self":{"href":"https://api.github.com/repos/warner/foolscap/pulls/comments/72480363"},"html":{"href":"https://github.com/warner/foolscap/pull/27#discussion_r72480363"},"pull_request":{"href":"https://api.github.com/repos/warner/foolscap/pulls/27"}}},"pull_request":{"url":"https://api.github.com/repos/warner/foolscap/pulls/27","id":77059946,"html_url":"https://github.com/warner/foolscap/pull/27","diff_url":"https://github.com/warner/foolscap/pull/27.diff","patch_url":"https://github.com/warner/foolscap/pull/27.patch","issue_url":"https://api.github.com/repos/warner/foolscap/issues/27","number":27,"state":"closed","locked":false,"title":"tor connection-handler","user":{"login":"warner","id":27146,"avatar_url":"https://avatars.githubusercontent.com/u/27146?v=3","gravatar_id":"","url":"https://api.github.com/users/warner","html_url":"https://github.com/warner","followers_url":"https://api.github.com/users/warner/followers","following_url":"https://api.github.com/users/warner/following{/other_user}","gists_url":"https://api.github.com/users/warner/gists{/gist_id}","starred_url":"https://api.github.com/users/warner/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/warner/subscriptions","organizations_url":"https://api.github.com/users/warner/orgs","repos_url":"https://api.github.com/users/warner/repos","events_url":"https://api.github.com/users/warner/events{/privacy}","received_events_url":"https://api.github.com/users/warner/received_events","type":"User","site_admin":false},"body":"Adds several connection handlers that can be used to connect to Tor hidden-service hints, or to use Tor to connect to normal TCP hints. For https://foolscap.lothar.com/trac/ticket/246 .\r\n\r\n@meejah, could you take a look at this and see what you think? I don't have a way to test it this week, and even if it worked I'm not sure it'd be using txtorcon correctly. But the idea is to be able to set up a tor-hint connection handler with one of three functions, depending upon what your tor setup is:\r\n\r\n* `default_socks_handler()` if you think there might be a Tor listening for SOCKS on localhost 9050/9150\r\n* `with_control_port(control_port_endpoint)` if you think Tor is listening for control messages on some known endpoint\r\n* `launch_tor(tor_binary=)` if you want to launch a new Tor (defaulting to searching $PATH)\r\n\r\nConnecting to the control port (or launching tor) should only happen once (I could be talked out of this, but I figure that if it fails the first time, then reconnecting/relaunching for every single hint is a waste of time), hence the whole `_when_connection()` thing.\r\n\r\nI copied the txtorcon-using code from Magic Wormhole, but I realized that I don't think I really tested the connect-to-pre-existing-control-port path, since it looks kind of incomplete.\r\n\r\nI suspect this will take some work before it's ready to land, but I think a PR is the easiest way to let others review it. If/when we get it into shape, I'll rebase/force-push and use this PR to land it.\r\n","created_at":"2016-07-12T04:29:08Z","updated_at":"2016-07-27T17:07:44Z","closed_at":"2016-07-27T00:34:03Z","merged_at":"2016-07-27T00:34:03Z","merge_commit_sha":"c77c03ef0e3e994719a11e0bd640e7036181c891","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/warner/foolscap/pulls/27/commits","review_comments_url":"https://api.github.com/repos/warner/foolscap/pulls/27/comments","review_comment_url":"https://api.github.com/repos/warner/foolscap/pulls/comments{/number}","comments_url":"https://api.github.com/repos/warner/foolscap/issues/27/comments","statuses_url":"https://api.github.com/repos/warner/foolscap/statuses/c77c03ef0e3e994719a11e0bd640e7036181c891","head":{"label":"warner:WIP-tor","ref":"WIP-tor","sha":"c77c03ef0e3e994719a11e0bd640e7036181c891","user":{"login":"warner","id":27146,"avatar_url":"https://avatars.githubusercontent.com/u/27146?v=3","gravatar_id":"","url":"https://api.github.com/users/warner","html_url":"https://github.com/warner","followers_url":"https://api.github.com/users/warner/followers","following_url":"https://api.github.com/users/warner/following{/other_user}","gists_url":"https://api.github.com/users/warner/gists{/gist_id}","starred_url":"https://api.github.com/users/warner/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/warner/subscriptions","organizations_url":"https://api.github.com/users/warner/orgs","repos_url":"https://api.github.com/users/warner/repos","events_url":"https://api.github.com/users/warner/events{/privacy}","received_events_url":"https://api.github.com/users/warner/received_events","type":"User","site_admin":false},"repo":{"id":1144169,"name":"foolscap","full_name":"warner/foolscap","owner":{"login":"warner","id":27146,"avatar_url":"https://avatars.githubusercontent.com/u/27146?v=3","gravatar_id":"","url":"https://api.github.com/users/warner","html_url":"https://github.com/warner","followers_url":"https://api.github.com/users/warner/followers","following_url":"https://api.github.com/users/warner/following{/other_user}","gists_url":"https://api.github.com/users/warner/gists{/gist_id}","starred_url":"https://api.github.com/users/warner/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/warner/subscriptions","organizations_url":"https://api.github.com/users/warner/orgs","repos_url":"https://api.github.com/users/warner/repos","events_url":"https://api.github.com/users/warner/events{/privacy}","received_events_url":"https://api.github.com/users/warner/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/warner/foolscap","description":"remote object-messaging for Python+Twisted","fork":false,"url":"https://api.github.com/repos/warner/foolscap","forks_url":"https://api.github.com/repos/warner/foolscap/forks","keys_url":"https://api.github.com/repos/warner/foolscap/keys{/key_id}","collaborators_url":"https://api.github.com/repos/warner/foolscap/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/warner/foolscap/teams","hooks_url":"https://api.github.com/repos/warner/foolscap/hooks","issue_events_url":"https://api.github.com/repos/warner/foolscap/issues/events{/number}","events_url":"https://api.github.com/repos/warner/foolscap/events","assignees_url":"https://api.github.com/repos/warner/foolscap/assignees{/user}","branches_url":"https://api.github.com/repos/warner/foolscap/branches{/branch}","tags_url":"https://api.github.com/repos/warner/foolscap/tags","blobs_url":"https://api.github.com/repos/warner/foolscap/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/warner/foolscap/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/warner/foolscap/git/refs{/sha}","trees_url":"https://api.github.com/repos/warner/foolscap/git/trees{/sha}","statuses_url":"https://api.github.com/repos/warner/foolscap/statuses/{sha}","languages_url":"https://api.github.com/repos/warner/foolscap/languages","stargazers_url":"https://api.github.com/repos/warner/foolscap/stargazers","contributors_url":"https://api.github.com/repos/warner/foolscap/contributors","subscribers_url":"https://api.github.com/repos/warner/foolscap/subscribers","subscription_url":"https://api.github.com/repos/warner/foolscap/subscription","commits_url":"https://api.github.com/repos/warner/foolscap/commits{/sha}","git_commits_url":"https://api.github.com/repos/warner/foolscap/git/commits{/sha}","comments_url":"https://api.github.com/repos/warner/foolscap/comments{/number}","issue_comment_url":"https://api.github.com/repos/warner/foolscap/issues/comments{/number}","contents_url":"https://api.github.com/repos/warner/foolscap/contents/{+path}","compare_url":"https://api.github.com/repos/warner/foolscap/compare/{base}...{head}","merges_url":"https://api.github.com/repos/warner/foolscap/merges","archive_url":"https://api.github.com/repos/warner/foolscap/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/warner/foolscap/downloads","issues_url":"https://api.github.com/repos/warner/foolscap/issues{/number}","pulls_url":"https://api.github.com/repos/warner/foolscap/pulls{/number}","milestones_url":"https://api.github.com/repos/warner/foolscap/milestones{/number}","notifications_url":"https://api.github.com/repos/warner/foolscap/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/warner/foolscap/labels{/name}","releases_url":"https://api.github.com/repos/warner/foolscap/releases{/id}","deployments_url":"https://api.github.com/repos/warner/foolscap/deployments","created_at":"2010-12-06T19:27:13Z","updated_at":"2016-06-30T15:01:35Z","pushed_at":"2016-07-27T00:35:16Z","git_url":"git://github.com/warner/foolscap.git","ssh_url":"git@github.com:warner/foolscap.git","clone_url":"https://github.com/warner/foolscap.git","svn_url":"https://github.com/warner/foolscap","homepage":"http://foolscap.lothar.com/trac","size":4974,"stargazers_count":30,"watchers_count":30,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":18,"mirror_url":null,"open_issues_count":6,"forks":18,"open_issues":6,"watchers":30,"default_branch":"master"}},"base":{"label":"warner:master","ref":"master","sha":"507620268fe62d0ec63965e6395c6bd831b386ac","user":{"login":"warner","id":27146,"avatar_url":"https://avatars.githubusercontent.com/u/27146?v=3","gravatar_id":"","url":"https://api.github.com/users/warner","html_url":"https://github.com/warner","followers_url":"https://api.github.com/users/warner/followers","following_url":"https://api.github.com/users/warner/following{/other_user}","gists_url":"https://api.github.com/users/warner/gists{/gist_id}","starred_url":"https://api.github.com/users/warner/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/warner/subscriptions","organizations_url":"https://api.github.com/users/warner/orgs","repos_url":"https://api.github.com/users/warner/repos","events_url":"https://api.github.com/users/warner/events{/privacy}","received_events_url":"https://api.github.com/users/warner/received_events","type":"User","site_admin":false},"repo":{"id":1144169,"name":"foolscap","full_name":"warner/foolscap","owner":{"login":"warner","id":27146,"avatar_url":"https://avatars.githubusercontent.com/u/27146?v=3","gravatar_id":"","url":"https://api.github.com/users/warner","html_url":"https://github.com/warner","followers_url":"https://api.github.com/users/warner/followers","following_url":"https://api.github.com/users/warner/following{/other_user}","gists_url":"https://api.github.com/users/warner/gists{/gist_id}","starred_url":"https://api.github.com/users/warner/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/warner/subscriptions","organizations_url":"https://api.github.com/users/warner/orgs","repos_url":"https://api.github.com/users/warner/repos","events_url":"https://api.github.com/users/warner/events{/privacy}","received_events_url":"https://api.github.com/users/warner/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/warner/foolscap","description":"remote object-messaging for Python+Twisted","fork":false,"url":"https://api.github.com/repos/warner/foolscap","forks_url":"https://api.github.com/repos/warner/foolscap/forks","keys_url":"https://api.github.com/repos/warner/foolscap/keys{/key_id}","collaborators_url":"https://api.github.com/repos/warner/foolscap/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/warner/foolscap/teams","hooks_url":"https://api.github.com/repos/warner/foolscap/hooks","issue_events_url":"https://api.github.com/repos/warner/foolscap/issues/events{/number}","events_url":"https://api.github.com/repos/warner/foolscap/events","assignees_url":"https://api.github.com/repos/warner/foolscap/assignees{/user}","branches_url":"https://api.github.com/repos/warner/foolscap/branches{/branch}","tags_url":"https://api.github.com/repos/warner/foolscap/tags","blobs_url":"https://api.github.com/repos/warner/foolscap/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/warner/foolscap/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/warner/foolscap/git/refs{/sha}","trees_url":"https://api.github.com/repos/warner/foolscap/git/trees{/sha}","statuses_url":"https://api.github.com/repos/warner/foolscap/statuses/{sha}","languages_url":"https://api.github.com/repos/warner/foolscap/languages","stargazers_url":"https://api.github.com/repos/warner/foolscap/stargazers","contributors_url":"https://api.github.com/repos/warner/foolscap/contributors","subscribers_url":"https://api.github.com/repos/warner/foolscap/subscribers","subscription_url":"https://api.github.com/repos/warner/foolscap/subscription","commits_url":"https://api.github.com/repos/warner/foolscap/commits{/sha}","git_commits_url":"https://api.github.com/repos/warner/foolscap/git/commits{/sha}","comments_url":"https://api.github.com/repos/warner/foolscap/comments{/number}","issue_comment_url":"https://api.github.com/repos/warner/foolscap/issues/comments{/number}","contents_url":"https://api.github.com/repos/warner/foolscap/contents/{+path}","compare_url":"https://api.github.com/repos/warner/foolscap/compare/{base}...{head}","merges_url":"https://api.github.com/repos/warner/foolscap/merges","archive_url":"https://api.github.com/repos/warner/foolscap/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/warner/foolscap/downloads","issues_url":"https://api.github.com/repos/warner/foolscap/issues{/number}","pulls_url":"https://api.github.com/repos/warner/foolscap/pulls{/number}","milestones_url":"https://api.github.com/repos/warner/foolscap/milestones{/number}","notifications_url":"https://api.github.com/repos/warner/foolscap/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/warner/foolscap/labels{/name}","releases_url":"https://api.github.com/repos/warner/foolscap/releases{/id}","deployments_url":"https://api.github.com/repos/warner/foolscap/deployments","created_at":"2010-12-06T19:27:13Z","updated_at":"2016-06-30T15:01:35Z","pushed_at":"2016-07-27T00:35:16Z","git_url":"git://github.com/warner/foolscap.git","ssh_url":"git@github.com:warner/foolscap.git","clone_url":"https://github.com/warner/foolscap.git","svn_url":"https://github.com/warner/foolscap","homepage":"http://foolscap.lothar.com/trac","size":4974,"stargazers_count":30,"watchers_count":30,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":18,"mirror_url":null,"open_issues_count":6,"forks":18,"open_issues":6,"watchers":30,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/warner/foolscap/pulls/27"},"html":{"href":"https://github.com/warner/foolscap/pull/27"},"issue":{"href":"https://api.github.com/repos/warner/foolscap/issues/27"},"comments":{"href":"https://api.github.com/repos/warner/foolscap/issues/27/comments"},"review_comments":{"href":"https://api.github.com/repos/warner/foolscap/pulls/27/comments"},"review_comment":{"href":"https://api.github.com/repos/warner/foolscap/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/warner/foolscap/pulls/27/commits"},"statuses":{"href":"https://api.github.com/repos/warner/foolscap/statuses/c77c03ef0e3e994719a11e0bd640e7036181c891"}}}}
{"actor":{"display_login":"meejah"}}