emqx_dashboard_https_SUITE.erl 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2020-2023 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. validate_https(VerifyPeerConf2, MaxConnection, DefaultSSLCert, verify_peer),
  190. ok.
  191. t_bad_certfile(_Config) ->
  192. Conf = #{
  193. dashboard => #{
  194. listeners => #{
  195. https => #{
  196. bind => 18084,
  197. certfile => <<"${EMQX_ETC_DIR}/certs/not_found_cert.pem">>
  198. }
  199. }
  200. }
  201. },
  202. emqx_common_test_helpers:load_config(emqx_dashboard_schema, Conf),
  203. ?assertMatch({error, [?NAME]}, emqx_dashboard:start_listeners()),
  204. ok.
  205. validate_https(Conf, MaxConnection, SSLCert, Verify) ->
  206. emqx_common_test_helpers:load_config(emqx_dashboard_schema, Conf),
  207. emqx_mgmt_api_test_util:init_suite([emqx_management], fun(X) -> X end),
  208. assert_ranch_options(MaxConnection, SSLCert, Verify),
  209. assert_https_request(),
  210. emqx_mgmt_api_test_util:end_suite([emqx_management]).
  211. assert_ranch_options(MaxConnections0, SSLCert, Verify) ->
  212. Middlewares = [emqx_dashboard_middleware, cowboy_router, cowboy_handler],
  213. [
  214. ?NAME,
  215. ranch_ssl,
  216. #{
  217. max_connections := MaxConnections,
  218. num_acceptors := _,
  219. socket_opts := SocketOpts
  220. },
  221. cowboy_tls,
  222. #{
  223. env := #{
  224. dispatch := {persistent_term, ?NAME},
  225. options := #{
  226. name := ?NAME,
  227. protocol := https,
  228. protocol_options := #{proxy_header := false},
  229. security := [#{basicAuth := []}, #{bearerAuth := []}],
  230. swagger_global_spec := _
  231. }
  232. },
  233. middlewares := Middlewares,
  234. proxy_header := false
  235. }
  236. ] = ranch_server:get_listener_start_args(?NAME),
  237. ?assertEqual(MaxConnections0, MaxConnections),
  238. ?assert(lists:member(inet, SocketOpts), SocketOpts),
  239. #{
  240. backlog := 1024,
  241. ciphers := Ciphers,
  242. port := 18084,
  243. send_timeout := 10000,
  244. verify := Verify,
  245. versions := Versions
  246. } = SocketMaps = maps:from_list(SocketOpts -- [inet]),
  247. %% without tlsv1.1 tlsv1
  248. ?assertMatch(['tlsv1.3', 'tlsv1.2'], Versions),
  249. ?assert(Ciphers =/= []),
  250. maps:foreach(
  251. fun(K, ConfVal) ->
  252. case maps:find(K, SocketMaps) of
  253. {ok, File} -> ?assertEqual(naive_env_interpolation(ConfVal), File);
  254. error -> ?assertEqual(<<"">>, ConfVal)
  255. end
  256. end,
  257. SSLCert
  258. ),
  259. ?assertMatch(
  260. #{
  261. env := #{dispatch := {persistent_term, ?NAME}},
  262. middlewares := Middlewares,
  263. proxy_header := false
  264. },
  265. ranch:get_protocol_options(?NAME)
  266. ),
  267. ok.
  268. assert_https_request() ->
  269. Headers = emqx_dashboard_SUITE:auth_header_(),
  270. lists:foreach(
  271. fun(Path) ->
  272. ApiPath = https_api_path([Path]),
  273. ?assertMatch(
  274. {ok, _},
  275. emqx_dashboard_SUITE:request_dashboard(get, ApiPath, Headers)
  276. )
  277. end,
  278. ?OVERVIEWS
  279. ).
  280. https_api_path(Parts) ->
  281. ?HOST_HTTPS ++ filename:join([?BASE_PATH | Parts]).
  282. http_api_path(Parts) ->
  283. ?HOST_HTTP ++ filename:join([?BASE_PATH | Parts]).
  284. naive_env_interpolation(Str0) ->
  285. Str1 = emqx_schema:naive_env_interpolation(Str0),
  286. %% assert all envs are replaced
  287. ?assertNot(lists:member($$, Str1)),
  288. Str1.
  289. default_ssl_cert() ->
  290. #{
  291. cacertfile => <<"${EMQX_ETC_DIR}/certs/cacert.pem">>,
  292. certfile => <<"${EMQX_ETC_DIR}/certs/cert.pem">>,
  293. keyfile => <<"${EMQX_ETC_DIR}/certs/key.pem">>
  294. }.