tahoe-lafs/src/allmydata/protocol_switch.py

116 lines
3.8 KiB
Python
Raw Normal View History

"""
Support for listening with both HTTP and Foolscap on the same port.
"""
from enum import Enum
from typing import Optional
from twisted.internet.protocol import Protocol
from twisted.python.failure import Failure
from twisted.internet.ssl import CertificateOptions
from twisted.web.server import Site
from twisted.protocols.tls import TLSMemoryBIOFactory
from foolscap.negotiate import Negotiation
from .storage.http_server import HTTPServer
2022-06-22 10:23:23 -04:00
class ProtocolMode(Enum):
"""Listening mode."""
2022-06-22 10:23:23 -04:00
UNDECIDED = 0
FOOLSCAP = 1
HTTP = 2
class PretendToBeNegotiation(type):
"""😱"""
def __instancecheck__(self, instance):
return (instance.__class__ == self) or isinstance(instance, Negotiation)
class FoolscapOrHttp(Protocol, metaclass=PretendToBeNegotiation):
"""
Based on initial query, decide whether we're talking Foolscap or HTTP.
Pretends to be a ``foolscap.negotiate.Negotiation`` instance.
"""
2022-06-22 10:23:23 -04:00
_foolscap: Optional[Negotiation] = None
_protocol_mode: ProtocolMode = ProtocolMode.UNDECIDED
_buffer: bytes = b""
def __init__(self, *args, **kwargs):
self._foolscap = Negotiation(*args, **kwargs)
def __setattr__(self, name, value):
2022-06-22 10:23:23 -04:00
if name in {
"_foolscap",
"_protocol_mode",
"_buffer",
"transport",
"__class__",
"_http",
2022-06-22 10:23:23 -04:00
}:
object.__setattr__(self, name, value)
else:
setattr(self._foolscap, name, value)
def __getattr__(self, name):
return getattr(self._foolscap, name)
def makeConnection(self, transport):
Protocol.makeConnection(self, transport)
self._foolscap.makeConnection(transport)
def initClient(self, *args, **kwargs):
2022-06-22 10:23:23 -04:00
# After creation, a Negotiation instance either has initClient() or
# initServer() called. SInce this is a client, we're never going to do
# HTTP. Relying on __getattr__/__setattr__ doesn't work, for some
# reason, so just mutate ourselves appropriately.
assert not self._buffer
2022-06-22 10:23:23 -04:00
self.__class__ = Negotiation
self.__dict__ = self._foolscap.__dict__
return self.initClient(*args, **kwargs)
def dataReceived(self, data: bytes) -> None:
if self._protocol_mode == ProtocolMode.FOOLSCAP:
return self._foolscap.dataReceived(data)
if self._protocol_mode == ProtocolMode.HTTP:
return self._http.dataReceived(data)
# UNDECIDED mode.
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
# and later data:
if self._buffer.startswith(b"GET /id/"):
# TODO or maybe just self.__class__ here too?
self._protocol_mode = ProtocolMode.FOOLSCAP
buf, self._buffer = self._buffer, b""
return self._foolscap.dataReceived(buf)
else:
self._protocol_mode = ProtocolMode.HTTP
certificate_options = CertificateOptions(
privateKey=self.certificate.privateKey.original,
certificate=self.certificate.original,
)
http_server = HTTPServer(self.storage_server, self.swissnum)
factory = TLSMemoryBIOFactory(
certificate_options, False, Site(http_server.get_resource())
)
protocol = factory.buildProtocol(self.transport.getPeer())
protocol.makeConnection(self.transport)
protocol.dataReceived(self._buffer)
# TODO __getattr__ or maybe change the __class__
self._http = protocol
def connectionLost(self, reason: Failure) -> None:
if self._protocol_mode == ProtocolMode.FOOLSCAP:
return self._foolscap.connectionLost(reason)