diff --git a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl index 88e4d83a5..3dee4c2a3 100644 --- a/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl +++ b/apps/emqx_authn/src/simple_authn/emqx_authn_http.erl @@ -152,11 +152,12 @@ create( #{ method := Method, url := RawURL, - headers := Headers, + headers := HeadersT, body := Body, request_timeout := RequestTimeout } = Config ) -> + Headers = ensure_header_name_type(HeadersT), {BsaeUrlWithPath, Query} = parse_fullpath(RawURL), URIMap = parse_url(BsaeUrlWithPath), ResourceId = emqx_authn_utils:make_resource_id(?MODULE), @@ -398,3 +399,14 @@ to_bin(L) when is_list(L) -> get_conf_val(Name, Conf) -> hocon_maps:get(?CONF_NS ++ "." ++ Name, Conf). + +ensure_header_name_type(Headers) -> + Fun = fun + (Key, _Val, Acc) when is_binary(Key) -> + Acc; + (Key, Val, Acc) when is_atom(Key) -> + Acc2 = maps:remove(Key, Acc), + BinKey = erlang:atom_to_binary(Key), + Acc2#{BinKey => Val} + end, + maps:fold(Fun, Headers, Headers). diff --git a/apps/emqx_gateway/test/emqx_coap_SUITE.erl b/apps/emqx_gateway/test/emqx_coap_SUITE.erl index d3f4f8014..e17c20f9d 100644 --- a/apps/emqx_gateway/test/emqx_coap_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_coap_SUITE.erl @@ -64,6 +64,12 @@ end_per_suite(_) -> {ok, _} = emqx:remove_config([<<"gateway">>, <<"coap">>]), emqx_mgmt_api_test_util:end_suite([emqx_gateway]). +default_config() -> + ?CONF_DEFAULT. + +mqtt_prefix() -> + ?MQTT_PREFIX. + %%-------------------------------------------------------------------- %% Test Cases %%-------------------------------------------------------------------- @@ -425,3 +431,8 @@ receive_deliver(Wait) -> after Wait -> {error, timeout} end. + +get_field(type, #coap_message{type = Type}) -> + Type; +get_field(method, #coap_message{method = Method}) -> + Method. diff --git a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl index 5ca1ae52f..07d9e8934 100644 --- a/apps/emqx_gateway/test/emqx_exproto_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_exproto_SUITE.erl @@ -40,6 +40,19 @@ -define(TCPOPTS, [binary, {active, false}]). -define(DTLSOPTS, [binary, {active, false}, {protocol, dtls}]). +%%-------------------------------------------------------------------- +-define(CONF_DEFAULT, << + "\n" + "gateway.exproto {\n" + " server.bind = 9100,\n" + " handler.address = \"http://127.0.0.1:9001\"\n" + " listeners.tcp.default {\n" + " bind = 7993,\n" + " acceptors = 8\n" + " }\n" + "}\n" +>>). + %%-------------------------------------------------------------------- %% Setups %%-------------------------------------------------------------------- @@ -84,6 +97,9 @@ listener_confs(Type) -> Default = #{bind => 7993, acceptors => 8}, #{Type => #{'default' => maps:merge(Default, socketopts(Type))}}. +default_config() -> + ?CONF_DEFAULT. + %%-------------------------------------------------------------------- %% Tests cases %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl b/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl new file mode 100644 index 000000000..92c403b11 --- /dev/null +++ b/apps/emqx_gateway/test/emqx_gateway_auth_ct.erl @@ -0,0 +1,211 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2022 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_gateway_auth_ct). + +-compile(nowarn_export_all). +-compile(export_all). + +-behaviour(gen_server). + +%% gen_server callbacks +-export([ + init/1, + handle_call/3, + handle_cast/2, + handle_info/2, + terminate/2, + code_change/3, + format_status/2 +]). + +-import( + emqx_gateway_test_utils, + [ + request/2, + request/3 + ] +). + +-include("emqx_authn.hrl"). +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("emqx/include/emqx_placeholder.hrl"). + +-define(CALL(Msg), gen_server:call(?MODULE, {?FUNCTION_NAME, Msg})). + +-define(HTTP_PORT, 37333). +-define(HTTP_PATH, "/auth"). +-define(GATEWAYS, [coap, lwm2m, mqttsn, stomp, exproto]). + +-define(CONFS, [ + emqx_coap_SUITE, + emqx_lwm2m_SUITE, + emqx_sn_protocol_SUITE, + emqx_stomp_SUITE, + emqx_exproto_SUITE +]). + +-record(state, {}). + +%%------------------------------------------------------------------------------ +%% API +%%------------------------------------------------------------------------------ + +group_names(Auths) -> + [{group, Auth} || Auth <- Auths]. + +init_groups(Suite, Auths) -> + All = emqx_common_test_helpers:all(Suite), + [{Auth, [], All} || Auth <- Auths]. + +start_auth(Name) -> + ?CALL(Name). + +stop_auth(Name) -> + ?CALL(Name). + +start() -> + gen_server:start({local, ?MODULE}, ?MODULE, [], []). + +stop() -> + gen_server:stop(?MODULE). + +%%------------------------------------------------------------------------------ +%% gen_server callbacks +%%------------------------------------------------------------------------------ + +init([]) -> + process_flag(trap_exit, true), + {ok, #state{}}. + +handle_call({start_auth, Name}, _From, State) -> + on_start_auth(Name), + {reply, ok, State}; +handle_call({stop_auth, Name}, _From, State) -> + on_stop_auth(Name), + {reply, ok, State}; +handle_call(_Request, _From, State) -> + Reply = ok, + {reply, Reply, State}. + +handle_cast(_Request, State) -> + {noreply, State}. + +handle_info(_Info, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +format_status(_Opt, Status) -> + Status. + +%%------------------------------------------------------------------------------ +%% Authenticators +%%------------------------------------------------------------------------------ + +on_start_auth(authn_http) -> + %% start test server + {ok, _} = emqx_authn_http_test_server:start_link(?HTTP_PORT, ?HTTP_PATH), + timer:sleep(1000), + + %% set authn for gateway + Setup = fun(Gateway) -> + Path = io_lib:format("/gateway/~ts/authentication", [Gateway]), + {204, _} = request(delete, Path), + {201, _} = request(post, Path, http_auth_config()) + end, + lists:foreach(Setup, ?GATEWAYS), + + %% set handler for test server + Handler = fun(Req0, State) -> + ct:pal("Authn Req:~p~nState:~p~n", [Req0, State]), + case cowboy_req:match_qs([username, password], Req0) of + #{ + username := <<"admin">>, + password := <<"public">> + } -> + Req = cowboy_req:reply(200, Req0); + _ -> + Req = cowboy_req:reply(400, Req0) + end, + {ok, Req, State} + end, + emqx_authn_http_test_server:set_handler(Handler), + + timer:sleep(500). + +on_stop_auth(authn_http) -> + Delete = fun(Gateway) -> + Path = io_lib:format("/gateway/~ts/authentication", [Gateway]), + {204, _} = request(delete, Path) + end, + lists:foreach(Delete, ?GATEWAYS), + ok = emqx_authn_http_test_server:stop(). + +%%------------------------------------------------------------------------------ +%% Configs +%%------------------------------------------------------------------------------ + +http_auth_config() -> + #{ + <<"mechanism">> => <<"password_based">>, + <<"enable">> => <<"true">>, + <<"backend">> => <<"http">>, + <<"method">> => <<"get">>, + <<"url">> => <<"http://127.0.0.1:37333/auth">>, + <<"body">> => #{<<"username">> => ?PH_USERNAME, <<"password">> => ?PH_PASSWORD}, + <<"headers">> => #{<<"X-Test-Header">> => <<"Test Value">>} + }. + +%%------------------------------------------------------------------------------ +%% Helpers +%%------------------------------------------------------------------------------ + +init_gateway_conf() -> + ok = emqx_common_test_helpers:load_config( + emqx_gateway_schema, + merge_conf([X:default_config() || X <- ?CONFS], []) + ). + +merge_conf([Conf | T], Acc) -> + case re:run(Conf, "\s*gateway\\.(.*)", [global, {capture, all_but_first, list}, dotall]) of + {match, [[Content]]} -> + merge_conf(T, [Content | Acc]); + _ -> + merge_conf(T, Acc) + end; +merge_conf([], Acc) -> + erlang:list_to_binary("gateway{" ++ string:join(Acc, ",") ++ "}"). + +with_resource(Init, Close, Fun) -> + Res = + case Init() of + {ok, X} -> X; + Other -> Other + end, + try + Fun(Res) + catch + C:R:S -> + erlang:raise(C, R, S) + after + Close(Res) + end. diff --git a/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl new file mode 100644 index 000000000..33c0a2eea --- /dev/null +++ b/apps/emqx_gateway/test/emqx_gateway_authn_SUITE.erl @@ -0,0 +1,253 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2020-2022 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_gateway_authn_SUITE). + +-compile(nowarn_export_all). +-compile(export_all). + +-include_lib("eunit/include/eunit.hrl"). +-include_lib("common_test/include/ct.hrl"). + +-import(emqx_gateway_auth_ct, [init_gateway_conf/0, with_resource/3]). + +-define(checkMatch(Guard), + (fun(Expr) -> + case (Expr) of + Guard -> + ok; + X__V -> + erlang:error( + {assertMatch, [ + {module, ?MODULE}, + {line, ?LINE}, + {expression, (??Expr)}, + {pattern, (??Guard)}, + {value, X__V} + ]} + ) + end + end) +). +-define(FUNCTOR(Expr), fun() -> Expr end). +-define(FUNCTOR(Arg, Expr), fun(Arg) -> Expr end). + +-define(AUTHNS, [authn_http]). + +all() -> + emqx_gateway_auth_ct:group_names(?AUTHNS). + +groups() -> + emqx_gateway_auth_ct:init_groups(?MODULE, ?AUTHNS). + +init_per_group(AuthName, Conf) -> + ct:pal("on group start:~p~n", [AuthName]), + {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), + emqx_gateway_auth_ct:start_auth(AuthName), + timer:sleep(500), + Conf. + +end_per_group(AuthName, Conf) -> + ct:pal("on group stop:~p~n", [AuthName]), + emqx_gateway_auth_ct:stop_auth(AuthName), + Conf. + +init_per_suite(Config) -> + emqx_config:erase(gateway), + init_gateway_conf(), + emqx_mgmt_api_test_util:init_suite([emqx_conf, emqx_authn, emqx_gateway]), + application:ensure_all_started(cowboy), + emqx_gateway_auth_ct:start(), + timer:sleep(500), + Config. + +end_per_suite(Config) -> + emqx_gateway_auth_ct:stop(), + emqx_config:erase(gateway), + emqx_mgmt_api_test_util:end_suite([cowboy, emqx_authn, emqx_gateway]), + Config. + +init_per_testcase(_Case, Config) -> + {ok, _} = emqx_cluster_rpc:start_link(node(), emqx_cluster_rpc, 1000), + Config. + +end_per_testcase(_Case, Config) -> + Config. + +%%------------------------------------------------------------------------------ +%% Tests +%%------------------------------------------------------------------------------ + +t_case_coap(_) -> + Login = fun(URI, Checker) -> + Action = fun(Channel) -> + Req = emqx_coap_SUITE:make_req(post), + Checker(emqx_coap_SUITE:do_request(Channel, URI, Req)) + end, + emqx_coap_SUITE:do(Action) + end, + Prefix = emqx_coap_SUITE:mqtt_prefix(), + RightUrl = + Prefix ++ + "/connection?clientid=client1&username=admin&password=public", + Login(RightUrl, ?checkMatch({ok, created, _Data})), + + LeftUrl = + Prefix ++ + "/connection?clientid=client1&username=bad&password=bad", + Login(LeftUrl, ?checkMatch({error, bad_request, _Data})), + ok. + +-record(coap_content, {content_format, payload = <<>>}). + +t_case_lwm2m(_) -> + MsgId = 12, + Mod = emqx_lwm2m_SUITE, + Epn = "urn:oma:lwm2m:oma:3", + Port = emqx_lwm2m_SUITE:default_port(), + Login = fun(URI, Checker) -> + with_resource( + ?FUNCTOR(gen_udp:open(0, [binary, {active, false}])), + ?FUNCTOR(Socket, gen_udp:close(Socket)), + fun(Socket) -> + Mod:test_send_coap_request( + Socket, + post, + Mod:sprintf(URI, [Port, Epn]), + #coap_content{ + content_format = <<"text/plain">>, + payload = <<", , , , ">> + }, + [], + MsgId + ), + + Checker(Mod:test_recv_coap_response(Socket)) + end + ) + end, + + MakeCheker = fun(Type, Method) -> + fun(Msg) -> + ?assertEqual(Type, emqx_coap_SUITE:get_field(type, Msg)), + ?assertEqual(Method, emqx_coap_SUITE:get_field(method, Msg)) + end + end, + + RightUrl = "coap://127.0.0.1:~b/rd?ep=~ts<=345&lwm2m=1&imei=admin&password=public", + Login(RightUrl, MakeCheker(ack, {ok, created})), + + LeftUrl = "coap://127.0.0.1:~b/rd?ep=~ts<=345&lwm2m=1&imei=bad&password=bad", + Login(LeftUrl, MakeCheker(ack, {error, bad_request})), + + NoInfoUrl = "coap://127.0.0.1:~b/rd?ep=~ts<=345&lwm2m=1", + Login(NoInfoUrl, MakeCheker(ack, {error, bad_request})), + ok. + +-define(SN_CONNACK, 16#05). + +t_case_emqx_sn(_) -> + Mod = emqx_sn_protocol_SUITE, + Login = fun(Username, Password, Expect) -> + RawCfg = emqx_conf:get_raw([gateway, mqttsn], #{}), + NewCfg = RawCfg#{ + <<"clientinfo_override">> => #{ + <<"username">> => Username, + <<"password">> => Password + } + }, + emqx_gateway_conf:update_gateway(mqttsn, NewCfg), + + with_resource( + ?FUNCTOR(gen_udp:open(0, [binary])), + ?FUNCTOR(Socket, gen_udp:close(Socket)), + fun(Socket) -> + Mod:send_connect_msg(Socket, <<"client_id_test1">>), + ?assertEqual(Expect, Mod:receive_response(Socket)) + end + ) + end, + Login(<<"badadmin">>, <<"badpassowrd">>, <<>>), + Login(<<"admin">>, <<"public">>, <<3, ?SN_CONNACK, 0>>), + ok. + +t_case_stomp(_) -> + Mod = emqx_stomp_SUITE, + Login = fun(Username, Password, Checker) -> + Fun = fun(Sock) -> + gen_tcp:send( + Sock, + Mod:serialize( + <<"CONNECT">>, + [ + {<<"accept-version">>, Mod:stomp_ver()}, + {<<"host">>, <<"127.0.0.1:61613">>}, + {<<"login">>, Username}, + {<<"passcode">>, Password}, + {<<"heart-beat">>, <<"1000,2000">>} + ] + ) + ), + {ok, Data} = gen_tcp:recv(Sock, 0), + {ok, Frame, _, _} = Mod:parse(Data), + Checker(Frame) + end, + Mod:with_connection(Fun) + end, + Login( + <<"admin">>, + <<"public">>, + ?FUNCTOR( + Frame, + ?assertEqual(<<"CONNECTED">>, Mod:get_field(command, Frame)) + ) + ), + Login(<<"bad">>, <<"bad">>, fun(Frame) -> + ?assertEqual(<<"ERROR">>, Mod:get_field(command, Frame)), + ?assertEqual(<<"Login Failed: not_authorized">>, Mod:get_field(body, Frame)) + end), + + ok. + +t_case_exproto(_) -> + Mod = emqx_exproto_SUITE, + SvrMod = emqx_exproto_echo_svr, + Svrs = SvrMod:start(), + Login = fun(Username, Password, Expect) -> + with_resource( + ?FUNCTOR(Mod:open(tcp)), + ?FUNCTOR(Sock, Mod:close(Sock)), + fun(Sock) -> + Client = #{ + proto_name => <<"demo">>, + proto_ver => <<"v0.1">>, + clientid => <<"test_client_1">>, + username => Username + }, + + ConnBin = SvrMod:frame_connect(Client, Password), + + Mod:send(Sock, ConnBin), + {ok, Recv} = Mod:recv(Sock, 5000), + C = ?FUNCTOR(Bin, emqx_json:decode(Bin, [return_maps])), + ?assertEqual(C(Expect), C(Recv)) + end + ) + end, + Login(<<"admin">>, <<"public">>, SvrMod:frame_connack(0)), + Login(<<"bad">>, <<"bad">>, SvrMod:frame_connack(1)), + SvrMod:stop(Svrs), + ok. diff --git a/apps/emqx_gateway/test/emqx_gateway_ctx_SUITE.erl b/apps/emqx_gateway/test/emqx_gateway_ctx_SUITE.erl index f0ca75e24..32e155068 100644 --- a/apps/emqx_gateway/test/emqx_gateway_ctx_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_gateway_ctx_SUITE.erl @@ -42,6 +42,7 @@ init_per_suite(Conf) -> Conf. end_per_suite(_Conf) -> + meck:unload(emqx_access_control), ok. %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl index c32bf7b70..39a4ac7aa 100644 --- a/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_lwm2m_SUITE.erl @@ -186,6 +186,12 @@ end_per_testcase(_AllTestCase, Config) -> emqtt:disconnect(?config(emqx_c, Config)), ok = application:stop(emqx_gateway). +default_config() -> + ?CONF_DEFAULT. + +default_port() -> + ?PORT. + %%-------------------------------------------------------------------- %% Cases %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl index cc1625c5e..084249764 100644 --- a/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_sn_protocol_SUITE.erl @@ -117,6 +117,9 @@ restart_mqttsn_with_subs_resume_off() -> #{<<"subs_resume">> => <<"false">>} ). +default_config() -> + ?CONF_DEFAULT. + %%-------------------------------------------------------------------- %% Test cases %%-------------------------------------------------------------------- diff --git a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl index ca61a63a4..5dbcd8df1 100644 --- a/apps/emqx_gateway/test/emqx_stomp_SUITE.erl +++ b/apps/emqx_gateway/test/emqx_stomp_SUITE.erl @@ -61,6 +61,12 @@ end_per_suite(_Cfg) -> emqx_mgmt_api_test_util:end_suite([emqx_gateway]), ok. +default_config() -> + ?CONF_DEFAULT. + +stomp_ver() -> + ?STOMP_VER. + %%-------------------------------------------------------------------- %% Test Cases %%-------------------------------------------------------------------- @@ -843,3 +849,8 @@ parse(Data) -> }, Parser = emqx_stomp_frame:initial_parse_state(ProtoEnv), emqx_stomp_frame:parse(Data, Parser). + +get_field(command, #stomp_frame{command = Command}) -> + Command; +get_field(body, #stomp_frame{body = Body}) -> + Body.