Packages

Library providing the capability to process and perform RTMP handshakes

Current section

Files

Jump to
eml_rtmp_handshake lib rtmp_handshake.ex
Raw

lib/rtmp_handshake.ex

defmodule RtmpHandshake do
@moduledoc """
Provides functionality to handle the RTMP handshake process.
## Examples
The following is an example of handling a handshake as a server:
# Since we are a server we don't know what handshake type
# the client will send
{handshake, %RtmpHandshake.ParseResult{}} = RtmpHandshake.new(:unknown)
c0_and_c1 = get_packets_0_and_1_from_client()
{handshake, %RtmpHandshake.ParseResult{
bytes_to_send: bytes,
current_state: :waiting_for_data
}} = RtmpHandshake.process_bytes(handshake, c0_and_c1)
send_bytes_to_client(bytes)
c2 = get_packet_c2_from_client()
{handshake, %RtmpHandshake.ParseResult{
current_state: :success
}} = RtmpHandshake.process_bytes(handshake, c2)
"""
require Logger
alias RtmpHandshake.OldHandshakeFormat, as: OldHandshakeFormat
alias RtmpHandshake.ParseResult, as: ParseResult
alias RtmpHandshake.HandshakeResult, as: HandshakeResult
alias RtmpHandshake.DigestHandshakeFormat, as: DigestHandshakeFormat
@type handshake_type :: :unknown | :old | :digest
@type is_valid_format_result :: :yes | :no | :unknown
@type start_time :: non_neg_integer
@type remaining_binary :: <<>>
@type binary_response :: <<>>
@type behaviour_state :: any
@type process_result :: {:success, start_time, binary_response, remaining_binary}
| {:incomplete, binary_response}
| :failure
@callback is_valid_format(<<>>) :: is_valid_format_result
@callback process_bytes(behaviour_state, <<>>) :: {behaviour_state, process_result}
@callback create_p0_and_p1_to_send(behaviour_state) :: {behaviour_state, <<>>}
defmodule State do
defstruct status: :pending,
handshake_state: nil,
handshake_type: :unknown,
remaining_binary: <<>>,
peer_start_timestamp: nil
end
@doc """
Creates a new finite state machine to handle the handshake process,
and preliminary parse results.
If a handshake type is specified we assume we are acting as a client
(since a server won't know what type of handshake to use until it
receives packets c0 and c1).
"""
@spec new(handshake_type) :: {%State{}, ParseResult.t}
def new(:old) do
{handshake_state, bytes_to_send} =
OldHandshakeFormat.new()
|> OldHandshakeFormat.create_p0_and_p1_to_send()
state = %State{handshake_type: :old, handshake_state: handshake_state}
result = %ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes_to_send}
{state, result}
end
def new(:digest) do
{handshake_state, bytes_to_send} =
DigestHandshakeFormat.new()
|> DigestHandshakeFormat.create_p0_and_p1_to_send()
state = %State{handshake_type: :digest, handshake_state: handshake_state}
result = %ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes_to_send}
{state, result}
end
def new(:unknown) do
state = %State{handshake_type: :unknown}
{state, %ParseResult{current_state: :waiting_for_data}}
end
@doc "Reads the passed in binary to proceed with the handshaking process"
@spec process_bytes(%State{}, <<>>) :: {%State{}, ParseResult.t}
def process_bytes(state = %State{handshake_type: :unknown}, binary) when is_binary(binary) do
state = %{state | remaining_binary: state.remaining_binary <> binary}
is_old_format = OldHandshakeFormat.is_valid_format(state.remaining_binary)
is_digest_format = DigestHandshakeFormat.is_valid_format(state.remaining_binary)
case {is_old_format, is_digest_format} do
{_, :yes} ->
handshake_state = DigestHandshakeFormat.new()
binary = state.remaining_binary
state = %{state |
remaining_binary: <<>>,
handshake_type: :digest,
handshake_state: handshake_state
}
# Processing bytes should trigger p0 and p1 to be sent
{state, result} = process_bytes(state, binary)
result = %{result | bytes_to_send: result.bytes_to_send}
{state, result}
{:yes, _} ->
{handshake_state, bytes_to_send} =
OldHandshakeFormat.new()
|> OldHandshakeFormat.create_p0_and_p1_to_send()
binary = state.remaining_binary
state = %{state |
remaining_binary: <<>>,
handshake_type: :old,
handshake_state: handshake_state
}
{state, result} = process_bytes(state, binary)
result = %{result | bytes_to_send: bytes_to_send <> result.bytes_to_send}
{state, result}
{:no, :no} ->
# No known handhsake format
{state, %ParseResult{current_state: :failure}}
_ ->
{state, %ParseResult{}}
end
end
def process_bytes(state = %State{handshake_type: :old}, binary) when is_binary(binary) do
case OldHandshakeFormat.process_bytes(state.handshake_state, binary) do
{handshake_state, :failure} ->
state = %{state | handshake_state: handshake_state}
{state, %ParseResult{current_state: :failure}}
{handshake_state, {:incomplete, bytes_to_send}} ->
state = %{state | handshake_state: handshake_state}
{state, %ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes_to_send}}
{handshake_state, {:success, start_time, response, remaining_binary}} ->
state = %{state |
handshake_state: handshake_state,
remaining_binary: remaining_binary,
peer_start_timestamp: start_time,
status: :complete
}
result = %ParseResult{current_state: :success, bytes_to_send: response}
{state, result}
end
end
def process_bytes(state = %State{handshake_type: :digest}, binary) when is_binary(binary) do
case DigestHandshakeFormat.process_bytes(state.handshake_state, binary) do
{handshake_state, :failure} ->
state = %{state | handshake_state: handshake_state}
{state, %ParseResult{current_state: :failure}}
{handshake_state, {:incomplete, bytes_to_send}} ->
state = %{state | handshake_state: handshake_state}
{state, %ParseResult{current_state: :waiting_for_data, bytes_to_send: bytes_to_send}}
{handshake_state, {:success, start_time, response, remaining_binary}} ->
state = %{state |
handshake_state: handshake_state,
remaining_binary: remaining_binary,
peer_start_timestamp: start_time,
status: :complete
}
result = %ParseResult{current_state: :success, bytes_to_send: response}
{state, result}
end
end
@doc """
After a handshake has been successfully completed this is called to
retrieve the peer's starting timestamp and any left over binary that
may need to be parsed later (not part of the handshake but instead
the beginning of the rtmp protocol).
"""
@spec get_handshake_result(%State{}) :: {%State{}, HandshakeResult.t}
def get_handshake_result(state = %State{status: :complete}) do
unparsed_binary = state.remaining_binary
{
%{state | remaining_binary: <<>>},
%HandshakeResult{
peer_start_timestamp: state.peer_start_timestamp,
remaining_binary: unparsed_binary
}
}
end
end