emqx_dashboard_https_SUITE.erl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2020-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
  3. %%
  4. %% Licensed under the Apache License, Version 2.0 (the "License");
  5. %% you may not use this file except in compliance with the License.
  6. %% You may obtain a copy of the License at
  7. %%
  8. %% http://www.apache.org/licenses/LICENSE-2.0
  9. %%
  10. %% Unless required by applicable law or agreed to in writing, software
  11. %% distributed under the License is distributed on an "AS IS" BASIS,
  12. %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. %% See the License for the specific language governing permissions and
  14. %% limitations under the License.
  15. %%--------------------------------------------------------------------
  16. -module(emqx_dashboard_https_SUITE).
  17. -compile(nowarn_export_all).
  18. -compile(export_all).
  19. -include_lib("eunit/include/eunit.hrl").
  20. -include_lib("snabbkaffe/include/snabbkaffe.hrl").
  21. -define(NAME, 'https:dashboard').
  22. -define(HOST_HTTPS, "https://127.0.0.1:18084").
  23. -define(HOST_HTTP, "http://127.0.0.1:18083").
  24. -define(BASE_PATH, "/api/v5").
  25. -define(OVERVIEWS, [
  26. "alarms",
  27. "banned",
  28. "stats",
  29. "metrics",
  30. "listeners",
  31. "clients",
  32. "subscriptions"
  33. ]).
  34. all() ->
  35. emqx_common_test_helpers:all(?MODULE).
  36. init_per_suite(Config) -> Config.
  37. end_per_suite(_Config) -> emqx_mgmt_api_test_util:end_suite([emqx_management]).
  38. init_per_testcase(_TestCase, Config) -> Config.
  39. end_per_testcase(_TestCase, _Config) -> emqx_mgmt_api_test_util:end_suite([emqx_management]).
  40. t_update_conf(_Config) ->
  41. Conf = #{
  42. dashboard => #{
  43. listeners => #{
  44. https => #{bind => 18084, ssl_options => #{depth => 5}},
  45. http => #{bind => 18083}
  46. }
  47. }
  48. },
  49. emqx_common_test_helpers:load_config(emqx_dashboard_schema, Conf),
  50. emqx_mgmt_api_test_util:init_suite([emqx_management], fun(X) -> X end),
  51. Headers = emqx_dashboard_SUITE:auth_header_(),
  52. {ok, Client1} = emqx_dashboard_SUITE:request_dashboard(
  53. get, https_api_path(["clients"]), Headers
  54. ),
  55. {ok, Client2} = emqx_dashboard_SUITE:request_dashboard(
  56. get, http_api_path(["clients"]), Headers
  57. ),
  58. Raw = emqx:get_raw_config([<<"dashboard">>]),
  59. ?assertEqual(
  60. 5,
  61. emqx_utils_maps:deep_get(
  62. [<<"listeners">>, <<"https">>, <<"ssl_options">>, <<"depth">>], Raw
  63. )
  64. ),
  65. ?assertEqual(Client1, Client2),
  66. ?check_trace(
  67. begin
  68. Raw1 = emqx_utils_maps:deep_put(
  69. [<<"listeners">>, <<"https">>, <<"bind">>], Raw, 0
  70. ),
  71. ?assertMatch({ok, _}, emqx:update_config([<<"dashboard">>], Raw1)),
  72. ?assertEqual(Raw1, emqx:get_raw_config([<<"dashboard">>])),
  73. {ok, _} = ?block_until(#{?snk_kind := regenerate_minirest_dispatch}, 10000),
  74. ok
  75. end,
  76. fun(ok, Trace) ->
  77. %% Don't start new listener, so is empty
  78. ?assertMatch([#{listeners := []}], ?of_kind(regenerate_minirest_dispatch, Trace))
  79. end
  80. ),
  81. {ok, Client3} = emqx_dashboard_SUITE:request_dashboard(
  82. get, http_api_path(["clients"]), Headers
  83. ),
  84. ?assertEqual(Client1, Client3),
  85. ?assertMatch(
  86. {error,
  87. {failed_connect, [
  88. _,
  89. {inet, [inet], econnrefused}
  90. ]}},
  91. emqx_dashboard_SUITE:request_dashboard(get, https_api_path(["clients"]), Headers)
  92. ),
  93. %% reset
  94. ?check_trace(
  95. begin
  96. ?assertMatch({ok, _}, emqx:update_config([<<"dashboard">>], Raw)),
  97. ?assertEqual(Raw, emqx:get_raw_config([<<"dashboard">>])),
  98. {ok, _} = ?block_until(#{?snk_kind := regenerate_minirest_dispatch}, 10000),
  99. ok
  100. end,
  101. fun(ok, Trace) ->
  102. %% start new listener('https:dashboard')
  103. ?assertMatch(
  104. [#{listeners := ['https:dashboard']}], ?of_kind(regenerate_minirest_dispatch, Trace)
  105. )
  106. end
  107. ),
  108. {ok, Client1} = emqx_dashboard_SUITE:request_dashboard(
  109. get, https_api_path(["clients"]), Headers
  110. ),
  111. {ok, Client2} = emqx_dashboard_SUITE:request_dashboard(
  112. get, http_api_path(["clients"]), Headers
  113. ),
  114. emqx_mgmt_api_test_util:end_suite([emqx_management]).
  115. t_default_ssl_cert(_Config) ->
  116. Conf = #{dashboard => #{listeners => #{https => #{bind => 18084}}}},
  117. validate_https(Conf, 512, default_ssl_cert(), verify_none),
  118. ok.
  119. t_compatibility_ssl_cert(_Config) ->
  120. MaxConnection = 1000,
  121. Conf = #{
  122. dashboard => #{
  123. listeners => #{
  124. https => #{
  125. bind => 18084,
  126. cacertfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/cacert.pem">>),
  127. certfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/cert.pem">>),
  128. keyfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/key.pem">>),
  129. max_connections => MaxConnection
  130. }
  131. }
  132. }
  133. },
  134. validate_https(Conf, MaxConnection, default_ssl_cert(), verify_none),
  135. ok.
  136. t_normal_ssl_cert(_Config) ->
  137. MaxConnection = 1024,
  138. Conf = #{
  139. dashboard => #{
  140. listeners => #{
  141. https => #{
  142. bind => 18084,
  143. ssl_options => #{
  144. cacertfile => naive_env_interpolation(
  145. <<"${EMQX_ETC_DIR}/certs/cacert.pem">>
  146. ),
  147. certfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/cert.pem">>),
  148. keyfile => naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/key.pem">>),
  149. depth => 5
  150. },
  151. max_connections => MaxConnection
  152. }
  153. }
  154. }
  155. },
  156. validate_https(Conf, MaxConnection, default_ssl_cert(), verify_none),
  157. ok.
  158. t_verify_cacertfile(_Config) ->
  159. MaxConnection = 1024,
  160. DefaultSSLCert = default_ssl_cert(),
  161. SSLCert = DefaultSSLCert#{cacertfile => <<"">>},
  162. %% default #{verify => verify_none}
  163. Conf = #{
  164. dashboard => #{
  165. listeners => #{
  166. https => #{
  167. bind => 18084,
  168. cacertfile => <<"">>,
  169. max_connections => MaxConnection
  170. }
  171. }
  172. }
  173. },
  174. validate_https(Conf, MaxConnection, SSLCert, verify_none),
  175. %% verify_peer but cacertfile is empty
  176. VerifyPeerConf1 = emqx_utils_maps:deep_put(
  177. [dashboard, listeners, https, verify],
  178. Conf,
  179. verify_peer
  180. ),
  181. emqx_common_test_helpers:load_config(emqx_dashboard_schema, VerifyPeerConf1),
  182. ?assertMatch({error, [?NAME]}, emqx_dashboard:start_listeners()),
  183. %% verify_peer and cacertfile is ok.
  184. VerifyPeerConf2 = emqx_utils_maps:deep_put(
  185. [dashboard, listeners, https, cacertfile],
  186. VerifyPeerConf1,
  187. naive_env_interpolation(<<"${EMQX_ETC_DIR}/certs/cacert.pem">>)
  188. ),
  189. %% we always test client with verify_none and no client cert is sent
  190. %% since the server is configured with verify_peer
  191. %% hence the expected observation on the client side is an error
  192. ErrorReason =
  193. try
  194. validate_https(VerifyPeerConf2, MaxConnection, DefaultSSLCert, verify_peer)
  195. catch
  196. error:{https_client_error, Reason} ->
  197. Reason
  198. end,
  199. %% There seems to be a race-condition causing the return value to vary a bit
  200. case ErrorReason of
  201. socket_closed_remotely ->
  202. ok;
  203. {ssl_error, _SslSock, {tls_alert, {certificate_required, _}}} ->
  204. ok;
  205. Other ->
  206. throw({unexpected, Other})
  207. end.
  208. t_bad_certfile(_Config) ->
  209. Conf = #{
  210. dashboard => #{
  211. listeners => #{
  212. https => #{
  213. bind => 18084,
  214. certfile => <<"${EMQX_ETC_DIR}/certs/not_found_cert.pem">>
  215. }
  216. }
  217. }
  218. },
  219. emqx_common_test_helpers:load_config(emqx_dashboard_schema, Conf),
  220. ?assertMatch({error, [?NAME]}, emqx_dashboard:start_listeners()),
  221. ok.
  222. validate_https(Conf, MaxConnection, SSLCert, Verify) ->
  223. emqx_common_test_helpers:load_config(emqx_dashboard_schema, Conf),
  224. emqx_mgmt_api_test_util:init_suite([emqx_management], fun(X) -> X end),
  225. try
  226. assert_ranch_options(MaxConnection, SSLCert, Verify),
  227. assert_https_request()
  228. after
  229. emqx_mgmt_api_test_util:end_suite([emqx_management])
  230. end.
  231. assert_ranch_options(MaxConnections0, SSLCert, Verify) ->
  232. Middlewares = [emqx_dashboard_middleware, cowboy_router, cowboy_handler],
  233. [
  234. ?NAME,
  235. ranch_ssl,
  236. #{
  237. max_connections := MaxConnections,
  238. num_acceptors := _,
  239. socket_opts := SocketOpts
  240. },
  241. cowboy_tls,
  242. #{
  243. env := #{
  244. dispatch := {persistent_term, ?NAME},
  245. options := #{
  246. name := ?NAME,
  247. protocol := https,
  248. protocol_options := #{proxy_header := false},
  249. security := [#{basicAuth := []}, #{bearerAuth := []}],
  250. swagger_global_spec := _
  251. }
  252. },
  253. middlewares := Middlewares,
  254. proxy_header := false
  255. }
  256. ] = ranch_server:get_listener_start_args(?NAME),
  257. ?assertEqual(MaxConnections0, MaxConnections),
  258. ?assert(lists:member(inet, SocketOpts), SocketOpts),
  259. #{
  260. backlog := 1024,
  261. ciphers := Ciphers,
  262. port := 18084,
  263. send_timeout := 10000,
  264. verify := Verify,
  265. versions := Versions
  266. } = SocketMaps = maps:from_list(SocketOpts -- [inet]),
  267. %% without tlsv1.1 tlsv1
  268. ?assertMatch(['tlsv1.3', 'tlsv1.2'], Versions),
  269. ?assert(Ciphers =/= []),
  270. maps:foreach(
  271. fun(K, ConfVal) ->
  272. case maps:find(K, SocketMaps) of
  273. {ok, File} -> ?assertEqual(naive_env_interpolation(ConfVal), File);
  274. error -> ?assertEqual(<<"">>, ConfVal)
  275. end
  276. end,
  277. SSLCert
  278. ),
  279. ?assertMatch(
  280. #{
  281. env := #{dispatch := {persistent_term, ?NAME}},
  282. middlewares := Middlewares,
  283. proxy_header := false
  284. },
  285. ranch:get_protocol_options(?NAME)
  286. ),
  287. ok.
  288. assert_https_request() ->
  289. Headers = emqx_dashboard_SUITE:auth_header_(),
  290. lists:foreach(
  291. fun(Path) ->
  292. ApiPath = https_api_path([Path]),
  293. case emqx_dashboard_SUITE:request_dashboard(get, ApiPath, Headers) of
  294. {ok, _} -> ok;
  295. {error, Reason} -> error({https_client_error, Reason})
  296. end
  297. end,
  298. ?OVERVIEWS
  299. ).
  300. https_api_path(Parts) ->
  301. ?HOST_HTTPS ++ filename:join([?BASE_PATH | Parts]).
  302. http_api_path(Parts) ->
  303. ?HOST_HTTP ++ filename:join([?BASE_PATH | Parts]).
  304. naive_env_interpolation(Str0) ->
  305. Str1 = emqx_schema:naive_env_interpolation(Str0),
  306. %% assert all envs are replaced
  307. ?assertNot(lists:member($$, Str1)),
  308. Str1.
  309. default_ssl_cert() ->
  310. #{
  311. cacertfile => <<"${EMQX_ETC_DIR}/certs/cacert.pem">>,
  312. certfile => <<"${EMQX_ETC_DIR}/certs/cert.pem">>,
  313. keyfile => <<"${EMQX_ETC_DIR}/certs/key.pem">>
  314. }.