Skip to content

Commit 73497a0

Browse files
authored
Merge pull request #84 from itamarst/83.i2p
Restore i2p support
2 parents 9adab19 + 10487c2 commit 73497a0

File tree

6 files changed

+200
-6
lines changed

6 files changed

+200
-6
lines changed

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
User visible changes in Foolscap
22

3+
## TBD
4+
5+
* The I2P connection handler has been restored.
6+
* Improved support for type checking with `mypy-zope`.
7+
38
## Release 20.4.0 (12-Apr-2020)
49

510
Foolscap has finally been ported to py3 (specifically py3.5+). It currently

doc/connection-handlers.rst

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ at least the following hint types:
6161
`HOSTNAME:PORT` via a Tor proxy. The only meaningful reason for putting a
6262
`tor:` hint in your FURL is if `HOSTNAME` ends in `.onion`, indicating that
6363
the Tub is listening on a Tor "onion service" (aka "hidden service").
64+
* `i2p:ADDR` : Like `tor:`, but use an I2P proxy. `i2p:ADDR:PORT` is also
65+
legal, although I2P services do not generally use port numbers.
6466

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

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

135147
.. code-block:: python
@@ -139,6 +151,11 @@ those connection hints will be ignored.
139151
tub.addConnectionHintHandler("tor", tor.default_socks())
140152
except ImportError:
141153
pass # we're missing txtorcon, oh well
154+
try:
155+
from foolscap.connections import i2p
156+
tub.addConnectionHintHandler("i2p", i2p.default(reactor))
157+
except ImportError:
158+
pass # we're missing txi2p
142159
143160
144161
Configuring Endpoints for Connection Handlers
@@ -230,7 +247,7 @@ Status delivery: the third argument to ``hint_to_endpoint()`` will be a
230247
one-argument callable named ``update_status()``. While the handler is trying
231248
to produce an endpoint, it may call ``update_status(status)`` with a (native)
232249
string argument each time the connection process has achieved some new state
233-
(e.g. ``launching tor``). This will be used by the
250+
(e.g. ``launching tor``, ``connecting to i2p``). This will be used by the
234251
``ConnectionInfo`` object to provide connection status to the application.
235252
Note that once the handler returns an endpoint (or the handler's Deferred
236253
finally fires), the status will be replaced by ``connecting``, and the

setup.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,12 @@ def run(self):
7272
"install_requires": ["six", "twisted[tls] >= 16.0.0", "pyOpenSSL"],
7373
"extras_require": {
7474
"dev": ["mock", "txtorcon >= 19.0.0",
75+
"txi2p-tahoe >= 0.3.5; python_version > '3.0'",
76+
"txi2p >= 0.3.2; python_version < '3.0'",
7577
"pywin32 ; sys_platform == 'win32'"],
7678
"tor": ["txtorcon >= 19.0.0"],
79+
"i2p": ["txi2p-tahoe >= 0.3.5; python_version > '3.0'",
80+
"txi2p >= 0.3.2; python_version < '3.0'"],
7781
},
7882
}
7983

src/foolscap/connections/i2p.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import re
2+
from twisted.internet.endpoints import clientFromString
3+
from twisted.internet.interfaces import IStreamClientEndpoint
4+
from txi2p.sam import SAMI2PStreamClientEndpoint
5+
from zope.interface import implementer
6+
7+
from foolscap.ipb import IConnectionHintHandler, InvalidHintError
8+
9+
HINT_RE=re.compile(r"^i2p:([A-Za-z.0-9\-]+)(:(\d+){1,5})?$")
10+
11+
@implementer(IConnectionHintHandler)
12+
class _RunningI2P:
13+
def __init__(self, sam_endpoint, **kwargs):
14+
assert IStreamClientEndpoint.providedBy(sam_endpoint)
15+
self._sam_endpoint = sam_endpoint
16+
self._kwargs = kwargs
17+
18+
def hint_to_endpoint(self, hint, reactor, update_status):
19+
# Return (endpoint, hostname), where "hostname" is what we pass to the
20+
# HTTP "Host:" header so a dumb HTTP server can be used to redirect us.
21+
mo = HINT_RE.search(hint)
22+
if not mo:
23+
raise InvalidHintError("unrecognized I2P hint")
24+
host, portnum = mo.group(1), int(mo.group(3)) if mo.group(3) else None
25+
kwargs = self._kwargs.copy()
26+
if not portnum and 'port' in kwargs:
27+
portnum = kwargs.pop('port')
28+
ep = SAMI2PStreamClientEndpoint.new(self._sam_endpoint, host, portnum, **kwargs)
29+
return ep, host
30+
31+
def describe(self):
32+
return "i2p"
33+
34+
def default(reactor, **kwargs):
35+
"""Return a handler which connects to a pre-existing I2P process on the
36+
default SAM port.
37+
"""
38+
return _RunningI2P(clientFromString(reactor, 'tcp:127.0.0.1:7656'), **kwargs)
39+
40+
def sam_endpoint(sam_port_endpoint, **kwargs):
41+
"""Return a handler which connects to a pre-existing I2P process on the
42+
given SAM port.
43+
- sam_endpoint: a ClientEndpoint which points at the SAM API
44+
"""
45+
return _RunningI2P(sam_port_endpoint, **kwargs)
46+
47+
def local_i2p(i2p_configdir=None):
48+
raise NotImplementedError
49+
50+
def launch(i2p_configdir=None, i2p_binary=None):
51+
raise NotImplementedError

