Packages

This is an implementation of Mark Qvist's Reticulum Network Stack, a 'cryptography-based networking stack for building local and wide-area networks with readily available hardware.'

Current section

Files

Jump to
reticulum lib rns config.ex
Raw

lib/rns/config.ex

defmodule RNS.Config do
@moduledoc """
Config parsing and genserver.
"""
@type t() :: tuple()
@type transport() :: boolean()
@type interfaces() :: [map()]
require Record
Record.defrecord(:config,
transport: false,
interfaces: [],
data_directory: "~/.local/share/reticulum_ex/"
)
require Logger
@doc "Get the transport value of the config record."
@spec transport(t()) :: transport()
def transport(config), do: config(config, :transport)
@doc "Get the interfaces(to start) of the config record."
@spec interfaces(t()) :: interfaces()
def interfaces(config), do: config(config, :interfaces)
@doc "Get the data directory of the config record."
@spec data_directory(t()) :: Path.t()
def data_directory(config), do: config(config, :data_directory)
@spec new(
transport: boolean(),
interfaces: [map()],
data_directory: Path.t()
) :: t()
def new(opts \\ []) do
config(
transport: Keyword.get(opts, :transport, false),
interfaces: Keyword.get(opts, :interfaces, []),
data_directory: Keyword.get(opts, :data_directory, "~/.local/share/reticulum_ex/")
)
end
@doc """
Read the config file and return a config record.
Will also create the config file and directories if they don't exist.
"""
@spec parse() :: {:ok, tuple()} | {:error, atom()}
def parse() do
config_path =
if File.exists?("reticulum_ex.toml") do
"reticulum_ex.toml"
else
"~/.config/reticulum_ex.toml"
end
|> Path.expand()
if not File.exists?(config_path) do
config_directory =
config_path
|> Path.dirname()
case File.mkdir_p(config_directory) do
:ok ->
File.write(config_path, example_config())
{:error, :eaccess} ->
Logger.emergency(
"Failed to write configuration: Read/write access needed for '#{config_directory}'!"
)
exit(:config_rw_required)
{:error, :enospc} ->
Logger.emergency("Failed to create configuration file: No space left on drive!")
exit(:config_no_space)
{:error, :enotdir} ->
Logger.emergency(
"Failed to create configuration file: One of the directories in the configuration path is not a directory!"
)
exit(:config_not_dir)
end
end
case Toml.decode_file(config_path) do
{:ok, config} ->
parse(config)
{:error, {:invalid_toml, _}} ->
Logger.emergency("Invalid configuration file TOML!")
{:error, :invalid_toml}
end
end
@spec parse(map()) :: tuple()
def parse(config_toml) when is_map(config_toml) do
interfaces =
Enum.reduce(config_toml["interfaces"], [], fn {name, interface_toml}, interfaces ->
case interface_toml["enabled"] do
true ->
case map_to_interface(interface_toml, name) do
{:ok, interface} ->
[interface | interfaces]
{:error, error} ->
Logger.warning(
"Failed to parse TOML in config on interface #{name}: #{error}. The interface won't be added."
)
interfaces
end
false ->
interfaces
nil ->
Logger.warning(
"Interface '#{name}' has not been enabled as the enabled field is not present in the config."
)
interfaces
end
end)
transport =
case config_toml["transport"] do
transport when is_boolean(transport) ->
transport
nil ->
Logger.debug(
"The config file does not contain 'transport'. We will instead use the default value, which is false(disabled)."
)
false
_ ->
Logger.warning(
"An invalid value has been given to 'transport' in the config, it can only be true(enabled) or false(disabled). We will instead use the default value, which is false(disabled)."
)
false
end
data_directory =
case config_toml["data_directory"] do
data_directory when is_binary(data_directory) ->
data_directory
nil ->
Logger.debug(
"The config file does not contain 'data_directory'. We will instead use the default value, which is '~/.local/share/reticulum_ex/'."
)
"~/.local/share/reticulum_ex/"
_ ->
Logger.warning(
"An invalid value has been given to 'data_directory' in the config, it should be a valid path. We will instead use the default value, which is '~/.local/share/reticulum_ex/'."
)
"~/.local/share/reticulum_ex/"
end
|> Path.expand()
case File.mkdir_p(data_directory) do
:ok ->
:ok
{:error, :eaccess} ->
Logger.emergency(
"Failed to create data directory: Read/write access needed for '#{data_directory}'!"
)
exit(:data_dir_rw_required)
{:error, :enospc} ->
Logger.emergency("Failed to create configuration file: No space left on drive!")
exit(:data_dir_no_space)
{:error, :enotdir} ->
Logger.emergency(
"Failed to create configuration file: One of the directories in the configuration path is not a directory!"
)
exit(:data_dir_not_dir)
end
config =
config(
interfaces: interfaces,
transport: transport,
data_directory: data_directory
)
{:ok, config}
end
# Converts a map of an interface object from parse/1 to the tuple used by the interface manager. If all goes well, of course!
@spec map_to_interface(map(), String.t()) ::
{:ok, {atom(), map()}} | {:error, :unknown_interface}
defp map_to_interface(interface_toml, name) do
type =
interface_toml["type"]
|> parse_interface_type()
case type do
:unknown ->
Logger.emergency("Unknown interface type on interface #{name}!")
{:error, :unknown_interface}
type ->
params = interface_params(interface_toml)
{:ok, {type, params}}
end
end
@spec interface_params(map()) :: map()
defp interface_params(interface_toml) do
Enum.reduce(interface_toml, %{}, fn {key, value}, params ->
case key do
"enabled" -> params
"type" -> params
_ -> Map.put(params, key, value)
end
end)
end
@spec parse_interface_type(String.t()) :: atom()
defp parse_interface_type(type) when is_binary(type) do
case type do
"TCPClient" ->
RNS.Interface.TCPClient
"TCPServer" ->
RNS.Interface.TCPServer
_ ->
:unknown
end
end
@spec example_config() :: String.t()
def example_config() do
"""
[interfaces]
[interfaces.nodns1]
type = "TCPClient"
host = "202.61.243.41"
port = 4965
enabled = false
[interfaces.server]
type = "TCPServer"
port = 4343
enabled = true
mode = "access_point"
"""
end
use GenServer
# GenServer client.
@spec start_link(t()) :: {:ok, pid()}
def start_link(state) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
@doc """
Subscribe to changes of transport's value.
The value is sent by the subscriber so that it can be compared with the actual one.
"""
@spec transport_subscribe(transport()) :: :ok
def transport_subscribe(my_transport) do
GenServer.cast(__MODULE__, {:transport_subscribe, self(), my_transport})
end
@spec fetch_config() :: t()
def fetch_config() do
GenServer.call(__MODULE__, :fetch_config)
end
# GenServer callbacks.
@impl true
def init(config) do
{:ok, {config, []}}
end
@impl true
def handle_cast({:transport_subscribe, from, its_transport}, {config, transport_subscribers}) do
transport = config(config, :transport)
# If the subscriber's transport value is different at the time it sent the subscribe message, send the correct value to it.
if its_transport != transport do
send(from, {:transport_change, transport})
end
new_state = {config, [from | transport_subscribers]}
{:noreply, new_state}
end
@impl true
def handle_call(:fetch_config, _from, {config, _} = state) do
{:reply, config, state}
end
end