tup

A TCP and TLS server. Describe it with new and the builder functions, then run it with start or hand it to a supervisor with supervised.

Types

How the socket feeds packets to the handler. Each mode is the matching active socket option in Erlang.

pub type ActiveState {
  Once
  Count(n: Int)
  Active
}

Constructors

  • Once

    One packet at a time. The connection arms the socket again after each handler run.

  • Count(n: Int)

    n packets at a time. The connection arms the socket again after the batch.

  • Active

    Every packet as soon as it arrives with no pause.

Where the server listens.

pub type Address {
  Tcp(interface: String, port: Int)
  Unix(path: String)
}

Constructors

  • Tcp(interface: String, port: Int)

    A TCP socket bound to interface; an IPv4 or IPv6 address or "localhost". With port 0 the system picks a free port.

  • Unix(path: String)

    A Unix domain socket at path. The path must be 1 to 107 bytes long with no NUL in it.

A server being described. Start from new, adjust it with the builder functions and hand it to start or supervised.

pub opaque type Builder(user_state, user_message)

Where the server’s certificate chain and private key come from. Every source must hold at least one certificate.

pub type Certificate {
  Disk(cert: String, key: String)
  EncryptedDisk(cert: String, key: String, password: String)
  Pem(cert: BitArray, key: BitArray)
  EncryptedPem(cert: BitArray, key: BitArray, password: String)
  Der(chain: List(BitArray), key: TlsPrivateKey)
}

Constructors

  • Disk(cert: String, key: String)

    PEM files on disk.

  • EncryptedDisk(cert: String, key: String, password: String)

    PEM files on disk, the key encrypted with password.

  • Pem(cert: BitArray, key: BitArray)

    PEM encoded bytes.

  • EncryptedPem(cert: BitArray, key: BitArray, password: String)

    PEM encoded bytes, the key encrypted with password.

  • Der(chain: List(BitArray), key: TlsPrivateKey)

    A DER encoded chain with the server’s own certificate first, and its key.

Whether clients are asked for a certificate and what happens to a client that sends none.

pub type ClientCertificates {
  Requested(trusting: TrustStore)
  Required(trusting: TrustStore)
}

Constructors

  • Requested(trusting: TrustStore)

    Ask for a certificate and check it against trusting when one comes. A client that sends none is let through.

  • Required(trusting: TrustStore)

    Ask for a certificate and refuse the handshake when none comes or it fails the check against trusting.

An accepted client connection handed to every callback. Write to it with send and look up either end of it with peer and local.

pub opaque type Connection

One end of a connection.

pub type Endpoint {
  TcpEndpoint(ip_address: IpAddress, port: Int)
  UnixEndpoint(path: String)
}

Constructors

  • TcpEndpoint(ip_address: IpAddress, port: Int)

    An address and a port on a TCP socket.

  • UnixEndpoint(path: String)

    The path of a Unix domain socket.

An IPv4 or IPv6 address.

pub type IpAddress {
  Ipv4(Int, Int, Int, Int)
  Ipv6(Int, Int, Int, Int, Int, Int, Int, Int)
}

Constructors

  • Ipv4(Int, Int, Int, Int)

    Four octets.

  • Ipv6(Int, Int, Int, Int, Int, Int, Int, Int)

    Eight 16 bit groups.

A message delivered to the handler.

pub type Message(user_message) {
  Incoming(BitArray)
  User(user_message)
}

Constructors

  • Incoming(BitArray)

    Bytes read from the socket.

  • User(user_message)

    A message picked up by the connection’s selector.

What a connection does once the handler has run. Build one with continue, stop or stop_abnormal.

pub opaque type Next(user_state, user_message)

The type a server’s process.Name is tagged with.

pub type Server

How session tickets are issued for TLS session resumption.

pub type TicketMode {
  NoTickets
  Stateful
  Stateless
}

Constructors

  • NoTickets

    No tickets are issued.

  • Stateful

    Tickets point at session state kept on the server.

  • Stateless

    Tickets carry the session state themselves, encrypted.

TLS settings for the server. Create them with tls and refine them with verifying_clients, with_alpn and session_tickets.

pub opaque type Tls

