source: trunk/src/allmydata/util/i2p_provider.py

Last change on this file was 024b5e4, checked in by Jean-Paul Calderone <exarkun@…>, at 2023-07-20T18:23:31Z

narrow the type annotation for another Listener method param

  • Property mode set to 100644
File size: 10.6 KB
Line 
1# -*- coding: utf-8 -*-
2
3from __future__ import annotations
4
5from typing import Any
6from typing_extensions import Literal
7
8import os
9
10from zope.interface import (
11    implementer,
12)
13
14from twisted.internet.defer import inlineCallbacks, returnValue
15from twisted.internet.endpoints import clientFromString
16from twisted.internet.error import ConnectionRefusedError, ConnectError
17from twisted.application import service
18from twisted.python.usage import Options
19
20from ..listeners import ListenerConfig
21from ..interfaces import (
22    IAddressFamily,
23)
24from ..node import _Config
25
26def create(reactor: Any, config: _Config) -> IAddressFamily:
27    """
28    Create a new Provider service (this is an IService so must be
29    hooked up to a parent or otherwise started).
30
31    If foolscap.connections.i2p or txi2p are not installed, then
32    Provider.get_i2p_handler() will return None. If 'tahoe.cfg' wants
33    to start an I2P Destination too, then this `create()` method will
34    throw a nice error (and startService will throw an ugly error).
35    """
36    provider = _Provider(config, reactor)
37    provider.check_dest_config()
38    return provider
39
40
41def _import_i2p():
42    # this exists to be overridden by unit tests
43    try:
44        from foolscap.connections import i2p
45        return i2p
46    except ImportError: # pragma: no cover
47        return None
48
49def _import_txi2p():
50    try:
51        import txi2p
52        return txi2p
53    except ImportError: # pragma: no cover
54        return None
55
56def is_available() -> bool:
57    """
58    Can this type of listener actually be used in this runtime
59    environment?
60
61    If its dependencies are missing then it cannot be.
62    """
63    return not (_import_i2p() is None or _import_txi2p() is None)
64
65def can_hide_ip() -> Literal[True]:
66    """
67    Can the transport supported by this type of listener conceal the
68    node's public internet address from peers?
69    """
70    return True
71
72def _try_to_connect(reactor, endpoint_desc, stdout, txi2p):
73    # yields True or None
74    ep = clientFromString(reactor, endpoint_desc)
75    d = txi2p.testAPI(reactor, 'SAM', ep)
76    def _failed(f):
77        # depending upon what's listening at that endpoint, we might get
78        # various errors. If this list is too short, we might expose an
79        # exception to the user (causing "tahoe create-node" to fail messily)
80        # when we're supposed to just try the next potential port instead.
81        # But I don't want to catch everything, because that may hide actual
82        # coding errors.
83        f.trap(ConnectionRefusedError, # nothing listening on TCP
84               ConnectError, # missing unix socket, or permission denied
85               #ValueError,
86               # connecting to e.g. an HTTP server causes an
87               # UnhandledException (around a ValueError) when the handshake
88               # fails to parse, but that's not something we can catch. The
89               # attempt hangs, so don't do that.
90               RuntimeError, # authentication failure
91               )
92        if stdout:
93            stdout.write("Unable to reach I2P SAM API at '%s': %s\n" %
94                         (endpoint_desc, f.value))
95        return None
96    d.addErrback(_failed)
97    return d
98
99@inlineCallbacks
100def _connect_to_i2p(reactor, cli_config, txi2p):
101    # we assume i2p is already running
102    ports_to_try = ["tcp:127.0.0.1:7656"]
103    if cli_config["i2p-sam-port"]:
104        ports_to_try = [cli_config["i2p-sam-port"]]
105    for port in ports_to_try:
106        accessible = yield _try_to_connect(reactor, port, cli_config.stdout,
107                                           txi2p)
108        if accessible:
109            returnValue(port) ; break # helps editor
110    else:
111        raise ValueError("unable to reach any default I2P SAM port")
112
113async def create_config(reactor: Any, cli_config: Options) -> ListenerConfig:
114    """
115    For a given set of command-line options, construct an I2P listener.
116
117    This includes allocating a new I2P address.
118    """
119    txi2p = _import_txi2p()
120    if not txi2p:
121        raise ValueError("Cannot create I2P Destination without txi2p. "
122                         "Please 'pip install tahoe-lafs[i2p]' to fix this.")
123    tahoe_config_i2p = [] # written into tahoe.cfg:[i2p]
124    private_dir = os.path.abspath(os.path.join(cli_config["basedir"], "private"))
125    # XXX We shouldn't carry stdout around by jamming it into the Options
126    # value.  See https://tahoe-lafs.org/trac/tahoe-lafs/ticket/4048
127    stdout = cli_config.stdout # type: ignore[attr-defined]
128    if cli_config["i2p-launch"]:
129        raise NotImplementedError("--i2p-launch is under development.")
130    else:
131        print("connecting to I2P (to allocate .i2p address)..", file=stdout)
132        sam_port = await _connect_to_i2p(reactor, cli_config, txi2p)
133        print("I2P connection established", file=stdout)
134        tahoe_config_i2p.append(("sam.port", sam_port))
135
136    external_port = 3457 # TODO: pick this randomly? there's no contention.
137
138    privkeyfile = os.path.join(private_dir, "i2p_dest.privkey")
139    sam_endpoint = clientFromString(reactor, sam_port)
140    print("allocating .i2p address...", file=stdout)
141    dest = await txi2p.generateDestination(reactor, privkeyfile, 'SAM', sam_endpoint)
142    print(".i2p address allocated", file=stdout)
143    i2p_port = "listen:i2p" # means "see [i2p]", calls Provider.get_listener()
144    i2p_location = "i2p:%s:%d" % (dest.host, external_port)
145
146    # in addition to the "how to launch/connect-to i2p" keys above, we also
147    # record information about the I2P service into tahoe.cfg.
148    # * "port" is the random "public Destination port" (integer), which
149    #   (when combined with the .i2p address) should match "i2p_location"
150    #   (which will be added to tub.location)
151    # * "private_key_file" points to the on-disk copy of the private key
152    #   material (although we always write it to the same place)
153
154    tahoe_config_i2p.extend([
155        ("dest", "true"),
156        ("dest.port", str(external_port)),
157        ("dest.private_key_file", os.path.join("private", "i2p_dest.privkey")),
158    ])
159
160    # tahoe_config_i2p: this is a dictionary of keys/values to add to the
161    # "[i2p]" section of tahoe.cfg, which tells the new node how to launch
162    # I2P in the right way.
163
164    # i2p_port: a server endpoint string, it will be added to tub.port=
165
166    # i2p_location: a foolscap connection hint, "i2p:B32_ADDR:PORT"
167
168    # We assume/require that the Node gives us the same data_directory=
169    # at both create-node and startup time. The data directory is not
170    # recorded in tahoe.cfg
171
172    return ListenerConfig([i2p_port], [i2p_location], {"i2p": tahoe_config_i2p})
173
174
175@implementer(IAddressFamily)
176class _Provider(service.MultiService):
177    def __init__(self, config, reactor):
178        service.MultiService.__init__(self)
179        self._config = config
180        self._i2p = _import_i2p()
181        self._txi2p = _import_txi2p()
182        self._reactor = reactor
183
184    def _get_i2p_config(self, *args, **kwargs):
185        return self._config.get_config("i2p", *args, **kwargs)
186
187    def get_listener(self):
188        # this is relative to BASEDIR, and our cwd should be BASEDIR
189        privkeyfile = self._get_i2p_config("dest.private_key_file")
190        external_port = self._get_i2p_config("dest.port")
191        sam_port = self._get_i2p_config("sam.port")
192        escaped_sam_port = sam_port.replace(':', '\:')
193        # for now, this returns a string, which then gets passed to
194        # endpoints.serverFromString . But it can also return an Endpoint
195        # directly, which means we don't need to encode all these options
196        # into a string
197        i2p_port = "i2p:%s:%s:api=SAM:apiEndpoint=%s" % \
198                   (privkeyfile, external_port, escaped_sam_port)
199        return i2p_port
200
201    def get_client_endpoint(self):
202        """
203        Get an ``IStreamClientEndpoint`` which will set up a connection to an I2P
204        address.
205
206        If I2P is not enabled or the dependencies are not available, return
207        ``None`` instead.
208        """
209        enabled = self._get_i2p_config("enabled", True, boolean=True)
210        if not enabled:
211            return None
212        if not self._i2p:
213            return None
214
215        sam_port = self._get_i2p_config("sam.port", None)
216        launch = self._get_i2p_config("launch", False, boolean=True)
217        configdir = self._get_i2p_config("i2p.configdir", None)
218        keyfile = self._get_i2p_config("dest.private_key_file", None)
219
220        if sam_port:
221            if launch:
222                raise ValueError("tahoe.cfg [i2p] must not set both "
223                                 "sam.port and launch")
224            ep = clientFromString(self._reactor, sam_port)
225            return self._i2p.sam_endpoint(ep, keyfile=keyfile)
226
227        if launch:
228            executable = self._get_i2p_config("i2p.executable", None)
229            return self._i2p.launch(i2p_configdir=configdir, i2p_binary=executable)
230
231        if configdir:
232            return self._i2p.local_i2p(configdir)
233
234        return self._i2p.default(self._reactor, keyfile=keyfile)
235
236    # Backwards compatibility alias
237    get_i2p_handler = get_client_endpoint
238
239    def check_dest_config(self):
240        if self._get_i2p_config("dest", False, boolean=True):
241            if not self._txi2p:
242                raise ValueError("Cannot create I2P Destination without txi2p. "
243                                 "Please 'pip install tahoe-lafs[i2p]' to fix.")
244
245            # to start an I2P server, we either need an I2P SAM port, or
246            # we need to launch I2P
247            sam_port = self._get_i2p_config("sam.port", None)
248            launch = self._get_i2p_config("launch", False, boolean=True)
249            configdir = self._get_i2p_config("i2p.configdir", None)
250            if not sam_port and not launch and not configdir:
251                raise ValueError("[i2p] dest = true, but we have neither "
252                                 "sam.port= nor launch=true nor configdir=")
253            if sam_port and launch:
254                raise ValueError("tahoe.cfg [i2p] must not set both "
255                                 "sam.port and launch")
256            if launch:
257                raise NotImplementedError("[i2p] launch is under development.")
258            # check that all the expected Destination-specific keys are present
259            def require(name):
260                if not self._get_i2p_config("dest.%s" % name, None):
261                    raise ValueError("[i2p] dest = true,"
262                                     " but dest.%s= is missing" % name)
263            require("port")
264            require("private_key_file")
265
266    def startService(self):
267        service.MultiService.startService(self)
268        # if we need to start I2P, now is the time
269        # TODO: implement i2p launching
270
271    @inlineCallbacks
272    def stopService(self):
273        # TODO: can we also stop i2p?
274        yield service.MultiService.stopService(self)
Note: See TracBrowser for help on using the repository browser.