├── config ├── dev.exs ├── prod.exs ├── test.exs └── config.exs ├── test ├── test_helper.exs ├── helper │ ├── format.ex │ ├── header.ex │ ├── attribute.ex │ ├── credentials.ex │ ├── params.ex │ └── xor_mapped_address.ex └── jerboa │ ├── format │ ├── body │ │ ├── attribute │ │ │ ├── data_test.exs │ │ │ ├── dont_fragment_test.exs │ │ │ ├── realm_test.exs │ │ │ ├── nonce_test.exs │ │ │ ├── lifetime_test.exs │ │ │ ├── username_test.exs │ │ │ ├── reservation_token_test.exs │ │ │ ├── even_port_test.exs │ │ │ ├── requested_transport_test.exs │ │ │ ├── channel_number_test.exs │ │ │ ├── error_code_test.exs │ │ │ └── xor_address_test.exs │ │ └── attribute_test.exs │ ├── body_test.exs │ ├── head_test.exs │ └── message_integrity_test.exs │ ├── client │ ├── protocol │ │ ├── send_test.exs │ │ ├── binding_test.exs │ │ ├── data_test.exs │ │ ├── create_permission_test.exs │ │ ├── channel_bind_test.exs │ │ ├── refresh_test.exs │ │ └── allocate_test.exs │ ├── protocol_test.exs │ └── relay_test.exs │ ├── params_test.exs │ └── format_test.exs ├── coveralls.json ├── .ebert.yml ├── lib ├── jerboa │ ├── format │ │ ├── header │ │ │ ├── magic_cookie.ex │ │ │ ├── identifier.ex │ │ │ ├── length.ex │ │ │ └── type.ex │ │ ├── body │ │ │ ├── attribute │ │ │ │ ├── xor_peer_address.ex │ │ │ │ ├── xor_mapped_address.ex │ │ │ │ ├── xor_relayed_address.ex │ │ │ │ ├── data.ex │ │ │ │ ├── dont_fragment.ex │ │ │ │ ├── nonce.ex │ │ │ │ ├── realm.ex │ │ │ │ ├── lifetime.ex │ │ │ │ ├── username.ex │ │ │ │ ├── even_port.ex │ │ │ │ ├── reservation_token.ex │ │ │ │ ├── channel_number.ex │ │ │ │ ├── requested_transport.ex │ │ │ │ ├── xor_address.ex │ │ │ │ └── error_code.ex │ │ │ └── attribute.ex │ │ ├── meta.ex │ │ ├── header.ex │ │ ├── body.ex │ │ └── message_integrity.ex │ ├── client │ │ ├── relay │ │ │ ├── channel.ex │ │ │ └── channels.ex │ │ ├── application.ex │ │ ├── supervisor.ex │ │ ├── protocol │ │ │ ├── data.ex │ │ │ ├── send.ex │ │ │ ├── refresh.ex │ │ │ ├── binding.ex │ │ │ ├── create_permission.ex │ │ │ ├── channel_bind.ex │ │ │ └── allocate.ex │ │ ├── transaction.ex │ │ ├── credentials.ex │ │ ├── protocol.ex │ │ └── relay.ex │ ├── channel_data.ex │ ├── params.ex │ └── format.ex └── jerboa.ex ├── .travis └── script.sh ├── LICENSE ├── .travis.yml ├── .gitignore ├── CHANGELOG.md ├── mix.exs ├── README.md ├── mix.lock └── .credo.exs /config/dev.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /config/prod.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, 4 | level: :warn 5 | -------------------------------------------------------------------------------- /coveralls.json: -------------------------------------------------------------------------------- 1 | { 2 | "skip_files": [ 3 | "lib/jerboa/client.ex", 4 | "lib/jerboa/client/" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.ebert.yml: -------------------------------------------------------------------------------- 1 | styleguide: false 2 | engines: 3 | credo: 4 | enabled: true 5 | fixme: 6 | enabled: true 7 | remark-lint: 8 | enabled: true 9 | exclude_paths: 10 | - config 11 | -------------------------------------------------------------------------------- /lib/jerboa/format/header/magic_cookie.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Header.MagicCookie do 2 | @moduledoc false 3 | 4 | def encode, do: <> 5 | 6 | def value, do: 0x2112A442 7 | end 8 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :logger, 4 | handle_sasl_reports: true, 5 | level: :info 6 | 7 | config :logger, :console, 8 | metadata: [:jerboa_client, :jerboa_server] 9 | 10 | import_config "#{Mix.env}.exs" 11 | -------------------------------------------------------------------------------- /.travis/script.sh: -------------------------------------------------------------------------------- 1 | PRESET=$1 2 | 3 | case $PRESET in 4 | test) 5 | MIX_ENV=test mix coveralls.travis --include system 6 | ;; 7 | dialyzer) 8 | mix dialyzer 9 | ;; 10 | *) 11 | echo "Invalid preset: $PRESET" 12 | exit 1 13 | ;; 14 | esac 15 | -------------------------------------------------------------------------------- /lib/jerboa.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa do 2 | @moduledoc """ 3 | STUN/TURN encoder, decoder and client library 4 | 5 | Jerboa consists of two components: 6 | * `Jerboa.Format` an encoding & decoding library for the STUN wire format 7 | * `Jerboa.Client` an Elixir STUN/TURN client 8 | """ 9 | 10 | end 11 | -------------------------------------------------------------------------------- /lib/jerboa/client/relay/channel.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Relay.Channel do 2 | @moduledoc false 3 | 4 | alias Jerboa.Client 5 | alias Jerboa.Format 6 | 7 | defstruct [:peer, :number, :timer_ref] 8 | 9 | @type t :: %__MODULE__{ 10 | peer: Client.address, 11 | number: Format.channel_number, 12 | timer_ref: reference 13 | } 14 | end 15 | -------------------------------------------------------------------------------- /test/helper/format.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Test.Helper.Format do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | 6 | def binding_request do 7 | Params.new() 8 | |> Params.put_class(:request) 9 | |> Params.put_method(:binding) 10 | end 11 | 12 | def bytes_for_body(<<_::16, x::16, _::128, _::size(x)-bytes>>) do 13 | x 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/xor_peer_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.XORPeerAddress do 2 | @moduledoc """ 3 | XOR-PEER-ADDRESS attribute as defined in the 4 | [TURN RFC](https://trac.tools.ietf.org/html/rfc5766#section-14.3) 5 | """ 6 | 7 | alias Jerboa.Format.Body.Attribute.XORAddress 8 | 9 | use XORAddress, type_code: 0x0012 10 | end 11 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/xor_mapped_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.XORMappedAddress do 2 | @moduledoc """ 3 | XOR Mapped Address attribute as defined in the [STUN 4 | RFC](https://tools.ietf.org/html/rfc5389#section-15.2) 5 | """ 6 | 7 | alias Jerboa.Format.Body.Attribute.XORAddress 8 | 9 | use XORAddress, type_code: 0x0020 10 | end 11 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/xor_relayed_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.XORRelayedAddress do 2 | @moduledoc """ 3 | XOR-RELAYED-ADDRESS attribute as defined in the 4 | [TURN RFC](https://trac.tools.ietf.org/html/rfc5766#section-14.5) 5 | """ 6 | 7 | alias Jerboa.Format.Body.Attribute.XORAddress 8 | 9 | use XORAddress, type_code: 0x0016 10 | end 11 | -------------------------------------------------------------------------------- /lib/jerboa/format/header/identifier.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Header.Identifier do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format.Meta 6 | 7 | @bit_size 96 8 | 9 | @spec encode(Meta.t) :: binary 10 | def encode(%Meta{params: %Params{identifier: x}}) 11 | when is_binary(x) and bit_size(x) === @bit_size do 12 | x 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/jerboa/client/application.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Application do 2 | @moduledoc false 3 | 4 | use Application 5 | 6 | def start(_, _) do 7 | Supervisor.start_link(children(), options()) 8 | end 9 | 10 | defp children do 11 | import Supervisor.Spec, warn: false 12 | [supervisor(Jerboa.Client.Supervisor, [])] 13 | end 14 | 15 | defp options do 16 | [strategy: :one_for_one, name: Jerboa.Client.Application] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/jerboa/client/supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Supervisor do 2 | @moduledoc false 3 | 4 | use Supervisor 5 | 6 | def start_link do 7 | Supervisor.start_link(__MODULE__, [], name: __MODULE__) 8 | end 9 | 10 | def init([]) do 11 | supervise(children(), strategy: :simple_one_for_one) 12 | end 13 | 14 | defp children do 15 | import Supervisor.Spec, warn: false 16 | [worker(Jerboa.Client.Worker, [], restart: :transient)] 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/helper/header.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Test.Helper.Header do 2 | @moduledoc false 3 | 4 | def first_2_bits(<>) do 5 | x 6 | end 7 | 8 | def type(<<_::2, x::14, _::144>>) do 9 | x 10 | end 11 | 12 | def magic_cookie(<<_::32, x::32, _::96>>) do 13 | x 14 | end 15 | 16 | def identifier(<<_::64, x::96-bits>>) do 17 | x 18 | end 19 | 20 | def identifier do 21 | :crypto.strong_rand_bytes(div(96, 8)) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/jerboa/channel_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.ChannelData do 2 | @moduledoc """ 3 | Data structure representing data sent over TURN channel 4 | """ 5 | 6 | defstruct [:channel_number, :data] 7 | 8 | @typedoc """ 9 | The main data structure representing data sent over TURN channel 10 | 11 | * `:number` is a number of channel which data came from 12 | * `:data` is a raw binary data 13 | """ 14 | @type t :: %__MODULE__{ 15 | channel_number: Jerboa.Format.channel_number, 16 | data: binary 17 | } 18 | end 19 | -------------------------------------------------------------------------------- /lib/jerboa/format/header/length.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Header.Length do 2 | @moduledoc false 3 | 4 | alias Jerboa.Format.Last2BitsError 5 | alias Jerboa.Format.Meta 6 | 7 | @spec encode(Meta.t) :: length :: binary 8 | def encode(%Meta{body: b}) when is_binary(b) do 9 | <> 10 | end 11 | 12 | def decode(<<_::14, 0::2>> = x) do 13 | {:ok, :binary.decode_unsigned x} 14 | end 15 | def decode(bin_length) do 16 | <> = bin_length 17 | {:error, Last2BitsError.exception(length: length)} 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/helper/attribute.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Test.Helper.Attribute do 2 | @moduledoc false 3 | 4 | def total(x) do 5 | x |> Keyword.values |> Enum.sum 6 | end 7 | 8 | def length_correct?(<<_::16, byte_length::16, _::size(byte_length)-bytes>>, byte_length) do 9 | true 10 | end 11 | def length_correct?(_, _), do: false 12 | 13 | def type(<>), do: type 14 | 15 | def value(<<_::32, val::binary>>), do: val 16 | 17 | def padding_length(value_length) do 18 | case rem(value_length, 4) do 19 | 0 -> 0 20 | n -> 4 - n 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-2017 Erlang Solutions Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | 3 | install: 4 | - mix local.rebar --force 5 | - mix local.hex --force 6 | - mix deps.get 7 | - mix compile 8 | - MIX_ENV=test mix compile 9 | 10 | script: 11 | .travis/script.sh $PRESET 12 | 13 | after_script: 14 | mix inch.report 15 | 16 | elixir: 17 | - 1.4.0 18 | otp_release: 19 | - 19.3 20 | - 18.3 21 | env: 22 | - PRESET=test 23 | 24 | matrix: 25 | include: 26 | - otp_release: 19.3 27 | elixir: 1.4.0 28 | env: PRESET=dialyzer 29 | 30 | cache: 31 | directories: 32 | - .dialyzer 33 | 34 | branches: 35 | only: 36 | - master 37 | -------------------------------------------------------------------------------- /test/helper/credentials.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Test.Helper.Credentials do 2 | @moduledoc false 3 | 4 | alias Jerboa.Client.Credentials 5 | 6 | @username "alice" 7 | @realm "wonderlan" 8 | @nonce "abcd" 9 | @secret "1234" 10 | 11 | @invalid_nonce "dcba" 12 | 13 | def final do 14 | %Credentials.Final{username: @username, realm: @realm, 15 | nonce: @nonce, secret: @secret} 16 | end 17 | 18 | def initial do 19 | %Credentials.Initial{username: @username, secret: @secret} 20 | end 21 | 22 | def invalid_nonce, do: @invalid_nonce 23 | 24 | def valid_nonce, do: @nonce 25 | end 26 | -------------------------------------------------------------------------------- /test/helper/params.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Test.Helper.Params do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format.Body.Attribute.{Username, Realm, Nonce} 6 | 7 | def username(params) do 8 | case Params.get_attr(params, Username) do 9 | %{value: u} -> u 10 | nil -> nil 11 | end 12 | end 13 | 14 | def realm(params) do 15 | case Params.get_attr(params, Realm) do 16 | %{value: r} -> r 17 | nil -> nil 18 | end 19 | end 20 | 21 | def nonce(params) do 22 | case Params.get_attr(params, Nonce) do 23 | %{value: n} -> n 24 | nil -> nil 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/helper/xor_mapped_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Test.Helper.XORMappedAddress do 2 | @moduledoc false 3 | 4 | alias Jerboa.Format.Body.Attribute.XORMappedAddress 5 | 6 | def struct(4) do 7 | struct(:ipv4, ip_4_a(), port()) 8 | end 9 | def struct(6) do 10 | struct(:ipv6, ip_6_a(), port()) 11 | end 12 | 13 | def ip_4_a do 14 | {0, 0, 0, 0} 15 | end 16 | 17 | def ip_6_a do 18 | :erlang.make_tuple(8, 0) 19 | end 20 | 21 | def port, do: 0 22 | 23 | def i do 24 | :crypto.strong_rand_bytes(div(96, 8)) 25 | end 26 | 27 | defp struct(f, a, p) do 28 | %XORMappedAddress{family: f, address: a, port: p} 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Emacs temporary files 23 | **/*~ 24 | **/#* 25 | **/.#* 26 | 27 | # Dialyzer PLTs 28 | /.dialyzer 29 | -------------------------------------------------------------------------------- /lib/jerboa/client/protocol/data.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.Data do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 6 | alias Jerboa.Format.Body.Attribute.Data 7 | alias Jerboa.Client 8 | 9 | @spec eval_indication(Params.t) 10 | :: {:ok, peer :: Client.address, binary} | :error 11 | def eval_indication(params) do 12 | with :indication <- Params.get_class(params), 13 | :data <- Params.get_method(params), 14 | %Data{content: data} <- Params.get_attr(params, Data), 15 | %XPA{address: addr, port: port} <- Params.get_attr(params, XPA) do 16 | {:ok, {addr, port}, data} 17 | else 18 | _ -> :error 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/jerboa/client/relay/channels.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Relay.Channels do 2 | @moduledoc false 3 | 4 | alias Jerboa.Client 5 | alias Jerboa.Client.Relay.Channel 6 | alias Jerboa.Format 7 | 8 | defstruct locked_peers: MapSet.new(), locked_numbers: MapSet.new(), 9 | lock_timer_refs: %{}, by_peer: %{}, by_number: %{} 10 | 11 | @type t :: %__MODULE__{ 12 | locked_peers: MapSet.t(peer :: Client.address), 13 | locked_numbers: MapSet.t(Format.channel_number), 14 | ## since we always atomically lock both peer and channel number, 15 | ## we can use one timer to unlock them both 16 | lock_timer_refs: %{Format.channel_number => timer_ref :: reference}, 17 | by_peer: %{peer :: Client.address => Channel.t}, 18 | by_number: %{Format.channel_number => Channel.t} 19 | } 20 | end 21 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.DataTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.Data 6 | alias Jerboa.Format.Meta 7 | 8 | test "decode/1 DATA attribute" do 9 | ptest content: string() do 10 | assert {:ok, _, %Data{content: ^content}} = Data.decode(content, %Meta{}) 11 | end 12 | end 13 | 14 | describe "encode/1" do 15 | test "DATA attribute with binary content" do 16 | ptest content: string() do 17 | assert content == %Data{content: content} |> Data.encode() 18 | end 19 | end 20 | 21 | test "DATA attribute with non-binary content" do 22 | assert_raise FunctionClauseError, fn -> 23 | %Data{content: 1} |> Data.encode() 24 | end 25 | end 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /lib/jerboa/client/protocol/send.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.Send do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format 6 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 7 | alias Jerboa.Format.Body.Attribute.Data 8 | alias Jerboa.Client 9 | alias Jerboa.Client.Protocol 10 | 11 | @spec indication(peer :: Client.address, data :: binary) 12 | :: Protocol.indication 13 | def indication(peer, data) do 14 | peer 15 | |> params(data) 16 | |> Format.encode() 17 | end 18 | 19 | @spec params(Client.address, binary) :: Params.t 20 | defp params({address, port}, data) do 21 | Params.new() 22 | |> Params.put_class(:indication) 23 | |> Params.put_method(:send) 24 | |> Params.put_attr(XPA.new(address, port)) 25 | |> Params.put_attr(%Data{content: data}) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/dont_fragment_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.DontFragmentTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.DontFragment 6 | alias Jerboa.Format.DontFragment.ValuePresentError 7 | alias Jerboa.Format.Meta 8 | 9 | describe "decode/1" do 10 | test "DONT-FRAGMENT attribute without a value (valid)" do 11 | value = <<>> 12 | assert {:ok, _, %DontFragment{}} = DontFragment.decode(value, %Meta{}) 13 | end 14 | 15 | test "DONT-FRAGMENT attribute with a value (invalid)" do 16 | ptest value: string(min: 1) do 17 | assert {:error, %ValuePresentError{}} = DontFragment.decode(value, %Meta{}) 18 | end 19 | end 20 | end 21 | 22 | test "encode/0 DONT-FRAGMENT attribute" do 23 | assert <<>> = DontFragment.encode() 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /test/jerboa/client/protocol/send_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.SendTest do 2 | use ExUnit.Case 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format 6 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 7 | alias Jerboa.Format.Body.Attribute.Data 8 | alias Jerboa.Client.Protocol.Send 9 | 10 | test "indication/2 returns encoded, not signed Send indication" do 11 | peer_addr = {127, 0, 0, 1} 12 | peer_port = 12_345 13 | data = "alicehasacat" 14 | 15 | indication = Send.indication({peer_addr, peer_port}, data) 16 | params = Format.decode!(indication) 17 | 18 | assert params.class == :indication 19 | assert params.method == :send 20 | refute params.signed? 21 | assert %XPA{address: ^peer_addr, port: ^peer_port} = 22 | Params.get_attr(params, XPA) 23 | assert %Data{content: data} == Params.get_attr(params, Data) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/jerboa/client/transaction.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Transaction do 2 | @moduledoc false 3 | ## Describes transaction to be sent and handled in the future 4 | 5 | alias Jerboa.Params 6 | alias Jerboa.Client.Credentials 7 | alias Jerboa.Client.Relay 8 | 9 | defstruct [:caller, :handler, :id, :context] 10 | 11 | @type caller :: GenServer.from 12 | @type id :: binary 13 | @type context :: map 14 | @type handler :: (response :: Params.t, Credentials.t, Relay.t, 15 | context -> result) 16 | @type result :: {reply :: term, Credentials.t, Relay.t} 17 | 18 | @type t :: %__MODULE__{ 19 | caller: GenServer.from, 20 | id: id, 21 | handler: handler, 22 | context: context 23 | } 24 | 25 | @spec new(caller, id, handler) :: t 26 | @spec new(caller, id, handler, context) :: t 27 | def new(caller, id, handler, context \\ %{}) do 28 | %__MODULE__{caller: caller, id: id, handler: handler, context: context} 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## v0.3.0 - 2017-05-30 9 | 10 | ### Added 11 | * encoding and decoding of ChannelData messages 12 | * encoding and decoding of ChannelBind method and CHANNEL-NUMBER attribute 13 | * support for TURN channels mechanism for data exchange (`Jerboa.Client.open_channel/2`) 14 | 15 | ### Fixed 16 | * multiple bugs around permissions by changing the design of acknowledging permissions - permissions are 17 | tracked only after a successful CreatePermission arrives from the server 18 | 19 | ## v0.2.0 - 2017-05-16 20 | 21 | ### Added 22 | * encoding and decoding of TURN attributes and methods (without channels) 23 | * TURN client behaviour - allocations, permissions, sending and receiving data over relay 24 | 25 | ## v0.1.0 - 2017-02-21 26 | 27 | ### Added 28 | * encoding and decoding of STUN messages format, header validation 29 | * encoding and decoding of XOR-MAPPED-ADDRESS attribute 30 | * basic STUN client utilities - sending Binding request and indication 31 | -------------------------------------------------------------------------------- /lib/jerboa/client/protocol/refresh.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.Refresh do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format.Body.Attribute.Lifetime 6 | alias Jerboa.Client 7 | alias Jerboa.Client.Protocol 8 | alias Jerboa.Client.Credentials 9 | 10 | @spec request(Credentials.t) :: Protocol.request 11 | def request(creds) do 12 | params = params(creds) 13 | Protocol.encode_request(params, creds) 14 | end 15 | 16 | @spec eval_response(response :: Params.t, Credentials.t) 17 | :: {:ok, lifetime :: non_neg_integer} 18 | | {:error, Client.error, Credentials.t} 19 | def eval_response(params, creds) do 20 | with :refresh <- Params.get_method(params), 21 | :success <- Params.get_class(params), 22 | %{duration: lifetime} <- Params.get_attr(params, Lifetime) do 23 | {:ok, lifetime} 24 | else 25 | :failure -> 26 | Protocol.eval_failure(params, creds) 27 | _ -> 28 | {:error, :bad_response, creds} 29 | end 30 | end 31 | 32 | @spec params(Credentials.t) :: Params.t 33 | defp params(creds) do 34 | creds 35 | |> Protocol.base_params() 36 | |> Params.put_class(:request) 37 | |> Params.put_method(:refresh) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/jerboa/client/protocol/binding.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.Binding do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format 6 | alias Jerboa.Format.Body.Attribute.XORMappedAddress, as: XMA 7 | alias Jerboa.Client 8 | alias Jerboa.Client.Protocol 9 | 10 | @spec request :: Protocol.request 11 | def request do 12 | params = params(:request) 13 | {params.identifier, Format.encode(params)} 14 | end 15 | 16 | @spec eval_response(response :: Params.t) 17 | :: {:ok, mapped_address :: Client.address} | {:error, :bad_response} 18 | def eval_response(params) do 19 | with %{address: addr, port: port} <- Params.get_attr(params, XMA), 20 | :binding <- Params.get_method(params), 21 | :success <- Params.get_class(params) do 22 | {:ok, {addr, port}} 23 | else 24 | _ -> {:error, :bad_response} 25 | end 26 | end 27 | 28 | @spec indication :: Protocol.indication 29 | def indication do 30 | :indication 31 | |> params() 32 | |> Format.encode() 33 | end 34 | 35 | @spec params(:request | :indication) :: Params.t 36 | defp params(class) do 37 | Params.new() 38 | |> Params.put_class(class) 39 | |> Params.put_method(:binding) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/data.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.Data do 2 | @moduledoc """ 3 | DATA attribute as defined in [TURN RFC](https://trac.tools.ietf.org/html/rfc5766#section-14.4) 4 | """ 5 | 6 | alias Jerboa.Format.Body.Attribute.{Decoder,Encoder} 7 | alias Jerboa.Format.Meta 8 | 9 | defstruct content: "" 10 | 11 | @typedoc """ 12 | Represents data sent between client and its peer 13 | """ 14 | @type t :: %__MODULE__{ 15 | content: binary 16 | } 17 | 18 | defimpl Encoder do 19 | alias Jerboa.Format.Body.Attribute.Data 20 | @type_code 0x0013 21 | 22 | @spec type_code(Data.t) :: integer 23 | def type_code(_), do: @type_code 24 | 25 | @spec encode(Data.t, Meta.t) :: {Meta.t, binary} 26 | def encode(attr, meta), do: {meta, Data.encode(attr)} 27 | end 28 | 29 | defimpl Decoder do 30 | alias Jerboa.Format.Body.Attribute.Data 31 | 32 | @spec decode(Data.t, value :: binary, meta :: Meta.t) 33 | :: {:ok, Meta.t, Data.t} | {:error, struct} 34 | def decode(_, value, meta), do: Data.decode(value, meta) 35 | end 36 | 37 | @doc false 38 | def encode(%__MODULE__{content: content}) when is_binary(content), do: content 39 | 40 | @doc false 41 | def decode(value, meta), do: {:ok, meta, %__MODULE__{content: value}} 42 | end 43 | -------------------------------------------------------------------------------- /lib/jerboa/client/protocol/create_permission.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.CreatePermission do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 6 | alias Jerboa.Client 7 | alias Jerboa.Client.Protocol 8 | alias Jerboa.Client.Credentials 9 | 10 | @spec request(Credentials.t, peer_addrs :: [Client.ip, ...]) 11 | :: Protocol.request 12 | def request(creds, peer_addrs) do 13 | params = params(creds, peer_addrs) 14 | Protocol.encode_request(params, creds) 15 | end 16 | 17 | @spec eval_response(response :: Params.t, Credentials.t) 18 | :: :ok | {:error, Client.error, Credentials.t} 19 | def eval_response(params, creds) do 20 | with :create_permission <- Params.get_method(params), 21 | :success <- Params.get_class(params) do 22 | :ok 23 | else 24 | :failure -> 25 | Protocol.eval_failure(params, creds) 26 | _ -> 27 | {:error, :bad_response, creds} 28 | end 29 | end 30 | 31 | @spec params(Credentials.t, [Client.ip, ...]) :: Params.t 32 | defp params(creds, peer_addrs) do 33 | xor_peer_addrs = Enum.map peer_addrs, fn addr -> XPA.new(addr, 0) end 34 | creds 35 | |> Protocol.base_params() 36 | |> Params.put_class(:request) 37 | |> Params.put_method(:create_permission) 38 | |> Params.put_attrs(xor_peer_addrs) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/realm_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.RealmTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.Realm 6 | alias Jerboa.Format.Realm.LengthError 7 | alias Jerboa.Format.Meta 8 | 9 | describe "decode/1" do 10 | test "REALM attribtue of valid length" do 11 | ptest value: string(max: Realm.max_chars) do 12 | assert {:ok, _, %Realm{value: ^value}} = Realm.decode(value, %Meta{}) 13 | end 14 | end 15 | 16 | test "REALM attribute of invalid length" do 17 | length = Realm.max_chars + 1 18 | value = String.duplicate("a", length) 19 | 20 | assert {:error, %LengthError{length: ^length}} = Realm.decode(value, %Meta{}) 21 | end 22 | end 23 | 24 | describe "encode/1" do 25 | test "REALM attribute with string value of valid length" do 26 | ptest value: string(max: Realm.max_chars) do 27 | assert value == %Realm{value: value} |> Realm.encode() 28 | end 29 | end 30 | 31 | test "REALM attribute with string value of invalid length" do 32 | value = String.duplicate("a", Realm.max_chars + 1) 33 | 34 | assert_raise ArgumentError, fn -> 35 | %Realm{value: value} |> Realm.encode() 36 | end 37 | end 38 | 39 | test "REALM attribute with non-string value" do 40 | assert_raise ArgumentError, fn -> 41 | %Realm{value: 123} |> Realm.encode() 42 | end 43 | end 44 | end 45 | 46 | end 47 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/nonce_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.NonceTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.Nonce 6 | alias Jerboa.Format.Nonce.LengthError 7 | alias Jerboa.Format.Meta 8 | 9 | describe "decode/1" do 10 | test "NONCE attribute of valid length" do 11 | ptest value: string(max: Nonce.max_chars) do 12 | assert {:ok, _, %Nonce{value: ^value}} = Nonce.decode(value, %Meta{}) 13 | end 14 | end 15 | 16 | test "NONCE attribute of invalid length" do 17 | length = Nonce.max_chars + 1 18 | value = String.duplicate("a", length) 19 | 20 | assert {:error, %LengthError{length: ^length}} = 21 | Nonce.decode(value, %Meta{}) 22 | end 23 | end 24 | 25 | describe "encode/1" do 26 | test "NONCE attribute with string value of valid length" do 27 | ptest value: string(max: Nonce.max_chars) do 28 | assert value == %Nonce{value: value} |> Nonce.encode() 29 | end 30 | end 31 | 32 | test "NONCE attribute with string value of invalid length" do 33 | value = String.duplicate("a", Nonce.max_chars + 1) 34 | 35 | assert_raise ArgumentError, fn -> 36 | %Nonce{value: value} |> Nonce.encode() 37 | end 38 | end 39 | 40 | test "NONCE attribute with non-string value" do 41 | assert_raise ArgumentError, fn -> 42 | %Nonce{value: 1} |> Nonce.encode() 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/lifetime_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.LifetimeTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.Lifetime 6 | alias Jerboa.Format.Lifetime.LengthError 7 | alias Jerboa.Format.Meta 8 | 9 | describe "decode/1" do 10 | test "LIFETIME attribute with valid length" do 11 | ptest duration: int(min: 0, max: Lifetime.max_duration) do 12 | lifetime = <> 13 | 14 | assert {:ok, _, %Lifetime{duration: ^duration}} = 15 | Lifetime.decode(lifetime, %Meta{}) 16 | end 17 | end 18 | 19 | test "LIFETIME attribute with invalid length" do 20 | ptest length: int(min: 5) do 21 | lifetime = for _ <- 1..length, into: <<>>, do: <> 22 | 23 | assert {:error, %LengthError{length: ^length}} = 24 | Lifetime.decode(lifetime, %Meta{}) 25 | end 26 | end 27 | end 28 | 29 | describe "encode/1" do 30 | test "LIFETIME attribute with valid value" do 31 | ptest duration: int(min: 0, max: Lifetime.max_duration) do 32 | assert <> == %Lifetime{duration: duration} |> Lifetime.encode() 33 | end 34 | end 35 | 36 | test "LIFETIME attribute with invalid value throws FunctionClauseError" do 37 | ptest duration: negative_int() do 38 | assert_raise FunctionClauseError, fn -> 39 | %Lifetime{duration: duration} |> Lifetime.encode() 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/jerboa/client/protocol/channel_bind.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.ChannelBind do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 6 | alias Jerboa.Format.Body.Attribute.ChannelNumber 7 | 8 | alias Jerboa.Client 9 | alias Jerboa.Client.Credentials 10 | alias Jerboa.Client.Protocol 11 | 12 | @spec request(Credentials.t, peer :: Client.address, 13 | Jerboa.Format.channel_number) :: Protocol.request 14 | def request(creds, peer, channel_number) do 15 | params = params(creds, peer, channel_number) 16 | Protocol.encode_request(params, creds) 17 | end 18 | 19 | @spec eval_response(resp :: Params.t, Credentials.t) 20 | :: :ok | {:error, Client.error, Credentials.t} 21 | def eval_response(params, creds) do 22 | with :channel_bind <- Params.get_method(params), 23 | :success <- Params.get_class(params) do 24 | :ok 25 | else 26 | :failure -> 27 | Protocol.eval_failure(params, creds) 28 | _ -> 29 | {:error, :bad_response, creds} 30 | end 31 | end 32 | 33 | ## Internals 34 | 35 | @spec params(Credentials.t, Client.address, Jerboa.Format.channel_number) 36 | :: Params.t 37 | defp params(creds, {ip, port}, channel_number) do 38 | creds 39 | |> Protocol.base_params() 40 | |> Params.put_class(:request) 41 | |> Params.put_method(:channel_bind) 42 | |> Params.put_attr(XPA.new(ip, port)) 43 | |> Params.put_attr(%ChannelNumber{number: channel_number}) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/jerboa/format/meta.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Meta do 2 | @moduledoc false 3 | 4 | ## Struct getting passed along decoding and encoding process, 5 | ## keeping decoded data and metadata used for further encoding 6 | ## and decoding 7 | 8 | alias Jerboa.Params 9 | 10 | # we assign `:params` to `Params.new()` at compile time to avoid 11 | # dialyzer warnings 12 | defstruct [header: <<>>, body: <<>>, length: 0, extra: <<>>, 13 | message_integrity: <<>>, length_up_to_integrity: 0, 14 | options: [], params: Params.new()] 15 | 16 | # Fields 17 | # * `:header`- binary header of a message 18 | # * `:body` - binary body of a message 19 | # * `:length` - length of body as specified in STUN header 20 | # * `:extra` - excess part of binary after `:length` bytes 21 | # (may happen when reading from TCP stream) 22 | # * `:message_integrity` - value of message integrity hash 23 | # extracted from STUN message when decoding 24 | # * `:length_up_to_integrity` - length of a message body up to 25 | # message integrity attribute, in bytes 26 | # * `:options` - additional options passed to encoding and 27 | # decoding 28 | # * `:params` - params being encoded or container for the ones 29 | # being decoded 30 | @type t :: %__MODULE__{ 31 | header: binary, 32 | body: binary, 33 | length: non_neg_integer, 34 | extra: binary, 35 | message_integrity: binary, 36 | length_up_to_integrity: non_neg_integer, 37 | options: Keyword.t, 38 | params: Params.t 39 | } 40 | end 41 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/dont_fragment.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.DontFragment do 2 | @moduledoc """ 3 | DONT-FRAGMENT attribute as defined in [TURN RFC](https://trac.tools.ietf.org/html/rfc5766#section-14.8) 4 | """ 5 | 6 | alias Jerboa.Format.Body.Attribute.{Decoder,Encoder} 7 | alias Jerboa.Format.DontFragment.ValuePresentError 8 | alias Jerboa.Format.Meta 9 | 10 | defstruct [] 11 | 12 | @typedoc """ 13 | Represent DONT-FRAGMENT attribute 14 | 15 | This attribute doesn't have any value associated with it. 16 | """ 17 | @type t :: %__MODULE__{} 18 | 19 | defimpl Encoder do 20 | alias Jerboa.Format.Body.Attribute.DontFragment 21 | @type_code 0x001A 22 | 23 | @spec type_code(DontFragment.t) :: integer 24 | def type_code(_), do: @type_code 25 | 26 | @spec encode(DontFragment.t, Meta.t) :: {Meta.t, binary} 27 | def encode(_attr, meta), do: {meta, DontFragment.encode()} 28 | end 29 | 30 | defimpl Decoder do 31 | alias Jerboa.Format.Body.Attribute.DontFragment 32 | 33 | @spec decode(DontFragment.t, value :: binary, meta :: Meta.t) 34 | :: {:ok, Meta.t, DontFragment.t} | {:error, struct} 35 | def decode(_, value, meta), do: DontFragment.decode(value, meta) 36 | end 37 | 38 | @doc false 39 | @spec encode :: <<>> 40 | def encode, do: <<>> 41 | 42 | @doc false 43 | @spec decode(value :: binary, meta :: Meta.t) 44 | :: {:ok, Meta.t, t} | {:error, struct} 45 | def decode(<<>>, meta), do: {:ok, meta, %__MODULE__{}} 46 | def decode(_, _), do: {:error, ValuePresentError.exception()} 47 | end 48 | -------------------------------------------------------------------------------- /lib/jerboa/format/header.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Header do 2 | @moduledoc false 3 | 4 | alias Jerboa.Format.Header.{Type,Length,MagicCookie,Identifier} 5 | alias Jerboa.Format.Meta 6 | 7 | @magic_cookie MagicCookie.encode 8 | 9 | @spec encode(Meta.t) :: Meta.t 10 | def encode(meta) do 11 | type = Type.encode(meta) 12 | length = Length.encode(meta) 13 | id = Identifier.encode(meta) 14 | %{meta | header: encode(type, length, id)} 15 | end 16 | 17 | defp encode(type, length, identifier) do 18 | <<0::2, type::bits, length::bytes, @magic_cookie::bytes, identifier::bytes>> 19 | end 20 | 21 | @spec decode(Meta.t) :: {:ok, Meta.t} | {:error, struct} 22 | def decode(%Meta{header: header, params: params} = meta) do 23 | with {:ok, t, l, id} <- destructure_header(header), 24 | {:ok, class, method} <- Type.decode(t), 25 | {:ok, length} <- Length.decode(l) do 26 | new_params = %{params | class: class, method: method, identifier: id} 27 | new_meta = %{meta | length: length, params: new_params} 28 | {:ok, new_meta} 29 | else 30 | {:error, _} = e -> 31 | e 32 | end 33 | end 34 | 35 | defp destructure_header(<<0::2, t::14-bits, l::16-bits, 36 | @magic_cookie::bytes, id::96-bits>>) do 37 | {:ok, t, l, id} 38 | end 39 | defp destructure_header(<<0::2, _::30, _::128>> = header) do 40 | {:error, Jerboa.Format.MagicCookieError.exception(header: header)} 41 | end 42 | defp destructure_header(<>) do 43 | {:error, Jerboa.Format.First2BitsError.exception(bits: b)} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/jerboa/client/credentials.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Credentials do 2 | @moduledoc false 3 | ## Data structures containing client's authentication data 4 | 5 | defstruct [:username, :realm, :secret, :nonce] 6 | 7 | @type t :: __MODULE__.Initial.t | __MODULE__.Final.t 8 | 9 | defmodule Initial do 10 | @moduledoc false 11 | 12 | defstruct [:username, :secret] 13 | 14 | @type t :: %__MODULE__{ 15 | username: String.t, 16 | secret: String.t 17 | } 18 | end 19 | 20 | defmodule Final do 21 | @moduledoc false 22 | 23 | defstruct [:username, :secret, :realm, :nonce] 24 | 25 | @type t :: %__MODULE__{ 26 | username: String.t, 27 | secret: String.t, 28 | realm: String.t, 29 | nonce: String.t 30 | } 31 | end 32 | 33 | @spec initial(String.t, String.t) :: __MODULE__.Initial.t 34 | def initial(username, secret) 35 | when is_binary(username) and is_binary(secret) do 36 | %Initial{username: username, secret: secret} 37 | end 38 | 39 | @spec finalize(__MODULE__.Initial.t, String.t, String.t) :: __MODULE__.Final.t 40 | def finalize(%Initial{} = creds, realm, nonce) 41 | when is_binary(realm) and is_binary(nonce) do 42 | %Final{ 43 | username: creds.username, 44 | secret: creds.secret, 45 | realm: realm, 46 | nonce: nonce 47 | } 48 | end 49 | 50 | @spec to_decode_opts(t) :: Keyword.t 51 | def to_decode_opts(%Initial{}), do: [] 52 | def to_decode_opts(%Final{} = creds) do 53 | creds |> Map.from_struct() |> Map.to_list() 54 | end 55 | 56 | @spec complete?(t) :: boolean 57 | def complete?(%Initial{}), do: false 58 | def complete?(%Final{}), do: true 59 | end 60 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/nonce.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.Nonce do 2 | @moduledoc """ 3 | NONCE attribute as defined in [STUN RFC](https://tools.ietf.org/html/rfc5389#section-15.8) 4 | """ 5 | 6 | alias Jerboa.Format.Body.Attribute.{Decoder,Encoder} 7 | alias Jerboa.Format.Nonce.LengthError 8 | alias Jerboa.Format.Meta 9 | 10 | defstruct value: "" 11 | 12 | @max_chars 128 13 | 14 | @typedoc """ 15 | Represent nonce's value 16 | """ 17 | @type t :: %__MODULE__{ 18 | value: String.t 19 | } 20 | 21 | defimpl Encoder do 22 | alias Jerboa.Format.Body.Attribute.Nonce 23 | @type_code 0x0015 24 | 25 | @spec type_code(Nonce.t) :: integer 26 | def type_code(_), do: @type_code 27 | 28 | @spec encode(Nonce.t, Meta.t) :: {Meta.t, binary} 29 | def encode(attr, meta), do: {meta, Nonce.encode(attr)} 30 | end 31 | 32 | defimpl Decoder do 33 | alias Jerboa.Format.Body.Attribute.Nonce 34 | 35 | @spec decode(Nonce.t, value :: binary, meta :: Meta.t) 36 | :: {:ok, Meta.t, Nonce.t} | {:error, struct} 37 | def decode(_, value, meta), do: Nonce.decode(value, meta) 38 | end 39 | 40 | @doc false 41 | def encode(%__MODULE__{value: value}) do 42 | if String.valid?(value) && String.length(value) <= @max_chars do 43 | value 44 | else 45 | raise ArgumentError 46 | end 47 | end 48 | 49 | @doc false 50 | def decode(value, meta) do 51 | length = String.length(value) 52 | if String.valid?(value) && length <= @max_chars do 53 | {:ok, meta, %__MODULE__{value: value}} 54 | else 55 | {:error, LengthError.exception(length: length)} 56 | end 57 | end 58 | 59 | @doc false 60 | def max_chars, do: @max_chars 61 | end 62 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/realm.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.Realm do 2 | @moduledoc """ 3 | REALM attribute as defined in [STUN RFC](https://tools.ietf.org/html/rfc5389#section-15.7) 4 | """ 5 | 6 | alias Jerboa.Format.Body.Attribute.{Decoder, Encoder} 7 | alias Jerboa.Format.Realm.LengthError 8 | alias Jerboa.Format.Meta 9 | 10 | defstruct value: "" 11 | 12 | @max_chars 128 13 | 14 | @typedoc """ 15 | Represents realm value used for authentication 16 | """ 17 | @type t :: %__MODULE__{ 18 | value: String.t 19 | } 20 | 21 | defimpl Encoder do 22 | alias Jerboa.Format.Body.Attribute.Realm 23 | @type_code 0x0014 24 | 25 | @spec type_code(Realm.t) :: integer 26 | def type_code(_), do: @type_code 27 | 28 | @spec encode(Realm.t, Meta.t) :: {Meta.t, binary} 29 | def encode(attr, meta), do: {meta, Realm.encode(attr)} 30 | end 31 | 32 | defimpl Decoder do 33 | alias Jerboa.Format.Body.Attribute.Realm 34 | 35 | @spec decode(Realm.t, value :: binary, Meta.t) 36 | :: {:ok, Meta.t, Realm.t} | {:error, struct} 37 | def decode(_, value, meta), do: Realm.decode(value, meta) 38 | end 39 | 40 | @doc false 41 | def encode(%__MODULE__{value: value}) do 42 | if String.valid?(value) && String.length(value) <= @max_chars do 43 | value 44 | else 45 | raise ArgumentError 46 | end 47 | end 48 | 49 | @doc false 50 | def decode(value, meta) do 51 | length = String.length(value) 52 | if String.valid?(value) && length <= @max_chars do 53 | {:ok, meta, %__MODULE__{value: value}} 54 | else 55 | {:error, LengthError.exception(length: length)} 56 | end 57 | end 58 | 59 | @doc false 60 | def max_chars, do: @max_chars 61 | end 62 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/username_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.UsernameTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.Username 6 | alias Jerboa.Format.Username.LengthError 7 | alias Jerboa.Format.Meta 8 | 9 | describe "decode/1" do 10 | test "USERNAME attribute of valid length" do 11 | ptest value: string(max: Username.max_length, chars: :ascii) do 12 | assert {:ok, _, %Username{value: ^value}} = Username.decode(value, %Meta{}) 13 | end 14 | end 15 | 16 | test "USERNAME attribute of invalid length" do 17 | length = Username.max_length + 1 18 | value = String.duplicate("a", length) 19 | 20 | assert {:error, %LengthError{length: ^length}} = Username.decode(value, %Meta{}) 21 | end 22 | end 23 | 24 | describe "encode/1" do 25 | test "USERNAME attribute with string value of valid length" do 26 | ptest value: string(max: Username.max_length, chars: :ascii) do 27 | assert value == %Username{value: value} |> Username.encode() 28 | end 29 | end 30 | 31 | test "USERNAME attribute with string value of invalid length" do 32 | value = String.duplicate("a", Username.max_length + 1) 33 | 34 | assert_raise ArgumentError, fn -> 35 | %Username{value: value} |> Username.encode() 36 | end 37 | end 38 | 39 | test "USERNAME attibute with non valid UTF-8 binary" do 40 | value = <<0xFFFF>> 41 | 42 | assert_raise ArgumentError, fn -> 43 | %Username{value: value} |> Username.encode() 44 | end 45 | end 46 | 47 | test "USERNAME attribute with non-string value" do 48 | assert_raise ArgumentError, fn -> 49 | %Username{value: 1} |> Username.encode() 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/lifetime.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.Lifetime do 2 | @moduledoc """ 3 | LIFETIME attribute as defined in [TURN RFC](https://trac.tools.ietf.org/html/rfc5766#section-14.2) 4 | """ 5 | 6 | alias Jerboa.Format.Body.Attribute.{Decoder,Encoder} 7 | alias Jerboa.Format.Lifetime.LengthError 8 | alias Jerboa.Format.Meta 9 | 10 | defstruct duration: 0 11 | 12 | @max_duration 2 |> :math.pow(32) |> :erlang.trunc() |> Kernel.-(1) 13 | 14 | @typedoc """ 15 | Represents a lifetime of the allocation 16 | 17 | * `:duration` is a duration of a lifetime in seconds 18 | """ 19 | @type t :: %__MODULE__{ 20 | duration: non_neg_integer 21 | } 22 | 23 | defimpl Encoder do 24 | alias Jerboa.Format.Body.Attribute.Lifetime 25 | @type_code 0x000D 26 | 27 | @spec type_code(Lifetime.t) :: integer 28 | def type_code(_), do: @type_code 29 | 30 | @spec encode(Lifetime.t, Meta.t) :: {Meta.t, binary} 31 | def encode(attr, meta), do: {meta, Lifetime.encode(attr)} 32 | end 33 | 34 | defimpl Decoder do 35 | alias Jerboa.Format.Body.Attribute.Lifetime 36 | 37 | @spec decode(Lifetime.t, value :: binary, meta :: Meta.t) 38 | :: {:ok, Meta.t, Lifetime.t} | {:error, struct} 39 | def decode(_, value, meta), do: Lifetime.decode(value, meta) 40 | end 41 | 42 | @doc false 43 | @spec encode(t) :: binary 44 | def encode(%__MODULE__{duration: duration}) 45 | when is_integer(duration) and (duration in 0..@max_duration) do 46 | <> 47 | end 48 | 49 | @doc false 50 | def decode(<>, meta) do 51 | {:ok, meta, %__MODULE__{duration: duration}} 52 | end 53 | def decode(value, _) do 54 | {:error, LengthError.exception(length: byte_size(value))} 55 | end 56 | 57 | @doc false 58 | def max_duration, do: @max_duration 59 | end 60 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/reservation_token_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.ReservationTokenTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.ReservationToken 6 | alias Jerboa.Format.ReservationToken.LengthError 7 | alias Jerboa.Format.Meta 8 | 9 | describe "decode/2" do 10 | test "RESERVATION-TOKEN with length less than 8 bytes" do 11 | ptest length: int(min: 0, max: 7), content: int(min: 0) do 12 | value = <> 13 | 14 | assert {:error, %LengthError{length: ^length}} = 15 | ReservationToken.decode(value, %Meta{}) 16 | end 17 | end 18 | 19 | test "RESERVATION-TOKEN with length more than 8 bytes" do 20 | ptest length: int(min: 9), content: int(min: 0) do 21 | value = <> 22 | 23 | assert {:error, %LengthError{length: ^length}} = 24 | ReservationToken.decode(value, %Meta{}) 25 | end 26 | end 27 | 28 | test "valid, 8 bytes long RESERVATION-TOKEN" do 29 | ptest content: int(min: 0) do 30 | value = <> 31 | 32 | assert {:ok, _, %ReservationToken{value: ^value}} = 33 | ReservationToken.decode(value, %Meta{}) 34 | end 35 | end 36 | end 37 | 38 | describe "encode/1" do 39 | test "RESERVATION-TOKEN with valid value" do 40 | ptest content: int(min: 0) do 41 | value = <> 42 | 43 | assert value == 44 | %ReservationToken{value: value} |> ReservationToken.encode() 45 | end 46 | end 47 | 48 | test "RESERVATION-TOKEN with invalid value" do 49 | assert_raise FunctionClauseError, fn -> 50 | %ReservationToken{value: 123} |> ReservationToken.encode() 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/even_port_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attrbute.EvenPortTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.EvenPort 6 | alias Jerboa.Format.EvenPort.FormatError 7 | alias Jerboa.Format.Meta 8 | 9 | describe "decode/2" do 10 | test "EVEN-PORT attribute of invalid length" do 11 | ptest value: string(min: 2) do 12 | assert {:error, %FormatError{}} = EvenPort.decode(value, %Meta{}) 13 | end 14 | end 15 | 16 | test "EVEN-PORT attribute of invalid format" do 17 | ptest first_bit: int(min: 0, max: 1), extra_bits: int(min: 1, max: 127) do 18 | value = <> 19 | 20 | assert {:error, %FormatError{}} = EvenPort.decode(value, %Meta{}) 21 | end 22 | end 23 | 24 | test "EVEN-PORT attribute with reserved bit set to 1" do 25 | value = <<1::1, 0::7>> 26 | 27 | assert {:ok, _, %EvenPort{reserved?: true}} = 28 | EvenPort.decode(value, %Meta{}) 29 | end 30 | 31 | test "EVEN-PORT attribute with reserved bit set to 0" do 32 | value = <<0::1, 0::7>> 33 | 34 | assert {:ok, _, %EvenPort{reserved?: false}} = 35 | EvenPort.decode(value, %Meta{}) 36 | end 37 | end 38 | 39 | describe "encode/1" do 40 | test "EVEN-PORT attribute with `:reserved?` set to false" do 41 | assert <<0::1, 0::7>> == %EvenPort{reserved?: false} |> EvenPort.encode() 42 | end 43 | 44 | test "EVEN-PORT attribute with `:reserved?` set to true" do 45 | assert <<1::1, 0::7>> == %EvenPort{reserved?: true} |> EvenPort.encode() 46 | end 47 | 48 | test "EVEN-PORT attribute with invalid `:reserved?` value" do 49 | assert_raise FunctionClauseError, fn -> 50 | %EvenPort{reserved?: "hi"} |> EvenPort.encode() 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/username.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.Username do 2 | @moduledoc """ 3 | USERNAME attribute as defined in [STUN RFC](https://tools.ietf.org/html/rfc5389#section-15.3) 4 | """ 5 | 6 | alias Jerboa.Format.Body.Attribute.{Decoder,Encoder} 7 | alias Jerboa.Format.Username.LengthError 8 | alias Jerboa.Format.Meta 9 | 10 | defstruct value: "" 11 | 12 | @max_length 512 13 | 14 | @typedoc """ 15 | Represents username used for authentication and message-integrity 16 | checks 17 | """ 18 | @type t :: %__MODULE__{ 19 | value: binary 20 | } 21 | 22 | defimpl Encoder do 23 | alias Jerboa.Format.Body.Attribute.Username 24 | @type_code 0x0006 25 | 26 | @spec type_code(Username.t) :: integer 27 | def type_code(_), do: @type_code 28 | 29 | @spec encode(Username.t, Meta.t) :: {Meta.t, binary} 30 | def encode(attr, meta), do: {meta, Username.encode(attr)} 31 | end 32 | 33 | defimpl Decoder do 34 | alias Jerboa.Format.Body.Attribute.Username 35 | 36 | @spec decode(Username.t, value :: binary, meta :: Meta.t) 37 | :: {:ok, Meta.t, Username.t} | {:error, struct} 38 | def decode(_, value, meta), do: Username.decode(value, meta) 39 | end 40 | 41 | @doc false 42 | def encode(%__MODULE__{value: value}) 43 | when is_binary(value) and byte_size(value) in 0..@max_length do 44 | if String.valid?(value) do 45 | value 46 | else 47 | raise ArgumentError 48 | end 49 | end 50 | def encode(_), do: raise ArgumentError 51 | 52 | @doc false 53 | def decode(value, meta) do 54 | length = byte_size(value) 55 | if String.valid?(value) && length <= @max_length do 56 | {:ok, meta, %__MODULE__{value: value}} 57 | else 58 | {:error, LengthError.exception(length: length)} 59 | end 60 | end 61 | 62 | @doc false 63 | def max_length, do: @max_length 64 | end 65 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/requested_transport_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.RequestedTransportTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.RequestedTransport 6 | alias Jerboa.Format.RequestedTransport.{LengthError, ProtocolError} 7 | alias Jerboa.Format.Meta 8 | 9 | describe "decode/2" do 10 | test "REQUESTED-TRANSPORT with valid length and protocol" do 11 | ptest rest: int(min: 0) do 12 | proto_code = Enum.random RequestedTransport.known_protocol_codes 13 | value = <> 14 | 15 | assert {:ok, _, %RequestedTransport{}} = 16 | RequestedTransport.decode(value, %Meta{}) 17 | end 18 | end 19 | 20 | test "REQUESTED-TRANSPORT with valid length and invalid protocol" do 21 | proto_code = 1 22 | value = <> 23 | 24 | assert {:ok, _, %RequestedTransport{protocol: :unknown}} = 25 | RequestedTransport.decode(value, %Meta{}) 26 | end 27 | 28 | test "REQUESTED-TRANSPORT with invalid length" do 29 | proto_code = Enum.random RequestedTransport.known_protocol_codes 30 | value = <> 31 | length = byte_size(value) 32 | 33 | assert {:error, %LengthError{length: ^length}} = 34 | RequestedTransport.decode(value, %Meta{}) 35 | end 36 | end 37 | 38 | describe "encode/1" do 39 | test "REQUESTED-TRANSPORT with valid protocol" do 40 | proto = Enum.random RequestedTransport.known_protocols 41 | 42 | assert %RequestedTransport{protocol: proto} |> RequestedTransport.encode() 43 | end 44 | 45 | test "REQUESTED-TRANSPORT with invalid protocol" do 46 | proto = :tcp 47 | 48 | assert_raise ArgumentError, fn -> 49 | %RequestedTransport{protocol: proto} |> RequestedTransport.encode() 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/even_port.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.EvenPort do 2 | @moduledoc """ 3 | EVEN-PORT attribute as defined in [TURN RFC](https://trac.tools.ietf.org/html/rfc5766#section-14.6) 4 | """ 5 | 6 | alias Jerboa.Format.Body.Attribute.{Encoder, Decoder} 7 | alias Jerboa.Format.EvenPort.FormatError 8 | alias Jerboa.Format.Meta 9 | 10 | defstruct reserved?: false 11 | 12 | @typedoc """ 13 | Represents EVEN-PORT attribute 14 | 15 | `:reserved?` field indicates if R bit of attribute is 16 | set to 0 (`false`) or 1 (`true`). 17 | """ 18 | @type t :: %__MODULE__{ 19 | reserved?: boolean 20 | } 21 | 22 | defimpl Encoder do 23 | alias Jerboa.Format.Body.Attribute.EvenPort 24 | @type_code 0x0018 25 | 26 | @spec type_code(EvenPort.t) :: integer 27 | def type_code(_), do: @type_code 28 | 29 | @spec encode(EvenPort.t, Meta.t) :: {Meta.t, binary} 30 | def encode(attr, meta), do: {meta, EvenPort.encode(attr)} 31 | end 32 | 33 | defimpl Decoder do 34 | alias Jerboa.Format.Body.Attribute.EvenPort 35 | 36 | @spec decode(EvenPort.t, value :: binary, Meta.t) 37 | :: {:ok, Meta.t, EvenPort.t} | {:error, struct} 38 | def decode(_, value, meta), do: EvenPort.decode(value, meta) 39 | end 40 | 41 | @doc false 42 | @spec encode(t) :: <<_::8>> 43 | def encode(%__MODULE__{reserved?: true}) do 44 | encode_bit(1) 45 | end 46 | def encode(%__MODULE__{reserved?: false}) do 47 | encode_bit(0) 48 | end 49 | 50 | @spec encode_bit(0 | 1) :: <<_::8>> 51 | defp encode_bit(bit), do: <> 52 | 53 | @doc false 54 | @spec decode(binary, Meta.t) :: {:ok, Meta.t, t} | {:error, struct} 55 | def decode(<>, meta) do 56 | reserved? = if r_bit == 1, do: true, else: false 57 | {:ok, meta, %__MODULE__{reserved?: reserved?}} 58 | end 59 | def decode(_, _) do 60 | {:error, FormatError.exception()} 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/reservation_token.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.ReservationToken do 2 | @moduledoc """ 3 | RESERVATION-TOKEN attribute as defined in 4 | [TURN RFC](https://trac.tools.ietf.org/html/rfc5766#section-14.9) 5 | """ 6 | 7 | alias Jerboa.Format.Body.Attribute.{Decoder, Encoder} 8 | alias Jerboa.Format.ReservationToken.LengthError 9 | alias Jerboa.Format.Meta 10 | 11 | defstruct value: "" 12 | 13 | @byte_length 8 14 | 15 | @typedoc """ 16 | Represents reservation token attribute value 17 | """ 18 | @type t :: %__MODULE__{ 19 | value: binary 20 | } 21 | 22 | @doc """ 23 | Create a new reservation token with a random value 24 | """ 25 | def new do 26 | %__MODULE__{value: :crypto.strong_rand_bytes(@byte_length)} 27 | end 28 | 29 | defimpl Encoder do 30 | alias Jerboa.Format.Body.Attribute.ReservationToken 31 | @type_code 0x0022 32 | 33 | @spec type_code(ReservationToken.t) :: integer 34 | def type_code(_), do: @type_code 35 | 36 | @spec encode(ReservationToken.t, Meta.t) :: {Meta.t, binary} 37 | def encode(attr, meta), do: {meta, ReservationToken.encode(attr)} 38 | end 39 | 40 | defimpl Decoder do 41 | alias Jerboa.Format.Body.Attribute.ReservationToken 42 | 43 | @spec decode(ReservationToken.t, value :: binary, Meta.t) 44 | :: {:ok, Meta.t, ReservationToken.t} | {:error, struct} 45 | def decode(_, value, meta), do: ReservationToken.decode(value, meta) 46 | end 47 | 48 | @doc false 49 | @spec encode(t) :: binary 50 | def encode(%__MODULE__{value: v}) 51 | when is_binary(v) and byte_size(v) == @byte_length, do: v 52 | 53 | @doc false 54 | @spec decode(binary, Meta.t) :: {:ok, Meta.t, t} | {:error, struct} 55 | def decode(value, meta) when byte_size(value) == @byte_length do 56 | {:ok, meta, %__MODULE__{value: value}} 57 | end 58 | def decode(value, _) do 59 | {:error, LengthError.exception(length: byte_size(value))} 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :jerboa, 6 | version: "0.3.0", 7 | name: "Jerboa", 8 | description: "STUN/TURN encoder, decoder and client library", 9 | source_url: "https://github.com/esl/jerboa", 10 | elixir: "~> 1.4", 11 | elixirc_paths: elixirc_paths(Mix.env), 12 | build_embedded: Mix.env == :prod, 13 | start_permanent: Mix.env == :prod, 14 | deps: deps(), 15 | package: package(), 16 | docs: docs(), 17 | dialyzer: dialyzer(), 18 | test_coverage: test_coverage(), 19 | preferred_cli_env: preferred_cli_env()] 20 | end 21 | 22 | def application do 23 | [mod: {Jerboa.Client.Application, []}, 24 | extra_applications: [:logger]] 25 | end 26 | 27 | defp elixirc_paths(:test), do: ["lib", "test/helper"] 28 | defp elixirc_paths(_), do: ["lib"] 29 | 30 | defp deps do 31 | [{:ex_doc, "~> 0.14", runtime: false, only: :dev}, 32 | {:credo, "~> 0.5", runtime: false, only: [:dev, :test]}, 33 | {:dialyxir, "~> 0.4", runtime: false, only: :dev}, 34 | {:excoveralls, "~> 0.5", runtime: false, only: :test}, 35 | {:inch_ex, "~> 0.5", runtime: false, only: :dev}, 36 | {:quixir, "~> 0.9", runtime: false, only: :test}] 37 | end 38 | 39 | defp package do 40 | [licenses: ["Apache 2.0"], 41 | maintainers: ["Erlang Solutions"], 42 | links: %{"GitHub" => "https://github.com/esl/jerboa"}] 43 | end 44 | 45 | defp docs do 46 | [main: "Jerboa", 47 | extras: ["README.md": [title: "Jerboa"]]] 48 | end 49 | 50 | defp dialyzer do 51 | [plt_core_path: ".dialyzer/", 52 | flags: ["-Wunmatched_returns", "-Werror_handling", 53 | "-Wrace_conditions", "-Wunderspecs"]] 54 | end 55 | 56 | defp test_coverage do 57 | [tool: ExCoveralls] 58 | end 59 | 60 | defp preferred_cli_env do 61 | ["coveralls": :test, "coveralls.detail": :test, 62 | "coveralls.travis": :test, "coveralls.html": :test] 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/channel_number.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.ChannelNumber do 2 | @moduledoc """ 3 | CHANNEL-NUMBER attribute as defined in [TURN RFC](https://trac.tools.ietf.org/html/rfc5766#section-14.1) 4 | """ 5 | 6 | alias Jerboa.Format.ChannelNumber.First2BitsError 7 | alias Jerboa.Format.Body.Attribute.{Decoder, Encoder} 8 | alias Jerboa.Format.Meta 9 | 10 | @min_number 0x4000 11 | @max_number 0x7FFF 12 | 13 | defstruct [:number] 14 | 15 | @typedoc """ 16 | Contains the number of the channel 17 | """ 18 | @type t :: %__MODULE__{ 19 | number: Jerboa.Format.channel_number 20 | } 21 | 22 | defimpl Encoder do 23 | alias Jerboa.Format.Body.Attribute.ChannelNumber 24 | @type_code 0x000C 25 | 26 | @spec type_code(ChannelNumber.t) :: integer 27 | def type_code(_), do: @type_code 28 | 29 | @spec encode(ChannelNumber.t, Meta.t) :: {Meta.t, binary} 30 | def encode(attr, meta), do: {meta, ChannelNumber.encode(attr)} 31 | end 32 | 33 | defimpl Decoder do 34 | alias Jerboa.Format.Body.Attribute.ChannelNumber 35 | 36 | @spec decode(ChannelNumber.t, value :: binary, meta :: Meta.t) 37 | :: {:ok, Meta.t, ChannelNumber.t} | {:error, struct} 38 | def decode(_, value, meta), do: ChannelNumber.decode(value, meta) 39 | end 40 | 41 | @doc false 42 | @spec encode(t) :: binary 43 | def encode(%__MODULE__{number: n}) when is_integer(n) and (n in @min_number..@max_number) do 44 | reserved = 0 45 | <> 46 | end 47 | 48 | @doc false 49 | @spec decode(binary, Meta.t) :: {:ok, Meta.t, t} | {:error, struct} 50 | def decode(<>, meta) do 51 | case number do 52 | <<0b01 :: size(2), _ :: size(14)>> -> 53 | <> = number 54 | {:ok, meta, %__MODULE__{number: as_integer}} 55 | <> -> 56 | {:error, First2BitsError.exception(bits: b)} 57 | end 58 | end 59 | 60 | @doc false 61 | def min_number, do: @min_number 62 | 63 | @doc false 64 | def max_number, do: @max_number 65 | 66 | end 67 | -------------------------------------------------------------------------------- /lib/jerboa/client/protocol.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol do 2 | @moduledoc false 3 | 4 | alias Jerboa.Client 5 | alias Jerboa.Client.Credentials 6 | alias Jerboa.Params 7 | alias Jerboa.ChannelData 8 | alias Jerboa.Format 9 | alias Jerboa.Format.Body.Attribute.{Username, Realm, Nonce, ErrorCode} 10 | 11 | require Logger 12 | 13 | @type request :: {id :: binary, packet :: binary} 14 | @type indication :: binary 15 | 16 | ## API 17 | 18 | @spec encode_channel_data(Format.channel_number, binary) :: binary 19 | def encode_channel_data(number, data) do 20 | %ChannelData{channel_number: number, data: data} 21 | |> Format.encode() 22 | end 23 | 24 | @spec encode_request(Params.t, Credentials.t) :: request 25 | def encode_request(params, creds) do 26 | opts = Credentials.to_decode_opts(creds) 27 | {params.identifier, Format.encode(params, opts)} 28 | end 29 | 30 | @spec decode!(packet :: binary, Credentials.t) 31 | :: Params.t | ChannelData.t | no_return 32 | def decode!(packet, creds) do 33 | opts = Credentials.to_decode_opts(creds) 34 | Format.decode!(packet, opts) 35 | end 36 | 37 | @spec base_params(Credentials.t) :: Params.t 38 | def base_params(creds) do 39 | params = Params.new() 40 | if Credentials.complete?(creds) do 41 | params 42 | |> Params.put_attr(%Username{value: creds.username}) 43 | |> Params.put_attr(%Realm{value: creds.realm}) 44 | |> Params.put_attr(%Nonce{value: creds.nonce}) 45 | else 46 | params 47 | end 48 | end 49 | 50 | @spec eval_failure(resp :: Params.t, Credentials.t) 51 | :: {:error, Client.error, Credentials.t} 52 | def eval_failure(params, creds) do 53 | nonce_attr = Params.get_attr(params, Nonce) 54 | error = Params.get_attr(params, ErrorCode) 55 | cond do 56 | is_nil error -> 57 | {:error, :bad_response, creds} 58 | error.name == :stale_nonce && nonce_attr -> 59 | new_creds = %{creds | nonce: nonce_attr.value} 60 | {:error, error.name, new_creds} 61 | error.name == :stale_nonce -> 62 | {:error, :bad_response, creds} 63 | true -> 64 | {:error, error.name, creds} 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/jerboa/client/protocol/binding_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.BindingTest do 2 | use ExUnit.Case 3 | 4 | alias Jerboa.{Params, Format} 5 | alias Jerboa.Format.Body.Attribute.XORMappedAddress, as: XMA 6 | alias Jerboa.Client.Protocol.Binding 7 | 8 | test "request/0 returns encoded binding request" do 9 | {id, request} = Binding.request() 10 | 11 | params = Format.decode!(request) 12 | 13 | assert id == params.identifier 14 | assert :request == Params.get_class(params) 15 | assert :binding == Params.get_method(params) 16 | end 17 | 18 | test "indication/0 returns encoded binding request" do 19 | request = Binding.indication() 20 | 21 | params = Format.decode!(request) 22 | 23 | assert :indication == Params.get_class(params) 24 | assert :binding == Params.get_method(params) 25 | end 26 | 27 | describe "eval_response/1" do 28 | test "returns server reflexive address on valid binding response" do 29 | address = {127, 0, 0, 1} 30 | port = 33_333 31 | params = 32 | Params.new() 33 | |> Params.put_class(:success) 34 | |> Params.put_method(:binding) 35 | |> Params.put_attr(XMA.new(address, port)) 36 | 37 | assert {:ok, {address, port}} == Binding.eval_response(params) 38 | end 39 | 40 | test "returns :bad_response on invalid message class" do 41 | address = {127, 0, 0, 1} 42 | port = 33_333 43 | params = 44 | Params.new() 45 | |> Params.put_class(:failure) 46 | |> Params.put_method(:binding) 47 | |> Params.put_attr(XMA.new(address, port)) 48 | 49 | assert {:error, :bad_response} == Binding.eval_response(params) 50 | end 51 | 52 | test "returns :bad_response on invalid message method" do 53 | address = {127, 0, 0, 1} 54 | port = 33_333 55 | params = 56 | Params.new() 57 | |> Params.put_class(:success) 58 | |> Params.put_method(:allocate) 59 | |> Params.put_attr(XMA.new(address, port)) 60 | 61 | assert {:error, :bad_response} == Binding.eval_response(params) 62 | end 63 | 64 | test "returns :bad_response wihtout XOR-MAPPED-ADDRESS" do 65 | params = 66 | Params.new() 67 | |> Params.put_class(:success) 68 | |> Params.put_method(:binding) 69 | 70 | assert {:error, :bad_response} == Binding.eval_response(params) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute do 2 | @moduledoc """ 3 | STUN protocol attributes 4 | """ 5 | 6 | alias Jerboa.Format.ComprehensionError 7 | alias Jerboa.Format.Body.Attribute.{XORMappedAddress, Lifetime, Data, Nonce, 8 | Username, Realm, ErrorCode, EvenPort, 9 | XORRelayedAddress, XORPeerAddress, 10 | RequestedTransport, DontFragment, 11 | ReservationToken, ChannelNumber} 12 | alias Jerboa.Format.Meta 13 | 14 | defprotocol Encoder do 15 | @moduledoc false 16 | 17 | @spec type_code(t) :: integer 18 | def type_code(attr) 19 | 20 | @spec encode(t, Meta.t) :: {Meta.t, binary} 21 | def encode(attr, meta) 22 | end 23 | 24 | defprotocol Decoder do 25 | @moduledoc false 26 | 27 | @spec decode(type :: t, value :: binary, meta :: Meta.t) 28 | :: {:ok, Meta.t, t} | {:error, struct} 29 | def decode(type, value, meta) 30 | end 31 | 32 | @known_attrs [XORMappedAddress, Lifetime, Data, Nonce, Username, Realm, 33 | ErrorCode, XORRelayedAddress, XORPeerAddress, 34 | RequestedTransport, DontFragment, EvenPort, ReservationToken, 35 | ChannelNumber] 36 | 37 | @biggest_16 65_535 38 | 39 | @type t :: struct 40 | 41 | @doc """ 42 | Retrieves attribute name from attribute struct 43 | """ 44 | @spec name(t) :: module 45 | def name(%{__struct__: name}), do: name 46 | 47 | @doc false 48 | @spec encode(Meta.t, struct) :: {Meta.t, <<_::32, _::_ * 8>>} 49 | def encode(meta, attr) do 50 | {meta, value} = Encoder.encode(attr, meta) 51 | type = Encoder.type_code(attr) 52 | {meta, encode_(type, value)} 53 | end 54 | 55 | @doc false 56 | @spec decode(Meta.t, type :: non_neg_integer, value :: binary) 57 | :: {:ok, Meta.t, t} | {:error, struct} | {:ignore, Meta.t} 58 | for attr <- @known_attrs do 59 | type = Encoder.type_code(struct(attr)) 60 | def decode(meta, unquote(type), value) do 61 | Decoder.decode(struct(unquote(attr)), value, meta) 62 | end 63 | end 64 | def decode(_, type, _) when type in 0x0000..0x7FFF do 65 | {:error, ComprehensionError.exception(attribute: type)} 66 | end 67 | def decode(meta, _, _), do: {:ignore, meta} 68 | 69 | defp encode_(type, value) when byte_size(value) < @biggest_16 do 70 | <> 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/requested_transport.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.RequestedTransport do 2 | @moduledoc """ 3 | REQUESTED-TRANSPORT attribute as defined in the 4 | [TURN RFC](https://trac.tools.ietf.org/html/rfc5766#section-14.7) 5 | """ 6 | 7 | alias Jerboa.Format.Body.Attribute.{Encoder, Decoder} 8 | alias Jerboa.Format.RequestedTransport.LengthError 9 | alias Jerboa.Format.Meta 10 | 11 | defstruct protocol: :udp 12 | 13 | @type protocol :: :udp | :unknown 14 | @type protocol_code :: 17 15 | 16 | @typedoc """ 17 | Represents transport requested for allocation 18 | 19 | Currently only `:udp` is a valid protocol value. This struct may also hold 20 | atom `:unknown` which means that Jerboa does not know given protocol number, 21 | but the attribute is formatteed correctly. 22 | """ 23 | @type t :: %__MODULE__{ 24 | protocol: protocol 25 | } 26 | 27 | defimpl Encoder do 28 | alias Jerboa.Format.Body.Attribute.RequestedTransport 29 | @type_code 0x0019 30 | 31 | @spec type_code(RequestedTransport.t) :: integer 32 | def type_code(_), do: @type_code 33 | 34 | @spec encode(RequestedTransport.t, Meta.t) :: {Meta.t, binary} 35 | def encode(attr, meta), do: {meta, RequestedTransport.encode(attr)} 36 | end 37 | 38 | defimpl Decoder do 39 | alias Jerboa.Format.Body.Attribute.RequestedTransport 40 | 41 | @spec decode(RequestedTransport.t, value :: binary, Meta.t) 42 | :: {:ok, Meta.t, RequestedTransport.t} | {:error, struct} 43 | def decode(_, value, meta), do: RequestedTransport.decode(value, meta) 44 | end 45 | 46 | @doc false 47 | def encode(%__MODULE__{protocol: proto}) do 48 | case proto_to_code(proto) do 49 | :error -> 50 | raise ArgumentError, "invalid protocol in REQUESTED-TRANSPORT attribute" 51 | proto_code -> 52 | <> 53 | end 54 | end 55 | 56 | @doc false 57 | def decode(<>, meta) do 58 | case code_to_proto(proto_code) do 59 | :error -> 60 | {:ok, meta, %__MODULE__{protocol: :unknown}} 61 | proto -> 62 | {:ok, meta, %__MODULE__{protocol: proto}} 63 | end 64 | end 65 | def decode(value, _) do 66 | {:error, LengthError.exception(length: byte_size(value))} 67 | end 68 | 69 | defp code_to_proto(17), do: :udp 70 | defp code_to_proto(_), do: :error 71 | 72 | defp proto_to_code(:udp), do: 17 73 | defp proto_to_code(_), do: :error 74 | 75 | @doc false 76 | def known_protocol_codes, do: [17] 77 | 78 | @doc false 79 | def known_protocols, do: [:udp] 80 | end 81 | -------------------------------------------------------------------------------- /test/jerboa/client/protocol/data_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.DataTest do 2 | use ExUnit.Case 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Client.Protocol.Data 6 | alias Jerboa.Format.Body.Attribute.Data, as: DataAttr 7 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 8 | 9 | describe "eval_indication/1" do 10 | test "returns peer address and data given valid data indication" do 11 | data = "alicehasacat" 12 | peer_addr = {127, 0, 0, 1} 13 | peer_port = 33_333 14 | params = 15 | Params.new() 16 | |> Params.put_class(:indication) 17 | |> Params.put_method(:data) 18 | |> Params.put_attr(%DataAttr{content: data}) 19 | |> Params.put_attr(XPA.new(peer_addr, peer_port)) 20 | 21 | assert {:ok, {peer_addr, peer_port}, data} == Data.eval_indication(params) 22 | end 23 | 24 | test "returns :error on invalid STUN class" do 25 | data = "alicehasacat" 26 | peer_addr = {127, 0, 0, 1} 27 | peer_port = 33_333 28 | params = 29 | Params.new() 30 | |> Params.put_class(:request) 31 | |> Params.put_method(:data) 32 | |> Params.put_attr(%DataAttr{content: data}) 33 | |> Params.put_attr(XPA.new(peer_addr, peer_port)) 34 | 35 | assert :error == Data.eval_indication(params) 36 | end 37 | 38 | test "returns :error on invalid STUN method" do 39 | data = "alicehasacat" 40 | peer_addr = {127, 0, 0, 1} 41 | peer_port = 33_333 42 | params = 43 | Params.new() 44 | |> Params.put_class(:indication) 45 | |> Params.put_method(:allocate) 46 | |> Params.put_attr(%DataAttr{content: data}) 47 | |> Params.put_attr(XPA.new(peer_addr, peer_port)) 48 | 49 | assert :error == Data.eval_indication(params) 50 | end 51 | 52 | test "returns :error without DATA attribute" do 53 | peer_addr = {127, 0, 0, 1} 54 | peer_port = 33_333 55 | params = 56 | Params.new() 57 | |> Params.put_class(:indication) 58 | |> Params.put_method(:data) 59 | |> Params.put_attr(XPA.new(peer_addr, peer_port)) 60 | 61 | assert :error == Data.eval_indication(params) 62 | end 63 | 64 | test "returns :error without XOR-PEER-ADDRESS attribute" do 65 | data = "alicehasacat" 66 | params = 67 | Params.new() 68 | |> Params.put_class(:indication) 69 | |> Params.put_method(:data) 70 | |> Params.put_attr(%DataAttr{content: data}) 71 | 72 | assert :error == Data.eval_indication(params) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jerboa 2 | 3 | [![Build Status](https://travis-ci.org/esl/jerboa.svg?branch=travis-ci-integration)](https://travis-ci.org/esl/jerboa) 4 | [![Inline docs](http://inch-ci.org/github/esl/jerboa.svg?branch=master)](http://inch-ci.org/github/esl/jerboa) 5 | [![Coverage Status](https://coveralls.io/repos/github/esl/jerboa/badge.svg?branch=master)](https://coveralls.io/github/esl/jerboa?branch=master) 6 | [![Ebert](https://ebertapp.io/github/esl/jerboa.svg)](https://ebertapp.io/github/esl/jerboa) 7 | 8 | [Documentation](https://hexdocs.pm/jerboa/0.3.0) 9 | 10 | 11 | STUN/TURN encoder, decoder and client library by [Erlang Solutions](https://www.erlang-solutions.com) 12 | 13 | Jerboa aims to provide simple APIs for common STUN/TURN use cases. It is used by [Fennec](https://github.com/esl/fennec) 14 | for encoding and decoding of STUN messages, as well as a testing tool. 15 | 16 | ### Installation 17 | 18 | Jerboa is available on [Hex](https://hex.pm/packages/jerboa). To use it, just add it to your dependencies: 19 | 20 | ```elixir 21 | def deps do 22 | [{:jerboa, "~> 0.3.0"}] 23 | end 24 | ``` 25 | 26 | ### Checklist of STUN/TURN/ICE methods supported by Jerboa's encoder/decoder 27 | 28 | - [x] Binding 29 | - [x] Allocate 30 | - [x] Refresh 31 | - [x] Send 32 | - [x] Data 33 | - [x] CreatePermission 34 | - [x] ChannelBind 35 | 36 | ### Checklist of STUN/TURN/ICE attributes supported by Jerboa's encoder/decoder 37 | 38 | #### Comprehension Required 39 | 40 | - [x] XOR-MAPPED-ADDRESS 41 | - [x] MESSAGE-INTEGRITY 42 | - [x] ERROR-CODE 43 | - [ ] UNKNOWN-ATTRIBUTES 44 | - [x] REALM 45 | - [x] NONCE 46 | - [x] CHANNEL-NUMBER 47 | - [x] LIFETIME 48 | - [x] XOR-PEER-ADDRESS 49 | - [x] DATA 50 | - [x] XOR-RELAYED-ADDRESS 51 | - [x] EVEN-PORT 52 | - [x] REQUESTED-TRANSPORT 53 | - [x] DONT-FRAGMENT 54 | - [x] RESERVATION-TOKEN 55 | - [ ] PRIORITY 56 | - [ ] USE-CANDIDATE 57 | - [ ] ICE-CONTROLLED 58 | - [ ] ICE-CONTROLLING 59 | 60 | #### Comprehension Optional 61 | 62 | - [ ] SOFTWARE 63 | - [ ] ALTERNATE-SERVER 64 | - [ ] FINGERPRINT 65 | 66 | ## License 67 | 68 | Copyright 2016-2017 Erlang Solutions Ltd. 69 | 70 | Licensed under the Apache License, Version 2.0 (the "License"); 71 | you may not use this file except in compliance with the License. 72 | You may obtain a copy of the License at 73 | 74 | http://www.apache.org/licenses/LICENSE-2.0 75 | 76 | Unless required by applicable law or agreed to in writing, software 77 | distributed under the License is distributed on an "AS IS" BASIS, 78 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 79 | See the License for the specific language governing permissions and 80 | limitations under the License. 81 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/channel_number_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.ChannelNumberTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.ChannelNumber 6 | alias Jerboa.Format.Meta 7 | 8 | describe "encode/1" do 9 | 10 | test "CHANNEL-NUMBER attribute with a valid number" do 11 | min = ChannelNumber.min_number() 12 | max = ChannelNumber.max_number() 13 | ptest number: int(min: min, max: max) do 14 | assert <> \ 15 | == %ChannelNumber{number: number} |> ChannelNumber.encode() 16 | end 17 | end 18 | 19 | test "CHANNEL-NUMBER with an invalid/reserved number" do 20 | ## See TURN RFC for the invalid range: 21 | ## - https://tools.ietf.org/html/rfc5766#section-11 22 | ## - https://tools.ietf.org/html/rfc5766#section-14.1 23 | ptest number: int(min: 0x0000, max: 0x3FFF) do 24 | assert_raise FunctionClauseError, fn -> 25 | %ChannelNumber{number: number} |> ChannelNumber.encode() 26 | end 27 | end 28 | ptest number: int(min: 0x8000, max: 0xFFFF) do 29 | assert_raise FunctionClauseError, fn -> 30 | %ChannelNumber{number: number} |> ChannelNumber.encode() 31 | end 32 | end 33 | end 34 | 35 | end 36 | 37 | describe "decode/2" do 38 | 39 | test "accepts binaries starting with 0b01" do 40 | ptest number: int(min: 0b0100_0000_0000_0000, max: 0b0111_1111_1111_1111) do 41 | encoded = <> 42 | assert {:ok, _, %ChannelNumber{number: number}} \ 43 | = ChannelNumber.decode(encoded, %Meta{}) 44 | end 45 | end 46 | 47 | test "rejects binaries starting with 0b00, 0b10, 0b11" do 48 | ptest number: int(min: 0b0, max: 0b0011_1111_1111_1111) do 49 | encoded = <> 50 | assert {:error, _} = ChannelNumber.decode(encoded, %Meta{}) 51 | end 52 | ptest number: int(min: 0b1000_0000_0000_0000, max: 0b1111_1111_1111_1111) do 53 | encoded = <> 54 | assert {:error, _} = ChannelNumber.decode(encoded, %Meta{}) 55 | end 56 | end 57 | 58 | end 59 | 60 | describe "decode/encode composition" do 61 | 62 | test "is an identity" do 63 | min = ChannelNumber.min_number() 64 | max = ChannelNumber.max_number() 65 | ptest number: int(min: min, max: max) do 66 | cn = %ChannelNumber{number: number} 67 | {:ok, _, new_cn} = 68 | cn 69 | |> ChannelNumber.encode() 70 | |> ChannelNumber.decode(%Meta{}) 71 | assert cn === new_cn 72 | end 73 | end 74 | 75 | end 76 | 77 | end 78 | -------------------------------------------------------------------------------- /lib/jerboa/format/body.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body do 2 | @moduledoc false 3 | 4 | alias Jerboa.Format.Body.Attribute 5 | alias Jerboa.Format.AttributeFormatError 6 | alias Jerboa.Format.Meta 7 | alias Jerboa.Format.MessageIntegrity 8 | 9 | @message_integrity MessageIntegrity.type_code 10 | @attr_header_bytes 4 11 | 12 | @spec encode(Meta.t) :: Meta.t 13 | def encode(%Meta{params: params} = meta) do 14 | Enum.reduce params.attributes, meta, &encode/2 15 | end 16 | 17 | @spec decode(Meta.t) :: {:ok, Meta.t} | {:error, struct} 18 | def decode(%Meta{length: 0, body: <<>>} = meta), do: {:ok, meta} 19 | def decode(%Meta{body: body} = meta) do 20 | case decode(meta, body) do 21 | {:ok, meta} -> 22 | {:ok, meta} 23 | {:error, _} = e -> 24 | e 25 | end 26 | end 27 | 28 | @spec encode(Attribute.t, Meta.t) :: Meta.t 29 | defp encode(attr, meta) do 30 | {meta, encoded} = Attribute.encode(meta, attr) 31 | %{meta | body: meta.body <> encoded <> padding(encoded)} 32 | end 33 | 34 | @spec decode(Meta.t, not_decoded :: binary) :: {:ok, Meta.t} | {:error, struct} 35 | defp decode(meta, <<@message_integrity::16, l::16, _v::size(l)-bytes, 36 | _::binary>> = body) do 37 | MessageIntegrity.extract(meta, body) 38 | end 39 | defp decode(meta, <>) do 40 | rest = strip_padding(r, l) 41 | meta = update_length_up_to_integrity(meta, l) 42 | case Attribute.decode(meta, t, v) do 43 | {:ignore, meta} -> 44 | decode meta, rest 45 | {:ok, meta, attr} -> 46 | params = meta.params 47 | new_params = %{params | attributes: [attr|params.attributes]} 48 | decode %{meta | params: new_params}, rest 49 | {:error, _} = e -> 50 | e 51 | end 52 | end 53 | defp decode(meta, <<>>) do 54 | {:ok, meta} 55 | end 56 | defp decode(_, _) do 57 | {:error, AttributeFormatError.exception()} 58 | end 59 | 60 | defp strip_padding(binary, attr_length) do 61 | padding_len = padding_length(attr_length) 62 | <<_::bytes-size(padding_len), rest::binary>> = binary 63 | rest 64 | end 65 | 66 | defp padding_length(length) do 67 | case rem(length, 4) do 68 | 0 -> 0 69 | n -> 4 - n 70 | end 71 | end 72 | 73 | defp padding(attr) do 74 | padding_length = padding_length(byte_size(attr)) 75 | String.duplicate(<<0>>, padding_length) 76 | end 77 | 78 | defp update_length_up_to_integrity(meta, attr_length) do 79 | new_length_up_to_integrity = 80 | meta.length_up_to_integrity + @attr_header_bytes + 81 | attr_length + padding_length(attr_length) 82 | %{meta | length_up_to_integrity: new_length_up_to_integrity} 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/jerboa/format/header/type.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Header.Type do 2 | @moduledoc false 3 | 4 | alias Jerboa.Format.UnknownMethodError 5 | alias Jerboa.Params 6 | alias Jerboa.Format.Meta 7 | 8 | defmodule Class do 9 | @moduledoc """ 10 | The STUN message classes 11 | """ 12 | 13 | @typedoc """ 14 | Either a request, indication, success or failure response 15 | """ 16 | @type t :: :request | :indication | :success | :failure 17 | 18 | @doc false 19 | def encode(:request), do: <<0::2>> 20 | def encode(:indication), do: <<1::2>> 21 | def encode(:success), do: <<2::2>> 22 | def encode(:failure), do: <<3::2>> 23 | 24 | @doc false 25 | def decode(<<0::2>>), do: :request 26 | def decode(<<1::2>>), do: :indication 27 | def decode(<<2::2>>), do: :success 28 | def decode(<<3::2>>), do: :failure 29 | end 30 | 31 | defmodule Method do 32 | @moduledoc """ 33 | The STUN message methods 34 | """ 35 | 36 | @typedoc """ 37 | The atom representing a STUN method 38 | """ 39 | @type t :: :binding 40 | | :allocate 41 | | :refresh 42 | | :send 43 | | :data 44 | | :create_permission 45 | | :channel_bind 46 | 47 | @doc false 48 | def encode(:binding), do: <<0x001::12>> 49 | def encode(:allocate), do: <<0x003::12>> 50 | def encode(:refresh), do: <<0x004::12>> 51 | def encode(:send), do: <<0x006::12>> 52 | def encode(:data), do: <<0x007::12>> 53 | def encode(:create_permission), do: <<0x008::12>> 54 | def encode(:channel_bind), do: <<0x009::12>> 55 | 56 | @doc false 57 | def decode(<<0x001::12>>), do: {:ok, :binding} 58 | def decode(<<0x003::12>>), do: {:ok, :allocate} 59 | def decode(<<0x004::12>>), do: {:ok, :refresh} 60 | def decode(<<0x006::12>>), do: {:ok, :send} 61 | def decode(<<0x007::12>>), do: {:ok, :data} 62 | def decode(<<0x008::12>>), do: {:ok, :create_permission} 63 | def decode(<<0x009::12>>), do: {:ok, :channel_bind} 64 | def decode(<>), do: {:error, UnknownMethodError.exception(method: m)} 65 | end 66 | 67 | @spec encode(Meta.t) :: type :: <<_::14>> 68 | def encode(%Meta{params: %Params{class: x, method: y}}) do 69 | encode(Class.encode(x), Method.encode(y)) 70 | end 71 | 72 | def decode(<>) do 73 | case Method.decode(<>) do 74 | {:ok, method} -> 75 | class = Class.decode(<>) 76 | {:ok, class, method} 77 | {:error, _} = e -> 78 | e 79 | end 80 | end 81 | 82 | defp encode(<>, <>) do 83 | <> 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], []}, 2 | "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, 3 | "credo": {:hex, :credo, "0.6.0", "44a82f82b94eeb4ba6092c89b8a6730ca1a3291c7940739d5acc8806d25ac991", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, optional: false]}]}, 4 | "dialyxir": {:hex, :dialyxir, "0.4.1", "236056d6acd25f740f336756c0f3b5dd6e2f0156074bc15f3b779aeee15390c8", [:mix], []}, 5 | "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, 6 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 7 | "excoveralls": {:hex, :excoveralls, "0.5.7", "5d26e4a7cdf08294217594a1b0643636accc2ad30e984d62f1d166f70629ff50", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, 8 | "exjsx": {:hex, :exjsx, "3.2.1", "1bc5bf1e4fd249104178f0885030bcd75a4526f4d2a1e976f4b428d347614f0f", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, optional: false]}]}, 9 | "hackney": {:hex, :hackney, "1.6.3", "d489d7ca2d4323e307bedc4bfe684323a7bf773ecfd77938f3ee8074e488e140", [:mix, :rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, 10 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 11 | "inch_ex": {:hex, :inch_ex, "0.5.5", "b63f57e281467bd3456461525fdbc9e158c8edbe603da6e3e4671befde796a3d", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, optional: false]}]}, 12 | "jsx": {:hex, :jsx, "2.8.1", "1453b4eb3615acb3e2cd0a105d27e6761e2ed2e501ac0b390f5bbec497669846", [:mix, :rebar3], []}, 13 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 14 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 15 | "poison": {:hex, :poison, "3.0.0", "625ebd64d33ae2e65201c2c14d6c85c27cc8b68f2d0dd37828fde9c6920dd131", [:mix], []}, 16 | "pollution": {:hex, :pollution, "0.9.0", "2d172aeedc444f0fc06a69b03bb6a560fb141bcafcc0c8189c29b10ee6e50893", [:mix], []}, 17 | "quixir": {:hex, :quixir, "0.9.1", "b9a45930f330ba485c1cb976afc9f5ceb14ebbe10faf755e0796bb971396c37c", [:mix], [{:pollution, "~> 0.9", [hex: :pollution, optional: false]}]}, 18 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}} 19 | -------------------------------------------------------------------------------- /.credo.exs: -------------------------------------------------------------------------------- 1 | %{ 2 | configs: [ 3 | %{ 4 | name: "default", 5 | files: %{ 6 | included: ["lib/", "test/"], 7 | excluded: [~r"/_build/", ~r"/deps/"] 8 | }, 9 | requires: [], 10 | strict: true, 11 | checks: [ 12 | {Credo.Check.Consistency.ExceptionNames}, 13 | {Credo.Check.Consistency.LineEndings}, 14 | {Credo.Check.Consistency.SpaceAroundOperators}, 15 | {Credo.Check.Consistency.SpaceInParentheses}, 16 | {Credo.Check.Consistency.TabsOrSpaces}, 17 | {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, 18 | 19 | {Credo.Check.Design.DuplicatedCode, excluded_macros: [:setup, :test]}, 20 | {Credo.Check.Design.TagTODO, exit_status: 0}, 21 | {Credo.Check.Design.TagFIXME}, 22 | {Credo.Check.Design.AliasUsage, false}, 23 | {Credo.Check.Readability.FunctionNames}, 24 | {Credo.Check.Readability.LargeNumbers}, 25 | {Credo.Check.Readability.MaxLineLength, priority: :low, max_length: 90}, 26 | {Credo.Check.Readability.ModuleAttributeNames}, 27 | {Credo.Check.Readability.ModuleDoc}, 28 | {Credo.Check.Readability.ModuleNames}, 29 | {Credo.Check.Readability.ParenthesesInCondition}, 30 | {Credo.Check.Readability.PredicateFunctionNames}, 31 | {Credo.Check.Readability.TrailingBlankLine}, 32 | {Credo.Check.Readability.TrailingWhiteSpace}, 33 | {Credo.Check.Readability.VariableNames}, 34 | {Credo.Check.Readability.Specs, false}, 35 | 36 | {Credo.Check.Refactor.ABCSize}, 37 | {Credo.Check.Refactor.CondStatements}, 38 | {Credo.Check.Refactor.FunctionArity}, 39 | {Credo.Check.Refactor.MatchInCondition}, 40 | {Credo.Check.Refactor.PipeChainStart}, 41 | {Credo.Check.Refactor.CyclomaticComplexity}, 42 | {Credo.Check.Refactor.NegatedConditionsInUnless}, 43 | {Credo.Check.Refactor.NegatedConditionsWithElse}, 44 | {Credo.Check.Refactor.Nesting}, 45 | {Credo.Check.Refactor.UnlessWithElse}, 46 | 47 | {Credo.Check.Warning.IExPry}, 48 | {Credo.Check.Warning.IoInspect}, 49 | {Credo.Check.Warning.NameRedeclarationByAssignment}, 50 | {Credo.Check.Warning.NameRedeclarationByCase}, 51 | {Credo.Check.Warning.NameRedeclarationByDef}, 52 | {Credo.Check.Warning.NameRedeclarationByFn}, 53 | {Credo.Check.Warning.OperationOnSameValues}, 54 | {Credo.Check.Warning.BoolOperationOnSameValues}, 55 | {Credo.Check.Warning.UnusedEnumOperation}, 56 | {Credo.Check.Warning.UnusedKeywordOperation}, 57 | {Credo.Check.Warning.UnusedListOperation}, 58 | {Credo.Check.Warning.UnusedStringOperation}, 59 | {Credo.Check.Warning.UnusedTupleOperation}, 60 | {Credo.Check.Warning.OperationWithConstantResult}, 61 | ] 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /test/jerboa/format/body_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.BodyTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Jerboa.Test.Helper.XORMappedAddress, as: XORMAHelper 5 | alias Jerboa.Test.Helper.Attribute, as: AHelper 6 | alias Jerboa.Format 7 | alias Jerboa.Format.Body 8 | alias Jerboa.Params 9 | alias Jerboa.Format.Body.Attribute.Data 10 | alias Jerboa.Format.Meta 11 | alias Jerboa.Format.MessageIntegrity, as: MI 12 | 13 | describe "Body.encode/2" do 14 | 15 | test "one (XORMappedAddress) attribute into one TLV field" do 16 | attr = XORMAHelper.struct(4) 17 | 18 | %Meta{body: bin} = Body.encode %Meta{params: %Params{attributes: [attr]}} 19 | 20 | assert bit_size(bin) === AHelper.total(type: 16, length: 16, value: 64) 21 | end 22 | 23 | test "appends padding to boundary of 4 bytes" do 24 | content = "Hello" 25 | attr = %Data{content: content} 26 | 27 | length = byte_size(content) 28 | padded_length = length + AHelper.padding_length(length) 29 | 30 | params = Params.new() |> Params.put_attr(attr) 31 | %Meta{body: body} = %Meta{params: params} |> Body.encode() 32 | 33 | assert <<_type::16, ^length::16, padded_value::binary>> = body 34 | assert byte_size(padded_value) == padded_length 35 | end 36 | end 37 | 38 | describe "Body.decode/1" do 39 | 40 | test "unknown comprehension required attribute results in :error tuple" do 41 | for type <- 0x0000..0x7FFF, not type in known_comprehension_required() do 42 | body = <> 43 | 44 | {:error, error} = Body.decode(%Meta{body: body}) 45 | assert %Format.ComprehensionError{attribute: ^type} = error 46 | end 47 | end 48 | 49 | test "ignores unknown comprehension optional attributes" do 50 | for type <- 0x8000..0xFFFF, not type in known_comprehension_optional() do 51 | body = <> 52 | assert {:ok, %Meta{params: params}} = Body.decode(%Meta{body: body}) 53 | assert %Params{attributes: []} = params 54 | end 55 | end 56 | 57 | test "strips value padding" do 58 | # DATA attribute with type, length and value with padding 59 | body = <<0x0013::16, 5::16, "Hello", 0, 0, 0>> 60 | 61 | assert {:ok, _} = Body.decode(%Meta{body: body}) 62 | end 63 | 64 | test "attribute of invalid length results in error" do 65 | body = <<0x0008::16, 1::16>> 66 | 67 | assert {:error, error} = Body.decode(%Meta{body: body}) 68 | assert %Format.AttributeFormatError{} = error 69 | end 70 | 71 | test "fails if message integrity has invalid length" do 72 | length = 19 73 | body = <<0x0008::16, length::16, 0::size(length)-unit(8)>> 74 | 75 | assert {:error, %MI.FormatError{}} = Body.decode(%Meta{body: body}) 76 | end 77 | end 78 | 79 | defp known_comprehension_required do 80 | [0x0020, 0x000D, 0x0013, 0x0015, 0x0006, 0x0014, 0x0009, 0x0012, 0x0016, 81 | 0x0019, 0x0008, 0x001A, 0x0018, 0x0022, 0x000C] 82 | end 83 | 84 | defp known_comprehension_optional do 85 | [] 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/jerboa/client/protocol/create_permission_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.CreatePermissionTest do 2 | use ExUnit.Case 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Client.Protocol 6 | alias Jerboa.Client.Protocol.CreatePermission 7 | alias Jerboa.Test.Helper.Params, as: PH 8 | alias Jerboa.Test.Helper.Credentials, as: CH 9 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 10 | alias Jerboa.Format.Body.Attribute.{Nonce, ErrorCode} 11 | 12 | test "request/2 returns valid create permission request signed with creds" do 13 | creds = CH.final() 14 | peer_addr1 = {127, 0, 0, 1} 15 | peer_addr2 = {127, 0, 0, 2} 16 | 17 | {id, request} = CreatePermission.request(creds, [peer_addr1, peer_addr2]) 18 | params = Protocol.decode!(request, creds) 19 | 20 | assert params.identifier == id 21 | assert params.class == :request 22 | assert params.method == :create_permission 23 | assert params.signed? 24 | assert params.verified? 25 | assert PH.username(params) == creds.username 26 | assert PH.realm(params) == creds.realm 27 | assert PH.nonce(params) == creds.nonce 28 | xor_peer_addrs = params |> Params.get_attrs(XPA) |> Enum.map(& &1.address) 29 | assert peer_addr1 in xor_peer_addrs 30 | assert peer_addr2 in xor_peer_addrs 31 | end 32 | 33 | describe "eval_response/2" do 34 | test "returns :ok on successful refresh response" do 35 | creds = CH.final() 36 | 37 | params = 38 | Params.new() 39 | |> Params.put_class(:success) 40 | |> Params.put_method(:create_permission) 41 | 42 | assert :ok == CreatePermission.eval_response(params, creds) 43 | end 44 | 45 | test "returns :bad_response on invalid STUN method" do 46 | creds = CH.final() 47 | 48 | params = 49 | Params.new() 50 | |> Params.put_class(:success) 51 | |> Params.put_method(:allocate) 52 | 53 | assert {:error, :bad_response, creds} == 54 | CreatePermission.eval_response(params, creds) 55 | end 56 | 57 | test "returns :bad_response on failure without ERROR-CODE" do 58 | creds = CH.final() 59 | 60 | params = 61 | Params.new() 62 | |> Params.put_class(:failure) 63 | |> Params.put_method(:create_permission) 64 | 65 | assert {:error, :bad_response, creds} == 66 | CreatePermission.eval_response(params, creds) 67 | end 68 | 69 | test "returns creds with updated nonce on :stale_nonce error" do 70 | creds = CH.final() |> Map.put(:nonce, "I'm expired") 71 | new_nonce = CH.valid_nonce() 72 | 73 | params = 74 | Params.new() 75 | |> Params.put_class(:failure) 76 | |> Params.put_method(:create_permission) 77 | |> Params.put_attr(%Nonce{value: new_nonce}) 78 | |> Params.put_attr(%ErrorCode{name: :stale_nonce}) 79 | 80 | assert {:error, :stale_nonce, %{creds | nonce: new_nonce}} == 81 | CreatePermission.eval_response(params, creds) 82 | end 83 | 84 | test "returns unchanged creds and error name on other errors" do 85 | creds = CH.final() 86 | error = :forbidden 87 | 88 | params = 89 | Params.new() 90 | |> Params.put_class(:failure) 91 | |> Params.put_method(:create_permission) 92 | |> Params.put_attr(%ErrorCode{name: error}) 93 | 94 | assert {:error, error, creds} == CreatePermission.eval_response(params, creds) 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/jerboa/client/protocol/channel_bind_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.ChannelBindTest do 2 | use ExUnit.Case 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Client.Protocol 6 | alias Jerboa.Client.Protocol.ChannelBind 7 | alias Jerboa.Test.Helper.Params, as: PH 8 | alias Jerboa.Test.Helper.Credentials, as: CH 9 | alias Jerboa.Format.Body.Attribute.XORPeerAddress, as: XPA 10 | alias Jerboa.Format.Body.Attribute.{ChannelNumber, Nonce, ErrorCode} 11 | 12 | test "request/2 returns valid channel bind request signed with creds" do 13 | creds = CH.final() 14 | channel_number = 0x4001 15 | peer_ip = {127, 0, 0, 1} 16 | peer_port = 1234 17 | peer = {peer_ip, peer_port} 18 | 19 | {id, request} = ChannelBind.request(creds, peer, channel_number) 20 | params = Protocol.decode!(request, creds) 21 | 22 | assert params.identifier == id 23 | assert params.class == :request 24 | assert params.method == :channel_bind 25 | assert params.signed? 26 | assert params.verified? 27 | assert PH.username(params) == creds.username 28 | assert PH.realm(params) == creds.realm 29 | assert PH.nonce(params) == creds.nonce 30 | assert %ChannelNumber{number: channel_number} == 31 | Params.get_attr(params, ChannelNumber) 32 | assert %XPA{address: ^peer_ip, port: ^peer_port, family: :ipv4} = 33 | Params.get_attr(params, XPA) 34 | end 35 | 36 | describe "eval_response/2" do 37 | test "returns :ok on successful channel bind response" do 38 | creds = CH.final() 39 | 40 | params = 41 | Params.new() 42 | |> Params.put_class(:success) 43 | |> Params.put_method(:channel_bind) 44 | 45 | assert :ok == ChannelBind.eval_response(params, creds) 46 | end 47 | 48 | test "returns :bad_response on invalid STUN method" do 49 | creds = CH.final() 50 | 51 | params = 52 | Params.new() 53 | |> Params.put_class(:success) 54 | |> Params.put_class(:allocate) 55 | 56 | assert {:error, :bad_response, creds} == 57 | ChannelBind.eval_response(params, creds) 58 | end 59 | 60 | test "returns :bad_response on failure without ERROR-CODE" do 61 | creds = CH.final() 62 | 63 | params = 64 | Params.new() 65 | |> Params.put_class(:failure) 66 | |> Params.put_method(:channel_bind) 67 | 68 | assert {:error, :bad_response, creds} == 69 | ChannelBind.eval_response(params, creds) 70 | end 71 | 72 | test "returns creds with updated nonce on :stale_nonce error" do 73 | creds = CH.final() |> Map.put(:nonce, "I'm expired") 74 | new_nonce = CH.valid_nonce() 75 | 76 | params = 77 | Params.new() 78 | |> Params.put_class(:failure) 79 | |> Params.put_method(:channel_bind) 80 | |> Params.put_attr(%Nonce{value: new_nonce}) 81 | |> Params.put_attr(%ErrorCode{name: :stale_nonce}) 82 | 83 | assert {:error, :stale_nonce, %{creds | nonce: new_nonce}} == 84 | ChannelBind.eval_response(params, creds) 85 | end 86 | 87 | test "returns unchanged creds and error name on other errors" do 88 | creds = CH.final() 89 | error = :forbidden 90 | 91 | params = 92 | Params.new() 93 | |> Params.put_class(:failure) 94 | |> Params.put_method(:channel_bind) 95 | |> Params.put_attr(%ErrorCode{name: error}) 96 | 97 | assert {:error, error, creds} == ChannelBind.eval_response(params, creds) 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/jerboa/client/protocol/refresh_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.RefreshTest do 2 | use ExUnit.Case 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Client.Protocol 6 | alias Jerboa.Client.Protocol.Refresh 7 | alias Jerboa.Test.Helper.Params, as: PH 8 | alias Jerboa.Test.Helper.Credentials, as: CH 9 | alias Jerboa.Format.Body.Attribute.{Lifetime, Nonce, ErrorCode} 10 | 11 | test "request/1 returns valid refresh request signed with credentials" do 12 | creds = CH.final() 13 | 14 | {id, request} = Refresh.request(creds) 15 | params = Protocol.decode!(request, creds) 16 | 17 | assert params.identifier == id 18 | assert params.class == :request 19 | assert params.method == :refresh 20 | assert params.signed? 21 | assert params.verified? 22 | assert PH.username(params) == creds.username 23 | assert PH.realm(params) == creds.realm 24 | assert PH.nonce(params) == creds.nonce 25 | end 26 | 27 | describe "eval_response/2" do 28 | test "returns new lifetime on successful refresh response" do 29 | creds = CH.final() 30 | lifetime = 600 31 | 32 | params = 33 | Params.new() 34 | |> Params.put_class(:success) 35 | |> Params.put_method(:refresh) 36 | |> Params.put_attr(%Lifetime{duration: lifetime}) 37 | 38 | assert {:ok, lifetime} == Refresh.eval_response(params, creds) 39 | end 40 | 41 | test "returns :bad_response on invalid STUN method" do 42 | creds = CH.final() 43 | lifetime = 600 44 | 45 | params = 46 | Params.new() 47 | |> Params.put_class(:success) 48 | |> Params.put_method(:allocate) 49 | |> Params.put_attr(%Lifetime{duration: lifetime}) 50 | 51 | assert {:error, :bad_response, creds} == 52 | Refresh.eval_response(params, creds) 53 | end 54 | 55 | test "returns :bad_response without LIFETIME" do 56 | creds = CH.final() 57 | 58 | params = 59 | Params.new() 60 | |> Params.put_class(:success) 61 | |> Params.put_method(:allocate) 62 | 63 | assert {:error, :bad_response, creds} == 64 | Refresh.eval_response(params, creds) 65 | end 66 | 67 | test "returns :bad_response on failure without ERROR-CODE" do 68 | creds = CH.final() 69 | 70 | params = 71 | Params.new() 72 | |> Params.put_class(:failure) 73 | |> Params.put_method(:refresh) 74 | 75 | assert {:error, :bad_response, creds} == 76 | Refresh.eval_response(params, creds) 77 | end 78 | 79 | test "returns creds with updated nonce on :stale_nonce error" do 80 | creds = CH.final() |> Map.put(:nonce, "I'm expired") 81 | new_nonce = CH.valid_nonce() 82 | 83 | params = 84 | Params.new() 85 | |> Params.put_class(:failure) 86 | |> Params.put_method(:refresh) 87 | |> Params.put_attr(%Nonce{value: new_nonce}) 88 | |> Params.put_attr(%ErrorCode{name: :stale_nonce}) 89 | 90 | assert {:error, :stale_nonce, %{creds | nonce: new_nonce}} == 91 | Refresh.eval_response(params, creds) 92 | end 93 | 94 | test "returns unchanged creds and error name on other errors" do 95 | creds = CH.final() 96 | error = :allocation_quota_reached 97 | 98 | params = 99 | Params.new() 100 | |> Params.put_class(:failure) 101 | |> Params.put_method(:refresh) 102 | |> Params.put_attr(%ErrorCode{name: error}) 103 | 104 | assert {:error, error, creds} == Refresh.eval_response(params, creds) 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /test/jerboa/client/protocol_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.ProtocolTest do 2 | use ExUnit.Case 3 | 4 | alias Jerboa.{Params, Format, ChannelData} 5 | alias Jerboa.Client.Protocol 6 | alias Jerboa.Format.Body.Attribute.{Nonce, ErrorCode} 7 | alias Jerboa.Test.Helper.Params, as: PH 8 | alias Jerboa.Test.Helper.Credentials, as: CH 9 | 10 | describe "encode_request/2" do 11 | test "signs params if given credentials are complete" do 12 | creds = CH.final() 13 | params = 14 | Params.new() 15 | |> Params.put_class(:request) 16 | |> Params.put_method(:allocate) 17 | 18 | {id, request} = Protocol.encode_request(params, creds) 19 | decoded = Format.decode!(request, secret: creds.secret, 20 | username: creds.username, realm: creds.realm) 21 | 22 | assert id == params.identifier 23 | assert decoded.signed? 24 | assert decoded.verified? 25 | end 26 | 27 | test "does not sign params if credentials are not complete" do 28 | creds = CH.initial() 29 | params = 30 | Params.new() 31 | |> Params.put_class(:request) 32 | |> Params.put_method(:allocate) 33 | 34 | {id, request} = Protocol.encode_request(params, creds) 35 | decoded = Format.decode!(request) 36 | 37 | assert id == params.identifier 38 | refute decoded.signed? 39 | refute decoded.verified? 40 | end 41 | end 42 | 43 | describe "base_params/1" do 44 | test "returns params with credentials if credentials are complete" do 45 | creds = CH.final() 46 | 47 | params = Protocol.base_params(creds) 48 | 49 | assert PH.username(params) == creds.username 50 | assert PH.realm(params) == creds.realm 51 | assert PH.nonce(params) == creds.nonce 52 | end 53 | 54 | test "returns params without credentials if credentials are not complete" do 55 | creds = CH.initial() 56 | 57 | params = Protocol.base_params(creds) 58 | 59 | refute PH.username(params) 60 | refute PH.realm(params) 61 | refute PH.nonce(params) 62 | end 63 | end 64 | 65 | describe "eval_failure/2" do 66 | test "returns :bad_response given params without error code" do 67 | creds = CH.final() 68 | params = Params.new() |> Params.put_class(:failure) 69 | 70 | assert {:error, :bad_response, creds} == 71 | Protocol.eval_failure(params, creds) 72 | end 73 | 74 | test "returns credentials with updated nonce if error reason is :stale_nonce" do 75 | new_nonce = "abcd" 76 | old_nonce = CH.invalid_nonce() 77 | creds = CH.final() |> Map.put(:nonce, old_nonce) 78 | params = 79 | Params.new() 80 | |> Params.put_class(:failure) 81 | |> Params.put_attr(%ErrorCode{name: :stale_nonce}) 82 | |> Params.put_attr(%Nonce{value: new_nonce}) 83 | 84 | result = Protocol.eval_failure(params, creds) 85 | 86 | assert {:error, :stale_nonce, new_creds} = result 87 | assert %{creds | nonce: new_nonce} == new_creds 88 | end 89 | 90 | test "returns :bad_response on :stale_nonce error without nonce attribute" do 91 | creds = CH.final() 92 | 93 | params = 94 | Params.new() 95 | |> Params.put_class(:failure) 96 | |> Params.put_attr(%ErrorCode{name: :stale_nonce}) 97 | 98 | assert {:error, :bad_response, creds} == 99 | Protocol.eval_failure(params, creds) 100 | end 101 | 102 | test "returns error name of error included in params" do 103 | creds = CH.final() 104 | error = :allocation_mismatch 105 | 106 | params = 107 | Params.new() 108 | |> Params.put_class(:failure) 109 | |> Params.put_attr(%ErrorCode{name: error}) 110 | 111 | assert {:error, error, creds} == 112 | Protocol.eval_failure(params, creds) 113 | end 114 | end 115 | 116 | test "encode_channel_data/2 encodes a channel data message" do 117 | channel_number = 0x4001 118 | data = "alicehasacat" 119 | 120 | encoded = Protocol.encode_channel_data(channel_number, data) 121 | 122 | assert {:ok, %ChannelData{channel_number: channel_number, data: data}} == 123 | Format.decode(encoded) 124 | end 125 | end 126 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/error_code_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.ErrorCodeTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Format.Body.Attribute.ErrorCode 6 | alias Jerboa.Format.ErrorCode.{FormatError, LengthError} 7 | alias Jerboa.Format.Meta 8 | 9 | describe "encode/1" do 10 | test "ERROR-CODE attribute with valid error code and reason" do 11 | ptest reason: string(max: ErrorCode.max_reason_length) do 12 | code = random_code() 13 | 14 | bin = binary_attr(code, reason) 15 | assert bin == %ErrorCode{code: code, reason: reason} |> ErrorCode.encode() 16 | end 17 | end 18 | 19 | test "ERROR-CODE attribute with valid error name and reason" do 20 | ptest reason: string(max: ErrorCode.max_reason_length) do 21 | name = random_name() 22 | 23 | assert <<0::21, _::11, ^reason::binary>> = 24 | %ErrorCode{name: name, reason: reason} |> ErrorCode.encode() 25 | end 26 | end 27 | 28 | test "ERROR-CODE with non UTF-8 binary as reason" do 29 | reason = <<0xFFFF>> 30 | code = random_code() 31 | 32 | assert_raise ArgumentError, fn -> 33 | %ErrorCode{code: code, reason: reason} |> ErrorCode.encode() 34 | end 35 | end 36 | 37 | test "ERROR-CODE with invalid error code" do 38 | assert_raise ArgumentError, fn -> 39 | %ErrorCode{code: 1, reason: "alice has a cat"} |> ErrorCode.encode() 40 | end 41 | end 42 | 43 | test "ERROR-CODE with invalid error name" do 44 | assert_raise ArgumentError, fn -> 45 | %ErrorCode{name: :not_an_error, reason: "alice has a cat"} |> ErrorCode.encode() 46 | end 47 | end 48 | end 49 | 50 | describe "decode/1" do 51 | test "ERROR-CODE shorter than 4 bytes" do 52 | ptest length: int(min: 0, max: 3), content: int(min: 0) do 53 | bin = <> 54 | 55 | assert {:error, %LengthError{length: ^length}} = ErrorCode.decode(bin, %Meta{}) 56 | end 57 | end 58 | 59 | test "ERROR-CODE with non UTF-8 reason" do 60 | code = random_code() 61 | reason = <<0xFFFF>> 62 | bin = binary_attr(code, reason) 63 | 64 | assert {:error, error} = ErrorCode.decode(bin, %Meta{}) 65 | assert %FormatError{} = error 66 | assert error.code == code 67 | assert error.reason == reason 68 | end 69 | 70 | test "ERROR-CODE with invalid error code" do 71 | code = 123 72 | reason = "alice has a cat" 73 | bin = binary_attr(code, reason) 74 | 75 | assert {:error, error} = ErrorCode.decode(bin, %Meta{}) 76 | assert %FormatError{} = error 77 | assert error.code == code 78 | assert error.reason == reason 79 | end 80 | 81 | test "valid ERROR-CODE attribute" do 82 | ptest reason: string(max: ErrorCode.max_reason_length) do 83 | code = random_code() 84 | 85 | bin = binary_attr(code, reason) 86 | 87 | assert {:ok, _, attr} = ErrorCode.decode(bin, %Meta{}) 88 | assert attr.code == code 89 | assert attr.name 90 | assert attr.reason == reason 91 | end 92 | end 93 | end 94 | 95 | describe "new/1" do 96 | test "returns filled in struct given valid error code" do 97 | code = 400 98 | 99 | assert %ErrorCode{name: :bad_request, code: code} == ErrorCode.new(code) 100 | end 101 | 102 | test "returns filled in struct given valid error name" do 103 | name = :bad_request 104 | 105 | assert %ErrorCode{name: name, code: 400} == ErrorCode.new(name) 106 | end 107 | 108 | test "raises on invalid error code" do 109 | assert_raise FunctionClauseError, fn -> 110 | ErrorCode.new(801) 111 | end 112 | end 113 | 114 | test "raises on invalid error name" do 115 | assert_raise FunctionClauseError, fn -> 116 | ErrorCode.new(:not_a_valid_error) 117 | end 118 | end 119 | end 120 | 121 | defp binary_attr(code, reason) do 122 | <<0::21, class(code)::3, number(code)::8, reason::binary>> 123 | end 124 | 125 | defp class(code), do: div(code, 100) 126 | 127 | defp number(code), do: rem(code, 100) 128 | 129 | defp random_code, do: ErrorCode.valid_codes() |> Enum.random() 130 | 131 | defp random_name, do: ErrorCode.valid_names() |> Enum.random() 132 | end 133 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/xor_address.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.XORAddress do 2 | @moduledoc false 3 | 4 | ## Functions for decoding and decoding XOR-*-ADDRESS attributes 5 | 6 | use Bitwise 7 | 8 | alias Jerboa.Format.Body.Attribute.{XORMappedAddress, XORPeerAddress, 9 | XORRelayedAddress} 10 | alias Jerboa.Format.XORAddress.{LengthError, IPFamilyError, IPArityError} 11 | alias Jerboa.Format.Meta 12 | alias Jerboa.Params 13 | 14 | @type t :: XORMappedAddress.t | XORPeerAddress.t | XORRelayedAddress.t 15 | 16 | @ipv4 <<0x01::8>> 17 | @ipv6 <<0x02::8>> 18 | @magic_cookie Jerboa.Format.Header.MagicCookie.value 19 | @most_significant_magic_16 @magic_cookie >>> 16 20 | 21 | defmacro __using__(type_code: type_code) do 22 | quote do 23 | defstruct [:family, :address, :port] 24 | 25 | @type t :: %__MODULE__{ 26 | family: :ipv4 | :ipv6, 27 | address: :inet.ip_address, 28 | port: :inet.port_number 29 | } 30 | 31 | @doc """ 32 | Creates new address struct and fills address family 33 | based on passed IP address format 34 | """ 35 | @spec new(address :: :inet.ip_address, port :: :inet.port_number) :: t 36 | def new({_, _, _, _} = addr, port) do 37 | %__MODULE__{family: :ipv4, address: addr, port: port} 38 | end 39 | def new({_, _, _, _, _, _, _, _} = addr, port) do 40 | %__MODULE__{family: :ipv6, address: addr, port: port} 41 | end 42 | 43 | defimpl Jerboa.Format.Body.Attribute.Encoder do 44 | def type_code(_), do: unquote(type_code) 45 | 46 | def encode(attr, meta) do 47 | value = 48 | Jerboa.Format.Body.Attribute.XORAddress.encode(attr, meta.params) 49 | {meta, value} 50 | end 51 | end 52 | 53 | defimpl Jerboa.Format.Body.Attribute.Decoder do 54 | def decode(attr, value, meta) do 55 | Jerboa.Format.Body.Attribute.XORAddress.decode(attr, value, meta) 56 | end 57 | end 58 | end 59 | end 60 | 61 | @spec encode(t, Params.t) :: binary 62 | def encode(%{family: :ipv4, address: a, port: p}, _params) do 63 | encode(@ipv4, ipv4_encode(a), p) 64 | end 65 | def encode(%{family: :ipv6, address: a, port: p}, %Params{identifier: i}) do 66 | encode(@ipv6, ipv6_encode(a, i), p) 67 | end 68 | 69 | @spec decode(t, value :: binary, Meta.t) 70 | :: {:ok, Meta.t, t} | {:error, struct} 71 | def decode(attr, <<_::8, @ipv4, port::16, addr::32-bits>>, meta) do 72 | {:ok, meta, attribute(attr, addr, port)} 73 | end 74 | def decode(attr, <<_::8, @ipv6, port::16, addr::128-bits>>, 75 | %Meta{params: %Params{identifier: id}} = meta) do 76 | {:ok, meta, attribute(attr, addr, port, id)} 77 | end 78 | def decode(_, value, _) when byte_size(value) != 20 and byte_size(value) != 8 do 79 | {:error, LengthError.exception(length: byte_size(value))} 80 | end 81 | def decode(_, <<_::8, f::8-bits, _::16, _::binary>>, _) when f == @ipv4 or f == @ipv6 do 82 | {:error, IPArityError.exception(family: f)} 83 | end 84 | def decode(_, <<_::8, f::8, _::binary>>, _) do 85 | {:error, IPFamilyError.exception(number: f)} 86 | end 87 | 88 | defp encode(family, addr, port) do 89 | <<0::8, family::8-bits, port(port)::16, addr::binary>> 90 | end 91 | 92 | defp ipv4_encode(addr) when tuple_size(addr) === 4 do 93 | addr |> binerize |> ipv4_decode |> binerize 94 | end 95 | 96 | defp ipv6_encode(addr, id) when tuple_size(addr) === 8 do 97 | addr |> binerize |> ipv6_decode(id) |> binerize 98 | end 99 | 100 | defp ipv4_decode(x_addr) when 32 === bit_size(x_addr) do 101 | <> = :crypto.exor x_addr, <<@magic_cookie::32>> 102 | {a, b, c, d} 103 | end 104 | 105 | defp ipv6_decode(x_addr, id) do 106 | <> = 107 | :crypto.exor(x_addr, <<@magic_cookie::32>> <> id) 108 | {a, b, c, d, e, f, g, h} 109 | end 110 | 111 | defp attribute(attr, x_addr, x_port) do 112 | struct attr, %{ 113 | family: :ipv4, 114 | address: ipv4_decode(x_addr), 115 | port: port(x_port) 116 | } 117 | end 118 | 119 | defp attribute(attr, x_addr, x_port, id) do 120 | struct attr, %{ 121 | family: :ipv6, 122 | address: ipv6_decode(x_addr, id), 123 | port: port(x_port) 124 | } 125 | end 126 | 127 | defp port(x_port) do 128 | x_port ^^^ @most_significant_magic_16 129 | end 130 | 131 | defp binerize({a, b, c, d}) do 132 | <> 133 | end 134 | defp binerize({a, b, c, d, e, f, g, h}) do 135 | <> 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/jerboa/client/protocol/allocate.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.Allocate do 2 | @moduledoc false 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format.Body.Attribute.XORMappedAddress, as: XMA 6 | alias Jerboa.Format.Body.Attribute.XORRelayedAddress, as: XRA 7 | alias Jerboa.Format.Body.Attribute.{RequestedTransport, Lifetime, Realm, 8 | Nonce, ErrorCode, EvenPort, 9 | ReservationToken} 10 | alias Jerboa.Client 11 | alias Jerboa.Client.Protocol 12 | alias Jerboa.Client.Credentials 13 | 14 | @spec request(Credentials.t, Client.allocate_opts) :: Protocol.request 15 | def request(creds, opts) do 16 | params = params(creds, opts) 17 | Protocol.encode_request(params, creds) 18 | end 19 | 20 | @spec eval_response(response :: Params.t, Credentials.t, Client.allocate_opts) 21 | :: {:ok, relayed_address :: Client.address, lifetime :: non_neg_integer} 22 | | {:ok, Client.address, non_neg_integer, reservation_token :: binary} 23 | | {:error, Client.error, Credentials.t} 24 | def eval_response(params, creds, opts) do 25 | with :allocate <- Params.get_method(params), 26 | :success <- Params.get_class(params), 27 | %{address: raddr, port: rport, family: :ipv4} <- Params.get_attr(params, XRA), 28 | %XMA{} <- Params.get_attr(params, XMA), 29 | %{duration: lifetime} <- Params.get_attr(params, Lifetime), 30 | :ok <- check_reservation_token(params, opts) do 31 | relayed_address = {raddr, rport} 32 | maybe_with_reservation_token(relayed_address, lifetime, params, opts) 33 | else 34 | :failure -> 35 | eval_failure(params, creds) 36 | _ -> 37 | {:error, :bad_response, creds} 38 | end 39 | end 40 | 41 | @spec check_reservation_token(Params.t, Client.allocate_opts) :: :ok | :error 42 | defp check_reservation_token(params, opts) do 43 | with {:ok, true} <- Keyword.fetch(opts, :reserve), 44 | %ReservationToken{} <- Params.get_attr(params, ReservationToken) do 45 | :ok 46 | else 47 | {:ok, _} -> 48 | :ok 49 | :error -> 50 | :ok 51 | _ -> 52 | :error 53 | end 54 | end 55 | 56 | @spec maybe_with_reservation_token(Client.address, non_neg_integer, Params.t, 57 | Client.allocate_opts) 58 | :: {:ok, relayed_address :: Client.address, lifetime :: non_neg_integer} 59 | | {:ok, Client.address, non_neg_integer, reservation_token :: binary} 60 | defp maybe_with_reservation_token(relayed_address, lifetime, params, opts) do 61 | case Keyword.fetch(opts, :reserve) do 62 | {:ok, true} -> 63 | %ReservationToken{value: token} = Params.get_attr(params, ReservationToken) 64 | {:ok, relayed_address, lifetime, token} 65 | _ -> 66 | {:ok, relayed_address, lifetime} 67 | end 68 | end 69 | 70 | @spec params(Credentials.t, Client.allocate_opts) :: Params.t 71 | defp params(creds, opts) do 72 | params = 73 | creds 74 | |> Protocol.base_params() 75 | |> Params.put_class(:request) 76 | |> Params.put_method(:allocate) 77 | |> Params.put_attr(%RequestedTransport{}) 78 | cond do 79 | opts[:reservation_token] -> 80 | token = opts[:reservation_token] 81 | params |> Params.put_attr(%ReservationToken{value: token}) 82 | opts[:reserve] == true -> 83 | params |> Params.put_attr(%EvenPort{reserved?: true}) 84 | opts[:even_port] == true -> 85 | params |> Params.put_attr(%EvenPort{reserved?: false}) 86 | true -> 87 | params 88 | end 89 | end 90 | 91 | @spec eval_failure(resp :: Params.t, Credentials.t) 92 | :: {:error, Client.error, Credentials.t} 93 | defp eval_failure(params, creds) do 94 | realm_attr = Params.get_attr(params, Realm) 95 | nonce_attr = Params.get_attr(params, Nonce) 96 | error = Params.get_attr(params, ErrorCode) 97 | cond do 98 | is_nil error -> 99 | {:error, :bad_response, creds} 100 | should_finalize_creds?(creds, error.name, realm_attr, nonce_attr) -> 101 | new_creds = 102 | Credentials.finalize(creds, realm_attr.value, nonce_attr.value) 103 | {:error, error.name, new_creds} 104 | error.name == :stale_nonce && nonce_attr -> 105 | new_creds = %{creds | nonce: nonce_attr.value} 106 | {:error, error.name, new_creds} 107 | true -> 108 | {:error, error.name, creds} 109 | end 110 | end 111 | 112 | @spec should_finalize_creds?(Credentials.t, ErrorCode.name, 113 | Realm.t | nil, Nonce.t | nil) :: boolean 114 | defp should_finalize_creds?(creds, :unauthorized, %Realm{}, %Nonce{}) do 115 | not Credentials.complete?(creds) 116 | end 117 | defp should_finalize_creds?(_, _, _, _), do: false 118 | end 119 | -------------------------------------------------------------------------------- /lib/jerboa/format/body/attribute/error_code.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.ErrorCode do 2 | @moduledoc """ 3 | ERROR-CODE attribute as defined in [STUN RFC](https://tools.ietf.org/html/rfc5389#section-15.6) 4 | """ 5 | 6 | alias Jerboa.Format.Body.Attribute.{Decoder,Encoder} 7 | alias Jerboa.Format.ErrorCode.{FormatError, LengthError} 8 | alias Jerboa.Format.Meta 9 | 10 | defstruct [:code, :name, reason: ""] 11 | 12 | @typedoc """ 13 | Represents error code of error response 14 | 15 | Struct fields 16 | * `:code` - integer representation of an error 17 | * `:name` - atom representation of an error 18 | * `:reason` 19 | """ 20 | @type t :: %__MODULE__{ 21 | code: code, 22 | name: name, 23 | reason: String.t 24 | } 25 | 26 | @type code :: 300 | 400 | 401 | 420 | 438 | 500 27 | | 403 | 437 | 441 | 442 | 486 | 508 28 | 29 | @type name :: :try_alternate 30 | | :bad_request 31 | | :unauthorized 32 | | :unknown_attribute 33 | | :stale_nonce 34 | | :server_error 35 | | :forbidden 36 | | :allocation_mismatch 37 | | :wrong_credentials 38 | | :unsupported_protocol 39 | | :allocation_quota_reached 40 | | :insufficient_capacity 41 | 42 | @valid_codes [300, 400, 401, 420, 438, 500, 43 | 403, 437, 441, 442, 486, 508] 44 | 45 | @valid_names [:try_alternate, 46 | :bad_request, 47 | :unauthorized, 48 | :unknown_attribute, 49 | :stale_nonce, 50 | :server_error, 51 | :forbidden, 52 | :allocation_mismatch, 53 | :wrong_credentials, 54 | :unsupported_protocol, 55 | :allocation_quota_reached, 56 | :insufficient_capacity] 57 | 58 | @max_reason_length 128 59 | 60 | @spec new(name | code) :: t 61 | def new(code_or_name) 62 | def new(code) when code in @valid_codes do 63 | %__MODULE__{code: code, name: code_to_name(code)} 64 | end 65 | def new(name) when name in @valid_names do 66 | %__MODULE__{name: name, code: name_to_code(name)} 67 | end 68 | 69 | defimpl Encoder do 70 | alias Jerboa.Format.Body.Attribute.ErrorCode 71 | @type_code 0x0009 72 | 73 | @spec type_code(ErrorCode.t) :: integer 74 | def type_code(_), do: @type_code 75 | 76 | @spec encode(ErrorCode.t, Meta.t) :: {Meta.t, binary} 77 | def encode(attr, meta), do: {meta, ErrorCode.encode(attr)} 78 | end 79 | 80 | defimpl Decoder do 81 | alias Jerboa.Format.Body.Attribute.ErrorCode 82 | 83 | @spec decode(ErrorCode.t, value :: binary, Meta.t) 84 | :: {:ok, Meta.t, ErrorCode.t} | {:error, struct} 85 | def decode(_, value, meta), do: ErrorCode.decode(value, meta) 86 | end 87 | 88 | @doc false 89 | def encode(%__MODULE__{code: code, name: name, reason: reason}) do 90 | error_code = code || name_to_code(name) 91 | if code_valid?(error_code) do 92 | encode(error_code, reason) 93 | else 94 | raise ArgumentError, "invalid or missing error code or name " <> 95 | "while encoding ERROR-CODE attribute" 96 | end 97 | end 98 | 99 | defp encode(error_code, reason) do 100 | if reason_valid?(reason) do 101 | error_class = div error_code, 100 102 | error_number = rem error_code, 100 103 | <<0::21, error_class::3, error_number::8>> <> reason 104 | else 105 | raise ArgumentError, "ERROR-CODE reason must be UTF-8 encoded binary" 106 | end 107 | end 108 | 109 | @doc false 110 | def decode(<<0::21, error_class::3, error_number::8, reason::binary>>, meta) do 111 | code = code(error_class, error_number) 112 | if reason_valid?(reason) && code_valid?(code) do 113 | {:ok, meta, 114 | %__MODULE__{ 115 | code: code, 116 | name: code_to_name(code), 117 | reason: reason 118 | }} 119 | else 120 | {:error, FormatError.exception(code: code, 121 | reason: reason)} 122 | end 123 | end 124 | def decode(bin, _) do 125 | {:error, LengthError.exception(length: byte_size(bin))} 126 | end 127 | 128 | for {code, name} <- List.zip([@valid_codes, @valid_names]) do 129 | defp code_to_name(unquote(code)), do: unquote(name) 130 | defp name_to_code(unquote(name)), do: unquote(code) 131 | defp code_valid?(unquote(code)), do: true 132 | end 133 | 134 | defp code_to_name(_), do: :error 135 | defp name_to_code(_), do: :error 136 | defp code_valid?(_), do: false 137 | 138 | defp reason_valid?(reason) do 139 | String.valid?(reason) && String.length(reason) <= @max_reason_length 140 | end 141 | 142 | defp code(class, number), do: class * 100 + number 143 | 144 | @doc false 145 | def max_reason_length, do: @max_reason_length 146 | 147 | @doc false 148 | def valid_codes, do: @valid_codes 149 | 150 | @doc false 151 | def valid_names, do: @valid_names 152 | 153 | end 154 | -------------------------------------------------------------------------------- /lib/jerboa/format/message_integrity.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.MessageIntegrity do 2 | @moduledoc false 3 | 4 | alias Jerboa.Format.Meta 5 | alias Jerboa.Params 6 | alias Jerboa.Format.Body.Attribute.Username 7 | alias Jerboa.Format.Body.Attribute.Realm 8 | alias Jerboa.Format.MessageIntegrity.FormatError 9 | 10 | @type_code 0x0008 11 | @hash_length 20 12 | @attr_length @hash_length + 4 13 | 14 | def type_code, do: @type_code 15 | 16 | @spec extract(Meta.t, binary) :: {:ok, Meta.t} | {:error, struct} 17 | def extract(meta, <<@type_code::16, @hash_length::16, 18 | hash::@hash_length-binary, _::binary>>) do 19 | new_meta = 20 | %{meta | message_integrity: hash, 21 | params: %{meta.params | signed?: true}} 22 | {:ok, new_meta} 23 | end 24 | def extract(_, _) do 25 | {:error, FormatError.exception()} 26 | end 27 | 28 | @spec apply(Meta.t) :: Meta.t 29 | def apply(meta) do 30 | if has_required_options?(meta) do 31 | apply_message_integrity(meta) 32 | else 33 | meta 34 | end 35 | end 36 | 37 | @spec verify(Meta.t) :: {:ok, Meta.t} 38 | def verify(meta) do 39 | verified? = 40 | with true <- signed?(meta), 41 | true <- has_required_options?(meta), 42 | :ok <- verify_message_integrity(meta) do 43 | true 44 | else 45 | _ -> false 46 | end 47 | {:ok, %{meta | params: %{meta.params | verified?: verified?}}} 48 | end 49 | 50 | @spec signed?(Meta.t) :: boolean 51 | defp signed?(meta), do: meta.params.signed? 52 | 53 | @spec has_required_options?(Meta.t) :: boolean 54 | defp has_required_options?(meta) do 55 | with {:ok, _} <- get_secret(meta), 56 | {:ok, _} <- get_username(meta), 57 | {:ok, _} <- get_realm(meta) do 58 | true 59 | else 60 | _ -> false 61 | end 62 | end 63 | 64 | @spec get_username(Meta.t) :: {:ok, String.t} | :error 65 | defp get_username(meta) do 66 | from_attr = Params.get_attr(meta.params, Username) 67 | from_opts = meta.options[:username] 68 | cond do 69 | from_attr -> {:ok, from_attr.value} 70 | from_opts -> {:ok, from_opts} 71 | true -> :error 72 | end 73 | end 74 | 75 | @spec get_secret(Meta.t) :: {:ok, String.t} | :error 76 | defp get_secret(meta) do 77 | case meta.options[:secret] do 78 | nil -> :error 79 | secret -> {:ok, secret} 80 | end 81 | end 82 | 83 | @spec get_realm(Meta.t) :: {:ok, String.t} | :error 84 | defp get_realm(meta) do 85 | from_attr = Params.get_attr(meta.params, Realm) 86 | from_opts = meta.options[:realm] 87 | cond do 88 | from_attr -> {:ok, from_attr.value} 89 | from_opts -> {:ok, from_opts} 90 | true -> :error 91 | end 92 | end 93 | 94 | @spec apply_message_integrity(Meta.t) :: Meta.t 95 | defp apply_message_integrity(meta) do 96 | key = calculate_hash_key(meta) 97 | data = get_hash_subject(meta) 98 | hash = calculate_hash(key, data) 99 | %{meta | body: meta.body <> attribute(hash), 100 | header: modify_header_length(meta.header)} 101 | end 102 | 103 | @spec verify_message_integrity(Meta.t) :: :ok | :error 104 | defp verify_message_integrity(meta) do 105 | key = calculate_hash_key(meta) 106 | data = meta |> amend_header_and_body() |> get_hash_subject() 107 | hash = calculate_hash(key, data) 108 | if hash == meta.message_integrity do 109 | :ok 110 | else 111 | :error 112 | end 113 | end 114 | 115 | @spec calculate_hash_key(Meta.t) :: binary 116 | defp calculate_hash_key(meta) do 117 | {:ok, username} = get_username(meta) 118 | {:ok, realm} = get_realm(meta) 119 | {:ok, secret} = get_secret(meta) 120 | :crypto.hash :md5, [username, ":", realm, ":", secret] 121 | end 122 | 123 | @spec calculate_hash(binary, iodata) :: binary 124 | def calculate_hash(key, data) do 125 | :crypto.hmac(:sha, key, data) 126 | end 127 | 128 | @spec get_hash_subject(Meta.t) :: iolist 129 | defp get_hash_subject(%Meta{header: header, body: body}) do 130 | [modify_header_length(header), body] 131 | end 132 | 133 | @spec modify_header_length(header :: <<_::32, _::_ * 8>>) :: <<_::32, _::_ * 8>> 134 | defp modify_header_length(<<0::2, type::14, length::16, rest::binary>>) do 135 | <<0::2, type::14, (length + @attr_length)::16, rest::binary>> 136 | end 137 | 138 | @spec attribute(hash :: binary) :: attribute :: <<_::32, _::_ * 8>> 139 | defp attribute(hash) do 140 | <<@type_code::16, @hash_length::16, hash::binary>> 141 | end 142 | 143 | @spec amend_header_and_body(Meta.t) :: Meta.t 144 | defp amend_header_and_body(meta) do 145 | length = meta.length_up_to_integrity 146 | 147 | <<0::2, type::14, _::16, header_rest::binary>> = meta.header 148 | amended_header = <<0::2, type::14, length::16, header_rest::binary>> 149 | 150 | <> = meta.body 151 | 152 | %{meta | body: amended_body, header: amended_header} 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /test/jerboa/format/head_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.HeaderTest do 2 | use ExUnit.Case, async: true 3 | use Quixir 4 | 5 | alias Jerboa.Format.Header 6 | alias Jerboa.Format.{First2BitsError, 7 | MagicCookieError, UnknownMethodError, Last2BitsError} 8 | alias Jerboa.Format.Header.{MagicCookie, Type.Method, Type.Class} 9 | alias Jerboa.Format.Header 10 | alias Jerboa.Params 11 | alias Jerboa.Format.Meta 12 | 13 | @magic MagicCookie.value 14 | @helpers [first_2_bits: 1, 15 | type: 1, 16 | magic_cookie: 1, 17 | identifier: 1 18 | ] 19 | 20 | describe "Header.encode/1" do 21 | 22 | test "header has all five pieces (and reasonable values)" do 23 | 24 | ## Given: 25 | import Jerboa.Test.Helper.Header, only: @helpers 26 | alias Jerboa.Test.Helper.Format, as: FormatHelper 27 | parameters = FormatHelper.binding_request() 28 | 29 | ## When: 30 | %Meta{header: bin} = Header.encode(%Meta{params: parameters}) 31 | 32 | ## Then: 33 | assert byte_size(bin) === 20 34 | assert first_2_bits(bin) === <<0::2>> 35 | assert type(bin) === 1 36 | assert FormatHelper.bytes_for_body(bin) === 0 37 | assert magic_cookie(bin) === 0x2112A442 38 | assert identifier(bin) === parameters.identifier 39 | end 40 | end 41 | 42 | describe "Header.decode/1" do 43 | 44 | test "fails given packet not starting with two zeroed bits" do 45 | ptest first_two: int(min: 1, max: 3), content: int(min: 0) do 46 | bit_length = 20 * 8 - 2 47 | packet = <> 48 | 49 | {:error, error} = Header.decode parameterize(packet) 50 | 51 | assert %First2BitsError{bits: ^first_two} = error 52 | end 53 | end 54 | 55 | test "fails given packet with invalid STUN magic cookie" do 56 | ptest before_magic: int(min: 0), magic: int(min: 0, max: @magic - 1), 57 | after_magic: int(min: 0) do 58 | packet = <<0::2, before_magic::30, magic::32, 59 | after_magic::unit(8)-size(12)>> 60 | <> = packet 61 | 62 | {:error, error} = Header.decode parameterize(packet) 63 | 64 | assert %MagicCookieError{header: ^header} = error 65 | end 66 | end 67 | 68 | test "fails if length isn't a multiple of 4" do 69 | ptest length: int(min: 0) do 70 | length = if rem(length, 4) == 0, do: length + 1, else: length 71 | packet = <<0::2, 1::14, length::16, @magic::32, 0::96>> 72 | 73 | {:error, error} = Header.decode parameterize(packet) 74 | 75 | assert %Last2BitsError{length: ^length} = error 76 | end 77 | end 78 | 79 | test "fails given packet with invalid STUN method" do 80 | ptest method: int(min: 1024, max: 4095), class: int(min: 0, max: 3) do 81 | <> = <> 82 | <> = <> 83 | packet = <<0::2, m2::5, c1::1, m1::3, c0::1, m0::4, 0::16, 84 | @magic::32, 0::96>> 85 | 86 | {:error, error} = Header.decode parameterize(packet) 87 | 88 | assert %UnknownMethodError{method: ^method} = error 89 | end 90 | end 91 | 92 | test "decodes class and method from the header" do 93 | ptest method: int(min: 1, max: 1), class: int(min: 0, max: 3) do 94 | bit_class = <> = <> 95 | bit_method = <> = <> 96 | packet = <<0::2, m2::5, c1::1, m1::3, c0::1, m0::4, 0::16, 97 | @magic::32, 0::96>> 98 | decoded_class = Class.decode(bit_class) 99 | {:ok, decoded_method} = Method.decode(bit_method) 100 | 101 | {:ok, %Meta{params: params}} = Header.decode parameterize(packet) 102 | 103 | assert decoded_method == params.method 104 | assert decoded_class == params.class 105 | end 106 | end 107 | end 108 | 109 | test "Header.Length.encode/1 encodes length on 16 bits (two bytes)" do 110 | x = Header.Length.encode(%Meta{body: <<0,1,0,1>>}) 111 | 112 | assert <<4::size(16)>> == x 113 | end 114 | 115 | test "Header.Type encode/1 and decode/1 return opposite results" do 116 | allowed = [binding: [:request, :success, :failure], 117 | allocate: [:request, :success, :failure], 118 | refresh: [:request, :success, :failure], 119 | create_permission: [:request, :success, :failure], 120 | channel_bind: [:request, :success, :failure], 121 | send: [:indication], 122 | data: [:indication]] 123 | 124 | for {method, classes} <- allowed do 125 | for class <- classes do 126 | params = %Params{class: class, method: method} 127 | 128 | bin = Header.Type.encode(%Meta{params: params}) 129 | 130 | assert {:ok, decoded_class, decoded_method} = Header.Type.decode(bin) 131 | assert decoded_class == class 132 | assert decoded_method == method 133 | end 134 | end 135 | end 136 | 137 | defp parameterize(x) do 138 | %Meta{header: x} 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /test/jerboa/params_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.ParamsTest do 2 | use ExUnit.Case 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Format.Body.Attribute 6 | alias Attribute.{XORMappedAddress, Data} 7 | 8 | test "new/0 returns fresh params struct with ID set" do 9 | p = Params.new 10 | 11 | assert p.identifier != nil 12 | assert byte_size(p.identifier) == 12 13 | end 14 | 15 | test "put_class/2 sets class field" do 16 | class = :request 17 | 18 | p = Params.new() |> Params.put_class(class) 19 | 20 | assert p.class == class 21 | end 22 | 23 | test "get_class/1 retrieves class field" do 24 | class = :request 25 | p = Params.new() |> Params.put_class(class) 26 | 27 | assert class == Params.get_class(p) 28 | end 29 | 30 | test "put_method/2 sets method field" do 31 | method = :binding 32 | 33 | p = Params.new() |> Params.put_method(method) 34 | 35 | assert p.method == method 36 | end 37 | 38 | test "get_method/1 retrieves method field" do 39 | method = :binding 40 | p = Params.new() |> Params.put_method(method) 41 | 42 | assert method == Params.get_method(p) 43 | end 44 | 45 | test "put_id/2 sets identifier field" do 46 | id = Params.generate_id() 47 | p = Params.new() |> Params.put_id(id) 48 | 49 | assert p.identifier == id 50 | end 51 | 52 | test "get_id/1 retrieves identifier field" do 53 | id = Params.generate_id() 54 | p = Params.new() |> Params.put_id(id) 55 | 56 | assert id == Params.get_id(p) 57 | end 58 | 59 | test "set_attrs/1 sets whole attributes list" do 60 | params = Params.new() |> Params.put_attr(%Data{}) 61 | attrs = List.duplicate(%XORMappedAddress{}, 3) 62 | 63 | p = params |> Params.set_attrs(attrs) 64 | 65 | assert p.attributes == attrs 66 | end 67 | 68 | test "get_attrs/1 retrieves whole attributes list" do 69 | attrs = List.duplicate(%XORMappedAddress{}, 3) 70 | p = Params.new |> Params.set_attrs(attrs) 71 | 72 | assert attrs == Params.get_attrs(p) 73 | end 74 | 75 | test "get_attrs/2 retrieves all attributes with given name" do 76 | attr1 = %XORMappedAddress{family: :ipv4} 77 | attr2 = %XORMappedAddress{family: :ipv6} 78 | attr3 = %Data{} 79 | 80 | p = Params.new() |> Params.set_attrs([attr1, attr2, attr3]) 81 | attrs = Params.get_attrs(p, XORMappedAddress) 82 | 83 | assert attr1 in attrs 84 | assert attr2 in attrs 85 | refute attr3 in attrs 86 | end 87 | 88 | describe "put_attr/3" do 89 | test "adds attribute to attributes list in params struct" do 90 | attr = %XORMappedAddress{family: :ipv4, 91 | address: {127, 0, 0, 1}, 92 | port: 3333} 93 | 94 | p = Params.new() |> Params.put_attr(attr) 95 | 96 | assert [attr] == Params.get_attrs(p) 97 | end 98 | 99 | test "overwrites existing attributes with the same name (with overwrite: true)" do 100 | attr1 = %XORMappedAddress{family: :ipv4, address: {127, 0, 0, 1}, port: 3333} 101 | attr2 = %XORMappedAddress{family: :ipv6, address: {0, 0, 0, 0, 0, 0, 0, 1}, 102 | port: 3333} 103 | attr3 = %XORMappedAddress{family: :ipv6, address: {0, 0, 0, 0, 0, 0, 0, 1}, 104 | port: 1234} 105 | 106 | p = 107 | Params.new() 108 | |> Params.set_attrs([attr1, attr2]) 109 | |> Params.put_attr(attr3, overwrite: true) 110 | 111 | assert [attr3] == Params.get_attrs(p) 112 | end 113 | 114 | test "does not overwrite exisiting attibutes (with overwrite: false)" do 115 | attr1 = %XORMappedAddress{family: :ipv4, address: {127, 0, 0, 1}, port: 3333} 116 | attr2 = %XORMappedAddress{family: :ipv6, address: {0, 0, 0, 0, 0, 0, 0, 1}, 117 | port: 3333} 118 | attr3 = %Data{} 119 | 120 | p = 121 | Params.new() 122 | |> Params.set_attrs([attr1, attr2]) 123 | |> Params.put_attr(attr3, overwrite: true) 124 | attrs = Params.get_attrs(p) 125 | 126 | assert attr1 in attrs 127 | assert attr2 in attrs 128 | assert attr3 in attrs 129 | end 130 | end 131 | 132 | describe "get_attr/2" do 133 | test "retrieves attribute by its name" do 134 | attr = %XORMappedAddress{family: :ipv4, address: {127, 0, 0, 1}, port: 3333} 135 | 136 | p = Params.new() |> Params.put_attr(attr) 137 | 138 | assert attr == Params.get_attr(p, XORMappedAddress) 139 | end 140 | 141 | test "returns nil if attribute is not present" do 142 | p = Params.new 143 | 144 | assert nil == Params.get_attr(p, XORMappedAddress) 145 | end 146 | end 147 | 148 | test "put_attrs/2 adds attributes to params struct" do 149 | attr1 = %XORMappedAddress{family: :ipv4} 150 | attr2 = %XORMappedAddress{family: :ipv6} 151 | attr3 = %Data{} 152 | 153 | p = 154 | Params.new() 155 | |> Params.set_attrs([attr1]) 156 | |> Params.put_attrs([attr2, attr3]) 157 | attrs = Params.get_attrs(p) 158 | 159 | assert attr1 in attrs 160 | assert attr2 in attrs 161 | assert attr3 in attrs 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /lib/jerboa/params.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Params do 2 | @moduledoc """ 3 | Data structure representing STUN message parameters 4 | """ 5 | 6 | alias Jerboa.Format.Header.Type.{Class, Method} 7 | alias Jerboa.Format.Body.Attribute 8 | 9 | defstruct [:class, :method, :identifier, attributes: [], 10 | signed?: false, verified?: false] 11 | 12 | @typedoc """ 13 | The main data structure representing STUN message parameters 14 | 15 | The following fields coresspond to the those described in the [STUN 16 | RFC](https://tools.ietf.org/html/rfc5389#section-6): 17 | 18 | * `class` is one of request, success or failure response, or indication 19 | * `method` is a STUN (or TURN) message method described in one of the respective RFCs 20 | * `identifier` is a unique transaction identifier 21 | * `attributes` is a list of STUN (or TURN) attributes as described in their 22 | respective RFCs 23 | * `signed?` indicates wheter STUN message was signed with MESSAGE-INTEGRITY 24 | attribute - it isn't important when encoding a message 25 | * `verified?` - indicates wheter MESSAGE-INTEGRITY from STUN message was 26 | successfully verified. Same as `signed?`, it's only relevant when decoding 27 | messages. Note that messages which are `verified?` are also `signed?`, but not 28 | the other way around. 29 | """ 30 | @type t :: %__MODULE__{ 31 | class: Class.t, 32 | method: Method.t, 33 | identifier: binary, 34 | attributes: [Attribute.t], 35 | signed?: boolean, 36 | verified?: boolean 37 | } 38 | 39 | @doc """ 40 | Returns params struct with filled in transaction id 41 | """ 42 | @spec new :: t 43 | def new do 44 | %__MODULE__{class: :request, 45 | method: :binding, 46 | identifier: generate_id()} 47 | end 48 | 49 | @doc """ 50 | Sets STUN class in params struct 51 | """ 52 | @spec put_class(t, Class.t) :: t 53 | def put_class(params, class) do 54 | %{params | class: class} 55 | end 56 | 57 | @doc """ 58 | Retrieves class field from params struct 59 | """ 60 | @spec get_class(t) :: Class.t | nil 61 | def get_class(%__MODULE__{class: class}), do: class 62 | 63 | @doc """ 64 | Sets STUN method in params struct 65 | """ 66 | @spec put_method(t, Method.t) :: t 67 | def put_method(params, method) do 68 | %{params | method: method} 69 | end 70 | 71 | @doc """ 72 | Retrieves method field from params struct 73 | """ 74 | @spec get_method(t) :: Method.t | nil 75 | def get_method(%__MODULE__{method: method}), do: method 76 | 77 | @doc """ 78 | Sets STUN transaction identifier in params struct 79 | """ 80 | @spec put_id(t, binary) :: t 81 | def put_id(params, id) do 82 | %{params | identifier: id} 83 | end 84 | 85 | @doc """ 86 | Retrieves transaction ID from params struct 87 | """ 88 | @spec get_id(t) :: binary | nil 89 | def get_id(%__MODULE__{identifier: id}), do: id 90 | 91 | @doc """ 92 | Retrieves all attributes from params struct 93 | """ 94 | @spec get_attrs(t) :: [Attribute.t] 95 | def get_attrs(%__MODULE__{attributes: attrs}), do: attrs 96 | 97 | @doc """ 98 | Retrieves all attributes with given name from params struct 99 | """ 100 | @spec get_attrs(t, attr_name :: module) :: [Attribute.t] 101 | def get_attrs(%__MODULE__{attributes: attrs}, attr_name) do 102 | Enum.filter attrs, & Attribute.name(&1) == attr_name 103 | end 104 | 105 | @doc """ 106 | Sets whole attributes list in params struct 107 | """ 108 | @spec set_attrs(t, [Attribute.t]) :: t 109 | def set_attrs(params, attrs) do 110 | %{params | attributes: attrs} 111 | end 112 | 113 | @doc """ 114 | Retrieves single attribute from params struct 115 | 116 | Returns `nil` if attribute is not present. 117 | """ 118 | @spec get_attr(t, attr_name :: module) :: Attribute.t | nil 119 | def get_attr(params, attr_name) do 120 | params.attributes 121 | |> Enum.find(fn a -> Attribute.name(a) === attr_name end) 122 | end 123 | 124 | @doc """ 125 | Puts single attribute in params struct 126 | 127 | `:overwrite` option determines wheter attributes of the same type 128 | will be removed and the new one will be put in their place. 129 | Defaults to `true`. 130 | """ 131 | @spec put_attr(t, Attribute.t, overwrite: boolean) :: t 132 | def put_attr(params, attr, opts \\ [overwrite: true]) 133 | def put_attr(params, attr, opts) do 134 | attrs = 135 | if opts[:overwrite] do 136 | params.attributes 137 | |> Enum.reject(fn a -> Attribute.name(a) === Attribute.name(attr) end) 138 | else 139 | params.attributes 140 | end 141 | %{params | attributes: [attr | attrs]} 142 | end 143 | 144 | @doc """ 145 | Adds list of attriubutes to params struct 146 | 147 | It's functionally equal to recursively calling `put_attr/2` 148 | with `overwrite: false` on params struct. 149 | """ 150 | @spec put_attrs(t, [Attribute.t]) :: t 151 | def put_attrs(params, attrs) when is_list(attrs) do 152 | Enum.reduce attrs, params, 153 | fn attr, acc -> put_attr(acc, attr, overwrite: false) end 154 | end 155 | 156 | @doc """ 157 | Generates STUN transaction ID 158 | """ 159 | @spec generate_id :: binary 160 | def generate_id do 161 | :crypto.strong_rand_bytes(12) 162 | end 163 | end 164 | -------------------------------------------------------------------------------- /test/jerboa/format/message_integrity_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.MessageIntegrityTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Params 6 | alias Jerboa.Format.Header 7 | alias Jerboa.Format.Body 8 | alias Jerboa.Format.MessageIntegrity 9 | alias Jerboa.Format.Meta 10 | alias Jerboa.Format.Body.Attribute.{Nonce, Realm, Username} 11 | 12 | @mi_attr_length 24 13 | 14 | describe "apply/1" do 15 | test "does not apply MI when secret is not given" do 16 | options = [username: "alice", realm: "wonderland"] 17 | body_without_mi = <<>> 18 | 19 | meta = %Meta{options: options} |> MessageIntegrity.apply() 20 | 21 | assert meta.body == body_without_mi 22 | end 23 | 24 | test "does not apply MI when username is not given" do 25 | options = [secret: "secret", realm: "wonderland"] 26 | meta = %Meta{options: options} 27 | 28 | assert meta == MessageIntegrity.apply(meta) 29 | end 30 | 31 | test "does not apply MI when realm is not given" do 32 | options = [username: "alice", secret: "secret"] 33 | meta = %Meta{options: options} 34 | 35 | assert meta == MessageIntegrity.apply(meta) 36 | end 37 | 38 | test "applies MI when necessary values are passed as options" do 39 | params = 40 | Params.new() 41 | |> Params.put_class(:request) 42 | |> Params.put_method(:allocate) 43 | |> Params.put_attr(%Nonce{value: "1234"}) 44 | options = [username: "alice", realm: "wonderland", secret: "secret"] 45 | meta = 46 | %Meta{params: params, options: options} 47 | |> Body.encode() 48 | |> Header.encode() 49 | 50 | new_meta = MessageIntegrity.apply(meta) 51 | 52 | assert byte_size(meta.body) + @mi_attr_length == byte_size(new_meta.body) 53 | end 54 | 55 | test "applies MI when username as realm are passed as attributes" do 56 | params = 57 | Params.new() 58 | |> Params.put_class(:request) 59 | |> Params.put_method(:allocate) 60 | |> Params.put_attr(%Nonce{value: "1234"}) 61 | |> Params.put_attr(%Username{value: "alice"}) 62 | |> Params.put_attr(%Realm{value: "wonderland"}) 63 | options = [secret: "secret"] 64 | meta = 65 | %Meta{params: params, options: options} 66 | |> Body.encode() 67 | |> Header.encode() 68 | 69 | new_meta = MessageIntegrity.apply(meta) 70 | 71 | assert byte_size(meta.body) + @mi_attr_length == byte_size(new_meta.body) 72 | end 73 | end 74 | 75 | describe "verify/1" do 76 | test "sets :verified? to false if message is not signed" do 77 | options = [secret: "secret", username: "alice", realm: "wonderland"] 78 | meta = %Meta{options: options} |> set_signed(false) 79 | 80 | assert {:ok, new_meta} = MessageIntegrity.verify(meta) 81 | refute new_meta.params.verified? 82 | end 83 | 84 | test "sets :verified? to false if secret is not given" do 85 | options = [username: "alice", realm: "wonderland"] 86 | meta = 87 | %Meta{options: options, message_integrity: "abcd"} 88 | |> set_signed() 89 | 90 | assert {:ok, new_meta} = MessageIntegrity.verify(meta) 91 | refute new_meta.params.verified? 92 | end 93 | 94 | test "sets :verified? to false if username is not given" do 95 | options = [secret: "secret", realm: "wonderland"] 96 | meta = %Meta{options: options, message_integrity: "abcd"} |> set_signed() 97 | 98 | assert {:ok, new_meta} = MessageIntegrity.verify(meta) 99 | refute new_meta.params.verified? 100 | end 101 | 102 | test "sets :verified? to false if realm is not given" do 103 | options = [secret: "secret", username: "alice"] 104 | meta = %Meta{options: options, message_integrity: "abcd"} |> set_signed() 105 | 106 | assert {:ok, new_meta} = MessageIntegrity.verify(meta) 107 | refute new_meta.params.verified? 108 | end 109 | 110 | test "sets :verified? to false if extracted MI is not valid 111 | (values passed as options)" do 112 | params = 113 | Params.new() 114 | |> Params.put_class(:request) 115 | |> Params.put_method(:allocate) 116 | |> Params.put_attr(%Nonce{value: "1234"}) 117 | options = [username: "alice", realm: "wonderland", secret: "secret"] 118 | meta = 119 | %Meta{params: params, options: options, message_integrity: "abcd"} 120 | |> Body.encode() 121 | |> Header.encode() 122 | |> set_signed() 123 | 124 | assert {:ok, new_meta} = MessageIntegrity.verify(meta) 125 | refute new_meta.params.verified? 126 | end 127 | 128 | test "set :verified? to false if extracted MI is not valid 129 | (values passed as attibutes)" do 130 | params = 131 | Params.new() 132 | |> Params.put_class(:request) 133 | |> Params.put_method(:allocate) 134 | |> Params.put_attr(%Nonce{value: "1234"}) 135 | |> Params.put_attr(%Username{value: "alice"}) 136 | |> Params.put_attr(%Realm{value: "wonderland"}) 137 | options = [secret: "secret"] 138 | meta = 139 | %Meta{params: params, options: options, message_integrity: "abcd"} 140 | |> Body.encode() 141 | |> Header.encode() 142 | |> set_signed() 143 | 144 | assert {:ok, new_meta} = MessageIntegrity.verify(meta) 145 | refute new_meta.params.verified? 146 | end 147 | end 148 | 149 | defp set_signed(meta, val \\ true) do 150 | %{meta | params: %{meta.params | signed?: val}} 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute/xor_address_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.Attribute.XORAddressTest do 2 | use ExUnit.Case 3 | use Quixir 4 | 5 | alias Jerboa.Test.Helper.XORMappedAddress, as: XORMAHelper 6 | 7 | alias Jerboa.Format.XORAddress.{IPFamilyError, LengthError, IPArityError} 8 | alias Jerboa.Format.Body.Attribute.{XORAddress, XORMappedAddress} 9 | alias Jerboa.Format.Header.MagicCookie 10 | alias Jerboa.Params 11 | alias Jerboa.Format.Meta 12 | 13 | describe "XORAddress.decode/3" do 14 | test "IPv4 XORAddress attribute" do 15 | ptest port: port_gen(), ip_addr: ip4_gen() do 16 | x_ip_addr = x_ip4_addr(ip_addr) 17 | x_port = x_port(port) 18 | attr = <> 19 | body = <<0x0020::16, 8::16, attr::binary>> 20 | struct = %XORMappedAddress{} 21 | 22 | result = XORAddress.decode(struct, attr, %Meta{body: body, length: 12}) 23 | 24 | assert {:ok, _, attr} = result 25 | assert %{family: :ipv4, 26 | address: ^ip_addr, 27 | port: ^port} = attr 28 | end 29 | end 30 | 31 | test "IPv6 XORAddress attribute" do 32 | ptest port: port_gen(), ip_addr: ip6_gen(), id: int(min: 0) do 33 | identifier = <> 34 | x_ip_addr = x_ip6_addr(ip_addr, identifier) 35 | x_port = x_port(port) 36 | attr = <> 37 | body = <<0x0020::16, 20::16, attr::binary>> 38 | struct = %XORMappedAddress{} 39 | meta = %Meta{body: body, length: 24, params: %Params{identifier: identifier}} 40 | 41 | result = XORAddress.decode(struct, attr, meta) 42 | 43 | assert {:ok, _, attr} = result 44 | assert %{family: :ipv6, 45 | address: ^ip_addr, 46 | port: ^port} = attr 47 | end 48 | end 49 | 50 | test "fails when attribute's value has invalid length" do 51 | ptest length: int(min: 9, max: 19), content: int(min: 0) do 52 | bit_length = length * 8 53 | attr = <> 54 | body = <<0x0020::16, length::16, attr::binary>> 55 | struct = %XORMappedAddress{} 56 | 57 | {:error, error} = XORAddress.decode(struct, attr, 58 | %Meta{body: body, length: byte_size(body)}) 59 | 60 | assert %LengthError{length: ^length} = error 61 | end 62 | end 63 | 64 | test "fails when address family is invalid" do 65 | ptest family: int(min: 3, max: 255), content: int(min: 0) do 66 | length = Enum.random([20, 8]) 67 | # the length of attribute value minu first zeroed byte and family 68 | content_length = length * 8 - 16 69 | attr = <> 70 | body = <<0x0020::16, length::16, attr::binary>> 71 | struct = %XORMappedAddress{} 72 | 73 | {:error, error} = XORAddress.decode(struct, attr, 74 | %Meta{body: body, length: byte_size(body)}) 75 | 76 | assert %IPFamilyError{number: ^family} = error 77 | end 78 | end 79 | 80 | test "fails when address does not match family" do 81 | for {family, addr_len} <- [{0x01, 128}, {0x02, 32}] do 82 | attr = <> 83 | body = <<0x0020::16, byte_size(attr)::16, attr::binary>> 84 | struct = %XORMappedAddress{} 85 | 86 | {:error, error} = XORAddress.decode(struct, attr, 87 | %Meta{body: body, length: byte_size(body)}) 88 | 89 | assert %IPArityError{family: <<^family::8>>} = error 90 | end 91 | end 92 | end 93 | 94 | describe "new/2" do 95 | test "fills IPv4 family given address and port" do 96 | address = {0, 0, 0, 0} 97 | port = 1234 98 | 99 | attr = XORMappedAddress.new(address, port) 100 | 101 | assert attr.family == :ipv4 102 | assert attr.address == address 103 | assert attr.port == port 104 | end 105 | 106 | test "fills IPv6 family given address and port" do 107 | address = {0, 0, 0, 0, 0, 0, 0, 0} 108 | port = 1234 109 | 110 | attr = XORMappedAddress.new(address, port) 111 | 112 | assert attr.family == :ipv6 113 | assert attr.address == address 114 | assert attr.port == port 115 | end 116 | end 117 | 118 | describe "XORAddress.encode/2" do 119 | 120 | test "IPv4" do 121 | attr = XORMAHelper.struct(4) 122 | 123 | bin = XORAddress.encode(attr, %Params{}) 124 | 125 | assert address_family(bin) === "IPv4" 126 | assert address_bits(bin) === 32 127 | assert x_port_number(bin) === XORMAHelper.port() 128 | assert x_address(bin) == XORMAHelper.ip_4_a() 129 | end 130 | 131 | test "IPv6" do 132 | i = XORMAHelper.i() 133 | attr = XORMAHelper.struct(6) 134 | params = %Params{identifier: i} 135 | 136 | bin = XORAddress.encode(attr, params) 137 | 138 | assert address_family(bin) === "IPv6" 139 | assert address_bits(bin) === 128 140 | assert x_port_number(bin) === XORMAHelper.port() 141 | assert x_address(bin, i) == XORMAHelper.ip_6_a() 142 | end 143 | end 144 | 145 | defp padding, do: 0 146 | 147 | defp ip_4, do: 0x01 148 | 149 | defp ip_6, do: 0x02 150 | 151 | defp x_port(port) do 152 | :crypto.exor(<>, most_significant_magic_16()) 153 | end 154 | 155 | defp x_ip4_addr({a3, a2, a1, a0}) do 156 | :crypto.exor(<>, MagicCookie.encode()) 157 | end 158 | 159 | defp x_ip6_addr(ip6_addr, identifier) do 160 | {a, b, c, d, e, f, g, h} = ip6_addr 161 | bin_addr = <> 162 | :crypto.exor(bin_addr, MagicCookie.encode() <> identifier) 163 | end 164 | 165 | defp most_significant_magic_16 do 166 | <> = MagicCookie.encode() 167 | x 168 | end 169 | 170 | defp port_gen do 171 | int(min: 0, max: 65_535) 172 | end 173 | 174 | defp ip4_gen do 175 | tuple(like: :erlang.make_tuple(4, byte())) 176 | end 177 | 178 | defp ip6_gen do 179 | tuple(like: :erlang.make_tuple(8, two_bytes())) 180 | end 181 | 182 | defp byte do 183 | int(min: 0, max: 255) 184 | end 185 | 186 | defp two_bytes do 187 | int(min: 0, max: 65_535) 188 | end 189 | 190 | defp address_family(<<_::8, 0x01::8, _::binary>>), do: "IPv4" 191 | defp address_family(<<_::8, 0x02::8, _::binary>>), do: "IPv6" 192 | 193 | defp address_bits(<<_::32, a::binary>>), do: bit_size(a) 194 | 195 | defp x_port_number(<<_::16, p::16-bits, _::binary>>) do 196 | <> = :crypto.exor(p, most_significant_magic_16()) 197 | x 198 | end 199 | 200 | defp x_address(<<_::32, a::32-bits>>) do 201 | <> = :crypto.exor(a, MagicCookie.encode()) 202 | {a, b, c, d} 203 | end 204 | 205 | defp x_address(<<_::32, a::128-bits>>, identifier) do 206 | <> = 207 | :crypto.exor(a, MagicCookie.encode() <> identifier) 208 | {a, b, c, d, e, f, g, h} 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/jerboa/client/relay.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Relay do 2 | @moduledoc false 3 | ## Data structure describing relay (allocation) 4 | 5 | alias Jerboa.Client 6 | alias Jerboa.Client.Relay.Channel 7 | alias Jerboa.Client.Relay.Channels 8 | alias Jerboa.Format 9 | 10 | @max_gen_channel_retries 10 11 | @min_channel_number 0x4000 12 | @max_channel_number 0x7FFF 13 | 14 | defstruct [:address, :lifetime, :timer_ref, permissions: %{}, 15 | channels: %Channels{}] 16 | 17 | @type permissions :: %{Client.ip => timer_ref :: reference} 18 | @type t :: %__MODULE__{ 19 | address: nil | Client.address, 20 | lifetime: nil | non_neg_integer, 21 | timer_ref: nil | reference, 22 | permissions: permissions, 23 | channels: %Channels{} 24 | } 25 | 26 | @spec active?(t) :: boolean 27 | def active?(relay), do: relay.address != nil 28 | 29 | @spec put_address(t, Client.address) :: t 30 | def put_address(relay, address), do: %__MODULE__{relay | address: address} 31 | 32 | @spec put_lifetime(t, lifetime :: non_neg_integer) :: t 33 | def put_lifetime(relay, lifetime), do: %__MODULE__{relay | lifetime: lifetime} 34 | 35 | @spec put_timer_ref(t, reference) :: t 36 | def put_timer_ref(relay, timer_ref) do 37 | %__MODULE__{relay | timer_ref: timer_ref} 38 | end 39 | 40 | @spec put_permissions(t, permissions) :: t 41 | def put_permissions(relay, permissions) do 42 | %__MODULE__{relay | permissions: permissions} 43 | end 44 | 45 | @spec remove_permission(t, Client.ip) :: t 46 | def remove_permission(relay, peer_addr) do 47 | permissions = Map.delete(relay.permissions, peer_addr) 48 | %__MODULE__{relay | permissions: permissions} 49 | end 50 | 51 | @spec has_permission?(t, peer :: Client.address) :: boolean 52 | def has_permission?(relay, {ip, _port}) do 53 | Enum.any?(relay.permissions, fn {peer_addr, _} -> 54 | peer_addr == ip 55 | end) 56 | end 57 | 58 | @spec get_permission_timers(t) :: [timer_ref :: reference] 59 | def get_permission_timers(relay) do 60 | relay.permissions 61 | |> Enum.map(fn {_, timer_ref} -> timer_ref end) 62 | end 63 | 64 | @spec put_channel(t, Channel.t, (Channel.t -> Channel.t)) :: t 65 | def put_channel(relay, channel, on_update \\ & &1) do 66 | by_peer = Map.update(relay.channels.by_peer, channel.peer, 67 | channel, on_update) 68 | by_number = Map.update(relay.channels.by_number, channel.number, 69 | channel, on_update) 70 | channels = %Channels{relay.channels | by_peer: by_peer, by_number: by_number} 71 | %__MODULE__{relay | channels: channels} 72 | end 73 | 74 | @spec remove_channel(t, peer :: Client.address, Format.channel_number) :: t 75 | def remove_channel(relay, peer, channel_number) do 76 | by_peer = Map.delete(relay.channels.by_peer, peer) 77 | by_number = Map.delete(relay.channels.by_number, channel_number) 78 | channels = %Channels{relay.channels | by_peer: by_peer, by_number: by_number} 79 | %__MODULE__{relay | channels: channels} 80 | end 81 | 82 | @spec get_channels(t) :: [Channel.t] 83 | def get_channels(relay) do 84 | Map.values(relay.channels.by_peer) 85 | end 86 | 87 | @spec get_channel_by_peer(t, Client.address) :: {:ok, Channel.t} | :error 88 | def get_channel_by_peer(relay, peer) do 89 | Map.fetch(relay.channels.by_peer, peer) 90 | end 91 | 92 | @spec get_channel_by_number(t, Format.channel_number) 93 | :: {:ok, Channel.t} | :error 94 | def get_channel_by_number(relay, number) do 95 | Map.fetch(relay.channels.by_number, number) 96 | end 97 | 98 | @spec has_channel_bound?(t, Client.address) :: boolean 99 | def has_channel_bound?(relay, peer) do 100 | case get_channel_by_peer(relay, peer) do 101 | {:ok, _} -> true 102 | _ -> false 103 | end 104 | end 105 | 106 | @spec gen_channel_number(t, peer :: Client.address) 107 | :: {:ok, Format.channel_number} 108 | | {:error, :peer_locked | :capacity_reached | :retries_limit_reached} 109 | def gen_channel_number(relay, peer) do 110 | cond do 111 | MapSet.member?(relay.channels.locked_peers, peer) -> 112 | {:error, :peer_locked} 113 | channel_capacity_reached?(relay) -> 114 | {:error, :capacity_reached} 115 | has_channel_bound?(relay, peer) -> 116 | {:ok, channel} = get_channel_by_peer(relay, peer) 117 | {:ok, channel.number} 118 | true -> 119 | do_gen_channel_number(relay) 120 | end 121 | end 122 | 123 | @spec lock_channel(t, peer :: Client.address, Format.channel_number) :: t 124 | def lock_channel(relay, peer, channel_number) do 125 | locked_numbers = MapSet.put(relay.channels.locked_numbers, channel_number) 126 | locked_peers = MapSet.put(relay.channels.locked_peers, peer) 127 | channels = %Channels{relay.channels | locked_numbers: locked_numbers, 128 | locked_peers: locked_peers} 129 | %__MODULE__{relay | channels: channels} 130 | end 131 | 132 | @spec unlock_channel(t, peer :: Client.address, Format.channel_number) :: t 133 | def unlock_channel(relay, peer, channel_number) do 134 | locked_numbers = MapSet.delete(relay.channels.locked_numbers, channel_number) 135 | locked_peers = MapSet.delete(relay.channels.locked_peers, peer) 136 | channels = %Channels{relay.channels | locked_numbers: locked_numbers, 137 | locked_peers: locked_peers} 138 | %__MODULE__{relay | channels: channels} 139 | end 140 | 141 | @spec put_lock_timer_ref(t, Format.channel_number, timer_ref :: reference) :: t 142 | def put_lock_timer_ref(relay, channel_number, timer_ref) do 143 | lock_timer_refs = Map.put(relay.channels.lock_timer_refs, 144 | channel_number, timer_ref) 145 | channels = %Channels{relay.channels | lock_timer_refs: lock_timer_refs} 146 | %__MODULE__{relay | channels: channels} 147 | end 148 | 149 | @spec remove_lock_timer_ref(t, Format.channel_number) :: t 150 | def remove_lock_timer_ref(relay, channel_number) do 151 | lock_timer_refs = Map.delete(relay.channels.lock_timer_refs, channel_number) 152 | channels = %Channels{relay.channels | lock_timer_refs: lock_timer_refs} 153 | %__MODULE__{relay | channels: channels} 154 | end 155 | 156 | @spec get_lock_timer_refs(t) :: [timer_ref :: reference] 157 | def get_lock_timer_refs(relay) do 158 | Map.values(relay.channels.lock_timer_refs) 159 | end 160 | 161 | @spec do_gen_channel_number(t) 162 | :: {:ok, Format.channel_number} | {:error, :retries_limit_reached} 163 | defp do_gen_channel_number(_, retry \\ 1) 164 | defp do_gen_channel_number(_, retry) when retry == @max_gen_channel_retries do 165 | {:error, :retries_limit_reached} 166 | end 167 | defp do_gen_channel_number(relay, retry) do 168 | channel_number = random_channel_number() 169 | if channel_taken_or_locked?(relay, channel_number) do 170 | do_gen_channel_number(relay, retry + 1) 171 | else 172 | {:ok, channel_number} 173 | end 174 | end 175 | 176 | @spec random_channel_number :: Format.channel_number 177 | defp random_channel_number do 178 | :rand.uniform(@max_channel_number - @min_channel_number) + 179 | @min_channel_number 180 | end 181 | 182 | @spec channel_capacity_reached?(t) :: boolean 183 | defp channel_capacity_reached?(relay) do 184 | taken = Enum.count(relay.channels.by_peer) 185 | locked = MapSet.size(relay.channels.locked_numbers) 186 | taken + locked >= @max_channel_number - @min_channel_number 187 | end 188 | 189 | @spec channel_taken_or_locked?(t, Format.channel_number) :: boolean 190 | defp channel_taken_or_locked?(relay, channel_number) do 191 | Map.has_key?(relay.channels.by_number, channel_number) or 192 | MapSet.member?(relay.channels.locked_numbers, channel_number) 193 | end 194 | end 195 | -------------------------------------------------------------------------------- /lib/jerboa/format.ex: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format do 2 | @moduledoc """ 3 | Encode and decode the STUN wire format 4 | """ 5 | 6 | alias Jerboa.Format.{Meta, Header, Body, MessageIntegrity} 7 | alias Jerboa.Format.{HeaderLengthError, BodyLengthError, 8 | First2BitsError, ChannelDataLengthError} 9 | alias Jerboa.Params 10 | alias Jerboa.ChannelData 11 | 12 | @typedoc """ 13 | Represents valid number of TURN channel 14 | """ 15 | @type channel_number :: 0x4000..0x7FFF 16 | 17 | @min_channel_number 0x4000 18 | @max_channel_number 0x7FFF 19 | 20 | @doc """ 21 | Encode a complete set of STUN Params or ChannelData into a binary suitable 22 | for writing to the network 23 | 24 | ## Calculating message integrity 25 | 26 | > This section applies only to encoding `Jerboa.Params` struct. 27 | 28 | In order to calculate message integrity over encoded message, 29 | encoder must know three values: username (as in USERNAME attribute), 30 | realm (REALM) and secret. 31 | 32 | Realm and username may be provided as attributes in params struct, 33 | or passed in options list. However attribute values will always override 34 | those found in options. Secret *must* be provided in option list. 35 | 36 | If any of these values are missing, message integrity won't be applied 37 | and encoding will succeed. None of these values (username, realm or secret) 38 | will be validated, so encoding will fail if, for example, provided username 39 | is an integer. 40 | 41 | Note that passing these values in options list *won't add them to 42 | message attributes list. 43 | 44 | ## Available options 45 | 46 | > This section applies only to encoding `Jerboa.Params` struct. 47 | 48 | * `:secret` - secret used for calculating message integrity 49 | * `:username` - username used for calculating message integrity 50 | if USERNAME attribute can't be found in the params struct 51 | * `:realm` - realm used for calculating message integrity 52 | if REALM attribute can't be found in params struct 53 | """ 54 | @spec encode(Params.t | ChannelData.t, options :: Keyword.t) :: binary 55 | def encode(params_or_channel_data, options \\ []) 56 | def encode(%Params{} = params, options) do 57 | %Meta{params: params, options: options} 58 | |> Body.encode() 59 | |> Header.encode() 60 | |> MessageIntegrity.apply() 61 | |> concatenate() 62 | end 63 | def encode(%ChannelData{channel_number: number, data: data}, _) 64 | when number in @min_channel_number..@max_channel_number and is_binary(data) do 65 | length = byte_size(data) 66 | <> 67 | end 68 | 69 | @doc """ 70 | The same as `decode/1` but raises one of various exceptions if the 71 | binary doesn't encode a STUN or ChannelData message 72 | """ 73 | @spec decode!(binary, options :: Keyword.t) 74 | :: Params.t | ChannelData.t 75 | | {Params.t | ChannelData.t, extra :: binary} 76 | | no_return 77 | def decode!(bin, options \\ []) do 78 | case decode(bin, options) do 79 | {:ok, params_or_channel_data} -> 80 | params_or_channel_data 81 | {:ok, params_or_channel_data, extra} -> 82 | {params_or_channel_data, extra} 83 | {:error, e} -> 84 | raise e 85 | end 86 | end 87 | 88 | @doc """ 89 | Decode a binary into a `Jerboa.Params` or `Jerboa.ChannelData` struct 90 | 91 | Return an `:ok` tuple or an `:error` tuple with an error struct if 92 | the binary doesn't encode a ChannelData message, a STUN message, 93 | or included message integrity is not valid (see "Verifying message 94 | integrity"). Returns `{:ok, params, extra}` if given binary was longer than 95 | declared in STUN or ChannelData header. 96 | 97 | ## Verifying message integrity 98 | 99 | > This section applies only to STUN messages. 100 | 101 | Similarly to `encode/2` decoder first looks for username and realm 102 | in the decoded message attributes or in the options list if there are 103 | no such attributes. 104 | 105 | Verification stage of decoding will never cause a decoding failure. 106 | To indicate what happened during verification, there are two fields 107 | in `Jerboa.Params` struct: `:signed?` and `:verified?`. 108 | 109 | `:signed?` is set to true **only** if the message being decoded has 110 | a MESSAGE-INTEGRITY attribute included. `:verified?` can never 111 | be true if `:signed?` is false (because there is simply nothing to 112 | verify). 113 | 114 | `:verified?` is only set to true when: 115 | * the message is `:signed?` 116 | * username, realm in the message attributes, or were passed as options 117 | and secret was passed as option 118 | * MESSAGE-INTEGRITY was successfully verified using algorithm described in RFC 119 | 120 | Otherwise, it's set to false. 121 | 122 | ## Available options 123 | 124 | Same as in `encode/2`. 125 | """ 126 | @spec decode(binary, options :: Keyword.t) 127 | :: {:ok, Params.t | ChannelData.t} 128 | | {:ok, Params.t | ChannelData.t, extra :: binary} 129 | | {:error, struct} 130 | def decode(binary, options \\ []) 131 | when is_binary(binary) do 132 | cond do 133 | stun_binary?(binary) -> 134 | decode_stun(binary, options) 135 | channel_data_binary?(binary) -> 136 | decode_channel_data(binary) 137 | bit_size(binary) >= 2 -> 138 | <> = binary 139 | {:error, First2BitsError.exception(bits: first_two)} 140 | true -> 141 | {:error, First2BitsError.exception(bits: <<>>)} 142 | end 143 | end 144 | 145 | @spec stun_binary?(binary) :: boolean 146 | defp stun_binary?(<<0::2-unit(1), _::bits>>), do: true 147 | defp stun_binary?(_), do: false 148 | 149 | @spec channel_data_binary?(binary) :: boolean 150 | defp channel_data_binary?(<<1::2-unit(1), _::bits>>), do: true 151 | defp channel_data_binary?(_), do: false 152 | 153 | @spec decode_stun(binary, options :: Keyword.t) 154 | :: {:ok, Params.t} | {:ok, Params.t, extra :: binary} | {:error, struct} 155 | defp decode_stun(bin, _) when is_binary(bin) and byte_size(bin) < 20 do 156 | {:error, HeaderLengthError.exception(binary: bin)} 157 | end 158 | defp decode_stun(<>, options) do 159 | meta = %Meta{header: header, body: body, options: options} 160 | with {:ok, meta} <- decode_header(meta), 161 | {:ok, meta} <- Body.decode(meta), 162 | {:ok, meta} <- MessageIntegrity.verify(meta) do 163 | maybe_with_extra(meta) 164 | end 165 | end 166 | 167 | defp concatenate(%Meta{header: header, body: body}) do 168 | header <> body 169 | end 170 | 171 | @spec decode_header(Meta.t) :: {:ok, Meta.t} | {:error, struct} 172 | defp decode_header(meta) do 173 | case Header.decode(meta) do 174 | {:ok, %Meta{body: body, length: length}} when byte_size(body) < length -> 175 | {:error, BodyLengthError.exception(length: byte_size(body))} 176 | {:ok, %Meta{body: body, length: length} = meta} when byte_size(body) > length -> 177 | <> = body 178 | {:ok, %{meta | extra: extra, body: trimmed_body}} 179 | {:ok, _} = result -> 180 | result 181 | {:error, _} = e -> 182 | e 183 | end 184 | end 185 | 186 | defp maybe_with_extra(%Meta{extra: <<>>} = meta), do: {:ok, meta.params} 187 | defp maybe_with_extra(%Meta{extra: extra} = meta) do 188 | {:ok, meta.params, extra} 189 | end 190 | 191 | @spec decode_channel_data(<<_::32, _::_ * 8>>) 192 | :: {:ok, ChannelData.t} 193 | | {:ok, ChannelData.t, extra :: binary} 194 | | {:error, struct} 195 | defp decode_channel_data(<>) when number in @min_channel_number..@max_channel_number do 197 | channel_data = %ChannelData{channel_number: number, data: data} 198 | case rest do 199 | <<>> -> {:ok, channel_data} 200 | extra -> {:ok, channel_data, extra} 201 | end 202 | end 203 | defp decode_channel_data(<<_::16, length::16, rest::binary>>) 204 | when byte_size(rest) < length do 205 | {:error, ChannelDataLengthError.exception(length: byte_size(rest))} 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /test/jerboa/client/relay_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.RelayTest do 2 | use ExUnit.Case 3 | 4 | @moduletag :now 5 | 6 | alias Jerboa.Client.Relay 7 | alias Jerboa.Client.Relay.Channel 8 | 9 | describe "active?/1" do 10 | test "returns true if relayed address is not nil" do 11 | relay = %Relay{address: {{127, 0, 0, 1}, 12_345}} 12 | 13 | assert Relay.active?(relay) 14 | end 15 | 16 | test "returns false if relayed address is nil" do 17 | relay = %Relay{address: nil} 18 | 19 | refute Relay.active?(relay) 20 | end 21 | end 22 | 23 | test "put_address/2 sets relay address" do 24 | address = {{127, 0, 0, 1}, 12_345} 25 | 26 | relay = %Relay{} |> Relay.put_address(address) 27 | 28 | assert relay.address == address 29 | end 30 | 31 | test "put_lifetime/2 sets relay lifetime" do 32 | lifetime = 600 33 | 34 | relay = %Relay{} |> Relay.put_lifetime(lifetime) 35 | 36 | assert relay.lifetime == lifetime 37 | end 38 | 39 | test "put_timer_ref/2 sets relay expiration timer reference" do 40 | timer_ref = make_ref() 41 | 42 | relay = %Relay{} |> Relay.put_timer_ref(timer_ref) 43 | 44 | assert relay.timer_ref == timer_ref 45 | end 46 | 47 | test "put_permissions/2 sets relay permissions" do 48 | permissions = %{{127, 0, 0, 1} => make_ref()} 49 | 50 | relay = %Relay{} |> Relay.put_permissions(permissions) 51 | 52 | assert relay.permissions == permissions 53 | end 54 | 55 | test "remove_permissions/2 deletes permission for given peer IP" do 56 | addr1 = {127, 0, 0, 1} 57 | addr2 = {172, 16, 0, 1} 58 | permissions = %{addr1 => make_ref(), addr2 => make_ref()} 59 | 60 | relay = 61 | %Relay{} 62 | |> Relay.put_permissions(permissions) 63 | |> Relay.remove_permission(addr1) 64 | permission_ips = Map.keys(relay.permissions) 65 | 66 | refute addr1 in permission_ips 67 | assert addr2 in permission_ips 68 | end 69 | 70 | test "has_permission?/2 check wheter there is permission for given peer" do 71 | addr1 = {127, 0, 0, 1} 72 | peer1 = {addr1, 12_345} 73 | addr2 = {172, 16, 0, 1} 74 | peer2 = {addr2, 12_345} 75 | permissions = %{addr1 => make_ref()} 76 | 77 | relay = %Relay{} |> Relay.put_permissions(permissions) 78 | 79 | assert Relay.has_permission?(relay, peer1) 80 | refute Relay.has_permission?(relay, peer2) 81 | end 82 | 83 | test "get_permission_timers/1 returns list of permission timers refs" do 84 | timer_ref1 = make_ref() 85 | timer_ref2 = make_ref() 86 | permissions = %{{127, 0, 0, 1} => timer_ref1} 87 | 88 | relay = %Relay{} |> Relay.put_permissions(permissions) 89 | timer_refs = Relay.get_permission_timers(relay) 90 | 91 | assert length(timer_refs) == 1 92 | assert timer_ref1 in timer_refs 93 | refute timer_ref2 in timer_refs 94 | end 95 | 96 | test "put_channel/2 adds a new channel if it wasn't present before" do 97 | peer = {{127, 0, 0, 1}, 12_345} 98 | channel_number = 0x4000 99 | channel = %Channel{peer: peer, number: channel_number, 100 | timer_ref: make_ref()} 101 | 102 | relay = %Relay{} |> Relay.put_channel(channel) 103 | 104 | assert {:ok, channel} == Relay.get_channel_by_number(relay, channel_number) 105 | assert {:ok, channel} == Relay.get_channel_by_peer(relay, peer) 106 | end 107 | 108 | test "put_channel/3 maps channel with a given functon it is " <> 109 | "already present" do 110 | peer = {{127, 0, 0, 1}, 12_345} 111 | channel_number = 0x4000 112 | channel = %Channel{peer: peer, number: channel_number, 113 | timer_ref: make_ref()} 114 | new_timer_ref = make_ref() 115 | 116 | relay = 117 | %Relay{} 118 | |> Relay.put_channel(channel) 119 | |> Relay.put_channel(channel, fn c -> %{c | timer_ref: new_timer_ref} end) 120 | 121 | updated_channel = %{channel | timer_ref: new_timer_ref} 122 | assert {:ok, updated_channel} == 123 | Relay.get_channel_by_number(relay, channel_number) 124 | assert {:ok, updated_channel} == Relay.get_channel_by_peer(relay, peer) 125 | end 126 | 127 | test "remove_channel/3 deletes the channel from the relay" do 128 | peer = {{127, 0, 0, 1}, 12_345} 129 | channel_number = 0x4000 130 | channel = %Channel{peer: peer, number: channel_number, 131 | timer_ref: make_ref()} 132 | relay = 133 | %Relay{} 134 | |> Relay.put_channel(channel) 135 | |> Relay.remove_channel(peer, channel_number) 136 | 137 | refute channel in Relay.get_channels(relay) 138 | assert :error == Relay.get_channel_by_number(relay, channel_number) 139 | assert :error == Relay.get_channel_by_peer(relay, peer) 140 | end 141 | 142 | test "get_channels/1 returns a list of all channels" do 143 | channel1 = %Channel{peer: {{127, 0, 0, 1}, 12_345}, number: 0x4000, 144 | timer_ref: make_ref()} 145 | channel2 = %Channel{peer: {{172, 16, 0, 1}, 12_345}, number: 0x4001, 146 | timer_ref: make_ref()} 147 | 148 | relay = %Relay{} |> Relay.put_channel(channel1) 149 | channels = Relay.get_channels(relay) 150 | 151 | assert length(channels) == 1 152 | assert channel1 in channels 153 | refute channel2 in channels 154 | end 155 | 156 | test "has_channel_bound?/2 returns true if there is channel for the " <> 157 | "given peer" do 158 | peer1 = {{127, 0, 0, 1}, 12_345} 159 | peer2 = {{172, 16, 0, 1}, 12_345} 160 | channel = %Channel{peer: peer1, number: 0x4000, timer_ref: make_ref()} 161 | 162 | relay = %Relay{} |> Relay.put_channel(channel) 163 | 164 | assert Relay.has_channel_bound?(relay, peer1) 165 | refute Relay.has_channel_bound?(relay, peer2) 166 | end 167 | 168 | describe "gen_channel_number/2" do 169 | test "returns :peer_locked if the peer is locked" do 170 | peer = {{127, 0, 0, 1}, 12_345} 171 | channel_number = 0x4000 172 | 173 | relay = %Relay{} |> Relay.lock_channel(peer, channel_number) 174 | 175 | assert {:error, :peer_locked} == Relay.gen_channel_number(relay, peer) 176 | end 177 | 178 | test "returns :capacity_reached if there are no more free channel numbers" do 179 | locked_range = 0x4000..0x5FFF 180 | taken_range = 0x6000..0x7FFF 181 | gen_peer = fn channel_number -> {{127, 0, 0, 1}, channel_number} end 182 | relay1 = Enum.reduce(locked_range, %Relay{}, fn number, relay -> 183 | Relay.lock_channel(relay, gen_peer.(number), number) 184 | end) 185 | relay2 = Enum.reduce(taken_range, relay1, fn number, relay -> 186 | channel = %Channel{number: number, peer: gen_peer.(number), 187 | timer_ref: make_ref()} 188 | Relay.put_channel(relay, channel) 189 | end) 190 | 191 | assert {:error, :capacity_reached} == Relay.gen_channel_number(relay2, 192 | gen_peer.(12_345)) 193 | end 194 | 195 | test "returns channel number if the peer has channel bound" do 196 | peer = {{127, 0, 0, 1}, 12_345} 197 | channel_number = 0x4000 198 | channel = %Channel{peer: peer, number: channel_number, timer_ref: make_ref} 199 | relay = %Relay{} |> Relay.put_channel(channel) 200 | 201 | assert {:ok, channel_number} == Relay.gen_channel_number(relay, peer) 202 | end 203 | 204 | test "returns new channel number if the peer does not have channel bound" do 205 | peer = {{127, 0, 0, 1}, 12_345} 206 | relay = %Relay{} 207 | 208 | assert {:ok, _} = Relay.gen_channel_number(relay, peer) 209 | end 210 | end 211 | 212 | test "lock and unlock channel" do 213 | peer = {{127, 0, 0, 1}, 12_345} 214 | channel_number = 0x4000 215 | 216 | relay1 = %Relay{} |> Relay.lock_channel(peer, channel_number) 217 | assert {:error, :peer_locked} = Relay.gen_channel_number(relay1, peer) 218 | 219 | relay2 = Relay.unlock_channel(relay1, peer, channel_number) 220 | assert {:ok, _} = Relay.gen_channel_number(relay2, peer) 221 | end 222 | 223 | test "add and remove lock timer reference" do 224 | timer_ref = make_ref() 225 | channel_number = 0x4000 226 | 227 | relay1 = %Relay{} |> Relay.put_lock_timer_ref(channel_number, timer_ref) 228 | assert timer_ref in Relay.get_lock_timer_refs(relay1) 229 | 230 | relay2 = %Relay{} |> Relay.remove_lock_timer_ref(channel_number) 231 | refute timer_ref in Relay.get_lock_timer_refs(relay2) 232 | end 233 | 234 | end 235 | -------------------------------------------------------------------------------- /test/jerboa/format/body/attribute_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Format.Body.AttributeTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Jerboa.Test.Helper.XORMappedAddress, as: XORMAHelper 5 | 6 | alias Jerboa.Format.Body.Attribute 7 | alias Jerboa.Format.Body.Attribute.{XORMappedAddress, Lifetime, Data, Nonce, 8 | Username, Realm, ErrorCode, EvenPort, 9 | XORPeerAddress, XORRelayedAddress, 10 | RequestedTransport, DontFragment, 11 | ReservationToken} 12 | alias Jerboa.Params 13 | alias Jerboa.Format.Meta 14 | 15 | import Jerboa.Test.Helper.Attribute, only: [total: 1, length_correct?: 2, 16 | type: 1, value: 1] 17 | 18 | describe "Attribute.encode/2" do 19 | 20 | test "IPv4 XORMappedAddress as a TLV" do 21 | attr = XORMAHelper.struct(4) 22 | meta = %Meta{params: Params.new} 23 | 24 | {_, bin} = Attribute.encode meta, attr 25 | 26 | assert type(bin) === 0x0020 27 | assert length_correct?(bin, total(address: 4, other: 4)) 28 | end 29 | 30 | test "IPv6 XORMappedAddress as a TLV" do 31 | i = XORMAHelper.i() 32 | attr = XORMAHelper.struct(6) 33 | params = %Params{identifier: i} 34 | meta = %Meta{params: params} 35 | 36 | {_, bin} = Attribute.encode meta, attr 37 | 38 | assert type(bin) === 0x0020 39 | assert length_correct?(bin, total(address: 16, other: 4)) 40 | end 41 | 42 | test "LIFETIME as a TLV" do 43 | duration = 12_345 44 | meta = %Meta{params: Params.new} 45 | 46 | {_, bin} = Attribute.encode(meta, %Lifetime{duration: duration}) 47 | 48 | assert type(bin) == 0x000D 49 | assert length_correct?(bin, total(duration: 4)) 50 | end 51 | 52 | test "DATA as a TLV" do 53 | content = "Hello" 54 | meta = %Meta{params: Params.new} 55 | 56 | {_, bin} = Attribute.encode(meta, %Data{content: content}) 57 | 58 | assert type(bin) == 0x0013 59 | assert length_correct?(bin, total(content: byte_size(content))) 60 | end 61 | 62 | test "NONCE as a TLV" do 63 | value = "12345" 64 | meta = %Meta{params: Params.new} 65 | 66 | {_, bin} = Attribute.encode(meta, %Nonce{value: value}) 67 | 68 | assert type(bin) == 0x0015 69 | assert length_correct?(bin, total(value: byte_size(value))) 70 | end 71 | 72 | test "USERNAME as a TLV" do 73 | value = "Hello" 74 | meta = %Meta{params: Params.new} 75 | 76 | {_, bin} = Attribute.encode(meta, %Username{value: value}) 77 | 78 | assert type(bin) == 0x0006 79 | assert length_correct?(bin, total(value: byte_size(value))) 80 | end 81 | 82 | test "REALM as a TLV" do 83 | value = "Super Server" 84 | meta = %Meta{params: Params.new} 85 | 86 | {_, bin} = Attribute.encode(meta, %Realm{value: value}) 87 | 88 | assert type(bin) == 0x0014 89 | assert length_correct?(bin, total(value: byte_size(value))) 90 | end 91 | 92 | test "ERROR-CODE as a TLV" do 93 | code = 400 94 | reason = "alice has a cat" 95 | meta = %Meta{params: Params.new} 96 | 97 | {_, bin} = Attribute.encode(meta, %ErrorCode{code: code, reason: reason}) 98 | 99 | assert type(bin) == 0x0009 100 | assert length_correct?(bin, total(padding_and_code: 4, reason: byte_size(reason))) 101 | end 102 | 103 | test "IPv4 XORPeerAddress as a TLV" do 104 | attr = %XORPeerAddress{port: 0, address: {0, 0, 0, 0}, family: :ipv4} 105 | meta = %Meta{params: Params.new()} 106 | 107 | {_, bin} = Attribute.encode meta, attr 108 | 109 | assert type(bin) === 0x0012 110 | assert length_correct?(bin, total(address: 4, other: 4)) 111 | end 112 | 113 | test "IPv6 XORPeerAddress as a TLV" do 114 | attr = %XORPeerAddress{port: 0, address: {0, 0, 0, 0, 0, 0, 0, 0}, 115 | family: :ipv6} 116 | meta = %Meta{params: Params.new()} 117 | 118 | {_, bin} = Attribute.encode meta, attr 119 | 120 | assert type(bin) === 0x0012 121 | assert length_correct?(bin, total(address: 16, other: 4)) 122 | end 123 | 124 | test "IPv4 XORRelayedAddress as a TLV" do 125 | attr = %XORRelayedAddress{port: 0, address: {0, 0, 0, 0}, family: :ipv4} 126 | meta = %Meta{params: Params.new()} 127 | 128 | {_, bin} = Attribute.encode meta, attr 129 | 130 | assert type(bin) === 0x0016 131 | assert length_correct?(bin, total(address: 4, other: 4)) 132 | end 133 | 134 | test "IPv6 XORRelayedAddress as a TLV" do 135 | attr = %XORRelayedAddress{port: 0, address: {0, 0, 0, 0, 0, 0, 0, 0}, 136 | family: :ipv6} 137 | meta = %Meta{params: Params.new()} 138 | 139 | {_, bin} = Attribute.encode meta, attr 140 | 141 | assert type(bin) === 0x0016 142 | assert length_correct?(bin, total(address: 16, other: 4)) 143 | end 144 | 145 | test "REQUESTED-TRANPOSRT as a TLV" do 146 | attr = %RequestedTransport{protocol: :udp} 147 | 148 | {_, bin} = Attribute.encode %Meta{}, attr 149 | 150 | assert type(bin) == 0x0019 151 | assert length_correct?(bin, total(protocol: 1, rffu: 3)) 152 | end 153 | 154 | test "DONT-FRAGMENT as a TLV" do 155 | attr = %DontFragment{} 156 | 157 | {_, bin} = Attribute.encode %Meta{}, attr 158 | 159 | assert type(bin) == 0x001A 160 | assert length_correct?(bin, total(value: 0)) 161 | end 162 | 163 | test "EVEN-PORT as a TLV"do 164 | attr = %EvenPort{} 165 | 166 | {_, bin} = Attribute.encode %Meta{}, attr 167 | 168 | assert type(bin) == 0x0018 169 | assert length_correct?(bin, total(value: 1)) 170 | end 171 | 172 | test "RESERVATION-TOKEN as a TLV" do 173 | attr = %ReservationToken{value: <<0::64>>} 174 | 175 | {_, bin} = Attribute.encode %Meta{}, attr 176 | 177 | assert type(bin) == 0x0022 178 | assert length_correct?(bin, total(value: 8)) 179 | end 180 | end 181 | 182 | describe "Attribute.decode/3 is opposite to encode/2 for" do 183 | test "XOR-MAPPED-ADDRESS" do 184 | attr = %XORMappedAddress{family: :ipv4, address: {0, 0, 0, 0}, port: 0} 185 | meta = %Meta{params: Params.new} 186 | 187 | {_, bin} = Attribute.encode(meta, attr) 188 | 189 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0020, value(bin)) 190 | end 191 | 192 | test "LIFETIME" do 193 | attr = %Lifetime{duration: 12_345} 194 | meta = %Meta{params: Params.new} 195 | 196 | {_, bin} = Attribute.encode(meta, attr) 197 | 198 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x000D, value(bin)) 199 | end 200 | 201 | test "DATA" do 202 | attr = %Data{content: "Hello"} 203 | meta = %Meta{params: Params.new} 204 | 205 | {_, bin} = Attribute.encode(meta, attr) 206 | 207 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0013, value(bin)) 208 | end 209 | 210 | test "NONCE" do 211 | attr = %Nonce{value: "12345"} 212 | meta = %Meta{params: Params.new} 213 | 214 | {_, bin} = Attribute.encode(meta, attr) 215 | 216 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0015, value(bin)) 217 | end 218 | 219 | test "USERNAME" do 220 | attr = %Username{value: "Hello"} 221 | meta = %Meta{params: Params.new} 222 | 223 | {_, bin} = Attribute.encode(meta, attr) 224 | 225 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0006, value(bin)) 226 | end 227 | 228 | test "REALM" do 229 | attr = %Realm{value: "Super Server"} 230 | meta = %Meta{params: Params.new} 231 | 232 | {_, bin} = Attribute.encode(meta, attr) 233 | 234 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0014, value(bin)) 235 | end 236 | 237 | test "ERROR-CODE" do 238 | attr = %ErrorCode{code: 400, name: :bad_request} 239 | meta = %Meta{params: Params.new} 240 | 241 | {_, bin} = Attribute.encode(meta, attr) 242 | 243 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0009, value(bin)) 244 | end 245 | 246 | test "XOR-PEER-ADDRESS" do 247 | attr = %XORPeerAddress{family: :ipv4, address: {0, 0, 0, 0}, port: 0} 248 | meta = %Meta{params: Params.new} 249 | 250 | {_, bin} = Attribute.encode(meta, attr) 251 | 252 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0012, value(bin)) 253 | end 254 | 255 | test "XOR-RELAYED-ADDRESS" do 256 | attr = %XORRelayedAddress{family: :ipv4, address: {0, 0, 0, 0}, port: 0} 257 | meta = %Meta{params: Params.new} 258 | 259 | {_, bin} = Attribute.encode(meta, attr) 260 | 261 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0016, value(bin)) 262 | end 263 | 264 | test "REQUESTED-TRANSPORT" do 265 | attr = %RequestedTransport{protocol: :udp} 266 | meta = %Meta{} 267 | 268 | {_, bin} = Attribute.encode(meta, attr) 269 | 270 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0019, value(bin)) 271 | end 272 | 273 | test "DONT-FRAGMENT" do 274 | attr = %DontFragment{} 275 | meta = %Meta{} 276 | 277 | {_, bin} = Attribute.encode(meta, attr) 278 | 279 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x001A, value(bin)) 280 | end 281 | 282 | test "EVEN-PORT" do 283 | attr = %EvenPort{} 284 | meta = %Meta{} 285 | 286 | {_, bin} = Attribute.encode(meta, attr) 287 | 288 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0018, value(bin)) 289 | end 290 | 291 | test "RESERVATION-TOKEN" do 292 | attr = %ReservationToken{value: <<0::64>>} 293 | meta = %Meta{} 294 | 295 | {_, bin} = Attribute.encode(meta, attr) 296 | assert {:ok, _, ^attr} = Attribute.decode(meta, 0x0022, value(bin)) 297 | end 298 | end 299 | end 300 | -------------------------------------------------------------------------------- /test/jerboa/format_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.FormatTest do 2 | use ExUnit.Case, async: true 3 | use Quixir 4 | 5 | alias Jerboa.Test.Helper.XORMappedAddress, as: XORMAHelper 6 | 7 | alias Jerboa.{Format, Params} 8 | alias Format.{HeaderLengthError, BodyLengthError, First2BitsError, 9 | ChannelDataLengthError} 10 | alias Jerboa.Format.Header.MagicCookie 11 | alias Jerboa.Format.Body.Attribute.{Username, Realm, Nonce} 12 | alias Jerboa.Format.Meta 13 | alias Jerboa.Format.Body.Attribute 14 | alias Jerboa.ChannelData 15 | 16 | @magic MagicCookie.value 17 | 18 | describe "Format.encode/2" do 19 | 20 | test "body follows header with length in header" do 21 | 22 | ## Given: 23 | import Jerboa.Test.Helper.Header, only: [identifier: 0] 24 | a = XORMAHelper.struct(4) 25 | 26 | ## When: 27 | bin = Format.encode %Params{ 28 | class: :success, 29 | method: :binding, 30 | identifier: identifier(), 31 | attributes: [a]} 32 | 33 | ## Then: 34 | assert Jerboa.Test.Helper.Format.bytes_for_body(bin) == 12 35 | end 36 | 37 | test "fails given ChannelData with invalid number" do 38 | ptest number: int(min: 0, max: 0x3FFF) do 39 | channel_data = %ChannelData{channel_number: number, data: ""} 40 | 41 | assert_raise FunctionClauseError, fn -> 42 | Format.encode channel_data 43 | end 44 | end 45 | end 46 | 47 | test "fails given ChannelData with non-binary data field" do 48 | channel_data = %ChannelData{channel_number: 0x4000, data: nil} 49 | 50 | assert_raise FunctionClauseError, fn -> 51 | Format.encode channel_data 52 | end 53 | end 54 | 55 | test "works as opossite to decode/2 for ChannelData" do 56 | ptest number: int(min: 0x4000, max: 0x7FFF), data: string() do 57 | channel_data = %ChannelData{channel_number: number, data: data} 58 | 59 | assert channel_data == 60 | channel_data |> Format.encode() |> Format.decode!() 61 | end 62 | end 63 | end 64 | 65 | describe "Format.decode/2" do 66 | 67 | test "fails given empty binary" do 68 | packet = <<>> 69 | 70 | assert {:error, %First2BitsError{bits: ^packet}} = Format.decode packet 71 | end 72 | 73 | test "fails given packet with invalid first two bits" do 74 | ptest first_two_val: int(min: 2, max: 3) do 75 | first_two = <> 76 | packet = <> 77 | 78 | assert {:error, %First2BitsError{bits: ^first_two}} = Format.decode packet 79 | end 80 | end 81 | 82 | test "fails given packet with not enough bytes for header" do 83 | ptest length: int(min: 2, max: 19), content: int(min: 0) do 84 | byte_length = length * 8 85 | packet = <> 86 | 87 | assert {:error, %HeaderLengthError{binary: ^packet}} = Format.decode packet 88 | end 89 | end 90 | 91 | test "fails given packet with too short message body" do 92 | ptest method: int(min: 1, max: 1), class: int(min: 0, max: 3), 93 | length: int(min: 1000), body: int(min: 0), 94 | body_length: int(min: 0, max: ^length - 1) do 95 | <> = <> 96 | <> = <> 97 | type = <> 98 | packet = <<0::2, type::bits, length::16, @magic::32, 0::96, 99 | body::unit(8)-size(body_length)>> 100 | 101 | {:error, error} = Format.decode packet 102 | 103 | assert %BodyLengthError{length: ^body_length} = error 104 | end 105 | end 106 | 107 | test "returns bytes after the length given in the header into the `extra` field" do 108 | ptest method: int(min: 1, max: 1), class: int(min: 0, max: 3), 109 | extra: string(min: 1) do 110 | <> = <> 111 | <> = <> 112 | type = <> 113 | packet = <<0::2, type::bits, 0::16, @magic::32, 0::96, 114 | extra::binary>> 115 | 116 | assert {:ok, _, ^extra} = Format.decode packet 117 | end 118 | end 119 | 120 | test "decodes valid ChannelData message" do 121 | ptest channel_number: int(min: 0x4000, max: 0x7FFF), length: int(min: 0), 122 | content: int(min: 0) do 123 | data = <> 124 | packet = <> 125 | 126 | assert {:ok, channel_data} = Format.decode packet 127 | assert %ChannelData{channel_number: channel_number, data: data} == 128 | channel_data 129 | end 130 | end 131 | 132 | test "decodes valid ChannelData message with extra bytes" do 133 | ptest channel_number: int(min: 0x4000, max: 0x7FFF), length: int(min: 0), 134 | content: int(min: 0), extra_length: int(min: 1) do 135 | data = <> 136 | extra = <<0::size(extra_length)-unit(8)>> 137 | packet = <> 138 | 139 | assert {:ok, channel_data, ^extra} = Format.decode packet 140 | assert %ChannelData{channel_number: channel_number, data: data} == 141 | channel_data 142 | end 143 | end 144 | 145 | test "fails to decode if ChannelData has too short data field" do 146 | ptest channel_number: int(min: 0x4000, max: 0x7FFF), length: int(min: 1), 147 | content: int(min: 0), length_offset: int(min: 1, max: ^length) do 148 | malformed_length = length - length_offset 149 | data = <> 150 | packet = <> 151 | 152 | assert {:error, %ChannelDataLengthError{length: ^malformed_length}} = 153 | Format.decode packet 154 | end 155 | end 156 | end 157 | 158 | describe "Format.decode!/2" do 159 | test "raises an exception upon failure" do 160 | packet = <<255, "Supercalifragilisticexpialidocious!"::binary>> 161 | 162 | assert_raise Jerboa.Format.First2BitsError, fn -> Format.decode!(packet) end 163 | end 164 | 165 | test "returns value without an :ok tuple" do 166 | packet = <<0::2, 1::14, 0::16, @magic::32, 0::96>> 167 | 168 | assert %Params{} = Format.decode!(packet) 169 | end 170 | end 171 | 172 | test "MI values passed as attributes shadow values passed as options" do 173 | username_one = "alice" 174 | realm_one = "wonderland" 175 | username_two = "harry" 176 | realm_two = "hogwarts" 177 | secret = "secret" 178 | 179 | bin = 180 | Params.new() 181 | |> Params.put_class(:request) 182 | |> Params.put_method(:allocate) 183 | |> Params.put_attr(%Username{value: username_one}) 184 | |> Params.put_attr(%Realm{value: realm_one}) 185 | |> Format.encode(username: username_two, realm: realm_two, secret: secret) 186 | 187 | assert {:ok, _} = Format.decode(bin, secret: secret) 188 | end 189 | 190 | test "MI applied with encode/2 is verified byd decode/2 given same secret" do 191 | username = "alice" 192 | realm = "wonderland" 193 | secret = "secret" 194 | 195 | bin = 196 | Params.new() 197 | |> Params.put_class(:request) 198 | |> Params.put_method(:allocate) 199 | |> Params.put_attr(%Username{value: username}) 200 | |> Params.put_attr(%Realm{value: realm}) 201 | |> Format.encode(secret: secret) 202 | 203 | assert {:ok, params} = Format.decode(bin, secret: secret) 204 | assert params.signed? 205 | assert params.verified? 206 | end 207 | 208 | test "decode/2 set :verified? to false given different secret 209 | than encode/2" do 210 | username = "alice" 211 | realm = "wonderland" 212 | secret = "secret" 213 | other_secret = "other_secret" 214 | 215 | bin = 216 | Params.new() 217 | |> Params.put_class(:request) 218 | |> Params.put_method(:allocate) 219 | |> Params.put_attr(%Username{value: username}) 220 | |> Params.put_attr(%Realm{value: realm}) 221 | |> Format.encode(secret: secret) 222 | 223 | assert {:ok, params} = Format.decode(bin, secret: other_secret) 224 | assert params.signed? 225 | refute params.verified? 226 | end 227 | 228 | test "decode/2 sets :verified? to false given different username 229 | than encode/2" do 230 | username = "alice" 231 | other_username = "harry" 232 | realm = "wonderland" 233 | secret = "secret" 234 | 235 | bin = 236 | Params.new() 237 | |> Params.put_class(:request) 238 | |> Params.put_method(:allocate) 239 | |> Params.put_attr(%Realm{value: realm}) 240 | |> Format.encode(secret: secret, username: username) 241 | 242 | assert {:ok, params} = 243 | Format.decode(bin, secret: secret, username: other_username) 244 | assert params.signed? 245 | refute params.verified? 246 | end 247 | 248 | test "decode/2 sets :verified? to false given different realm 249 | than encode/2" do 250 | username = "alice" 251 | realm = "wonderland" 252 | other_realm = "hogwarts" 253 | secret = "secret" 254 | 255 | bin = 256 | Params.new() 257 | |> Params.put_class(:request) 258 | |> Params.put_method(:allocate) 259 | |> Params.put_attr(%Username{value: username}) 260 | |> Format.encode(secret: secret, realm: realm) 261 | 262 | assert {:ok, params} = 263 | Format.decode(bin, secret: secret, realm: other_realm) 264 | assert params.signed? 265 | refute params.verified? 266 | end 267 | 268 | test "attributes after MI are ignored" do 269 | secret = "secret" 270 | bin = 271 | Params.new() 272 | |> Params.put_class(:request) 273 | |> Params.put_method(:allocate) 274 | |> Params.put_attr(%Username{value: "alice"}) 275 | |> Params.put_attr(%Realm{value: "wonderland"}) 276 | |> Format.encode(secret: secret) 277 | {_, extra_attr} = Attribute.encode(%Meta{}, %Nonce{value: "1234"}) 278 | <> = bin 279 | <<0::2, type::14, length::16, header_rest::binary>> = header 280 | new_length = length + byte_size(extra_attr) 281 | new_header = <<0::2, type::14, (new_length)::16, header_rest::binary>> 282 | modified_bin = new_header <> body <> extra_attr 283 | 284 | assert {:ok, params} = Format.decode(modified_bin, secret: secret) 285 | assert %Username{} = Params.get_attr(params, Username) 286 | assert %Realm{} = Params.get_attr(params, Realm) 287 | assert nil == Params.get_attr(params, Nonce) 288 | end 289 | 290 | test "sets :signed? and :verified? to false if there is no MI attribute" do 291 | bin = 292 | Params.new() 293 | |> Params.put_class(:request) 294 | |> Params.put_method(:allocate) 295 | |> Format.encode() 296 | 297 | assert {:ok, params} = Format.decode(bin) 298 | refute params.signed? 299 | refute params.verified? 300 | end 301 | end 302 | -------------------------------------------------------------------------------- /test/jerboa/client/protocol/allocate_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Jerboa.Client.Protocol.AllocateTest do 2 | use ExUnit.Case 3 | 4 | alias Jerboa.Params 5 | alias Jerboa.Client.Credentials 6 | alias Jerboa.Client.Protocol 7 | alias Jerboa.Client.Protocol.Allocate 8 | alias Jerboa.Test.Helper.Params, as: PH 9 | alias Jerboa.Test.Helper.Credentials, as: CH 10 | alias Jerboa.Format.Body.Attribute.XORMappedAddress, as: XMA 11 | alias Jerboa.Format.Body.Attribute.XORRelayedAddress, as: XRA 12 | alias Jerboa.Format.Body.Attribute.{Lifetime, ErrorCode, Nonce, Realm, 13 | EvenPort, ReservationToken} 14 | 15 | describe "request/2" do 16 | test "returns valid allocate request signed with credentials" do 17 | creds = CH.final() 18 | 19 | {id, request} = Allocate.request(creds, []) 20 | params = Protocol.decode!(request, creds) 21 | 22 | assert params.identifier == id 23 | assert params.class == :request 24 | assert params.method == :allocate 25 | assert params.signed? 26 | assert params.verified? 27 | assert nil == Params.get_attr(params, EvenPort) 28 | assert PH.username(params) == creds.username 29 | assert PH.realm(params) == creds.realm 30 | assert PH.nonce(params) == creds.nonce 31 | end 32 | 33 | test "returns valid allocate request with EVEN-PORT attribute" do 34 | creds = CH.final() 35 | 36 | {id, request} = Allocate.request(creds, even_port: true) 37 | params = Protocol.decode!(request, creds) 38 | 39 | assert params.identifier == id 40 | assert params.class == :request 41 | assert params.method == :allocate 42 | assert params.signed? 43 | assert params.verified? 44 | assert %EvenPort{reserved?: false} == Params.get_attr(params, EvenPort) 45 | assert PH.username(params) == creds.username 46 | assert PH.realm(params) == creds.realm 47 | assert PH.nonce(params) == creds.nonce 48 | end 49 | 50 | test "returns valid allocate request with reserved EVEN-PORT attribute" do 51 | creds = CH.final() 52 | 53 | {id, request} = Allocate.request(creds, reserve: true) 54 | params = Protocol.decode!(request, creds) 55 | 56 | assert params.identifier == id 57 | assert params.class == :request 58 | assert params.method == :allocate 59 | assert params.signed? 60 | assert params.verified? 61 | assert %EvenPort{reserved?: true} == Params.get_attr(params, EvenPort) 62 | assert PH.username(params) == creds.username 63 | assert PH.realm(params) == creds.realm 64 | assert PH.nonce(params) == creds.nonce 65 | end 66 | 67 | test "returns valid allocate request with RESERVATION-TOKEN attribute" do 68 | creds = CH.final() 69 | token = <<0::8 * 8>> # token must be 8 bytes long 70 | 71 | {id, request} = Allocate.request(creds, reservation_token: token) 72 | params = Protocol.decode!(request, creds) 73 | 74 | assert params.identifier == id 75 | assert params.class == :request 76 | assert params.method == :allocate 77 | assert params.signed? 78 | assert params.verified? 79 | assert %ReservationToken{value: token} == 80 | Params.get_attr(params, ReservationToken) 81 | assert PH.username(params) == creds.username 82 | assert PH.realm(params) == creds.realm 83 | assert PH.nonce(params) == creds.nonce 84 | end 85 | end 86 | 87 | describe "eval_response/2" do 88 | test "returns relayed address and lifetime on successful allocate response" do 89 | creds = CH.final() 90 | address = {127, 0, 0, 1} 91 | port = 33_333 92 | lifetime = 600 93 | 94 | params = 95 | Params.new() 96 | |> Params.put_class(:success) 97 | |> Params.put_method(:allocate) 98 | |> Params.put_attr(%Lifetime{duration: lifetime}) 99 | |> Params.put_attr(XRA.new(address, port)) 100 | |> Params.put_attr(XMA.new(address, port)) 101 | 102 | assert {:ok, {address, port}, lifetime} == 103 | Allocate.eval_response(params, creds, []) 104 | end 105 | 106 | test "returns :bad_response on invalid STUN method" do 107 | creds = CH.final() 108 | address = {127, 0, 0, 1} 109 | port = 33_333 110 | lifetime = 600 111 | 112 | params = 113 | Params.new() 114 | |> Params.put_class(:success) 115 | |> Params.put_method(:binding) 116 | |> Params.put_attr(%Lifetime{duration: lifetime}) 117 | |> Params.put_attr(XRA.new(address, port)) 118 | |> Params.put_attr(XMA.new(address, port)) 119 | 120 | assert {:error, :bad_response, creds} == 121 | Allocate.eval_response(params, creds, []) 122 | end 123 | 124 | test "returns :bad_response without XOR-RELAYED-ADDRESS" do 125 | creds = CH.final() 126 | address = {127, 0, 0, 1} 127 | port = 33_333 128 | lifetime = 600 129 | 130 | params = 131 | Params.new() 132 | |> Params.put_class(:success) 133 | |> Params.put_method(:allocate) 134 | |> Params.put_attr(%Lifetime{duration: lifetime}) 135 | |> Params.put_attr(XMA.new(address, port)) 136 | 137 | assert {:error, :bad_response, creds} == 138 | Allocate.eval_response(params, creds, []) 139 | end 140 | 141 | test "returns :bad_response without XOR-MAPPED-ADDRESS" do 142 | creds = CH.final() 143 | address = {127, 0, 0, 1} 144 | port = 33_333 145 | lifetime = 600 146 | 147 | params = 148 | Params.new() 149 | |> Params.put_class(:success) 150 | |> Params.put_method(:allocate) 151 | |> Params.put_attr(%Lifetime{duration: lifetime}) 152 | |> Params.put_attr(XRA.new(address, port)) 153 | 154 | assert {:error, :bad_response, creds} == 155 | Allocate.eval_response(params, creds, []) 156 | end 157 | 158 | test "returns :bad_response without LIFETIME" do 159 | creds = CH.final() 160 | address = {127, 0, 0, 1} 161 | port = 33_333 162 | 163 | params = 164 | Params.new() 165 | |> Params.put_class(:success) 166 | |> Params.put_method(:allocate) 167 | |> Params.put_attr(XRA.new(address, port)) 168 | |> Params.put_attr(XMA.new(address, port)) 169 | 170 | assert {:error, :bad_response, creds} == 171 | Allocate.eval_response(params, creds, []) 172 | end 173 | 174 | test "returns :bad_response on failure without ERROR-CODE" do 175 | creds = CH.final() 176 | 177 | params = 178 | Params.new() 179 | |> Params.put_class(:failure) 180 | |> Params.put_method(:allocate) 181 | 182 | assert {:error, :bad_response, creds} == 183 | Allocate.eval_response(params, creds, []) 184 | end 185 | 186 | test "returns creds with updated nonce on :stale_nonce error" do 187 | creds = CH.final() |> Map.put(:nonce, "I'm expired") 188 | new_nonce = CH.final() 189 | 190 | params = 191 | Params.new() 192 | |> Params.put_class(:failure) 193 | |> Params.put_method(:allocate) 194 | |> Params.put_attr(%Nonce{value: new_nonce}) 195 | |> Params.put_attr(%ErrorCode{name: :stale_nonce}) 196 | 197 | assert {:error, :stale_nonce, %{creds | nonce: new_nonce}} == 198 | Allocate.eval_response(params, creds, []) 199 | end 200 | 201 | test "returns complete creds on :unauthorized" do 202 | creds = CH.initial() 203 | realm = "wonderland" 204 | nonce = "dcba" 205 | 206 | params = 207 | Params.new() 208 | |> Params.put_class(:failure) 209 | |> Params.put_method(:allocate) 210 | |> Params.put_attr(%Nonce{value: nonce}) 211 | |> Params.put_attr(%Realm{value: realm}) 212 | |> Params.put_attr(%ErrorCode{name: :unauthorized}) 213 | 214 | assert {:error, :unauthorized, creds} = 215 | Allocate.eval_response(params, creds, []) 216 | assert Credentials.complete?(creds) 217 | assert %{realm: ^realm, nonce: ^nonce} = creds 218 | end 219 | 220 | test "returns unchanged creds on :unauthorized when realm in creds is not nil" do 221 | creds = CH.final() 222 | realm = "wonderland" 223 | nonce = "dcba" 224 | 225 | params = 226 | Params.new() 227 | |> Params.put_class(:failure) 228 | |> Params.put_method(:allocate) 229 | |> Params.put_attr(%Nonce{value: nonce}) 230 | |> Params.put_attr(%Realm{value: realm}) 231 | |> Params.put_attr(%ErrorCode{name: :unauthorized}) 232 | 233 | assert {:error, :unauthorized, creds} == 234 | Allocate.eval_response(params, creds, []) 235 | end 236 | 237 | test "returns error name and unchanged creds on other errors" do 238 | creds = CH.final() 239 | error = :allocation_mismatch 240 | 241 | params = 242 | Params.new() 243 | |> Params.put_class(:failure) 244 | |> Params.put_method(:allocate) 245 | |> Params.put_attr(%ErrorCode{name: error}) 246 | 247 | assert {:error, error, creds} == Allocate.eval_response(params, creds, []) 248 | end 249 | 250 | test "returns :bad_response on response without reservation token" do 251 | creds = CH.final() 252 | address = {127, 0, 0, 1} 253 | port = 33_333 254 | lifetime = 600 255 | 256 | params = 257 | Params.new() 258 | |> Params.put_class(:success) 259 | |> Params.put_method(:allocate) 260 | |> Params.put_attr(%Lifetime{duration: lifetime}) 261 | |> Params.put_attr(XRA.new(address, port)) 262 | |> Params.put_attr(XMA.new(address, port)) 263 | 264 | assert {:error, :bad_response, creds} == 265 | Allocate.eval_response(params, creds, [reserve: true]) 266 | end 267 | 268 | test "returns reservation token if it was requested" do 269 | creds = CH.final() 270 | address = {127, 0, 0, 1} 271 | port = 33_333 272 | lifetime = 600 273 | token = "12345678" 274 | 275 | params = 276 | Params.new() 277 | |> Params.put_class(:success) 278 | |> Params.put_method(:allocate) 279 | |> Params.put_attr(%Lifetime{duration: lifetime}) 280 | |> Params.put_attr(XRA.new(address, port)) 281 | |> Params.put_attr(XMA.new(address, port)) 282 | |> Params.put_attr(%ReservationToken{value: token}) 283 | 284 | assert {:ok, {address, port}, lifetime, token} == 285 | Allocate.eval_response(params, creds, [reserve: true]) 286 | end 287 | 288 | test "doesn't return reservation token if it wasn't request" do 289 | creds = CH.final() 290 | address = {127, 0, 0, 1} 291 | port = 33_333 292 | lifetime = 600 293 | token = "12345678" 294 | 295 | params = 296 | Params.new() 297 | |> Params.put_class(:success) 298 | |> Params.put_method(:allocate) 299 | |> Params.put_attr(%Lifetime{duration: lifetime}) 300 | |> Params.put_attr(XRA.new(address, port)) 301 | |> Params.put_attr(XMA.new(address, port)) 302 | |> Params.put_attr(%ReservationToken{value: token}) 303 | 304 | assert {:ok, {address, port}, lifetime} == 305 | Allocate.eval_response(params, creds, []) 306 | end 307 | end 308 | end 309 | --------------------------------------------------------------------------------