A DER encoded private key tagged with its format.

pub type TlsPrivateKey {
  RsaPrivateKey(BitArray)
  DsaPrivateKey(BitArray)
  EcPrivateKey(BitArray)
  PrivateKeyInfo(BitArray)
}

Constructors

  • RsaPrivateKey(BitArray)

    A PKCS #1 RSAPrivateKey.

  • DsaPrivateKey(BitArray)

    A DSAPrivateKey.

  • EcPrivateKey(BitArray)

    A SEC 1 ECPrivateKey.

  • PrivateKeyInfo(BitArray)

    A PKCS #8 PrivateKeyInfo.

The authorities client certificates are checked against. Must hold at least one certificate.

pub type TrustStore {
  SystemTrustStore
  TrustDisk(path: String)
  TrustPem(bytes: BitArray)
  TrustDer(certificates: List(BitArray))
}

Constructors

  • SystemTrustStore

    The authorities installed on the operating system.

  • TrustDisk(path: String)

    A PEM file on disk.

  • TrustPem(bytes: BitArray)

    PEM encoded bytes.

  • TrustDer(certificates: List(BitArray))

    DER encoded certificates.

Values

pub fn active_state(
  builder: Builder(user_state, user_message),
  active_state: ActiveState,
) -> Builder(user_state, user_message)

Sets the active state every connection starts in. Defaults to Once.

builder
|> tup.active_state(Count(10))
pub fn buffer_size(
  builder: Builder(user_state, user_message),
  bytes: Int,
) -> Builder(user_state, user_message)

The most bytes one read hands to your handler. Erlang’s default is about 9 KB.

A larger buffer means fewer and bigger Incoming messages which pays off when clients send a lot of data. It costs the memory on every connection so provide careful values.

builder
|> tup.buffer_size(65_536)
pub fn connection_count(
  name: process.Name(Server),
) -> Result(Int, Nil)

How many connections are open right now. Returns Error(Nil) when no server is running under name.

tup.connection_count(name)
// -> Ok(12)
pub fn continue(
  state: user_state,
) -> Next(user_state, user_message)

Keeps the connection open and carries state into the next message.

fn handler(_connection, count, message) {
  case message {
    Incoming(_data) -> tup.continue(count + 1)
    User(_message) -> tup.continue(count)
  }
}
pub fn describe_socket_error(error: socket.SocketError) -> String

Describes a socket error as text.

case tup.send(connection, data) {
  Ok(Nil) -> Nil
  Error(error) -> io.println(describe_socket_error(error))
}
pub fn endpoint_to_string(endpoint: Endpoint) -> String

Formats an endpoint as text: the address and port of a TCP endpoint or the path of a Unix one.

tup.endpoint_to_string(TcpEndpoint(Ipv4(127, 0, 0, 1), 3000))
// -> "127.0.0.1:3000"
pub fn force_ipv6(
  builder: Builder(user_state, user_message),
) -> Builder(user_state, user_message)

Listen only on IPv6. IPv4 clients are refused.

Starting fails when the address is an IPv4 or a unix socket path.

pub fn infinite_shutdown_timeout(
  builder: Builder(user_state, user_message),
) -> Builder(user_state, user_message)

Wait for every connection to finish when the server shuts down, however long that takes. A connection that never finishes keeps the shutdown from ever completing, so only use this when every handler is sure to return.

builder
|> tup.infinite_shutdown_timeout
pub fn ip_address_to_string(address: IpAddress) -> String

Formats an address as text.

tup.ip_address_to_string(Ipv4(127, 0, 0, 1))
// -> "127.0.0.1"
pub fn listen_endpoint(
  name: process.Name(Server),
  within timeout: Int,
) -> Result(Endpoint, Nil)

The endpoint the named server listens on, with the port the system picked when the server was started on port 0. Waits up to timeout milliseconds for the listener to answer.

Returns Error(Nil) when no server runs under name, when it is suspended or when the listener does not answer in time.

let name = process.new_name("tup")
let assert Ok(_started) =
  builder
  |> tup.listening(on: Tcp(interface: "127.0.0.1", port: 0))
  |> tup.named(name)
  |> tup.start

