From 3c4fbfa5222db8693d09765d2dfb36f5ad4db9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=9Aled=C5=BA?= Date: Tue, 17 Oct 2023 12:06:40 +0200 Subject: [PATCH] Handle server side DTLS handshake (#1) --- examples/example.exs | 2 +- lib/ex_webrtc/peer_connection.ex | 162 +++++++++++++++++++++++-------- lib/ex_webrtc/utils.ex | 10 ++ mix.exs | 9 +- mix.lock | 17 ++++ 5 files changed, 156 insertions(+), 44 deletions(-) create mode 100644 lib/ex_webrtc/utils.ex diff --git a/examples/example.exs b/examples/example.exs index 2b481582..4000e310 100644 --- a/examples/example.exs +++ b/examples/example.exs @@ -1,4 +1,4 @@ -Mix.install([{:gun, "~> 2.0.1"}, {:ex_webrtc, path: "./", force: true}, {:jason, "~> 1.4.0"}]) +Mix.install([{:gun, "~> 2.0.1"}, {:ex_webrtc, path: "./"}, {:jason, "~> 1.4.0"}]) require Logger Logger.configure(level: :info) diff --git a/lib/ex_webrtc/peer_connection.ex b/lib/ex_webrtc/peer_connection.ex index 8b6c04c3..6564083e 100644 --- a/lib/ex_webrtc/peer_connection.ex +++ b/lib/ex_webrtc/peer_connection.ex @@ -3,10 +3,14 @@ defmodule ExWebRTC.PeerConnection do use GenServer + require Logger + alias __MODULE__.Configuration alias ExICE.ICEAgent alias ExWebRTC.{IceCandidate, SessionDescription} + import ExWebRTC.Utils + @type peer_connection() :: GenServer.server() @type offer_options() :: [ice_restart: boolean()] @@ -20,45 +24,49 @@ defmodule ExWebRTC.PeerConnection do :current_remote_desc, :pending_remote_desc, :ice_agent, + :ice_state, + :dtls_client, + :dtls_buffered_packets, + dtls_finished: false, transceivers: [], signaling_state: :stable ] - @dummy_sdp " - v=0\r\n - o=- 7596991810024734139 2 IN IP4 127.0.0.1\r\n - s=-\r\n - t=0 0\r\n - a=group:BUNDLE 0\r\n - a=extmap-allow-mixed\r\n - a=msid-semantic: WMS\r\n - m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126\r\n - c=IN IP4 0.0.0.0\r\n - a=rtcp:9 IN IP4 0.0.0.0\r\n - a=ice-ufrag:vx/1\r\n - a=ice-pwd:ldFUrCsXvndFY2L1u0UQ7ikf\r\n - a=ice-options:trickle\r\n - a=fingerprint:sha-256 76:61:77:1E:7C:2E:BB:CD:19:B5:27:4E:A7:40:84:06:6B:17:97:AB:C4:61:90:16:EE:96:9F:9E:BD:42:96:3D\r\n - a=setup:passive\r\n - a=mid:0\r\n - a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n - a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n - a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n - a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n - a=recvonly\r\n - a=rtcp-mux\r\n - a=rtpmap:111 opus/48000/2\r\n - a=rtcp-fb:111 transport-cc\r\n - a=fmtp:111 minptime=10;useinbandfec=1\r\n - a=rtpmap:63 red/48000/2\r\n - a=fmtp:63 111/111\r\n - a=rtpmap:9 G722/8000\r\n - a=rtpmap:0 PCMU/8000\r\n - a=rtpmap:8 PCMA/8000\r\n - a=rtpmap:13 CN/8000\r\n - a=rtpmap:110 telephone-event/48000\r\n - a=rtpmap:126 telephone-event/8000\r\n - " + @dummy_sdp """ + v=0 + o=- 7596991810024734139 2 IN IP4 127.0.0.1 + s=- + t=0 0 + a=group:BUNDLE 0 + a=extmap-allow-mixed + a=msid-semantic: WMS + m=audio 9 UDP/TLS/RTP/SAVPF 111 63 9 0 8 13 110 126 + c=IN IP4 0.0.0.0 + a=rtcp:9 IN IP4 0.0.0.0 + a=ice-ufrag:vx/1 + a=ice-pwd:ldFUrCsXvndFY2L1u0UQ7ikf + a=ice-options:trickle + a=fingerprint:sha-256 76:61:77:1E:7C:2E:BB:CD:19:B5:27:4E:A7:40:84:06:6B:17:97:AB:C4:61:90:16:EE:96:9F:9E:BD:42:96:3D + a=setup:passive + a=mid:0 + a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level + a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time + a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 + a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid + a=recvonly + a=rtcp-mux + a=rtpmap:111 opus/48000/2 + a=rtcp-fb:111 transport-cc + a=fmtp:111 minptime=10;useinbandfec=1 + a=rtpmap:63 red/48000/2 + a=fmtp:63 111/111 + a=rtpmap:9 G722/8000 + a=rtpmap:0 PCMU/8000 + a=rtpmap:8 PCMA/8000 + a=rtpmap:13 CN/8000 + a=rtpmap:110 telephone-event/48000 + a=rtpmap:126 telephone-event/8000 + """ #### API #### @@ -114,8 +122,15 @@ defmodule ExWebRTC.PeerConnection do |> Enum.filter(&String.starts_with?(&1, "stun:")) {:ok, ice_agent} = ICEAgent.start_link(:controlled, stun_servers: stun_servers) + {:ok, dtls_client} = ExDTLS.start_link(client_mode: false, dtls_srtp: true) + + state = %__MODULE__{ + owner: owner, + config: config, + ice_agent: ice_agent, + dtls_client: dtls_client + } - state = %__MODULE__{owner: owner, config: config, ice_agent: ice_agent} {:ok, state} end @@ -129,14 +144,24 @@ defmodule ExWebRTC.PeerConnection do when state.signaling_state in [:have_remote_offer, :have_local_pranswer] do {:ok, ufrag, pwd} = ICEAgent.get_local_credentials(state.ice_agent) + {:ok, dtls_fingerprint} = ExDTLS.get_cert_fingerprint(state.dtls_client) + sdp = ExSDP.parse!(@dummy_sdp) media = hd(sdp.media) attrs = Enum.map(media.attributes, fn - {:ice_ufrag, _} -> {:ice_ufrag, ufrag} - {:ice_pwd, _} -> {:ice_pwd, pwd} - other -> other + {:ice_ufrag, _} -> + {:ice_ufrag, ufrag} + + {:ice_pwd, _} -> + {:ice_pwd, pwd} + + {:fingerprint, {hash_function, _}} -> + {:fingerprint, {hash_function, hex_dump(dtls_fingerprint)}} + + other -> + other end) media = Map.put(media, :attributes, attrs) @@ -188,6 +213,16 @@ defmodule ExWebRTC.PeerConnection do {:reply, :ok, state} end + @impl true + def handle_info({:ex_ice, _from, :connected}, state) do + if state.dtls_buffered_packets do + Logger.debug("Sending buffered DTLS packets") + ICEAgent.send_data(state.ice_agent, state.dtls_buffered_packets) + end + + {:noreply, %__MODULE__{state | ice_state: :connected, dtls_buffered_packets: nil}} + end + @impl true def handle_info({:ex_ice, _from, {:new_candidate, candidate}}, state) do candidate = %IceCandidate{ @@ -202,9 +237,56 @@ defmodule ExWebRTC.PeerConnection do {:noreply, state} end + @impl true + def handle_info({:ex_ice, _from, {:data, data}}, %{dtls_finished: false} = state) do + case ExDTLS.process(state.dtls_client, data) do + {:handshake_packets, packets} when state.ice_state in [:connected, :completed] -> + :ok = ICEAgent.send_data(state.ice_agent, packets) + {:noreply, state} + + {:handshake_packets, packets} -> + Logger.debug(""" + Generated local DTLS packets but ICE is not in the connected or completed state yet. + We will send those packets once ICE is ready. + """) + + {:noreply, %__MODULE__{state | dtls_buffered_packets: packets}} + + {:handshake_finished, _keying_material, packets} -> + Logger.debug("DTLS handshake finished") + ICEAgent.send_data(state.ice_agent, packets) + {:noreply, %__MODULE__{state | dtls_finished: true}} + + {:handshake_finished, _keying_material} -> + Logger.debug("DTLS handshake finished") + {:noreply, %__MODULE__{state | dtls_finished: true}} + + :handshake_want_read -> + {:noreply, state} + end + end + + @impl true + def handle_info({:ex_dtls, _from, {:retransmit, packets}}, state) + when state.ice_state in [:connected, :completed] do + ICEAgent.send_data(state.ice_agent, packets) + {:noreply, state} + end + + @impl true + def handle_info( + {:ex_dtls, _from, {:retransmit, packets}}, + %{dtls_buffered_packets: packets} = state + ) do + # we got DTLS packets from the other side but + # we haven't established ICE connection yet so + # packets to retransmit have to be the same as dtls_buffered_packets + {:noreply, state} + end + @impl true def handle_info(msg, state) do - IO.inspect(msg, label: :OTHER_MSG) + Logger.info("OTHER MSG #{inspect(msg)}") {:noreply, state} end diff --git a/lib/ex_webrtc/utils.ex b/lib/ex_webrtc/utils.ex new file mode 100644 index 00000000..2d6d9b43 --- /dev/null +++ b/lib/ex_webrtc/utils.ex @@ -0,0 +1,10 @@ +defmodule ExWebRTC.Utils do + @moduledoc false + + @spec hex_dump(binary()) :: String.t() + def hex_dump(binary) do + binary + |> :binary.bin_to_list() + |> Enum.map_join(":", &Base.encode16(<<&1>>)) + end +end diff --git a/mix.exs b/mix.exs index 49a74684..e864894a 100644 --- a/mix.exs +++ b/mix.exs @@ -45,12 +45,15 @@ defmodule ExWebRTC.MixProject do defp deps do [ + {:ex_sdp, "~> 0.11"}, + {:ex_ice, "~> 0.1"}, + {:ex_dtls, "~> 0.13"}, + + # dev/test {:excoveralls, "~> 0.14", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.30", only: :dev, runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, - {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, - {:ex_sdp, "~> 0.11"}, - {:ex_ice, "~> 0.1"} + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false} ] end diff --git a/mix.lock b/mix.lock index 51dc4eb8..2e194d91 100644 --- a/mix.lock +++ b/mix.lock @@ -1,20 +1,37 @@ %{ "bunch": {:hex, :bunch, "1.6.0", "4775f8cdf5e801c06beed3913b0bd53fceec9d63380cdcccbda6be125a6cfd54", [:mix], [], "hexpm", "ef4e9abf83f0299d599daed3764d19e8eac5d27a5237e5e4d5e2c129cfeb9a22"}, + "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"}, + "bundlex": {:hex, :bundlex, "1.2.0", "a89869208a019376a38e8a10e1bd573dcbeae8addd381c2cd74e2817010bef8f", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, "~> 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:secure_random, "~> 0.5", [hex: :secure_random, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "d2182b91a2a53847baadf4745ad2291853e786ad28671f474a611e7703dbca9b"}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, + "castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"}, "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, "dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"}, "earmark_parser": {:hex, :earmark_parser, "1.4.34", "b0fbb4fd333ee7e9babc07e9573796850759cd12796fcf2fec59cf0031cbaad9", [:mix], [], "hexpm", "cc0d7a6f2367e4504867b4ec38ceee24e89ee6bca9c7b94a6d940f54aba2e8d5"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.30.6", "5f8b54854b240a2b55c9734c4b1d0dd7bdd41f71a095d42a70445c03cf05a281", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "bd48f2ddacf4e482c727f9293d9498e0881597eae6ddc3d9562bd7923375109f"}, + "ex_dtls": {:hex, :ex_dtls, "0.13.0", "4d7631eefc19a8820d4f79883f379ff2ad642976bda55493d4ec4e5d10d6c078", [:mix], [{:unifex, "~> 1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "3ece30967006ec12a4088e60514cb08847814fba8b8a21aca3862e5d1fd4a6bc"}, "ex_ice": {:hex, :ex_ice, "0.1.0", "2653c884872d8769cf9fc655c74002a63ed6c21be1b3c2badfa42bdc74de2355", [:mix], [{:ex_stun, "~> 0.1.0", [hex: :ex_stun, repo: "hexpm", optional: false]}], "hexpm", "e2539a321f87f31997ba974d532d00511e5828f2f113b550b1ef6aa799dd2ffe"}, "ex_sdp": {:hex, :ex_sdp, "0.11.0", "19e3af1d70b945381752db3139dfc22a19da1e9394036721449b7fb8c49fe039", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "7a3fe42f4ec0c18de09b10464829c27482d81d9c50c21bdebdbcfe17d2046408"}, "ex_stun": {:hex, :ex_stun, "0.1.0", "252474bf4c8519fbf4bc0fbfc6a1b846a634b1478c65dbbfb4b6ab4e33c2a95a", [:mix], [], "hexpm", "629fc8be45b624a92522f81d85ba001877b1f0745889a2419bdb678790d7480c"}, "excoveralls": {:hex, :excoveralls, "0.17.1", "83fa7906ef23aa7fc8ad7ee469c357a63b1b3d55dd701ff5b9ce1f72442b2874", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "95bc6fda953e84c60f14da4a198880336205464e75383ec0f570180567985ae0"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, + "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, + "nimble_options": {:hex, :nimble_options, "1.0.2", "92098a74df0072ff37d0c12ace58574d26880e522c22801437151a159392270e", [:mix], [], "hexpm", "fd12a8db2021036ce12a309f26f564ec367373265b53e25403f0ee697380f1b8"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, + "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, + "req": {:hex, :req, "0.4.4", "a17b6bec956c9af4f08b5d8e8a6fc6e4edf24ccc0ac7bf363a90bba7a0f0138c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2618c0493444fee927d12073afb42e9154e766b3f4448e1011f0d3d551d1a011"}, + "secure_random": {:hex, :secure_random, "0.5.1", "c5532b37c89d175c328f5196a0c2a5680b15ebce3e654da37129a9fe40ebf51b", [:mix], [], "hexpm", "1b9754f15e3940a143baafd19da12293f100044df69ea12db5d72878312ae6ab"}, + "shmex": {:hex, :shmex, "0.5.0", "7dc4fb1a8bd851085a652605d690bdd070628717864b442f53d3447326bcd3e8", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "b67bb1e22734758397c84458dbb746519e28eac210423c267c7248e59fc97bdc"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "unifex": {:hex, :unifex, "1.1.0", "26b1bcb6c3b3454e1ea15f85b2e570aaa5b5c609566aa9f5c2e0a8b213379d6b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "d8f47e9e3240301f5b20eec5792d1d4341e1a3a268d94f7204703b48da4aaa06"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, + "zarex": {:hex, :zarex, "1.0.3", "a9e9527a1c31df7f39499819bd76ccb15b0b4e479eed5a4a40db9df7ad7db25c", [:mix], [], "hexpm", "4400a7d33bbf222383ce9a3d5ec9411798eb2b12e86c65ad8e6ac08d8116ca8b"}, }