src/foolscap/test/check-connections-client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
TUBID = "qy4aezcyd3mppt7arodl4mzaguls6m2o"
1111
ONION = "kwmjlhmn5runa4bv.onion"
1212
ONIONPORT = 16545
13+
I2P = "???"
14+
I2PPORT = 0
1315
LOCALPORT = 7006
1416

1517
# Then run 'check-connections-client.py tcp', then with 'socks', then with
@@ -44,8 +46,18 @@
4446
tub.removeAllConnectionHintHandlers()
4547
tub.addConnectionHintHandler("tor", h)
4648
furl = "pb://%s@tor:%s:%d/calculator" % (TUBID, ONION, ONIONPORT)
49+
elif which in ("i2p-default", "i2p-sam"):
50+
from foolscap.connections import i2p
51+
if which == "i2p-default":
52+
h = i2p.default(reactor)
53+
else:
54+
sam_ep = clientFromString(reactor, sys.argv[2])
55+
h = i2p.sam_endpoint(sam_ep)
56+
tub.removeAllConnectionHintHandlers()
57+
tub.addConnectionHintHandler("i2p", h)
58+
furl = "pb://%s@i2p:%s:%d/calculator" % (TUBID, I2P, I2PPORT)
4759
else:
48-
print("run as 'check-connections-client.py [tcp|tor-default|tor-socks|tor-control|tor-launch]'")
60+
print("run as 'check-connections-client.py [tcp|tor-default|tor-socks|tor-control|tor-launch|i2p-default|i2p-sam]'")
4961
sys.exit(1)
5062
print("using %s: %s" % (which, furl))
5163