tup.listen_endpoint(name, within: 1000)
// -> Ok(TcpEndpoint(Ipv4(127, 0, 0, 1), 54321))
pub fn listening(
  builder: Builder(user_state, user_message),
  on address: Address,
) -> Builder(user_state, user_message)

Sets where the server listens. Defaults to TCP on 127.0.0.1 port 3000.

builder
|> tup.listening(on: Tcp(interface: "0.0.0.0", port: 8080))
pub fn local(connection: Connection) -> Endpoint

The server’s end of a connection.

tup.local(connection)
// -> TcpEndpoint(Ipv4(127, 0, 0, 1), 3000)
pub fn named(
  builder: Builder(user_state, user_message),
  name: process.Name(Server),
) -> Builder(user_state, user_message)

Registers the started server under name. listen_endpoint, suspend, resume and connection_count look the server up by it.

let name = process.new_name("tup")

builder
|> tup.named(name)
pub fn new(
  on_init on_init: fn(Connection, process.Selector(user_message)) -> #(
    user_state,
    process.Selector(user_message),
  ),
  handler handler: fn(
    Connection,
    user_state,
    Message(user_message),
  ) -> Next(user_state, user_message),
  on_close on_close: fn(user_state) -> Nil,
) -> Builder(user_state, user_message)

Creates a builder from the callbacks every connection runs.

on_init runs once the connection is accepted. It receives the connection and a selector and returns the initial state together with the selector user messages are received on.

handler runs for every Message and returns what the connection does next.

on_close runs once the connection has ended.

By default the server listens on 127.0.0.1 port 3000 over plain TCP, starts every connection in the Once active state, runs 20 acceptors and gives connections 15 seconds to finish on shutdown. Each of these can be changed with the builder function.

