💾 Archived View for mozz.us › jetforce › jetforce › server.py captured on 2020-09-24 at 00:55:03.

View Raw

More Information

➡️ Next capture (2022-07-16)

-=-=-=-=-=-=-

from __future__ import annotations

import socket
import sys
import typing

from twisted.internet import reactor
from twisted.internet.base import ReactorBase
from twisted.internet.endpoints import SSL4ServerEndpoint
from twisted.internet.protocol import Factory
from twisted.internet.tcp import Port

from .__version__ import __version__
from .protocol import GeminiProtocol
from .tls import GeminiCertificateOptions, generate_ad_hoc_certificate

if sys.stderr.isatty():
    CYAN = "\033[36m\033[1m"
    RESET = "\033[0m"
else:
    CYAN = ""
    RESET = ""


ABOUT = fr"""
{CYAN}You are now riding on...
_________    _____________
______  /______  /___  __/_______________________
___ _  /_  _ \  __/_  /_ _  __ \_  ___/  ___/  _ \
/ /_/ / /  __/ /_ _  __/ / /_/ /  /   / /__ /  __/
\____/  \___/\__/ /_/    \____//_/    \___/ \___/{RESET}

An Experimental Gemini Server, v{__version__}
https://github.com/michael-lazar/jetforce
"""


class GeminiServer(Factory):
    """
    Wrapper around twisted's TCP server that handles most of the setup and
    plumbing for you.
    """

    protocol_class = GeminiProtocol

    # The TLS twisted interface class is confusingly named SSL4, even though it
    # will accept either IPv4 & IPv6 interfaces.
    endpoint_class = SSL4ServerEndpoint

    def __init__(
        self,
        app: typing.Callable,
        reactor: ReactorBase = reactor,
        host: str = "127.0.0.1",
        port: int = 1965,
        hostname: str = "localhost",
        certfile: typing.Optional[str] = None,
        keyfile: typing.Optional[str] = None,
        cafile: typing.Optional[str] = None,
        capath: typing.Optional[str] = None,
    ):
        if certfile is None:
            self.log_message("Generating ad-hoc certificate files...")
            certfile, keyfile = generate_ad_hoc_certificate(hostname)

        self.app = app
        self.reactor = reactor
        self.host = host
        self.port = port
        self.hostname = hostname
        self.certfile = certfile
        self.keyfile = keyfile
        self.cafile = cafile
        self.capath = capath

    def log_access(self, message: str) -> None:
        """
        Log standard "access log"-type information.
        """
        print(message, file=sys.stdout)

    def log_message(self, message: str) -> None:
        """
        Log special messages like startup info or a traceback error.
        """
        print(message, file=sys.stderr)

    def on_bind_interface(self, port: Port) -> None:
        """
        Log when the server binds to an interface.
        """
        sock_ip, sock_port, *_ = port.socket.getsockname()
        if port.addressFamily == socket.AF_INET:
            self.log_message(f"Listening on {sock_ip}:{sock_port}")
        else:
            self.log_message(f"Listening on [{sock_ip}]:{sock_port}")

    def buildProtocol(self, addr) -> GeminiProtocol:
        """
        This method is invoked by twisted once for every incoming connection.

        It builds the instance of the protocol class, which is what actually
        implements the Gemini protocol.
        """
        return GeminiProtocol(self, self.app)

    def run(self) -> None:
        """
        This is the main server loop.
        """
        self.log_message(ABOUT)
        self.log_message(f"Server hostname is {self.hostname}")
        self.log_message(f"TLS Certificate File: {self.certfile}")
        self.log_message(f"TLS Private Key File: {self.keyfile}")

        certificate_options = GeminiCertificateOptions(
            certfile=self.certfile,
            keyfile=self.keyfile,
            cafile=self.cafile,
            capath=self.capath,
        )

        interfaces = [self.host] if self.host else ["0.0.0.0", "::"]
        for interface in interfaces:
            endpoint = self.endpoint_class(
                reactor=self.reactor,
                port=self.port,
                sslContextFactory=certificate_options,
                interface=interface,
            )
            endpoint.listen(self).addCallback(self.on_bind_interface)

        self.reactor.run()