defmodule RNS.GenDestination do # TODO: support timeouts. @moduledoc """ This module is used to create processes listening for packets and link requests on a destination. Packet -> run function Link request -> start another process for link """ # behaviour-related stuff # NOTE: 'child' means the module using RNS.GenDestination. @type child_state() :: any() @type stop_reason() :: any() @type opts() :: [ announce_on_start: boolean(), announce_interval: integer() ] @type state() :: tuple() # The init function, called by start_link @callback init(any()) :: {:ok, child_state()} | {:ok, child_state(), timeout()} | {:stop, stop_reason()} @callback handle_data(RNS.Packet.data(), RNS.Interface.t(), child_state()) :: {:ok, child_state()} | {:ok, child_state(), timeout()} | {:stop, stop_reason()} # TODO: handle link requests, if they are accepted a new process should be created, running some other callbacks. # @callback handle_link_request() # This is so `use RNS.GenDestination` automatically adds the behaviour and childspecs. defmacro __using__(opts) do quote location: :keep, bind_quoted: [opts: opts] do @behaviour RNS.GenDestination def child_spec(init_arg) do %{ id: __MODULE__, start: {__MODULE__, :start_link, [init_arg]} } end defoverridable child_spec: 1 # TODO: implement some functions for this behaviour, to make some functions optional. end end # process-related stuff require Logger require Record Record.defrecord(:state, module: nil, destination: nil, child_state: nil, opts: [] ) @spec start_link(atom(), RNS.Destination.t(), any(), opts()) :: {:ok, pid()} | {:error, any()} def start_link(module, destination, child_opts, opts \\ []), do: GenServer.start_link(__MODULE__, {module, destination, child_opts, opts}) @spec start(atom(), RNS.Destination.t(), any(), opts()) :: {:ok, pid()} | {:error, any()} def start(module, destination, child_opts, opts \\ []), do: GenServer.start(__MODULE__, {module, destination, child_opts, opts}) use GenServer @impl true @doc false @spec init({atom(), RNS.Destination.t(), any(), opts()}) :: {:stop, stop_reason()} | {:ok, state(), timeout()} def init({module, destination, child_opts, opts}) do # run the 'child' module's init function. case module.init(child_opts) do {:ok, child_state} -> do_init(module, destination, child_state, opts, :infinity) {:ok, child_state, timeout} when is_integer(timeout) -> do_init(module, destination, child_state, opts, timeout) {:stop, reason} -> {:stop, reason} end end @spec do_init(atom(), RNS.Destination.t(), child_state(), opts(), timeout()) :: {:ok, state(), timeout()} defp do_init(module, destination, child_state, opts, child_timeout) do interface = RNS.Interface.new("GenDestination##{RNS.Destination.to_string(destination)}", self(), virtual: true) RNS.InterfaceManager.interface_started(interface) destination = RNS.Destination.change_interface(destination, interface) RNS.DestinationStore.put(destination) if Keyword.get(opts, :announce_on_start, true) do RNS.Destination.broadcast_announce(destination, update_announce: true) end state = state( module: module, destination: destination, child_state: child_state, opts: opts ) {:ok, state, shortest_timeout(state, child_timeout)} end @spec shortest_timeout(state(), timeout()) :: timeout() defp shortest_timeout(state, _child_timeout) do # TODO: get the smallest of all the different timeouts(child, announce) opts = state(state, :opts) Keyword.get(opts, :announce_interval, :infinity) end @impl true def handle_info({:inbound, packet, source}, state) do case RNS.Packet.packet_type(packet) do :data -> handle_data_packet(packet, source, state) end end @spec handle_data_packet(RNS.Packet.t(), RNS.Interface.t(), state()) :: {:noreply, state()} defp handle_data_packet(packet, source, state) do destination = state(state, :destination) identity = destination |> RNS.Destination.identity() packet_destination_type = RNS.Packet.destination_type(packet) destination_type = RNS.Destination.type(destination) case packet_destination_type do :single when destination_type == :single -> case RNS.Packet.decrypt(packet, identity) do {:ok, packet} -> # TODO: allow handle_data to decide whether to prove or not(:prove/:noprove instead of :ok). RNS.Interface.send(RNS.Packet.DataPacketProof.new(RNS.Packet.hash(packet), identity), source) handle_decrypted_data(RNS.Packet.data(packet), source, state) {:error, reason} -> Logger.warning("(RNS.GenDestination) Failed to decrypt data packet to destination #{RNS.Destination.to_string(destination)}, error: #{inspect(reason)}.") {:noreply, state} end :plain when destination_type == :plain -> handle_decrypted_data(RNS.Packet.data(packet), source, state) destination_type -> Logger.warning("(RNS.GenDestination) Received packet with unsupported destination type: #{destination_type}.") end end @spec handle_decrypted_data(RNS.Packet.data(), RNS.Interface.t(), state()) :: {:noreply, state()} defp handle_decrypted_data(data, source, state) do child_return = state(state, :module).handle_data( data, source, state(state, :child_state) ) case child_return do {:ok, new_child_state} -> {:noreply, state(state, child_state: new_child_state)} {:stop, reason} -> {:stop, reason} end end end