tup.new(
  on_init: fn(_connection, selector) { #(0, selector) },
  handler: fn(connection, count, message) {
    case message {
      Incoming(data) -> {
        let _ = tup.send(connection, bytes_tree.from_bit_array(data))
        tup.continue(count + 1)
      }
      User(_message) -> tup.continue(count)
    }
  },
  on_close: fn(_count) { Nil },
)
pub fn on_shutdown(
  builder: Builder(user_state, user_message),
  on_shutdown: fn(Connection, user_state) -> Nil,
) -> Builder(user_state, user_message)

Sets a callback that runs in every open connection when the server shuts down, ahead of on_close and within the shutdown timeout. Nothing runs by default.

builder
|> tup.on_shutdown(fn(connection, _state) {
  let _ = tup.send(connection, bytes_tree.from_string("bye\n"))
  Nil
})
pub fn peer(connection: Connection) -> Endpoint

The client’s end of a connection.

tup.peer(connection)
// -> TcpEndpoint(Ipv4(192, 168, 1, 20), 51234)
pub fn pool_size(
  builder: Builder(user_state, user_message),
  pool_size: Int,
) -> Builder(user_state, user_message)

How many acceptors wait for connections at once. Defaults to 20. Must be greater than zero.

builder
|> tup.pool_size(100)
pub fn resume(name: process.Name(Server)) -> Result(Nil, Nil)

Open the listen socket again and start accepting connections after suspend. With port 0 the system picks a new port.

Returns Error(Nil) when no server is running under name or when the socket can’t be opened again, for example because another program took the port while the server was suspended.

tup.resume(name)
// -> Ok(Nil)
pub fn send(
  connection: Connection,
  data: bytes_tree.BytesTree,
) -> Result(Nil, socket.SocketError)

Sends data to the client. Returns the socket error when the write fails.

tup.send(connection, bytes_tree.from_string("hello\n"))
// -> Ok(Nil)
pub fn session_tickets(tls: Tls, mode: TicketMode) -> Tls

Sets how session tickets are issued. Defaults to Stateless.

tls(certificate)
|> tup.session_tickets(NoTickets)
pub fn shutdown_timeout(
  builder: Builder(user_state, user_message),
  milliseconds: Int,
) -> Builder(user_state, user_message)

How long each connection gets to finish when the server shuts down before it is killed. A connection still running at the deadline is killed without on_shutdown or on_close completing. Defaults to 15 seconds. Must not be negative.

builder
|> tup.shutdown_timeout(5000)
pub fn socket(
  connection: Connection,
) -> #(socket.Transport, socket.Socket)

The transport and the socket behind a connection.

let #(transport, socket) = tup.socket(connection)
socket.send(transport, socket, data)
pub fn start(
  builder: Builder(user_state, user_message),
) -> Result(
  actor.Started(relay_supervisor.Supervisor),
  actor.StartError,
)

Starts the server linked to the calling process.

Fails with actor.InitFailed when a setting is out of range, a certificate, key or trust store cannot be read or holds nothing, an ALPN protocol is malformed, or name is already registered. A listen socket that cannot be opened comes back as an Error as well, instead of crashing the caller.

let assert Ok(_started) = tup.start(builder)
process.sleep_forever()
pub fn stop() -> Next(user_state, user_message)

Closes the connection.

case message {
  Incoming(<<"quit\n">>) -> tup.stop()
  _message -> tup.continue(state)
}
pub fn stop_abnormal(
  reason: String,
) -> Next(user_state, user_message)

Closes the connection and exits abnormally with reason.

tup.stop_abnormal("client sent an invalid frame")
pub fn supervised(
  builder: Builder(user_state, user_message),
) -> supervision.ChildSpecification(relay_supervisor.Supervisor)

A child specification that runs the server under a supervisor.

static_supervisor.new(static_supervisor.OneForOne)
|> static_supervisor.add(tup.supervised(builder))
|> static_supervisor.start
pub fn suspend(name: process.Name(Server)) -> Result(Nil, Nil)

Stop accepting connections and close the listen socket. Connections that are already open keep running.

While suspended the new clients are refused and listen_endpoint returns Error(Nil).

Returns Error(Nil) when no server is running under the name.

let assert Ok(Nil) = tup.suspend(name)

tup.listen_endpoint(name, within: 1000)
// -> Error(Nil)
pub fn tls(certificate: Certificate) -> Tls

Creates TLS settings around certificate. By default the clients are not asked for a certificate, no ALPN protocols are offered and session tickets are stateless. Every TLS server speaks TLS 1.2 and 1.3, applies its own cipher order and refuses client renegotiation.

tup.tls(tup.Disk(cert: "priv/cert.pem", key: "priv/key.pem"))
pub fn unmap_ipv4(address: IpAddress) -> IpAddress

Extracts the IPv4 address inside an IPv4 mapped IPv6 address.

tup.unmap_ipv4(Ipv6(0, 0, 0, 0, 0, 0xffff, 0x7f00, 0x0001))
// -> Ipv4(127, 0, 0, 1)
pub fn verifying_clients(tls: Tls, on: ClientCertificates) -> Tls

Asks clients for a certificate and verifies it against a trust store.

tup.tls(tup.Disk(cert: "priv/cert.pem", key: "priv/key.pem"))
|> tup.verifying_clients(on: tup.Required(trusting: tup.TrustDisk("priv/ca.pem")))
pub fn with_active_state(
  next: Next(user_state, user_message),
  active_state: ActiveState,
) -> Next(user_state, user_message)

Sets the socket’s active state from now on. Has no effect on a stop.

tup.continue(state)
|> tup.with_active_state(Count(10))
pub fn with_alpn(tls: Tls, protocols: List(String)) -> Tls

Sets the ALPN protocols the server offers, most preferred first. Each must be 1 to 255 bytes long. Duplicates are dropped.

tls(certificate)
|> with_alpn(["h2", "http/1.1"])
pub fn with_selector(
  next: Next(user_state, user_message),
  selector: process.Selector(user_message),
) -> Next(user_state, user_message)

Sets the selector the connection receives user messages on from now on. Has no effect on a stop.

let selector =
  process.new_selector()
  |> process.select(subject)

tup.continue(state)
|> tup.with_selector(selector)
pub fn with_tls(
  builder: Builder(user_state, user_message),
  tls: Tls,
) -> Builder(user_state, user_message)

Wraps every connection in TLS with the given settings.

builder
|> tup.with_tls(tup.tls(tup.Disk(cert: "priv/cert.pem", key: "priv/key.pem")))
Search Document