Skip to content

Commit

Permalink
Merge pull request #84 from itamarst/83.i2p
Browse files Browse the repository at this point in the history
Restore i2p support
  • Loading branch information
itamarst committed Jun 25, 2021
2 parents 9adab19 + 10487c2 commit 73497a0
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 6 deletions.
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
User visible changes in Foolscap

## TBD

* The I2P connection handler has been restored.
* Improved support for type checking with `mypy-zope`.

## Release 20.4.0 (12-Apr-2020)

Foolscap has finally been ported to py3 (specifically py3.5+). It currently
Expand Down
23 changes: 20 additions & 3 deletions doc/connection-handlers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ at least the following hint types:
`HOSTNAME:PORT` via a Tor proxy. The only meaningful reason for putting a
`tor:` hint in your FURL is if `HOSTNAME` ends in `.onion`, indicating that
the Tub is listening on a Tor "onion service" (aka "hidden service").
* `i2p:ADDR` : Like `tor:`, but use an I2P proxy. `i2p:ADDR:PORT` is also
legal, although I2P services do not generally use port numbers.

Built-In Connection Handlers
----------------------------
Expand Down Expand Up @@ -125,11 +127,21 @@ Foolscap's built-in connection handlers are:
and can speed up the second invocation of the program considerably. If not
provided, a ephemeral temporary directory is used (and deleted at
shutdown).
* `i2p.default(reactor)` : This uses the "SAM" protocol over the default I2P
daemon port (localhost:7656) to reach an I2P server. Most I2P daemons are
listening on this port.
* `i2p.sam_endpoint(endpoint)` : This uses SAM on an alternate port to reach
the I2P daemon.
* (future) `i2p.local_i2p(configdir=None)` : When implemented, this will
contact an already-running I2P daemon by reading it's configuration to find
a contact method.
* (future) `i2p.launch(configdir=None, binary=None)` : When implemented, this
will launch a new I2P daemon (with arguments similar to `tor.launch`).

Applications which want to enable as many connection-hint types as possible
should simply install the `tor.default_socks()` handler
should simply install the `tor.default_socks()` and `i2p.default()` handlers
if they can be imported. This will Just Work(tm) if the most common
deployments of Tor are installed+running on the local machine. If not,
deployments of Tor/I2P are installed+running on the local machine. If not,
those connection hints will be ignored.

.. code-block:: python
Expand All @@ -139,6 +151,11 @@ those connection hints will be ignored.
tub.addConnectionHintHandler("tor", tor.default_socks())
except ImportError:
pass # we're missing txtorcon, oh well
try:
from foolscap.connections import i2p
tub.addConnectionHintHandler("i2p", i2p.default(reactor))
except ImportError:
pass # we're missing txi2p
Configuring Endpoints for Connection Handlers
Expand Down Expand Up @@ -230,7 +247,7 @@ Status delivery: the third argument to ``hint_to_endpoint()`` will be a
one-argument callable named ``update_status()``. While the handler is trying
to produce an endpoint, it may call ``update_status(status)`` with a (native)
string argument each time the connection process has achieved some new state
(e.g. ``launching tor``). This will be used by the
(e.g. ``launching tor``, ``connecting to i2p``). This will be used by the
``ConnectionInfo`` object to provide connection status to the application.
Note that once the handler returns an endpoint (or the handler's Deferred
finally fires), the status will be replaced by ``connecting``, and the
Expand Down
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,12 @@ def run(self):
"install_requires": ["six", "twisted[tls] >= 16.0.0", "pyOpenSSL"],
"extras_require": {
"dev": ["mock", "txtorcon >= 19.0.0",
"txi2p-tahoe >= 0.3.5; python_version > '3.0'",
"txi2p >= 0.3.2; python_version < '3.0'",
"pywin32 ; sys_platform == 'win32'"],
"tor": ["txtorcon >= 19.0.0"],
"i2p": ["txi2p-tahoe >= 0.3.5; python_version > '3.0'",
"txi2p >= 0.3.2; python_version < '3.0'"],
},
}

Expand Down
51 changes: 51 additions & 0 deletions src/foolscap/connections/i2p.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import re
from twisted.internet.endpoints import clientFromString
from twisted.internet.interfaces import IStreamClientEndpoint
from txi2p.sam import SAMI2PStreamClientEndpoint
from zope.interface import implementer

from foolscap.ipb import IConnectionHintHandler, InvalidHintError

