defmodule Wampex.Authentication do
@moduledoc false
- defstruct [:authid, :authmethods]
+ @enforce_keys [:authid, :authmethods, :secret]
+ defstruct [:authid, :authmethods, :secret]
+
+ @callback handle(challenge :: binary(), auth :: Authentication.t()) :: {binary(), map()}
@type t :: %__MODULE__{
authid: binary(),
- authmethods: [binary()]
+ authmethods: [binary()],
+ secret: binary()
}
+
+ def handle(
+ {"wampcra", %{"challenge" => ch, "salt" => salt, "iterations" => it, "keylen" => len}},
+ auth
+ ) do
+ {auth
+ |> Map.get(:secret)
+ |> pbkdf2(salt, it, len)
+ |> hash_challenge(ch), %{}}
+ end
+
+ def handle({"wampcra", %{"challenge" => ch}}, auth) do
+ {auth
+ |> Map.get(:secret)
+ |> hash_challenge(ch), %{}}
+ end
+
+ defp hash_challenge(secret, challenge) do
+ :sha256
+ |> :crypto.hmac(secret, challenge)
+ |> :base64.encode()
+ end
+
+ defp pbkdf2(secret, salt, iterations, keylen) do
+ {:ok, derived} = :pbkdf2.pbkdf2(:sha256, secret, salt, iterations, keylen)
+ Base.encode64(derived)
+ end
end
@moduledoc """
Handles requests and responses for low-level Peer/Session interactions
"""
- alias StatesLanguage, as: SL
- alias Wampex.Session
-
alias Wampex.Role
@behaviour Role
@hello 1
@welcome 2
@abort 3
+ @challenge 4
+ @authenticate 5
@goodbye 6
@error 8
}
end
+ defmodule Authenticate do
+ @moduledoc false
+ @enforce_keys [:signature, :extra]
+ defstruct [:signature, :extra]
+
+ @type t :: %__MODULE__{
+ signature: binary(),
+ extra: map()
+ }
+ end
+
@impl true
def add(roles), do: roles
[@goodbye, opts, r]
end
+ @spec authenticate(Authenticate.t()) :: Wampex.message()
+ def authenticate(%Authenticate{signature: s, extra: e}) do
+ [@authenticate, s, e]
+ end
+
@impl true
- def handle([@welcome, session_id, _dets], %SL{data: %Session{} = data} = sl) do
- {%SL{sl | data: %Session{data | id: session_id}}, [{:next_event, :internal, :noop}], nil,
- {:ok, session_id}}
+ def handle([@welcome, session_id, _dets], sl) do
+ {sl, [{:next_event, :internal, :noop}], nil, {:update, :id, session_id}}
end
@impl true
{sl, [{:next_event, :internal, :goodbye}], nil, {:update, :goodbye, reason}}
end
+ @impl true
+ def handle([@challenge, auth_method, extra], sl) do
+ {sl, [{:next_event, :internal, :challenge}], nil, {:update, :challenge, {auth_method, extra}}}
+ end
+
@impl true
def handle([@error, type, id, dets, error], sl) do
handle([@error, type, id, dets, error, [], %{}], sl)
alias Wampex.Realm
alias Wampex.Authentication
alias Wampex.Role.Peer
- alias Wampex.Role.Peer.Hello
+ alias Wampex.Role.Peer.{Authenticate, Hello}
alias Wampex.Serializer.MessagePack
alias __MODULE__, as: Sess
alias Wampex.Transport.WebSocket
:name,
:roles,
:authentication,
+ :challenge,
+ message_queue: [],
request_id: 0,
protocol: "wamp.2.msgpack",
transport: WebSocket,
realm: Realm.t(),
name: module() | nil,
roles: [module()],
- authentication: Authentication.t(),
+ authentication: Authentication.t() | nil,
+ challenge: {binary(), binary()} | nil,
+ message_queue: [],
request_id: integer(),
protocol: binary(),
transport: module(),
def handle_call(
{:send_request, request},
from,
- _,
+ "Established",
%SL{data: %Sess{request_id: r_id, transport: tt, transport_pid: t} = sess} = data
) do
request_id = do_send(r_id, tt, t, request)
end
@impl true
- def handle_cast(
+ def handle_call(
{:send_request, request},
+ from,
_,
+ %SL{data: %Sess{message_queue: mq} = sess} = data
+ ) do
+ Logger.info("Queueing request: #{inspect(request)}")
+ {:ok, %SL{data | data: %Sess{sess | message_queue: [{request, from} | mq]}}, []}
+ end
+
+ @impl true
+ def handle_cast(
+ {:send_request, request},
+ "Established",
%SL{data: %Sess{request_id: r_id, transport: tt, transport_pid: t} = sess} = data
) do
request_id = do_send(r_id, tt, t, request)
}, []}
end
+ @impl true
+ def handle_cast(
+ {:send_request, request},
+ _,
+ %SL{data: %Sess{message_queue: mq} = sess} = data
+ ) do
+ Logger.info("Queueing request: #{inspect(request)}")
+ {:ok, %SL{data | data: %Sess{sess | message_queue: [{request, nil} | mq]}}, []}
+ end
+
@impl true
def handle_resource(
"Hello",
{:ok, data, [{:next_event, :internal, :hello_sent}]}
end
+ @impl true
+ def handle_resource(
+ "HandleChallenge",
+ _,
+ "Challenge",
+ %SL{
+ data: %Sess{transport: tt, transport_pid: t, challenge: challenge, authentication: auth}
+ } = sl
+ ) do
+ {signature, extra} = auth.__struct__.handle(challenge, auth)
+ tt.send_request(t, Peer.authenticate(%Authenticate{signature: signature, extra: extra}))
+ {:ok, sl, [{:next_event, :internal, :challenged}]}
+ end
+
@impl true
def handle_resource(
"GoodBye",
end
@impl true
- def handle_resource("Established", _, "Established", data) do
+ def handle_resource(
+ "Established",
+ _,
+ "Established",
+ %SL{
+ data: %Sess{request_id: r_id, transport: tt, transport_pid: t, message_queue: mq} = sess
+ } = data
+ ) do
+ {request_id, requests} =
+ mq
+ |> Enum.reverse()
+ |> Enum.reduce({r_id, []}, fn {request, from}, {id, requests} ->
+ r = do_send(id, tt, t, request)
+ {r, [{r, from} | requests]}
+ end)
+
+ requests =
+ Enum.filter(requests, fn
+ {_, nil} -> false
+ {_, _} -> true
+ end)
+
+ {:ok,
+ %SL{
+ data
+ | data: %Sess{sess | requests: requests, request_id: request_id, message_queue: []}
+ }, []}
+ end
+
+ @impl true
+ def handle_resource("HandleHandshake", _, "Handshake", data) do
{:ok, data, []}
end
end
@impl true
- def handle_info({:set_message, message}, "Established", %SL{data: %Sess{} = sess} = data) do
+ def handle_info({:set_message, message}, _, %SL{data: %Sess{} = sess} = data) do
{:ok, %SL{data | data: %Sess{sess | message: message}},
[{:next_event, :internal, :message_received}]}
end
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
{:jason, "~> 1.1"},
{:msgpack, "~> 0.7.0"},
+ {:pbkdf2, "~> 2.0"},
{:states_language, "~> 0.2"},
{:websockex, "~> 0.4"}
]
"msgpack": {:hex, :msgpack, "0.7.0", "128ae0a2227c7e7a2847c0f0f73551c268464f8c1ee96bffb920bc0a5712b295", [:rebar3], [], "hexpm", "4649353da003e6f438d105e4b1e0f17757f6f5ec8687a6f30875ff3ac4ce2a51"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
+ "pbkdf2": {:hex, :pbkdf2, "2.0.0", "11c23279fded5c0027ab3996cfae77805521d7ef4babde2bd7ec04a9086cf499", [:rebar3], [], "hexpm", "1e793ce6fdb0576613115714deae9dfc1d1537eaba74f07efb36de139774488d"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"states_language": {:hex, :states_language, "0.2.8", "f9dfd3c0bd9a9d7bda25ef315f2d90944cd6b2022a7f3c403deb1d4ec451825e", [:mix], [{:elixpath, "~> 0.1.0", [hex: :elixpath, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:json_xema, "~> 0.4.0", [hex: :json_xema, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xema, "~> 0.11.0", [hex: :xema, repo: "hexpm", optional: false]}], "hexpm", "a5231691e7cb37fe32dc7de54c2dc86d1d60e84c4f0379f3246e55be2a85ec78"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"Type": "Task",
"Resource": "Hello",
"TransitionEvent": ":hello_sent",
- "Next": "Established",
+ "Next": "Handshake",
"Catch": [
{
"ErrorEquals": [":abort"],
]
},
+ "Handshake": {
+ "Type": "Choice",
+ "Resource": "HandleHandshake",
+ "Choices": [
+ {
+ "StringEquals": ":message_received",
+ "Next": "HandleMessage"
+ },
+ {
+ "StringEquals": ":abort",
+ "Next": "Abort"
+ }
+ ]
+ },
"Established": {
"Type": "Choice",
"Resource": "Established",
{
"StringEquals": ":goodbye",
"Next": "GoodBye"
+ },
+ {
+ "StringEquals": ":challenge",
+ "Next": "Challenge"
}
]
},
+ "Challenge": {
+ "Type": "Task",
+ "Resource": "HandleChallenge",
+ "Next": "Handshake",
+ "TransitionEvent": ":challenged"
+ },
"Event": {
"Type": "Task",
"Resource": "HandleEvent",
@realm %Realm{name: "com.myrealm"}
@roles [Callee, Caller, Publisher, Subscriber]
@device "as987d9a8sd79a87ds"
- @auth %Authentication{authid: "entone", authmethods: ["wampcra"]}
+ @auth %Authentication{authid: "entone", authmethods: ["wampcra"], secret: "test1234"}
@session %Session{url: @url, realm: @realm, roles: @roles, authentication: @auth}
})
)
- assert_receive {:event, _, _, _, _, _}
+ assert_receive {:event, _, _, _, _, _}, 2000
end
end