Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/dtls #3

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
Expand Down
41 changes: 41 additions & 0 deletions lib/coap/acceptor.ex
Original file line number Diff line number Diff line change
@@ -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
14 changes: 11 additions & 3 deletions lib/coap/phoenix/listener.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
203 changes: 203 additions & 0 deletions lib/coap/secure_socket_server.ex
Original file line number Diff line number Diff line change
@@ -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