HINT_RE=re.compile(r"^i2p:([A-Za-z.0-9\-]+)(:(\d+){1,5})?$")

@implementer(IConnectionHintHandler)
class _RunningI2P:
def __init__(self, sam_endpoint, **kwargs):
assert IStreamClientEndpoint.providedBy(sam_endpoint)
self._sam_endpoint = sam_endpoint
self._kwargs = kwargs

def hint_to_endpoint(self, hint, reactor, update_status):
# Return (endpoint, hostname), where "hostname" is what we pass to the
# HTTP "Host:" header so a dumb HTTP server can be used to redirect us.
mo = HINT_RE.search(hint)
if not mo:
raise InvalidHintError("unrecognized I2P hint")
host, portnum = mo.group(1), int(mo.group(3)) if mo.group(3) else None
kwargs = self._kwargs.copy()
if not portnum and 'port' in kwargs:
portnum = kwargs.pop('port')
ep = SAMI2PStreamClientEndpoint.new(self._sam_endpoint, host, portnum, **kwargs)
return ep, host

def describe(self):
return "i2p"

def default(reactor, **kwargs):
"""Return a handler which connects to a pre-existing I2P process on the
default SAM port.
"""
return _RunningI2P(clientFromString(reactor, 'tcp:127.0.0.1:7656'), **kwargs)

def sam_endpoint(sam_port_endpoint, **kwargs):
"""Return a handler which connects to a pre-existing I2P process on the
given SAM port.
- sam_endpoint: a ClientEndpoint which points at the SAM API
"""
return _RunningI2P(sam_port_endpoint, **kwargs)

def local_i2p(i2p_configdir=None):
raise NotImplementedError

def launch(i2p_configdir=None, i2p_binary=None):
raise NotImplementedError
14 changes: 13 additions & 1 deletion src/foolscap/test/check-connections-client.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
TUBID = "qy4aezcyd3mppt7arodl4mzaguls6m2o"
ONION = "kwmjlhmn5runa4bv.onion"
ONIONPORT = 16545
I2P = "???"
I2PPORT = 0
LOCALPORT = 7006

# Then run 'check-connections-client.py tcp', then with 'socks', then with
Expand Down Expand Up @@ -44,8 +46,18 @@
tub.removeAllConnectionHintHandlers()
tub.addConnectionHintHandler("tor", h)
furl = "pb://%s@tor:%s:%d/calculator" % (TUBID, ONION, ONIONPORT)
elif which in ("i2p-default", "i2p-sam"):
from foolscap.connections import i2p
if which == "i2p-default":
h = i2p.default(reactor)
else:
sam_ep = clientFromString(reactor, sys.argv[2])
h = i2p.sam_endpoint(sam_ep)
tub.removeAllConnectionHintHandlers()
tub.addConnectionHintHandler("i2p", h)
furl = "pb://%s@i2p:%s:%d/calculator" % (TUBID, I2P, I2PPORT)
else:
print("run as 'check-connections-client.py [tcp|tor-default|tor-socks|tor-control|tor-launch]'")
print("run as 'check-connections-client.py [tcp|tor-default|tor-socks|tor-control|tor-launch|i2p-default|i2p-sam]'")
sys.exit(1)
print("using %s: %s" % (which, furl))

