Packages

Implementation of the W3C WebRTC API

Retired package: Security issue - Missing DTLS fingerprint check in client-role handshake; weakens auth. Upgrade to 0.15.1/0.16.1.
Security advisory: This version has known vulnerabilities. View advisories

Current section

Files

Jump to
ex_webrtc lib ex_webrtc sdp_utils.ex
Raw

lib/ex_webrtc/sdp_utils.ex

defmodule ExWebRTC.SDPUtils do
@moduledoc false
alias ExRTP.Packet.Extension
alias ExSDP.Attribute.{Extmap, RID, Simulcast, SSRC}
alias ExWebRTC.RTPCodecParameters
@type extension() :: {Extension.SourceDescription, atom()}
@spec ensure_valid(ExSDP.t()) :: :ok | {:error, term()}
def ensure_valid(sdp) do
with :ok <- ensure_non_empty(sdp),
:ok <- ensure_mid(sdp),
:ok <- ensure_bundle(sdp),
:ok <- ensure_data_channel_valid(sdp) do
ensure_rtcp_mux(sdp)
end
end
defp ensure_non_empty(sdp) do
if Enum.empty?(sdp.media) do
{:error, :empty_sdp}
else
:ok
end
end
defp ensure_mid(sdp) do
sdp.media
|> Enum.reduce_while({:ok, []}, fn media, {:ok, acc} ->
case ExSDP.get_attributes(media, :mid) do
[{:mid, mid}] -> {:cont, {:ok, [mid | acc]}}
[] -> {:halt, {:error, :missing_mid}}
other when is_list(other) -> {:halt, {:error, :duplicated_mid}}
end
end)
|> case do
{:ok, mids} -> if Enum.uniq(mids) == mids, do: :ok, else: {:error, :duplicated_mid}
error -> error
end
end
defp ensure_bundle(sdp) do
groups = ExSDP.get_attributes(sdp, ExSDP.Attribute.Group)
mline_mids = get_bundle_mids(sdp.media)
case filter_groups(groups, "BUNDLE") do
[%ExSDP.Attribute.Group{semantics: "BUNDLE", mids: group_mids}] ->
case {mline_mids -- group_mids, group_mids -- mline_mids} do
{[], []} -> :ok
{_, []} -> {:error, :non_exhaustive_bundle_group}
_other -> {:error, :invalid_bundle_group}
end
[] ->
{:error, :missing_bundle_group}
other when is_list(other) ->
{:error, :multiple_bundle_groups}
end
end
defp filter_groups(groups, to_filter) do
Enum.filter(groups, fn %ExSDP.Attribute.Group{semantics: name} -> name == to_filter end)
end
defp ensure_data_channel_valid(sdp) do
with [data_mline] <- Enum.filter(sdp.media, &data_channel?/1),
"webrtc-datachannel" <- data_mline.fmt,
sctp_port when not is_nil(sctp_port) <- ExSDP.get_attribute(data_mline, "sctp-port") do
:ok
else
[] -> :ok
mlines when is_list(mlines) -> {:error, :multiple_data_mlines}
fmt when is_binary(fmt) -> {:error, :invalid_datachannel_format}
nil -> {:error, :no_sctp_port}
end
end
defp ensure_rtcp_mux(sdp) do
# Firefox does not add `rtcp_mux` in rejected mlines
sdp.media
|> Enum.reject(&(rejected?(&1) or data_channel?(&1)))
|> Enum.all?(&(ExSDP.get_attribute(&1, :rtcp_mux) == :rtcp_mux))
|> case do
true -> :ok
false -> {:error, :missing_rtcp_mux}
end
end
@spec get_media_direction(ExSDP.Media.t()) ::
:sendrecv | :sendonly | :recvonly | :inactive | nil
def get_media_direction(media) do
Enum.find(media.attributes, fn attr ->
attr in [:sendrecv, :sendonly, :recvonly, :inactive]
end)
end
@spec get_rids(ExSDP.Media.t()) :: [String.t()] | nil
def get_rids(media) do
Enum.flat_map(media.attributes, fn
%RID{direction: :send, id: id} -> [id]
_other -> []
end)
|> case do
[] -> nil
other -> other
end
end
@spec reverse_simulcast(ExSDP.Media.t()) :: [ExSDP.Attribute.t()]
def reverse_simulcast(media) do
Enum.flat_map(media.attributes, fn
%RID{direction: :send} = rid -> [%RID{rid | direction: :recv}]
%Simulcast{send: send, recv: recv} -> [%Simulcast{send: recv, recv: send}]
_other -> []
end)
end
@spec get_bundle_mids([ExSDP.Media.t()]) :: [String.t()]
def get_bundle_mids(mlines) do
# Rejected m-lines are not included in the BUNDLE group.
# See RFC 8829, sec. 5.2.2, p. 10.
Enum.map(mlines, fn mline ->
unless rejected?(mline) do
{:mid, mid} = ExSDP.get_attribute(mline, :mid)
mid
end
end)
|> Enum.reject(&(&1 == nil))
end
@spec get_stream_ids(ExSDP.Media.t()) :: [String.t()]
def get_stream_ids(media) do
ExSDP.get_attributes(media, :msid)
|> Enum.reject(fn msid -> msid.id == "-" end)
|> Enum.map(fn msid -> msid.id end)
end
@spec get_ice_lite(ExSDP.t()) :: boolean()
def get_ice_lite(sdp), do: ExSDP.get_attribute(sdp, :ice_lite) != nil
@spec get_ice_credentials(ExSDP.t()) ::
{:ok, {binary(), binary()} | nil}
| {:error,
:missing_ice_pwd
| :missing_ice_ufrag
| :missing_ice_credentials
| :conflicting_ice_credentials}
def get_ice_credentials(sdp) do
session_creds = do_get_ice_credentials(sdp)
mline_creds =
sdp.media
|> Enum.reject(&rejected?/1)
|> Enum.map(&do_get_ice_credentials/1)
case {session_creds, mline_creds} do
# no session creds and no mlines (empty SDP)
{{nil, nil}, []} ->
{:ok, nil}
# session creds but no mlines (empty SDP)
{session_creds, []} ->
{:ok, session_creds}
{{nil, nil}, mline_creds} ->
with :ok <- ensure_ice_credentials_present(mline_creds),
:ok <- ensure_ice_credentials_unique(mline_creds) do
{:ok, List.first(mline_creds)}
end
{{s_ufrag, s_pwd} = session_creds, mline_creds} ->
creds =
Enum.map([session_creds | mline_creds], fn
{nil, nil} -> session_creds
{nil, pwd} -> {s_ufrag, pwd}
{ufrag, nil} -> {ufrag, s_pwd}
other -> other
end)
case ensure_ice_credentials_unique(creds) do
:ok -> {:ok, List.first(creds)}
error -> error
end
end
end
@spec get_ice_candidates(ExSDP.t()) :: [String.t()]
def get_ice_candidates(sdp) do
sdp.media
|> Enum.flat_map(&ExSDP.get_attributes(&1, "candidate"))
|> Enum.map(fn {"candidate", attr} -> attr end)
end
@spec add_ice_candidates(ExSDP.t(), [String.t()]) :: ExSDP.t()
def add_ice_candidates(sdp, candidates) do
# We only add candidates to the first mline
# as we don't support bundle-policies other than "max-bundle".
# See RFC 8829, sec. 4.1.1.
candidates = Enum.map(candidates, &{"candidate", &1})
if sdp.media != [] do
mline =
sdp.media
|> List.first()
|> ExSDP.add_attributes(candidates)
media = List.replace_at(sdp.media, 0, mline)
%ExSDP{sdp | media: media}
else
sdp
end
end
@spec get_dtls_role(ExSDP.t()) ::
{:ok, :active | :passive | :actpass}
| {:error, :missing_dtls_role | :conflicting_dtls_roles}
def get_dtls_role(sdp) do
session_role =
case ExSDP.get_attribute(sdp, :setup) do
{:setup, setup} -> setup
nil -> nil
end
mline_roles =
sdp.media
|> Enum.flat_map(&ExSDP.get_attributes(&1, :setup))
|> Enum.map(fn {:setup, setup} -> setup end)
case {session_role, mline_roles} do
{nil, []} ->
{:error, :missing_dtls_role}
{session_role, []} ->
{:ok, session_role}
{session_role, mline_roles} ->
roles =
if session_role != nil do
mline_roles ++ [session_role]
else
mline_roles
end
case Enum.uniq(roles) do
[role] -> {:ok, role}
_other -> {:error, :conflicting_dtls_roles}
end
end
end
@spec get_cert_fingerprint(ExSDP.t()) ::
{:ok, {:fingerprint, {atom(), binary()}}}
| {:error, :missing_cert_fingerprint | :conflicting_cert_fingerprints}
def get_cert_fingerprint(sdp) do
session_fingerprint = ExSDP.get_attribute(sdp, :fingerprint)
mline_fingerprints = Enum.map(sdp.media, &ExSDP.get_attribute(&1, :fingerprint))
case {session_fingerprint, mline_fingerprints} do
{nil, []} ->
{:error, :missing_cert_fingerprint}
{session_fingerprint, []} ->
{:ok, session_fingerprint}
{nil, mline_fingerprints} ->
with :ok <- ensure_fingerprints_present(mline_fingerprints),
:ok <- ensure_fingerprints_unique(mline_fingerprints) do
{:ok, List.first(mline_fingerprints)}
else
{:error, :no_cert_fingerprints} -> {:error, :missing_cert_fingerprint}
other -> other
end
{session_fingerprint, mline_fingerprints} ->
with :ok <- ensure_fingerprints_present(mline_fingerprints),
:ok <- ensure_fingerprints_unique([session_fingerprint | mline_fingerprints]) do
{:ok, session_fingerprint}
else
{:error, :no_cert_fingerprints} -> {:ok, session_fingerprint}
other -> other
end
end
end
@spec get_extensions(ExSDP.t()) :: [Extmap.t()]
def get_extensions(sdp) do
# we assume that, if extension is present in multiple mlines, the IDs are the same (RFC 8285)
Enum.flat_map(sdp.media, &ExSDP.get_attributes(&1, Extmap))
end
@spec get_rtp_codec_parameters(ExSDP.t() | ExSDP.Media.t()) :: [RTPCodecParameters.t()]
def get_rtp_codec_parameters(sdp_or_mline)
def get_rtp_codec_parameters(%ExSDP{} = sdp), do: do_get_rtp_codec_parameters(sdp.media)
def get_rtp_codec_parameters(%ExSDP.Media{} = mline),
do: do_get_rtp_codec_parameters(List.wrap(mline))
defp do_get_rtp_codec_parameters(mlines) do
Enum.flat_map(mlines, fn mline ->
rtp_mappings = ExSDP.get_attributes(mline, :rtpmap)
fmtps = ExSDP.get_attributes(mline, :fmtp)
all_rtcp_fbs = ExSDP.get_attributes(mline, :rtcp_feedback)
rtp_mappings
|> Enum.map(fn rtp_mapping ->
fmtp = Enum.find(fmtps, &(&1.pt == rtp_mapping.payload_type))
rtcp_fbs = Enum.filter(all_rtcp_fbs, &(&1.pt == rtp_mapping.payload_type))
RTPCodecParameters.new(mline.type, rtp_mapping, fmtp, rtcp_fbs)
end)
end)
end
@spec get_payload_to_mid(ExSDP.t()) :: %{(pt :: non_neg_integer()) => mid :: binary()}
def get_payload_to_mid(sdp) do
# if payload type is used in more than 1 mline, it cannot be used to identify the mline
# thus, it is not placed in the returned map
sdp.media
|> Enum.flat_map(fn mline ->
{:mid, mid} = ExSDP.get_attribute(mline, :mid)
encodings = ExSDP.get_attributes(mline, :rtpmap)
Enum.map(encodings, &{&1.payload_type, mid})
end)
|> Enum.reduce(%{}, fn
{pt, _mid}, acc when is_map_key(acc, pt) -> Map.put(acc, pt, nil)
{pt, mid}, acc -> Map.put(acc, pt, mid)
end)
|> Enum.reject(fn {_, v} -> is_nil(v) end)
|> Map.new()
end
@spec get_ssrc_to_mid(ExSDP.t() | ExSDP.Media.t()) :: %{
(ssrc :: String.t()) => mid :: String.t()
}
def get_ssrc_to_mid(%ExSDP{media: media}) do
Enum.reduce(media, %{}, fn mline, acc ->
Map.merge(acc, get_ssrc_to_mid(mline))
end)
end
def get_ssrc_to_mid(%ExSDP.Media{} = mline) do
case ExSDP.get_attribute(mline, :mid) do
{:mid, mid} ->
mline
|> ExSDP.get_attributes(:ssrc)
|> Map.new(fn %SSRC{id: ssrc} -> {ssrc, mid} end)
_other ->
%{}
end
end
@spec find_mline_by_mid(ExSDP.t(), binary()) :: ExSDP.Media.t() | nil
def find_mline_by_mid(sdp, mid) do
Enum.find(sdp.media, fn mline ->
{:mid, mline_mid} = ExSDP.get_attribute(mline, :mid)
mline_mid == mid
end)
end
@spec find_free_mline_idx(ExSDP.t(), [non_neg_integer()]) :: non_neg_integer() | nil
def find_free_mline_idx(sdp, indices) do
sdp.media
|> Stream.with_index()
|> Enum.find_index(fn {mline, idx} -> mline.port == 0 and idx not in indices end)
end
@spec rejected?(ExSDP.Media.t()) :: boolean()
def rejected?(%ExSDP.Media{port: 0} = media) do
# An m-line is only rejected when its port is set to 0,
# and there is no `bundle-only` attribute.
# See RFC 8843, sec. 6.
"bundle-only" not in media.attributes
end
def rejected?(%ExSDP.Media{}), do: false
@spec data_channel?(ExSDP.Media.t()) :: boolean()
def data_channel?(%ExSDP.Media{fmt: "webrtc-datachannel"}), do: true
def data_channel?(%ExSDP.Media{fmt: ["webrtc-datachannel"]}), do: true
def data_channel?(%ExSDP.Media{}), do: false
defp do_get_ice_credentials(sdp_or_mline) do
ice_ufrag =
case ExSDP.get_attribute(sdp_or_mline, :ice_ufrag) do
{:ice_ufrag, ice_ufrag} -> ice_ufrag
nil -> nil
end
ice_pwd =
case ExSDP.get_attribute(sdp_or_mline, :ice_pwd) do
{:ice_pwd, ice_pwd} -> ice_pwd
nil -> nil
end
{ice_ufrag, ice_pwd}
end
defp ensure_ice_credentials_present(creds) do
creds
|> Enum.find(fn {ice_ufrag, ice_pwd} -> ice_ufrag == nil or ice_pwd == nil end)
|> case do
{nil, nil} ->
{:error, :missing_ice_credentials}
{nil, _} ->
{:error, :missing_ice_ufrag}
{_, nil} ->
{:error, :missing_ice_pwd}
nil ->
:ok
end
end
defp ensure_ice_credentials_unique(creds) do
case Enum.uniq(creds) do
[_] -> :ok
_ -> {:error, :conflicting_ice_credentials}
end
end
defp ensure_fingerprints_present(fingerprints) do
cond do
Enum.all?(fingerprints, &(&1 == nil)) -> {:error, :no_cert_fingerprints}
Enum.any?(fingerprints, &(&1 == nil)) -> {:error, :missing_cert_fingerprint}
true -> :ok
end
end
defp ensure_fingerprints_unique(fingerprints) do
case Enum.uniq(fingerprints) do
[_] -> :ok
_ -> {:error, :conflicting_cert_fingerprints}
end
end
end