feat(psk): support psk
This commit is contained in:
parent
e737f18548
commit
7c61bc18cf
|
@ -0,0 +1,38 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2019-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_tls_psk).
|
||||||
|
|
||||||
|
-include("logger.hrl").
|
||||||
|
|
||||||
|
%% SSL PSK Callbacks
|
||||||
|
-export([lookup/3]).
|
||||||
|
|
||||||
|
-type psk_identity() :: string().
|
||||||
|
-type psk_user_state() :: term().
|
||||||
|
|
||||||
|
-spec lookup(psk, psk_identity(), psk_user_state()) -> {ok, SharedSecret :: binary()} | error.
|
||||||
|
lookup(psk, PSKIdentity, UserState) ->
|
||||||
|
try emqx_hooks:run_fold('tls_handshake.psk_lookup', [PSKIdentity], UserState) of
|
||||||
|
SharedSecret when is_binary(SharedSecret) -> {ok, SharedSecret};
|
||||||
|
Error ->
|
||||||
|
?LOG(error, "Look PSK for PSKID ~p error: ~p", [PSKIdentity, Error]),
|
||||||
|
error
|
||||||
|
catch
|
||||||
|
Except:Error:Stacktrace ->
|
||||||
|
?LOG(error, "Lookup PSK failed, ~0p: ~0p", [{Except,Error}, Stacktrace]),
|
||||||
|
error
|
||||||
|
end.
|
|
@ -0,0 +1,2 @@
|
||||||
|
myclient1:8c701116e9127c57a99d5563709af3deaca75563e2c4dd0865701ae839fb6d79
|
||||||
|
myclient2:d1e617d3b963757bfc21dad3fea169716c3a2f053f23decaea5cdfaabd04bfc4
|
|
@ -0,0 +1,7 @@
|
||||||
|
psk {
|
||||||
|
enable = true
|
||||||
|
|
||||||
|
# boot_file = {{ platform_data_dir }}/boot.psk
|
||||||
|
|
||||||
|
# delimiter = ":"
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
{deps, []}.
|
||||||
|
|
||||||
|
{edoc_opts, [{preprocess, true}]}.
|
||||||
|
{erl_opts, [warn_unused_vars,
|
||||||
|
warn_shadow_vars,
|
||||||
|
warnings_as_errors,
|
||||||
|
warn_unused_import,
|
||||||
|
warn_obsolete_guard,
|
||||||
|
debug_info,
|
||||||
|
{parse_transform}]}.
|
||||||
|
|
||||||
|
{xref_checks, [undefined_function_calls, undefined_functions,
|
||||||
|
locals_not_used, deprecated_function_calls,
|
||||||
|
warnings_as_errors, deprecated_functions]}.
|
||||||
|
|
||||||
|
{cover_enabled, true}.
|
||||||
|
{cover_opts, [verbose]}.
|
||||||
|
{cover_export_enabled, true}.
|
|
@ -0,0 +1,15 @@
|
||||||
|
%% -*- mode: erlang -*-
|
||||||
|
{application, emqx_psk,
|
||||||
|
[{description, "EMQ X PSK"},
|
||||||
|
{vsn, "5.0.0"}, % strict semver, bump manually!
|
||||||
|
{modules, []},
|
||||||
|
{registered, [emqx_psk_sup]},
|
||||||
|
{applications, [kernel,stdlib]},
|
||||||
|
{mod, {emqx_psk_app,[]}},
|
||||||
|
{env, []},
|
||||||
|
{licenses, ["Apache-2.0"]},
|
||||||
|
{maintainers, ["EMQ X Team <contact@emqx.io>"]},
|
||||||
|
{links, [{"Homepage", "https://emqx.io/"},
|
||||||
|
{"Github", "https://github.com/emqx/emqx"}
|
||||||
|
]}
|
||||||
|
]}.
|
|
@ -0,0 +1,224 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_psk).
|
||||||
|
|
||||||
|
-behaviour(gen_server).
|
||||||
|
|
||||||
|
-include_lib("emqx/include/logger.hrl").
|
||||||
|
|
||||||
|
-export([ load/0
|
||||||
|
, unload/0
|
||||||
|
, on_psk_lookup/2
|
||||||
|
, import/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
-export([ start_link/0
|
||||||
|
, stop/0
|
||||||
|
]).
|
||||||
|
|
||||||
|
%% gen_server callbacks
|
||||||
|
-export([ init/1
|
||||||
|
, handle_call/3
|
||||||
|
, handle_cast/2
|
||||||
|
, handle_info/2
|
||||||
|
, terminate/2
|
||||||
|
, code_change/3
|
||||||
|
]).
|
||||||
|
|
||||||
|
-record(psk_entry, {psk_id :: binary(),
|
||||||
|
shared_secret :: binary()}).
|
||||||
|
|
||||||
|
-export([mnesia/1]).
|
||||||
|
|
||||||
|
-boot_mnesia({mnesia, [boot]}).
|
||||||
|
-copy_mnesia({mnesia, [copy]}).
|
||||||
|
|
||||||
|
-define(TAB, ?MODULE).
|
||||||
|
-define(PSK_SHARD, emqx_psk_shard).
|
||||||
|
|
||||||
|
-define(DEFAULT_DELIMITER, <<":">>).
|
||||||
|
|
||||||
|
-define(CR, 13).
|
||||||
|
-define(LF, 10).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Mnesia bootstrap
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
%% @doc Create or replicate tables.
|
||||||
|
-spec(mnesia(boot | copy) -> ok).
|
||||||
|
mnesia(boot) ->
|
||||||
|
ok = ekka_mnesia:create_table(?TAB, [
|
||||||
|
{rlog_shard, ?PSK_SHARD},
|
||||||
|
{disc_copies, [node()]},
|
||||||
|
{record_name, psk_entry},
|
||||||
|
{attributes, record_info(fields, psk_entry)},
|
||||||
|
{storage_properties, [{ets, [{read_concurrency, true}]}]}]);
|
||||||
|
|
||||||
|
mnesia(copy) ->
|
||||||
|
ok = ekka_mnesia:copy_table(?TAB, disc_copies).
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% APIs
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
load() ->
|
||||||
|
emqx:hook('tls_handshake.psk_lookup', {?MODULE, on_psk_lookup, []}).
|
||||||
|
|
||||||
|
unload() ->
|
||||||
|
emqx:unhook('tls_handshake.psk_lookup', {?MODULE, on_psk_lookup, []}).
|
||||||
|
|
||||||
|
on_psk_lookup(PSKIdentity, _UserState) ->
|
||||||
|
case mnesia:dirty_read(?TAB, PSKIdentity) of
|
||||||
|
[#psk_entry{shared_secret = SharedSecret}] ->
|
||||||
|
{stop, SharedSecret};
|
||||||
|
_ ->
|
||||||
|
ignore
|
||||||
|
end.
|
||||||
|
|
||||||
|
import(SrcFile) ->
|
||||||
|
gen_server:call(?MODULE, {import, SrcFile}).
|
||||||
|
|
||||||
|
-spec start_link() -> {ok, pid()} | ignore | {error, term()}.
|
||||||
|
start_link() ->
|
||||||
|
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
|
||||||
|
|
||||||
|
-spec stop() -> ok.
|
||||||
|
stop() ->
|
||||||
|
gen_server:stop(?MODULE).
|
||||||
|
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% gen_server callbacks
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
init(_Opts) ->
|
||||||
|
case is_enable() of
|
||||||
|
true -> load();
|
||||||
|
false -> ok
|
||||||
|
end,
|
||||||
|
case boot_file() of
|
||||||
|
undefined -> ok;
|
||||||
|
BootFile -> import_psks(BootFile)
|
||||||
|
end,
|
||||||
|
{ok, #{}}.
|
||||||
|
|
||||||
|
handle_call({import, SrcFile}, _From, State) ->
|
||||||
|
{reply, import_psks(SrcFile), State};
|
||||||
|
|
||||||
|
handle_call(Req, _From, State) ->
|
||||||
|
?LOG(error, "Unexpected call: ~p", [Req]),
|
||||||
|
{reply, ignored, State}.
|
||||||
|
|
||||||
|
handle_cast(Req, State) ->
|
||||||
|
?LOG(error, "Unexpected case: ~p", [Req]),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
handle_info(Info, State) ->
|
||||||
|
?LOG(error, "Unexpected info: ~p", [Info]),
|
||||||
|
{noreply, State}.
|
||||||
|
|
||||||
|
terminate(_Reason, _State) ->
|
||||||
|
unload(),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
code_change(_OldVsn, State, _Extra) ->
|
||||||
|
{ok, State}.
|
||||||
|
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
%% Internal functions
|
||||||
|
%%------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
is_enable() ->
|
||||||
|
emqx_config:get([psk, enable]).
|
||||||
|
|
||||||
|
boot_file() ->
|
||||||
|
emqx_config:get([psk, boot_file], undefined).
|
||||||
|
|
||||||
|
delimiter() ->
|
||||||
|
emqx_config:get([psk, delimiter], ?DEFAULT_DELIMITER).
|
||||||
|
|
||||||
|
import_psks(SrcFile) ->
|
||||||
|
case file:open(SrcFile, [read, raw, binary, read_ahead]) of
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason};
|
||||||
|
{ok, Io} ->
|
||||||
|
case Result = import_psks(Io, delimiter()) of
|
||||||
|
ok -> ignore;
|
||||||
|
{error, Reason} ->
|
||||||
|
?LOG("Failed to import psk from ~s due to ~p", [SrcFile, Reason])
|
||||||
|
end,
|
||||||
|
_ = file:close(Io),
|
||||||
|
Result
|
||||||
|
end.
|
||||||
|
|
||||||
|
import_psks(Io, Delimiter) ->
|
||||||
|
case get_psks(Io, Delimiter, 50) of
|
||||||
|
{ok, Entries} ->
|
||||||
|
_ = trans(fun insert_psks/1, Entries),
|
||||||
|
import_psks(Io, Delimiter);
|
||||||
|
{eof, Entries} ->
|
||||||
|
_ = trans(fun insert_psks/1, Entries),
|
||||||
|
ok;
|
||||||
|
{error, Reaosn} ->
|
||||||
|
{error, Reaosn}
|
||||||
|
end.
|
||||||
|
|
||||||
|
get_psks(Io, Delimiter, Max) ->
|
||||||
|
get_psks(Io, Delimiter, Max, []).
|
||||||
|
|
||||||
|
get_psks(_Io, _Delimiter, 0, Acc) ->
|
||||||
|
{ok, Acc};
|
||||||
|
get_psks(Io, Delimiter, Remaining, Acc) ->
|
||||||
|
case file:read_line(Io) of
|
||||||
|
{ok, Line} ->
|
||||||
|
case binary:split(Line, Delimiter) of
|
||||||
|
[PSKIdentity, SharedSecret] ->
|
||||||
|
NSharedSecret = trim_crlf(SharedSecret),
|
||||||
|
get_psks(Io, Delimiter, Remaining - 1, [{PSKIdentity, NSharedSecret} | Acc]);
|
||||||
|
_ ->
|
||||||
|
{error, {bad_format, Line}}
|
||||||
|
end;
|
||||||
|
eof ->
|
||||||
|
{eof, Acc};
|
||||||
|
{error, Reason} ->
|
||||||
|
{error, Reason}
|
||||||
|
end.
|
||||||
|
|
||||||
|
insert_psks(Entries) ->
|
||||||
|
lists:foreach(fun(Entry) ->
|
||||||
|
insert_psk(Entry)
|
||||||
|
end, Entries).
|
||||||
|
|
||||||
|
insert_psk({PSKIdentity, SharedSecret}) ->
|
||||||
|
mnesia:write(?TAB, #psk_entry{psk_id = PSKIdentity, shared_secret = SharedSecret}, write).
|
||||||
|
|
||||||
|
trim_crlf(Bin) ->
|
||||||
|
Size = byte_size(Bin),
|
||||||
|
case binary:at(Bin, Size - 1) of
|
||||||
|
?LF ->
|
||||||
|
case binary:at(Bin, Size - 2) of
|
||||||
|
?CR -> binary:part(Bin, 0, Size - 2);
|
||||||
|
_ -> binary:part(Bin, 0, Size - 1)
|
||||||
|
end;
|
||||||
|
_ -> Bin
|
||||||
|
end.
|
||||||
|
|
||||||
|
trans(Fun, Args) ->
|
||||||
|
case ekka_mnesia:transaction(?PSK_SHARD, Fun, Args) of
|
||||||
|
{atomic, Res} -> Res;
|
||||||
|
{aborted, Reason} -> {error, Reason}
|
||||||
|
end.
|
|
@ -0,0 +1,30 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_psk_app).
|
||||||
|
|
||||||
|
-behaviour(application).
|
||||||
|
|
||||||
|
-export([ start/2
|
||||||
|
, stop/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
start(_Type, _Args) ->
|
||||||
|
{ok, Sup} = emqx_psk_sup:start_link(),
|
||||||
|
{ok, Sup}.
|
||||||
|
|
||||||
|
stop(_State) ->
|
||||||
|
ok.
|
|
@ -0,0 +1,45 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_psk_schema).
|
||||||
|
|
||||||
|
-behaviour(hocon_schema).
|
||||||
|
|
||||||
|
-include_lib("typerefl/include/types.hrl").
|
||||||
|
|
||||||
|
-export([ roots/0
|
||||||
|
, fields/1
|
||||||
|
]).
|
||||||
|
|
||||||
|
roots() -> [].
|
||||||
|
|
||||||
|
fields("psk") ->
|
||||||
|
[ {enable, fun enable/1}
|
||||||
|
, {boot_file, fun boot_file/1}
|
||||||
|
, {delimiter, fun delimiter/1}
|
||||||
|
].
|
||||||
|
|
||||||
|
enable(type) -> boolean();
|
||||||
|
enable(default) -> false;
|
||||||
|
enable(_) -> undefined.
|
||||||
|
|
||||||
|
boot_file(type) -> binary();
|
||||||
|
boot_file(nullable) -> true;
|
||||||
|
boot_file(_) -> undefined.
|
||||||
|
|
||||||
|
delimiter(type) -> binary();
|
||||||
|
delimiter(default) -> <<":">>;
|
||||||
|
delimiter(_) -> undefined.
|
|
@ -0,0 +1,35 @@
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
%% Copyright (c) 2020-2021 EMQ Technologies Co., Ltd. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
%% you may not use this file except in compliance with the License.
|
||||||
|
%% You may obtain a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing, software
|
||||||
|
%% distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
%% See the License for the specific language governing permissions and
|
||||||
|
%% limitations under the License.
|
||||||
|
%%--------------------------------------------------------------------
|
||||||
|
|
||||||
|
-module(emqx_psk_sup).
|
||||||
|
|
||||||
|
-behaviour(supervisor).
|
||||||
|
|
||||||
|
-export([start_link/0]).
|
||||||
|
|
||||||
|
-export([init/1]).
|
||||||
|
|
||||||
|
start_link() ->
|
||||||
|
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
|
||||||
|
|
||||||
|
init([]) ->
|
||||||
|
{ok, {{one_for_one, 10, 3600},
|
||||||
|
[#{id => emqx_psk,
|
||||||
|
start => {emqx_psk, start_link, []},
|
||||||
|
restart => permanent,
|
||||||
|
shutdown => 5000,
|
||||||
|
type => worker,
|
||||||
|
modules => [emqx_psk]}]}}.
|
Loading…
Reference in New Issue