feat(psk): support psk

This commit is contained in:
zhouzb 2021-09-18 16:59:28 +08:00
parent e737f18548
commit 7c61bc18cf
9 changed files with 414 additions and 0 deletions

View File

@ -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.

View File

@ -0,0 +1,2 @@
myclient1:8c701116e9127c57a99d5563709af3deaca75563e2c4dd0865701ae839fb6d79
myclient2:d1e617d3b963757bfc21dad3fea169716c3a2f053f23decaea5cdfaabd04bfc4

View File

@ -0,0 +1,7 @@
psk {
enable = true
# boot_file = {{ platform_data_dir }}/boot.psk
# delimiter = ":"
}

View File

@ -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}.

View File

@ -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"}
]}
]}.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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]}]}}.