From 7c61bc18cf6bf81cd5924bb1bbd6bfead005b143 Mon Sep 17 00:00:00 2001 From: zhouzb Date: Sat, 18 Sep 2021 16:59:28 +0800 Subject: [PATCH] feat(psk): support psk --- apps/emqx/src/emqx_tls_psk.erl | 38 +++++ apps/emqx_psk/data/boot.psk | 2 + apps/emqx_psk/etc/emqx_psk.conf | 7 + apps/emqx_psk/rebar.config | 18 +++ apps/emqx_psk/src/emqx_psk.app.src | 15 ++ apps/emqx_psk/src/emqx_psk.erl | 224 ++++++++++++++++++++++++++ apps/emqx_psk/src/emqx_psk_app.erl | 30 ++++ apps/emqx_psk/src/emqx_psk_schema.erl | 45 ++++++ apps/emqx_psk/src/emqx_psk_sup.erl | 35 ++++ 9 files changed, 414 insertions(+) create mode 100644 apps/emqx/src/emqx_tls_psk.erl create mode 100644 apps/emqx_psk/data/boot.psk create mode 100644 apps/emqx_psk/etc/emqx_psk.conf create mode 100644 apps/emqx_psk/rebar.config create mode 100644 apps/emqx_psk/src/emqx_psk.app.src create mode 100644 apps/emqx_psk/src/emqx_psk.erl create mode 100644 apps/emqx_psk/src/emqx_psk_app.erl create mode 100644 apps/emqx_psk/src/emqx_psk_schema.erl create mode 100644 apps/emqx_psk/src/emqx_psk_sup.erl diff --git a/apps/emqx/src/emqx_tls_psk.erl b/apps/emqx/src/emqx_tls_psk.erl new file mode 100644 index 000000000..7a8238182 --- /dev/null +++ b/apps/emqx/src/emqx_tls_psk.erl @@ -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. diff --git a/apps/emqx_psk/data/boot.psk b/apps/emqx_psk/data/boot.psk new file mode 100644 index 000000000..7e1a1edf5 --- /dev/null +++ b/apps/emqx_psk/data/boot.psk @@ -0,0 +1,2 @@ +myclient1:8c701116e9127c57a99d5563709af3deaca75563e2c4dd0865701ae839fb6d79 +myclient2:d1e617d3b963757bfc21dad3fea169716c3a2f053f23decaea5cdfaabd04bfc4 diff --git a/apps/emqx_psk/etc/emqx_psk.conf b/apps/emqx_psk/etc/emqx_psk.conf new file mode 100644 index 000000000..d964f4e23 --- /dev/null +++ b/apps/emqx_psk/etc/emqx_psk.conf @@ -0,0 +1,7 @@ +psk { + enable = true + + # boot_file = {{ platform_data_dir }}/boot.psk + + # delimiter = ":" +} diff --git a/apps/emqx_psk/rebar.config b/apps/emqx_psk/rebar.config new file mode 100644 index 000000000..73696b033 --- /dev/null +++ b/apps/emqx_psk/rebar.config @@ -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}. diff --git a/apps/emqx_psk/src/emqx_psk.app.src b/apps/emqx_psk/src/emqx_psk.app.src new file mode 100644 index 000000000..3d749abf6 --- /dev/null +++ b/apps/emqx_psk/src/emqx_psk.app.src @@ -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 "]}, + {links, [{"Homepage", "https://emqx.io/"}, + {"Github", "https://github.com/emqx/emqx"} + ]} + ]}. diff --git a/apps/emqx_psk/src/emqx_psk.erl b/apps/emqx_psk/src/emqx_psk.erl new file mode 100644 index 000000000..0968dad4c --- /dev/null +++ b/apps/emqx_psk/src/emqx_psk.erl @@ -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. diff --git a/apps/emqx_psk/src/emqx_psk_app.erl b/apps/emqx_psk/src/emqx_psk_app.erl new file mode 100644 index 000000000..95e947291 --- /dev/null +++ b/apps/emqx_psk/src/emqx_psk_app.erl @@ -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. diff --git a/apps/emqx_psk/src/emqx_psk_schema.erl b/apps/emqx_psk/src/emqx_psk_schema.erl new file mode 100644 index 000000000..c8c8ea7e4 --- /dev/null +++ b/apps/emqx_psk/src/emqx_psk_schema.erl @@ -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. diff --git a/apps/emqx_psk/src/emqx_psk_sup.erl b/apps/emqx_psk/src/emqx_psk_sup.erl new file mode 100644 index 000000000..2c5181735 --- /dev/null +++ b/apps/emqx_psk/src/emqx_psk_sup.erl @@ -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]}]}}.