Services

Crate hopf-core defines the process seams; protocol crates plug in by implementing ProtocolHandler and/or registering listeners through a Service-shaped helper (FtpService, SmtpService, HTTP endpoint factories).

Layering

Application Service / factory
        │
        ▼
HandlerFactory  →  Box<dyn ProtocolHandler>   (per connection / stream)
        │
        ▼
Endpoint  (TCP Connection | QuicStreamEndpoint | …)
        │
        ▼
Worker reactor (affinity for life)

HTTP inserts an extra Stream layer: the ProtocolHandler is H1Endpoint / CleartextHttpEndpoint / …, which drives ServerHandler. FTP/SMTP implement ProtocolHandler directly on the control connection.

ProtocolHandler

Invoked on the owning reactor thread — must not block. Panics are caught at the connection boundary (log + close).

Method When
connected Socket registered on a worker (not on the accept thread)
receive Plaintext bytes; advance the cursor (*data = &data[n..]); suffix preserved
disconnected Peer closed / endpoint finished closing (clean FIN / local shutdown)
security_established TLS/QUIC active (ALPN / cipher in SecurityInfo); default no-op
error Unrecoverable I/O or protocol error. On QUIC, also used for abnormal teardown (peer CONNECTION_CLOSE, idle timeout, STOP_SENDING) with typed sources such as hopf_quic::QuicConnectionCloseError — not in addition to disconnected
use hopf_core::{Endpoint, ProtocolHandler};
use std::io;

struct Echo;

impl ProtocolHandler for Echo {
    fn connected(&mut self, _endpoint: &mut dyn Endpoint) {}

    fn receive(&mut self, endpoint: &mut dyn Endpoint, data: &mut &[u8]) {
        endpoint.send(data);
        *data = &[]; // consume
    }

    fn disconnected(&mut self, _endpoint: &mut dyn Endpoint) {}

    fn error(&mut self, _endpoint: &mut dyn Endpoint, _err: &io::Error) {}
}

Register with TcpListenerConfig::new(addr, || Box::new(Echo)). NopHandler is the no-op default for tests / unused uni streams.

Service trait

pub trait Service: Send {
    fn start(&mut self, runtime: &Runtime) -> std::io::Result<()>;
    fn stop(&mut self);
    fn tcp_listeners(&self) -> &[TcpListenerConfig] { &[] }
}
Method Role
start Initialise app resources; register bindings via Runtime APIs (or Composition)
stop Tear down listeners / resources
tcp_listeners Optional static set — transitional; prefer Runtime registration

Runtime::start_service(&mut service) calls start. Composition owns the listen/dial script for declarative apps — a frozen tcp_listeners slice is not the preferred model.

Runtime::add_tcp_listener

let (bound, id) = rt.add_tcp_listener(config)?;
// later
rt.remove_binding(id);

Returns local SocketAddr (useful with port 0) and opaque BindingId.

How protocol services plug in

Crate Registration style Handler SPI
hopf-http HandlerFactory → CleartextHttpEndpoint / AlpnHttpEndpoint / H1Endpoint::server / listen_h3 ServerHandlerFactory
hopf-ftp FtpService::start(Arc<Runtime>) builds TcpListenerConfig FtpConnectionHandlerFactory
hopf-smtp SmtpService::start (or SimpleRelayService / LocalDeliveryService) SmtpHandlerFactory
hopf-webdav / websocket / grpc Supply a ServerHandlerFactory to an HTTP endpoint Stream-level factories
hopf-dns Forwarder / resolver on reactor UDP datagram / connector helpers

Common pattern: protocol crate owns config + factory; start closes over Arc<Runtime> when the protocol needs secondary listeners (FTP PASV).

HTTP factories

use std::sync::Arc;
use hopf_core::{ProtocolHandler, Runtime, TcpListenerConfig};
use hopf_http::{CleartextHttpEndpoint, HttpLimits, ServerHandlerFactory};

fn listen_http(rt: &Runtime, app: Arc<dyn ServerHandlerFactory>) -> std::io::Result<()> {
    let limits = HttpLimits::default();
    rt.add_tcp_listener(TcpListenerConfig::new(addr, move || {
        Box::new(CleartextHttpEndpoint::new(Arc::clone(&app), limits))
            as Box<dyn ProtocolHandler>
    }))?;
    Ok(())
}

Higher-level HTTP apps (WebDAV, WebSocket, gRPC) implement ServerHandlerFactory and reuse the same listen wiring. Auth wrappers (BasicAuthFactory, …) also implement ServerHandlerFactory. See http/server.md.

FTP / SMTP factories

FTP

use hopf_auth::PasswordTrustPolicy;
use hopf_ftp::{FtpConfig, FtpService};
use hopf_core::Runtime;
use std::sync::Arc;

let policy = PasswordTrustPolicy::new().with_user("alice", "s3cret").shared();
let config = FtpConfig::new("127.0.0.1:2121".parse()?, "/tmp/ftp", policy);
let service = FtpService::new(config);
// or FtpService::with_handler_factory(config, custom_factory);
let addr = service.start(Arc::new(/* Runtime */))?;

FtpService::control_listener builds the TcpListenerConfig (TLS via with_tls / implicit_ftps on FtpConfig). PASV data listeners are added later through the held Arc<Runtime>.

SMTP

use hopf_smtp::{SmtpConfig, SmtpService};
use std::sync::Arc;

let config = SmtpConfig::new("127.0.0.1:2525".parse()?, "mail.example")
    .auth_required(false);
let service = SmtpService::new(config);
// or with_handler_factory / AcceptAllSmtpHandlerFactory
let addr = service.start(Arc::clone(&runtime))?;

Relay deployments use SimpleRelayService; local MX uses LocalDeliveryService (both wrap SmtpService). See ftp.md and smtp.md for full knob tables.

Examples

Example Service pattern
examples/echo Raw ProtocolHandler
examples/http-hello HTTP ServerHandlerFactory + cleartext/ALPN endpoint
examples/ftp FtpService::start
examples/smtp SmtpService / relay
examples/webdav HTTP factory from hopf-webdav

Limitations

See also