src/foolscap/test/test_connection.py

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import mock
33
from zope.interface import implementer
44
from twisted.trial import unittest
5-
from twisted.internet import defer, reactor
5+
from twisted.internet import endpoints, defer, reactor
66
from twisted.internet.endpoints import clientFromString
77
from twisted.internet.defer import inlineCallbacks
88
from twisted.internet.interfaces import IStreamClientEndpoint
@@ -11,7 +11,7 @@
1111
from foolscap.api import Tub
1212
from foolscap.info import ConnectionInfo
1313
from foolscap.connection import get_endpoint
14-
from foolscap.connections import tcp, tor
14+
from foolscap.connections import tcp, tor, i2p
1515
from foolscap.tokens import NoLocationHintsError
1616
from foolscap.ipb import InvalidHintError
1717
from foolscap.test.common import (certData_low, certData_high, Target,
@@ -538,3 +538,108 @@ def make_takes_status(arg, update_status):
538538
self.assertIsInstance(ep, txtorcon.endpoints.TorClientEndpoint)
539539
self.assertEqual(host, "foo.onion")
540540
self.assertEqual(h._socks_desc, "tcp:127.0.0.1:1234")
541+
542+
543+
544+
class I2P(unittest.TestCase):
545+
@inlineCallbacks
546+
def test_default(self):
547+
with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
548+
sep.new = n = mock.Mock()
549+
n.return_value = expected_ep = object()
550+
h = i2p.default(reactor, misc_kwarg="foo")
551+
res = yield h.hint_to_endpoint("i2p:fppym.b32.i2p", reactor,
552+
discard_status)
553+
self.assertEqual(len(n.mock_calls), 1)
554+
args = n.mock_calls[0][1]
555+
got_sep, got_host, got_portnum = args
556+
self.assertIsInstance(got_sep, endpoints.TCP4ClientEndpoint)
557+
self.failUnlessEqual(got_sep._host, "127.0.0.1") # fragile
558+
self.failUnlessEqual(got_sep._port, 7656)
559+
self.failUnlessEqual(got_host, "fppym.b32.i2p")
560+
self.failUnlessEqual(got_portnum, None)
561+
kwargs = n.mock_calls[0][2]
562+
self.failUnlessEqual(kwargs, {"misc_kwarg": "foo"})
563+
564+
ep, host = res
565+
self.assertIdentical(ep, expected_ep)
566+
self.assertEqual(host, "fppym.b32.i2p")
567+
self.assertEqual(h.describe(), "i2p")
568+
569+
@inlineCallbacks
570+
def test_default_with_portnum(self):
571+
# I2P addresses generally don't use port numbers, but the parser is
572+
# supposed to handle them
573+
with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
574+
sep.new = n = mock.Mock()
575+
n.return_value = expected_ep = object()
576+
h = i2p.default(reactor)
577+
res = yield h.hint_to_endpoint("i2p:fppym.b32.i2p:1234", reactor,
578+
discard_status)
579+
self.assertEqual(len(n.mock_calls), 1)
580+
args = n.mock_calls[0][1]
581+
got_sep, got_host, got_portnum = args
582+
self.assertIsInstance(got_sep, endpoints.TCP4ClientEndpoint)
583+
self.failUnlessEqual(got_sep._host, "127.0.0.1") # fragile
584+
self.failUnlessEqual(got_sep._port, 7656)
585+
self.failUnlessEqual(got_host, "fppym.b32.i2p")
586+
self.failUnlessEqual(got_portnum, 1234)
587+
ep, host = res
588+
self.assertIdentical(ep, expected_ep)
589+
self.assertEqual(host, "fppym.b32.i2p")
590+
591+
@inlineCallbacks
592+
def test_default_with_portnum_kwarg(self):
593+
# setting extra kwargs on the handler should provide a default for
594+
# the portnum. sequential calls with/without portnums in the hints
595+
# should get the right values.
596+
h = i2p.default(reactor, port=1234)
597+
598+
with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
599+
sep.new = n = mock.Mock()
600+
yield h.hint_to_endpoint("i2p:fppym.b32.i2p", reactor,
601+
discard_status)
602+
got_portnum = n.mock_calls[0][1][2]
603+
self.failUnlessEqual(got_portnum, 1234)
604+
605+
with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
606+
sep.new = n = mock.Mock()
607+
yield h.hint_to_endpoint("i2p:fppym.b32.i2p:3456", reactor,
608+
discard_status)
609+
got_portnum = n.mock_calls[0][1][2]
610+
self.failUnlessEqual(got_portnum, 3456)
611+
612+
with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
613+
sep.new = n = mock.Mock()
614+
yield h.hint_to_endpoint("i2p:fppym.b32.i2p", reactor,
615+
discard_status)
616+
got_portnum = n.mock_calls[0][1][2]
617+
self.failUnlessEqual(got_portnum, 1234)
618+
619+
def test_default_badhint(self):
620+
h = i2p.default(reactor)
621+
d = defer.maybeDeferred(h.hint_to_endpoint, "i2p:not@a@hint", reactor,
622+
discard_status)
623+
f = self.failureResultOf(d, InvalidHintError)
624+
self.assertEqual(str(f.value), "unrecognized I2P hint")
625+
626+
@inlineCallbacks
627+
def test_sam_endpoint(self):
628+
with mock.patch("foolscap.connections.i2p.SAMI2PStreamClientEndpoint") as sep:
629+
sep.new = n = mock.Mock()
630+
n.return_value = expected_ep = object()
631+
my_ep = FakeHostnameEndpoint(reactor, "localhost", 1234)
632+
h = i2p.sam_endpoint(my_ep, misc_kwarg="foo")
633+
res = yield h.hint_to_endpoint("i2p:fppym.b32.i2p", reactor,
634+
discard_status)
635+
self.assertEqual(len(n.mock_calls), 1)
636+
args = n.mock_calls[0][1]
637+
got_sep, got_host, got_portnum = args
638+
self.assertIdentical(got_sep, my_ep)
639+
self.failUnlessEqual(got_host, "fppym.b32.i2p")
640+
self.failUnlessEqual(got_portnum, None)
641+
kwargs = n.mock_calls[0][2]
642+
self.failUnlessEqual(kwargs, {"misc_kwarg": "foo"})
643+
ep, host = res
644+
self.assertIdentical(ep, expected_ep)
645+
self.assertEqual(host, "fppym.b32.i2p")

0 commit comments

Comments
 (0)