diff --git a/README.md b/README.md index 0c9671f..92fe2e1 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,15 @@ config :my_app, MyApp.Coap.Endpoint, coap: [port: 5683, ack_timeout: 5000, processing_delay: 4500] ``` +or, for a DTLS secure connection: + +``` +config :my_app, MyApp.Coap.Endpoint, + http: false, https: false, server: false, + coap: [port: 5683, ssl: []] +``` + + In `lib/my_app.ex` add supervisor and listener for the endpoint: ``` diff --git a/lib/coap/acceptor.ex b/lib/coap/acceptor.ex new file mode 100644 index 0000000..4c6c3e7 --- /dev/null +++ b/lib/coap/acceptor.ex @@ -0,0 +1,41 @@ +defmodule CoAP.Acceptor do + # Takes a listen_socket + # Accepts on that listen_socket + # Passes the resulting socket back to SecureSocketServer, which takes ownership, does handshake + # Loops + use GenServer + + # This could maybe be done without a GenServer … + + import Logger, only: [debug: 1] + + @accept_timeout 1000 + + def start_link([server, listen_socket]) do + GenServer.start_link(__MODULE__, [server, listen_socket]) + end + + def init([server, listen_socket]) do + send(self(), :accept) + + {:ok, [server, listen_socket]} + end + + def handle_info(:accept, [server, listen_socket] = state) do + :ssl.transport_accept(listen_socket, @accept_timeout) + |> process_with(server) + + # Loop + send(self(), :accept) + + {:noreply, state} + end + + defp process_with({:ok, socket}, server) do + send(server, {:process, socket}) + end + + defp process_with({:error, reason}, _server) do + debug("Error accepting DTLS socket: #{reason}") + end +end diff --git a/lib/coap/phoenix/listener.ex b/lib/coap/phoenix/listener.ex index ace5a5c..31a7a3b 100644 --- a/lib/coap/phoenix/listener.ex +++ b/lib/coap/phoenix/listener.ex @@ -36,9 +36,17 @@ defmodule CoAP.Phoenix.Listener do info("Starting CoAP.Phoenix.Listener: #{inspect(config)}") - {:ok, server} = CoAP.SocketServer.start_link([{@adapter, endpoint}, config[:port], config]) - # _TODO: ref and monitor? - # _TODO: die if server dies? + {:ok, server} = + case config[:ssl] do + nil -> + CoAP.SocketServer.start_link([{@adapter, endpoint}, config[:port], config]) + + _ -> + CoAP.SecureSocketServer.start_link([{@adapter, endpoint}, config[:port], config[:ssl]]) + end + + # TODO: ref and monitor? + # TODO: die if server dies? {:ok, %{endpoint: endpoint, config: config, server: server}} end diff --git a/lib/coap/secure_socket_server.ex b/lib/coap/secure_socket_server.ex new file mode 100644 index 0000000..e19d09f --- /dev/null +++ b/lib/coap/secure_socket_server.ex @@ -0,0 +1,203 @@ +defmodule CoAP.SecureSocketServer do + @moduledoc """ + CoAP.SocketServer holds a reference to a server, or is held by a client. + It contains the UDP port either listening (for a server) or used by a client. + + When a new UDP packet is received, the socket_server attempts to look up an + existing connection, or establish a new connection as necessary. + + This registry of connections is mapped by a connection_id, a tuple of + `{ip, port, token}` so that subsequent messages exchanged will be routed + to the appropriate `CoAP.Connection`. + + A socket_server should generally not be started directly. It will be started + automatically by a `CoAP.Client` or by a server like `CoAP.Phoenix.Listener`. + + A socket_server will receive and handle a few messages. `:udp` from the udp socket, + `:deliver` from the `CoAP.Connection` when a message is being sent, and `:DOWN` + when a monitored connection is complete and the process ends. + """ + + # TODO: make this a supervisor with SecureSocket children for each accept + + use GenServer + + import CoAP.Util.BinaryFormatter, only: [to_hex: 1] + import Logger, only: [debug: 1] + + alias CoAP.Message + + @handshake_timeout 5000 + + def start_link(args) do + GenServer.start_link(__MODULE__, args) + end + + # init with port 5163/config (server), or 0 (client) + + # endpoint => server + @doc """ + init function for a server e.g., phoenix endpoint + + Open a udp socket on the given port and store in state + Initialize connections and monitors empty maps in state + """ + def init([endpoint, port, ssl_config]) do + {:ok, listen_socket} = + :ssl.listen(port, [:binary, {:active, false}, {:reuseaddr, true}, {:protocol, :dtls}]) + + acceptor = CoAP.Acceptor.start_link([self(), listen_socket]) + + {:ok, + %{ + port: port, + listen_socket: listen_socket, + acceptor: acceptor, + endpoint: endpoint, + ssl_config: ssl_config, + connections: %{}, + monitors: %{} + }} + end + + # Used by Connection to start a udp port + # endpoint => client + @doc """ + init function for a client + + Opens a socket for sending (and receiving responses on a random listener) + Does not listen on any known port for new messages + Started by a `Connection` to deliver a client request message + """ + # def init([endpoint, {host, port, token}, connection]) do + # {:ok, socket} = :gen_udp.open(0, [:binary, {:active, true}, {:reuseaddr, true}]) + # + # # TODO: use handle_continue to do this + # ip = normalize_host(host) + # connection_id = {ip, port, token} + # + # ref = Process.monitor(connection) + # + # {:ok, + # %{ + # port: 0, + # socket: socket, + # endpoint: endpoint, + # connections: %{connection_id => connection}, + # monitors: %{ref => connection_id} + # }} + # end + + def handle_info({:process, handshake_socket}, state) do + {:ok, socket} = :ssl.handshake(handshake_socket, @handshake_timeout) + :ok = :ssl.controlling_process(socket, self()) + :ok = :ssl.setopts(socket, [{:active, true}]) + + # TODO: do we need to store the socket? + {:noreply, state} + end + + @doc """ + Receive udp packets, forward to the appropriate connection + """ + def handle_info({:ssl, socket, data}, state) do + {:ok, {peer_ip, peer_port}} = :ssl.peername(socket) + + debug("CoAP socket received raw data #{to_hex(data)} from #{inspect({peer_ip, peer_port})}") + + message = Message.decode(data) + + {connection, new_state} = connection_for({peer_ip, peer_port, message.token}, state) + + # TODO: if it's alive? + send(connection, {:receive, message}) + # TODO: error if dead process + + {:noreply, new_state} + end + + @doc """ + Deliver messages to be sent to a peer + """ + def handle_info({:deliver, message, {host, port} = _peer}, %{socket: socket} = state) do + data = Message.encode(message) + + ip = normalize_host(host) + + debug("CoAP socket sending raw data #{to_hex(data)} to #{inspect({ip, port})}") + + :ssl.send(socket, data) + + {:noreply, state} + end + + # TODO: Do we need to do this when using connection supervisor? + @doc """ + Handles message for completed connection + Removes complete connection from the registry and monitoring + """ + def handle_info({:DOWN, ref, :process, _from, reason}, %{monitors: monitors} = state) do + connection_id = Map.get(monitors, ref) + + debug( + "CoAP socket received DOWN:#{reason} in CoAP.SocketServer from:#{inspect(connection_id)}" + ) + + {:noreply, + %{ + state + | connections: Map.delete(state.connections, connection_id), + monitors: Map.delete(monitors, ref) + }} + end + + defp connection_for(connection_id, state) do + # TODO: switch to socket? + connection = Map.get(state.connections, connection_id) + + case connection do + nil -> + {:ok, conn} = start_connection(self(), state.endpoint, connection_id) + + { + conn, + %{ + state + | connections: Map.put(state.connections, connection_id, conn), + monitors: Map.put(state.monitors, Process.monitor(conn), connection_id) + } + } + + _ -> + {connection, state} + end + end + + # TODO: move to CoAP + defp start_connection(server, endpoint, peer) do + DynamicSupervisor.start_child( + CoAP.ConnectionSupervisor, + { + CoAP.Connection, + [server, endpoint, peer] + } + ) + end + + defp normalize_host(host) when is_tuple(host), do: host + + defp normalize_host(host) when is_binary(host) do + host + |> to_charlist() + |> normalize_host() + end + + defp normalize_host(host) when is_list(host) do + host + |> :inet.getaddr(:inet) + |> case do + {:ok, ip} -> ip + {:error, _reason} -> nil + end + end +end