source: trunk/src/allmydata/protocol_switch.py

Last change on this file was bc78dbc2, checked in by Itamar Turner-Trauring <itamar@…>, at 2023-07-05T14:21:40Z

Point to correct ticket

  • Property mode set to 100644
File size: 8.1 KB
Line 
1"""
2Support for listening with both HTTPS and Foolscap on the same port.
3
4The goal is to make the transition from Foolscap to HTTPS-based protocols as
5simple as possible, with no extra configuration needed.  Listening on the same
6port means a user upgrading Tahoe-LAFS will automatically get HTTPS working
7with no additional changes.
8
9Use ``create_tub_with_https_support()`` creates a new ``Tub`` that has its
10``negotiationClass`` modified to be a new subclass tied to that specific
11``Tub`` instance.  Calling ``tub.negotiationClass.add_storage_server(...)``
12then adds relevant information for a storage server once it becomes available
13later in the configuration process.
14"""
15
16from __future__ import annotations
17
18from itertools import chain
19from typing import cast
20
21from twisted.internet.protocol import Protocol
22from twisted.internet.interfaces import IDelayedCall, IReactorFromThreads
23from twisted.internet.ssl import CertificateOptions
24from twisted.web.server import Site
25from twisted.protocols.tls import TLSMemoryBIOFactory
26from twisted.internet import reactor
27
28from hyperlink import DecodedURL
29from foolscap.negotiate import Negotiation
30from foolscap.api import Tub
31
32from .storage.http_server import HTTPServer, build_nurl
33from .storage.server import StorageServer
34
35
36class _PretendToBeNegotiation(type):
37    """
38    Metaclass that allows ``_FoolscapOrHttps`` to pretend to be a
39    ``Negotiation`` instance, since Foolscap does some checks like
40    ``assert isinstance(protocol, tub.negotiationClass)`` in its internals,
41    and sometimes that ``protocol`` is a ``_FoolscapOrHttps`` instance, but
42    sometimes it's a ``Negotiation`` instance.
43    """
44
45    def __instancecheck__(self, instance):
46        return issubclass(instance.__class__, self) or isinstance(instance, Negotiation)
47
48
49class _FoolscapOrHttps(Protocol, metaclass=_PretendToBeNegotiation):
50    """
51    Based on initial query, decide whether we're talking Foolscap or HTTP.
52
53    Additionally, pretends to be a ``foolscap.negotiate.Negotiation`` instance,
54    since these are created by Foolscap's ``Tub``, by setting this to be the
55    tub's ``negotiationClass``.
56
57    Do not instantiate directly, use ``create_tub_with_https_support(...)``
58    instead.  The way this class works is that a new subclass is created for a
59    specific ``Tub`` instance.
60    """
61
62    # These are class attributes; they will be set by
63    # create_tub_with_https_support() and add_storage_server().
64
65    # The Twisted HTTPS protocol factory wrapping the storage server HTTP API:
66    https_factory: TLSMemoryBIOFactory
67    # The tub that created us:
68    tub: Tub
69
70    @classmethod
71    def add_storage_server(
72        cls, storage_server: StorageServer, swissnum: bytes
73    ) -> set[DecodedURL]:
74        """
75        Update a ``_FoolscapOrHttps`` subclass for a specific ``Tub`` instance
76        with the class attributes it requires for a specific storage server.
77
78        Returns the resulting NURLs.
79        """
80        # We need to be a subclass:
81        assert cls != _FoolscapOrHttps
82        # The tub instance must already be set:
83        assert hasattr(cls, "tub")
84        assert isinstance(cls.tub, Tub)
85
86        # Tub.myCertificate is a twisted.internet.ssl.PrivateCertificate
87        # instance.
88        certificate_options = CertificateOptions(
89            privateKey=cls.tub.myCertificate.privateKey.original,
90            certificate=cls.tub.myCertificate.original,
91        )
92
93        http_storage_server = HTTPServer(cast(IReactorFromThreads, reactor), storage_server, swissnum)
94        cls.https_factory = TLSMemoryBIOFactory(
95            certificate_options,
96            False,
97            Site(http_storage_server.get_resource()),
98        )
99
100        storage_nurls = set()
101        # Individual hints can be in the form
102        # "tcp:host:port,tcp:host:port,tcp:host:port".
103        for location_hint in chain.from_iterable(
104            hints.split(",") for hints in cls.tub.locationHints
105        ):
106            if location_hint.startswith("tcp:") or location_hint.startswith("tor:"):
107                scheme, hostname, port = location_hint.split(":")
108                if scheme == "tcp":
109                    subscheme = None
110                else:
111                    subscheme = "tor"
112                    # If we're listening on Tor, the hostname needs to have an
113                    # .onion TLD.
114                    assert hostname.endswith(".onion")
115                # The I2P scheme is yet not supported by the HTTP client, so we
116                # don't want generate a NURL that won't work. This will be
117                # fixed in https://tahoe-lafs.org/trac/tahoe-lafs/ticket/4037
118                port = int(port)
119                storage_nurls.add(
120                    build_nurl(
121                        hostname,
122                        port,
123                        str(swissnum, "ascii"),
124                        cls.tub.myCertificate.original.to_cryptography(),
125                        subscheme
126                    )
127                )
128
129        return storage_nurls
130
131    def __init__(self, *args, **kwargs):
132        self._foolscap: Negotiation = Negotiation(*args, **kwargs)
133
134    def __setattr__(self, name, value):
135        if name in {"_foolscap", "_buffer", "transport", "__class__", "_timeout"}:
136            object.__setattr__(self, name, value)
137        else:
138            setattr(self._foolscap, name, value)
139
140    def __getattr__(self, name):
141        return getattr(self._foolscap, name)
142
143    def _convert_to_negotiation(self):
144        """
145        Convert self to a ``Negotiation`` instance.
146        """
147        self.__class__ = Negotiation  # type: ignore
148        self.__dict__ = self._foolscap.__dict__
149
150    def initClient(self, *args, **kwargs):
151        # After creation, a Negotiation instance either has initClient() or
152        # initServer() called. Since this is a client, we're never going to do
153        # HTTP, so we can immediately become a Negotiation instance.
154        assert not hasattr(self, "_buffer")
155        self._convert_to_negotiation()
156        return self.initClient(*args, **kwargs)
157
158    def connectionMade(self):
159        self._buffer: bytes = b""
160        self._timeout: IDelayedCall = reactor.callLater(
161            30, self.transport.abortConnection
162        )
163
164    def connectionLost(self, reason):
165        if self._timeout.active():
166            self._timeout.cancel()
167
168    def dataReceived(self, data: bytes) -> None:
169        """Handle incoming data.
170
171        Once we've decided which protocol we are, update self.__class__, at
172        which point all methods will be called on the new class.
173        """
174        self._buffer += data
175        if len(self._buffer) < 8:
176            return
177
178        # Check if it looks like a Foolscap request. If so, it can handle this
179        # and later data, otherwise assume HTTPS.
180        self._timeout.cancel()
181        if self._buffer.startswith(b"GET /id/"):
182            # We're a Foolscap Negotiation server protocol instance:
183            transport = self.transport
184            buf = self._buffer
185            self._convert_to_negotiation()
186            self.makeConnection(transport)
187            self.dataReceived(buf)
188            return
189        else:
190            # We're a HTTPS protocol instance, serving the storage protocol:
191            assert self.transport is not None
192            protocol = self.https_factory.buildProtocol(self.transport.getPeer())
193            protocol.makeConnection(self.transport)
194            protocol.dataReceived(self._buffer)
195
196            # Update the factory so it knows we're transforming to a new
197            # protocol object (we'll do that next)
198            value = self.https_factory.protocols.pop(protocol)
199            self.https_factory.protocols[self] = value
200
201            # Transform self into the TLS protocol 🪄
202            self.__class__ = protocol.__class__
203            self.__dict__ = protocol.__dict__
204
205
206def create_tub_with_https_support(**kwargs) -> Tub:
207    """
208    Create a new Tub that also supports HTTPS.
209
210    This involves creating a new protocol switch class for the specific ``Tub``
211    instance.
212    """
213    the_tub = Tub(**kwargs)
214
215    class FoolscapOrHttpForTub(_FoolscapOrHttps):
216        tub = the_tub
217
218    the_tub.negotiationClass = FoolscapOrHttpForTub  # type: ignore
219    return the_tub
Note: See TracBrowser for help on using the repository browser.