Expand Down
109 changes: 107 additions & 2 deletions src/foolscap/test/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import mock
from zope.interface import implementer
from twisted.trial import unittest
from twisted.internet import defer, reactor
from twisted.internet import endpoints, defer, reactor
from twisted.internet.endpoints import clientFromString
from twisted.internet.defer import inlineCallbacks
from twisted.internet.interfaces import IStreamClientEndpoint
Expand All @@ -11,7 +11,7 @@
from foolscap.api import Tub
from foolscap.info import ConnectionInfo
from foolscap.connection import get_endpoint
from foolscap.connections import tcp, tor
from foolscap.connections import tcp, tor, i2p
from foolscap.tokens import NoLocationHintsError
from foolscap.ipb import InvalidHintError
from foolscap.test.common import (certData_low, certData_high, Target,
Expand Down Expand Up @@ -538,3 +538,108 @@ def make_takes_status(arg, update_status):
self.assertIsInstance(ep, txtorcon.endpoints.TorClientEndpoint)
self.assertEqual(host, "foo.onion")
self.assertEqual(h._socks_desc, "tcp:127.0.0.1:1234")



class I2P(unittest.TestCase):
@inlineCallbacks
def test_default(self):
with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
sep.new = n = mock.Mock()
n.return_value = expected_ep = object()
h = i2p.default(reactor, misc_kwarg="foo")
res = yield h.hint_to_endpoint("i2p:fppym.b32.i2p", reactor,
discard_status)
self.assertEqual(len(n.mock_calls), 1)
args = n.mock_calls[0][1]
got_sep, got_host, got_portnum = args
self.assertIsInstance(got_sep, endpoints.TCP4ClientEndpoint)
self.failUnlessEqual(got_sep._host, "127.0.0.1") # fragile
self.failUnlessEqual(got_sep._port, 7656)
self.failUnlessEqual(got_host, "fppym.b32.i2p")
self.failUnlessEqual(got_portnum, None)
kwargs = n.mock_calls[0][2]
self.failUnlessEqual(kwargs, {"misc_kwarg": "foo"})

ep, host = res
self.assertIdentical(ep, expected_ep)
self.assertEqual(host, "fppym.b32.i2p")
self.assertEqual(h.describe(), "i2p")

@inlineCallbacks
def test_default_with_portnum(self):
# I2P addresses generally don't use port numbers, but the parser is
# supposed to handle them
with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
sep.new = n = mock.Mock()
n.return_value = expected_ep = object()
h = i2p.default(reactor)
res = yield h.hint_to_endpoint("i2p:fppym.b32.i2p:1234", reactor,
discard_status)
self.assertEqual(len(n.mock_calls), 1)
args = n.mock_calls[0][1]
got_sep, got_host, got_portnum = args
self.assertIsInstance(got_sep, endpoints.TCP4ClientEndpoint)
self.failUnlessEqual(got_sep._host, "127.0.0.1") # fragile
self.failUnlessEqual(got_sep._port, 7656)
self.failUnlessEqual(got_host, "fppym.b32.i2p")
self.failUnlessEqual(got_portnum, 1234)
ep, host = res
self.assertIdentical(ep, expected_ep)
self.assertEqual(host, "fppym.b32.i2p")

@inlineCallbacks
def test_default_with_portnum_kwarg(self):
# setting extra kwargs on the handler should provide a default for
# the portnum. sequential calls with/without portnums in the hints
# should get the right values.
h = i2p.default(reactor, port=1234)

with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
sep.new = n = mock.Mock()
yield h.hint_to_endpoint("i2p:fppym.b32.i2p", reactor,
discard_status)
got_portnum = n.mock_calls[0][1][2]
self.failUnlessEqual(got_portnum, 1234)

with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
sep.new = n = mock.Mock()
yield h.hint_to_endpoint("i2p:fppym.b32.i2p:3456", reactor,
discard_status)
got_portnum = n.mock_calls[0][1][2]
self.failUnlessEqual(got_portnum, 3456)

with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
sep.new = n = mock.Mock()
yield h.hint_to_endpoint("i2p:fppym.b32.i2p", reactor,
discard_status)
got_portnum = n.mock_calls[0][1][2]
self.failUnlessEqual(got_portnum, 1234)

def test_default_badhint(self):
h = i2p.default(reactor)
d = defer.maybeDeferred(h.hint_to_endpoint, "i2p:not@a@hint", reactor,
discard_status)
f = self.failureResultOf(d, InvalidHintError)
self.assertEqual(str(f.value), "unrecognized I2P hint")

@inlineCallbacks
def test_sam_endpoint(self):
with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
sep.new = n = mock.Mock()
n.return_value = expected_ep = object()
my_ep = FakeHostnameEndpoint(reactor, "localhost", 1234)
h = i2p.sam_endpoint(my_ep, misc_kwarg="foo")
res = yield h.hint_to_endpoint("i2p:fppym.b32.i2p", reactor,
discard_status)
self.assertEqual(len(n.mock_calls), 1)
args = n.mock_calls[0][1]
got_sep, got_host, got_portnum = args
self.assertIdentical(got_sep, my_ep)
self.failUnlessEqual(got_host, "fppym.b32.i2p")
self.failUnlessEqual(got_portnum, None)
kwargs = n.mock_calls[0][2]
self.failUnlessEqual(kwargs, {"misc_kwarg": "foo"})
ep, host = res
self.assertIdentical(ep, expected_ep)
self.assertEqual(host, "fppym.b32.i2p")

0 comments on commit 73497a0

Please sign in to comment.