tahoe-lafs/src/allmydata/protocol_switch.py

196 lines
7.1 KiB
Python
Raw Normal View History

"""
2022-06-30 14:21:21 -04:00
Support for listening with both HTTPS and Foolscap on the same port.
The goal is to make the transition from Foolscap to HTTPS-based protocols as
simple as possible, with no extra configuration needed. Listening on the same
port means a user upgrading Tahoe-LAFS will automatically get HTTPS working
with no additional changes.
2022-06-30 14:26:36 -04:00
Use ``support_foolscap_and_https()`` to create a new subclass for a ``Tub``
instance, and then ``add_storage_server()`` on the resulting class to add the
relevant information for a storage server once it becomes available later in
the configuration process.
2022-06-30 14:21:21 -04:00
"""
2022-07-29 09:57:18 -04:00
from __future__ import annotations
from twisted.internet.protocol import Protocol
2022-06-23 12:47:33 -04:00
from twisted.internet.interfaces import IDelayedCall
from twisted.internet.ssl import CertificateOptions
from twisted.web.server import Site
from twisted.protocols.tls import TLSMemoryBIOFactory
2022-06-23 12:47:33 -04:00
from twisted.internet import reactor
2022-07-29 09:57:18 -04:00
from hyperlink import DecodedURL
from foolscap.negotiate import Negotiation
2022-06-30 14:26:36 -04:00
from foolscap.api import Tub
2022-07-29 09:57:18 -04:00
from .storage.http_server import HTTPServer, build_nurl
2022-06-23 07:59:43 -04:00
from .storage.server import StorageServer
2022-06-22 10:23:23 -04:00
2022-06-30 14:21:21 -04:00
class _PretendToBeNegotiation(type):
"""
Metaclass that allows ``_FoolscapOrHttps`` to pretend to be a ``Negotiation``
instance, since Foolscap has some ``assert isinstance(protocol,
Negotiation`` checks.
"""
def __instancecheck__(self, instance):
return (instance.__class__ == self) or isinstance(instance, Negotiation)
2022-06-30 14:21:21 -04:00
class _FoolscapOrHttps(Protocol, metaclass=_PretendToBeNegotiation):
"""
Based on initial query, decide whether we're talking Foolscap or HTTP.
2022-06-30 14:21:21 -04:00
Additionally, pretends to be a ``foolscap.negotiate.Negotiation`` instance,
since these are created by Foolscap's ``Tub``, by setting this to be the
tub's ``negotiationClass``.
2022-07-29 10:12:24 -04:00
Do not instantiate directly, use ``support_foolscap_and_https(tub)``
instead. The way this class works is that a new subclass is created for a
specific ``Tub`` instance.
"""
2022-06-22 10:23:23 -04:00
2022-07-29 10:12:24 -04:00
# These are class attributes; they will be set by
# support_foolscap_and_https() and add_storage_server().
2022-07-29 10:12:24 -04:00
# The Twisted HTTPS protocol factory wrapping the storage server HTTP API:
https_factory: TLSMemoryBIOFactory
# The tub that created us:
tub: Tub
2022-06-23 07:59:43 -04:00
2022-07-29 10:12:24 -04:00
# This is an instance attribute; it will be set in connectionMade().
2022-06-23 12:47:33 -04:00
_timeout: IDelayedCall
2022-06-30 14:26:36 -04:00
@classmethod
2022-07-29 09:57:18 -04:00
def add_storage_server(
cls, storage_server: StorageServer, swissnum: bytes
) -> set[DecodedURL]:
2022-06-30 14:26:36 -04:00
"""
2022-07-29 10:12:24 -04:00
Update a ``_FoolscapOrHttps`` subclass for a specific ``Tub`` instance
with the class attributes it requires for a specific storage server.
2022-07-29 09:57:18 -04:00
Returns the resulting NURLs.
2022-06-30 14:26:36 -04:00
"""
2022-07-29 10:12:24 -04:00
# We need to be a subclass:
assert cls != _FoolscapOrHttps
# The tub instance must already be set:
assert hasattr(cls, "tub")
assert isinstance(cls.tub, Tub)
# Tub.myCertificate is a twisted.internet.ssl.PrivateCertificate
# instance.
certificate_options = CertificateOptions(
privateKey=cls.tub.myCertificate.privateKey.original,
certificate=cls.tub.myCertificate.original,
)
2022-07-29 10:12:24 -04:00
http_storage_server = HTTPServer(storage_server, swissnum)
cls.https_factory = TLSMemoryBIOFactory(
certificate_options,
False,
2022-07-29 10:12:24 -04:00
Site(http_storage_server.get_resource()),
)
2022-06-30 14:26:36 -04:00
2022-07-29 09:57:18 -04:00
storage_nurls = set()
for location_hint in cls.tub.locationHints:
if location_hint.startswith("tcp:"):
_, hostname, port = location_hint.split(":")
port = int(port)
storage_nurls.add(
build_nurl(
hostname,
port,
str(swissnum, "ascii"),
cls.tub.myCertificate.original.to_cryptography(),
)
)
2022-07-29 10:15:23 -04:00
# TODO this is probably where we'll have to support Tor and I2P?
# See https://tahoe-lafs.org/trac/tahoe-lafs/ticket/3888#comment:9
# for discussion (there will be separate tickets added for those at
# some point.)
2022-07-29 09:57:18 -04:00
return storage_nurls
def __init__(self, *args, **kwargs):
2022-06-23 12:41:01 -04:00
self._foolscap: Negotiation = Negotiation(*args, **kwargs)
self._buffer: bytes = b""
def __setattr__(self, name, value):
if name in {"_foolscap", "_buffer", "transport", "__class__", "_timeout"}:
object.__setattr__(self, name, value)
else:
setattr(self._foolscap, name, value)
def __getattr__(self, name):
return getattr(self._foolscap, name)
2022-06-23 12:43:46 -04:00
def _convert_to_negotiation(self):
"""
2022-06-23 12:44:17 -04:00
Convert self to a ``Negotiation`` instance.
2022-06-23 12:43:46 -04:00
"""
self.__class__ = Negotiation # type: ignore
self.__dict__ = self._foolscap.__dict__
def initClient(self, *args, **kwargs):
2022-06-22 10:23:23 -04:00
# After creation, a Negotiation instance either has initClient() or
2022-06-23 12:43:46 -04:00
# initServer() called. Since this is a client, we're never going to do
# HTTP, so we can immediately become a Negotiation instance.
assert not self._buffer
self._convert_to_negotiation()
2022-06-22 10:23:23 -04:00
return self.initClient(*args, **kwargs)
2022-06-23 12:47:33 -04:00
def connectionMade(self):
self._timeout = reactor.callLater(30, self.transport.abortConnection)
def dataReceived(self, data: bytes) -> None:
2022-06-23 12:41:01 -04:00
"""Handle incoming data.
2022-06-23 12:41:01 -04:00
Once we've decided which protocol we are, update self.__class__, at
which point all methods will be called on the new class.
"""
self._buffer += data
if len(self._buffer) < 8:
return
2022-06-22 10:23:23 -04:00
# Check if it looks like a Foolscap request. If so, it can handle this
2022-06-23 12:47:33 -04:00
# and later data, otherwise assume HTTPS.
self._timeout.cancel()
if self._buffer.startswith(b"GET /id/"):
2022-06-23 12:51:07 -04:00
# We're a Foolscap Negotiation server protocol instance:
2022-06-23 12:43:46 -04:00
transport = self.transport
buf = self._buffer
self._convert_to_negotiation()
self.makeConnection(transport)
self.dataReceived(buf)
return
else:
2022-06-23 12:51:07 -04:00
# We're a HTTPS protocol instance, serving the storage protocol:
2022-06-23 12:41:47 -04:00
assert self.transport is not None
protocol = self.https_factory.buildProtocol(self.transport.getPeer())
protocol.makeConnection(self.transport)
protocol.dataReceived(self._buffer)
# Update the factory so it knows we're transforming to a new
# protocol object (we'll do that next)
value = self.https_factory.protocols.pop(protocol)
self.https_factory.protocols[self] = value
# Transform self into the TLS protocol 🪄
2022-06-23 12:41:01 -04:00
self.__class__ = protocol.__class__
self.__dict__ = protocol.__dict__
2022-06-23 07:59:43 -04:00
2022-06-30 14:26:36 -04:00
def support_foolscap_and_https(tub: Tub):
2022-06-30 14:21:21 -04:00
"""
Create a new Foolscap-or-HTTPS protocol class for a specific ``Tub``
instance.
"""
the_tub = tub
2022-06-30 14:21:21 -04:00
class FoolscapOrHttpForTub(_FoolscapOrHttps):
tub = the_tub
2022-06-23 07:59:43 -04:00
tub.negotiationClass = FoolscapOrHttpForTub # type: ignore