Current section
Files
Jump to
Current section
Files
lib/tortoise/connection/receiver.ex
defmodule Tortoise.Connection.Receiver do
@moduledoc false
use GenStateMachine
alias Tortoise.Connection.Controller
alias Tortoise.Events
defstruct client_id: nil, transport: nil, socket: nil, buffer: <<>>
alias __MODULE__, as: State
def start_link(opts) do
client_id = Keyword.fetch!(opts, :client_id)
data = %State{client_id: client_id}
GenStateMachine.start_link(__MODULE__, data, name: via_name(client_id))
end
defp via_name(client_id) do
Tortoise.Registry.via_name(__MODULE__, client_id)
end
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
def handle_socket(client_id, {transport, socket}) do
{:ok, pid} = GenStateMachine.call(via_name(client_id), {:handle_socket, transport, socket})
case transport.controlling_process(socket, pid) do
:ok ->
:ok
{:error, reason} when reason in [:closed, :einval] ->
# todo, this is an edge case, figure out what to do here
:ok
end
end
@impl true
def init(%State{} = data) do
{:ok, :disconnected, data}
end
@impl true
# receiving data on the network connection
def handle_event(:info, {transport, socket, tcp_data}, _, %{socket: socket} = data)
when transport in [:tcp, :ssl] do
next_actions = [
{:next_event, :internal, :activate_socket},
{:next_event, :internal, :consume_buffer}
]
new_data = %{data | buffer: <<data.buffer::binary, tcp_data::binary>>}
{:keep_state, new_data, next_actions}
end
# Dropped connection: tell the connection process that it should
# attempt to get a new network socket; unfortunately we cannot just
# monitor the socket port in the connection process as a transport
# method such as the SSL based one will pass an opaque data
# structure around instead of a port that can be monitored.
def handle_event(:info, {transport, socket}, _state, %{socket: socket} = data)
when transport in [:tcp_closed, :ssl_closed] do
# should we empty the buffer?
# communicate to the world that we have dropped the connection
:ok = Events.dispatch(data.client_id, :status, :down)
{:next_state, :disconnected, %{data | socket: nil}}
end
# activate network socket for incoming traffic
def handle_event(:internal, :activate_socket, _state_name, %State{transport: nil}) do
{:stop, :no_transport}
end
def handle_event(:internal, :activate_socket, _state_name, data) do
case data.transport.setopts(data.socket, active: :once) do
:ok ->
:keep_state_and_data
{:error, :einval} ->
# @todo consider if there could be a buffer we should drain at this point
{:next_state, :disconnected, data}
end
end
# consume buffer
def handle_event(:internal, :consume_buffer, _state_name, %{buffer: <<>>}) do
:keep_state_and_data
end
# consuming message
def handle_event(
:internal,
:consume_buffer,
{:connected, {:receiving_variable, length}},
%State{buffer: buffer} = data
)
when byte_size(buffer) >= length do
<<package::binary-size(length), rest::binary>> = buffer
next_state = {:connected, :receiving_fixed_header}
next_actions = [
{:next_event, :internal, {:emit, package}},
{:next_event, :internal, :consume_buffer}
]
new_data = %{data | buffer: rest}
{:next_state, next_state, new_data, next_actions}
end
def handle_event(:internal, :consume_buffer, {:connected, {:receiving_variable, _}}, _data) do
# await more bytes
:keep_state_and_data
end
# what should happen here?
def handle_event(:internal, :consume_buffer, :disconnected, _data) do
# we might be disconnected, but could we have data in the buffer still ?
# perhaps we should consume that
:keep_state_and_data
end
# receiving fixed header
def handle_event(:internal, :consume_buffer, {:connected, :receiving_fixed_header}, data) do
case parse_fixed_header(data.buffer) do
{:ok, length} ->
new_state = {:connected, {:receiving_variable, length}}
next_actions = [{:next_event, :internal, :consume_buffer}]
{:next_state, new_state, data, next_actions}
:cont ->
:keep_state_and_data
{:error, :invalid_header_length} ->
{:stop, {:protocol_violation, :invalid_header_length}}
end
end
def handle_event(:internal, {:emit, package}, _, data) do
:ok = Controller.handle_incoming(data.client_id, package)
:keep_state_and_data
end
def handle_event({:call, from}, {:handle_socket, transport, socket}, :disconnected, data) do
new_state = {:connected, :receiving_fixed_header}
next_actions = [
{:reply, from, {:ok, self()}},
{:next_event, :internal, :activate_socket},
{:next_event, :internal, :consume_buffer}
]
# better reset the buffer
new_data = %State{data | transport: transport, socket: socket, buffer: <<>>}
{:next_state, new_state, new_data, next_actions}
end
defp parse_fixed_header(<<_::8, 0::1, length::7, _::binary>>) do
{:ok, length + 2}
end
# 2 bytes
defp parse_fixed_header(<<_::8, 1::1, a::7, 0::1, b::7, _::binary>>) do
<<length::integer-size(14)>> = <<b::7, a::7>>
{:ok, length + 3}
end
# 3 bytes
defp parse_fixed_header(<<_::8, 1::1, a::7, 1::1, b::7, 0::1, c::7, _::binary>>) do
<<length::integer-size(21)>> = <<c::7, b::7, a::7>>
{:ok, length + 4}
end
# 4 bytes
defp parse_fixed_header(<<_::8, 1::1, a::7, 1::1, b::7, 1::1, c::7, 0::1, d::7, _::binary>>) do
<<length::integer-size(28)>> = <<d::7, c::7, b::7, a::7>>
{:ok, length + 5}
end
defp parse_fixed_header(header) when byte_size(header) > 5 do
{:error, :invalid_header_length}
end
defp parse_fixed_header(_) do
:cont
end
end