emqx_gateway_api_clients.erl 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2021-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_gateway_api_clients).
  17. -include("emqx_gateway_http.hrl").
  18. -include_lib("typerefl/include/types.hrl").
  19. -include_lib("hocon/include/hoconsc.hrl").
  20. -include_lib("emqx/include/logger.hrl").
  21. -behaviour(minirest_api).
  22. -import(hoconsc, [mk/2, ref/1, ref/2]).
  23. -import(
  24. emqx_gateway_http,
  25. [
  26. return_http_error/2,
  27. with_gateway/2
  28. ]
  29. ).
  30. %% minirest/dashboard_swagger behaviour callbacks
  31. -export([
  32. api_spec/0,
  33. paths/0,
  34. schema/1
  35. ]).
  36. -export([
  37. roots/0,
  38. fields/1,
  39. namespace/0
  40. ]).
  41. %% http handlers
  42. -export([
  43. clients/2,
  44. clients_insta/2,
  45. subscriptions/2
  46. ]).
  47. %% internal exports (for client query)
  48. -export([
  49. qs2ms/2,
  50. run_fuzzy_filter/2,
  51. format_channel_info/1,
  52. format_channel_info/2,
  53. client_info_mountpoint/1
  54. ]).
  55. -define(TAGS, [<<"Gateway Clients">>]).
  56. %%--------------------------------------------------------------------
  57. %% APIs
  58. %%--------------------------------------------------------------------
  59. api_spec() ->
  60. emqx_dashboard_swagger:spec(?MODULE, #{check_schema => true, translate_body => true}).
  61. paths() ->
  62. [
  63. "/gateways/:name/clients",
  64. "/gateways/:name/clients/:clientid",
  65. "/gateways/:name/clients/:clientid/subscriptions",
  66. "/gateways/:name/clients/:clientid/subscriptions/:topic"
  67. ].
  68. -define(CLIENT_QSCHEMA, [
  69. {<<"node">>, atom},
  70. {<<"clientid">>, binary},
  71. {<<"username">>, binary},
  72. {<<"ip_address">>, ip},
  73. {<<"conn_state">>, atom},
  74. {<<"clean_start">>, atom},
  75. {<<"proto_ver">>, binary},
  76. {<<"like_clientid">>, binary},
  77. {<<"like_username">>, binary},
  78. {<<"gte_created_at">>, timestamp},
  79. {<<"lte_created_at">>, timestamp},
  80. {<<"gte_connected_at">>, timestamp},
  81. {<<"lte_connected_at">>, timestamp},
  82. %% special keys for lwm2m protocol
  83. {<<"endpoint_name">>, binary},
  84. {<<"like_endpoint_name">>, binary},
  85. {<<"gte_lifetime">>, integer},
  86. {<<"lte_lifetime">>, integer}
  87. ]).
  88. clients(get, #{
  89. bindings := #{name := Name0},
  90. query_string := QString
  91. }) ->
  92. Fun = fun(GwName, _) ->
  93. TabName = emqx_gateway_cm:tabname(info, GwName),
  94. Result =
  95. case maps:get(<<"node">>, QString, undefined) of
  96. undefined ->
  97. emqx_mgmt_api:cluster_query(
  98. TabName,
  99. QString,
  100. ?CLIENT_QSCHEMA,
  101. fun ?MODULE:qs2ms/2,
  102. fun ?MODULE:format_channel_info/2
  103. );
  104. Node0 ->
  105. case emqx_utils:safe_to_existing_atom(Node0) of
  106. {ok, Node1} ->
  107. QStringWithoutNode = maps:without([<<"node">>], QString),
  108. emqx_mgmt_api:node_query(
  109. Node1,
  110. TabName,
  111. QStringWithoutNode,
  112. ?CLIENT_QSCHEMA,
  113. fun ?MODULE:qs2ms/2,
  114. fun ?MODULE:format_channel_info/2
  115. );
  116. {error, _} ->
  117. {error, Node0, {badrpc, <<"invalid node">>}}
  118. end
  119. end,
  120. case Result of
  121. {error, page_limit_invalid} ->
  122. {400, #{code => <<"INVALID_PARAMETER">>, message => <<"page_limit_invalid">>}};
  123. {error, Node, Error} ->
  124. Message = list_to_binary(
  125. io_lib:format("bad rpc call ~p, Reason ~p", [Node, Error])
  126. ),
  127. {500, #{code => <<"NODE_DOWN">>, message => Message}};
  128. Response ->
  129. {200, Response}
  130. end
  131. end,
  132. with_gateway(Name0, Fun).
  133. clients_insta(get, #{
  134. bindings := #{
  135. name := Name0,
  136. clientid := ClientId
  137. }
  138. }) ->
  139. with_gateway(Name0, fun(GwName, _) ->
  140. case
  141. emqx_gateway_http:lookup_client(
  142. GwName,
  143. ClientId,
  144. {?MODULE, format_channel_info}
  145. )
  146. of
  147. [ClientInfo] ->
  148. {200, ClientInfo};
  149. [ClientInfo | _More] ->
  150. ?SLOG(warning, #{
  151. msg => "more_than_one_channel_found",
  152. clientid => ClientId
  153. }),
  154. {200, ClientInfo};
  155. [] ->
  156. return_http_error(404, "Client not found")
  157. end
  158. end);
  159. clients_insta(delete, #{
  160. bindings := #{
  161. name := Name0,
  162. clientid := ClientId
  163. }
  164. }) ->
  165. with_gateway(Name0, fun(GwName, _) ->
  166. case emqx_gateway_http:kickout_client(GwName, ClientId) of
  167. {error, not_found} ->
  168. return_http_error(404, "Client not found");
  169. _ ->
  170. {204}
  171. end
  172. end).
  173. %% List the established subscriptions with mountpoint
  174. subscriptions(get, #{
  175. bindings := #{
  176. name := Name0,
  177. clientid := ClientId
  178. }
  179. }) ->
  180. with_gateway(Name0, fun(GwName, _) ->
  181. case emqx_gateway_http:list_client_subscriptions(GwName, ClientId) of
  182. {error, not_found} ->
  183. return_http_error(404, "client process not found");
  184. {error, ignored} ->
  185. return_http_error(
  186. 400, "get subscriptions failed: unsupported"
  187. );
  188. {error, Reason} ->
  189. return_http_error(400, Reason);
  190. {ok, Subs} ->
  191. {200, Subs}
  192. end
  193. end);
  194. %% Create the subscription without mountpoint
  195. subscriptions(post, #{
  196. bindings := #{
  197. name := Name0,
  198. clientid := ClientId
  199. },
  200. body := Body
  201. }) ->
  202. with_gateway(Name0, fun(GwName, _) ->
  203. case {maps:get(<<"topic">>, Body, undefined), subopts(Body)} of
  204. {undefined, _} ->
  205. return_http_error(400, "Miss topic property");
  206. {Topic, SubOpts} ->
  207. case
  208. emqx_gateway_http:client_subscribe(
  209. GwName, ClientId, Topic, SubOpts
  210. )
  211. of
  212. {error, not_found} ->
  213. return_http_error(
  214. 404, "client process not found"
  215. );
  216. {error, ignored} ->
  217. return_http_error(
  218. 400, "subscribe failed: unsupported"
  219. );
  220. {error, Reason} ->
  221. return_http_error(400, Reason);
  222. {ok, {NTopic, NSubOpts}} ->
  223. {201, maps:merge(NSubOpts, #{topic => NTopic})}
  224. end
  225. end
  226. end);
  227. %% Remove the subscription without mountpoint
  228. subscriptions(delete, #{
  229. bindings := #{
  230. name := Name0,
  231. clientid := ClientId,
  232. topic := Topic
  233. }
  234. }) ->
  235. with_gateway(Name0, fun(GwName, _) ->
  236. case lookup_topic(GwName, ClientId, Topic) of
  237. {ok, _} ->
  238. case emqx_gateway_http:client_unsubscribe(GwName, ClientId, Topic) of
  239. {error, ignored} ->
  240. return_http_error(
  241. 400, "unsubscribe failed: unsupported"
  242. );
  243. _ ->
  244. {204}
  245. end;
  246. {error, not_found} ->
  247. return_http_error(404, "Resource not found")
  248. end
  249. end).
  250. %%--------------------------------------------------------------------
  251. %% Utils
  252. subopts(Req) ->
  253. SubOpts = #{
  254. qos => maps:get(<<"qos">>, Req, 0),
  255. rap => maps:get(<<"rap">>, Req, 0),
  256. nl => maps:get(<<"nl">>, Req, 0),
  257. rh => maps:get(<<"rh">>, Req, 1)
  258. },
  259. SubProps = extra_sub_props(maps:get(<<"sub_props">>, Req, #{})),
  260. case maps:size(SubProps) of
  261. 0 -> SubOpts;
  262. _ -> maps:put(sub_props, SubProps, SubOpts)
  263. end.
  264. extra_sub_props(Props) ->
  265. maps:filter(
  266. fun(_, V) -> V =/= undefined end,
  267. #{subid => maps:get(<<"subid">>, Props, undefined)}
  268. ).
  269. lookup_topic(GwName, ClientId, Topic) ->
  270. Mountpoints = emqx_gateway_http:lookup_client(
  271. GwName,
  272. ClientId,
  273. {?MODULE, client_info_mountpoint}
  274. ),
  275. case emqx_gateway_http:list_client_subscriptions(GwName, ClientId) of
  276. {ok, Subscriptions} ->
  277. case
  278. [
  279. S
  280. || S = #{topic := Topic0} <- Subscriptions,
  281. Mountpoint <- Mountpoints,
  282. Topic0 == emqx_mountpoint:mount(Mountpoint, Topic)
  283. ]
  284. of
  285. [] ->
  286. {error, not_found};
  287. Filtered ->
  288. {ok, Filtered}
  289. end;
  290. Error ->
  291. Error
  292. end.
  293. client_info_mountpoint({_, #{clientinfo := #{mountpoint := Mountpoint}}, _}) ->
  294. Mountpoint.
  295. %%--------------------------------------------------------------------
  296. %% QueryString to MatchSpec
  297. -spec qs2ms(atom(), {list(), list()}) -> emqx_mgmt_api:match_spec_and_filter().
  298. qs2ms(_Tab, {Qs, Fuzzy}) ->
  299. #{match_spec => qs2ms(Qs), fuzzy_fun => fuzzy_filter_fun(Fuzzy)}.
  300. qs2ms(Qs) ->
  301. {MtchHead, Conds} = qs2ms(Qs, 2, {#{}, []}),
  302. [{{'$1', MtchHead, '_'}, Conds, ['$_']}].
  303. qs2ms([], _, {MtchHead, Conds}) ->
  304. {MtchHead, lists:reverse(Conds)};
  305. qs2ms([{Key, '=:=', Value} | Rest], N, {MtchHead, Conds}) ->
  306. NMtchHead = emqx_mgmt_util:merge_maps(MtchHead, ms(Key, Value)),
  307. qs2ms(Rest, N, {NMtchHead, Conds});
  308. qs2ms([Qs | Rest], N, {MtchHead, Conds}) ->
  309. Holder = binary_to_atom(
  310. iolist_to_binary(["$", integer_to_list(N)]), utf8
  311. ),
  312. NMtchHead = emqx_mgmt_util:merge_maps(
  313. MtchHead, ms(element(1, Qs), Holder)
  314. ),
  315. NConds = put_conds(Qs, Holder, Conds),
  316. qs2ms(Rest, N + 1, {NMtchHead, NConds}).
  317. put_conds({_, Op, V}, Holder, Conds) ->
  318. [{Op, Holder, V} | Conds];
  319. put_conds({_, Op1, V1, Op2, V2}, Holder, Conds) ->
  320. [
  321. {Op2, Holder, V2},
  322. {Op1, Holder, V1}
  323. | Conds
  324. ].
  325. ms(clientid, X) ->
  326. #{clientinfo => #{clientid => X}};
  327. ms(username, X) ->
  328. #{clientinfo => #{username => X}};
  329. ms(ip_address, X) ->
  330. #{clientinfo => #{peerhost => X}};
  331. ms(conn_state, X) ->
  332. #{conn_state => X};
  333. ms(clean_start, X) ->
  334. #{conninfo => #{clean_start => X}};
  335. ms(proto_ver, X) ->
  336. #{conninfo => #{proto_ver => X}};
  337. ms(connected_at, X) ->
  338. #{conninfo => #{connected_at => X}};
  339. ms(created_at, X) ->
  340. #{session => #{created_at => X}};
  341. %% lwm2m fields
  342. ms(endpoint_name, X) ->
  343. #{clientinfo => #{endpoint_name => X}};
  344. ms(lifetime, X) ->
  345. #{clientinfo => #{lifetime => X}}.
  346. %%--------------------------------------------------------------------
  347. %% Fuzzy filter funcs
  348. fuzzy_filter_fun([]) ->
  349. undefined;
  350. fuzzy_filter_fun(Fuzzy) ->
  351. {fun ?MODULE:run_fuzzy_filter/2, [Fuzzy]}.
  352. run_fuzzy_filter(_, []) ->
  353. true;
  354. run_fuzzy_filter(
  355. E = {_, #{clientinfo := ClientInfo}, _},
  356. [{Key, like, SubStr} | Fuzzy]
  357. ) ->
  358. Val =
  359. case maps:get(Key, ClientInfo, <<>>) of
  360. undefined -> <<>>;
  361. V -> V
  362. end,
  363. binary:match(Val, SubStr) /= nomatch andalso run_fuzzy_filter(E, Fuzzy).
  364. %%--------------------------------------------------------------------
  365. %% format funcs
  366. format_channel_info(ChannInfo) ->
  367. format_channel_info(node(), ChannInfo).
  368. format_channel_info(WhichNode, {_, Infos, Stats} = R) ->
  369. Node = maps:get(node, Infos, WhichNode),
  370. ClientInfo = maps:get(clientinfo, Infos, #{}),
  371. ConnInfo = maps:get(conninfo, Infos, #{}),
  372. SessInfo = maps:get(session, Infos, #{}),
  373. FetchX = [
  374. {node, ClientInfo, Node},
  375. {clientid, ClientInfo},
  376. {username, ClientInfo},
  377. {mountpoint, ClientInfo},
  378. {proto_name, ConnInfo},
  379. {proto_ver, ConnInfo},
  380. {ip_address, {peername, ConnInfo, fun peer_to_binary_addr/1}},
  381. {port, {peername, ConnInfo, fun peer_to_port/1}},
  382. {is_bridge, ClientInfo, false},
  383. {connected_at, {connected_at, ConnInfo, fun emqx_utils_calendar:epoch_to_rfc3339/1}},
  384. {disconnected_at, {disconnected_at, ConnInfo, fun emqx_utils_calendar:epoch_to_rfc3339/1}},
  385. {connected, {conn_state, Infos, fun conn_state_to_connected/1}},
  386. {keepalive, ClientInfo, 0},
  387. {clean_start, ConnInfo, true},
  388. {expiry_interval, ConnInfo, 0},
  389. {created_at, {created_at, SessInfo, fun emqx_utils_calendar:epoch_to_rfc3339/1}},
  390. {subscriptions_cnt, Stats, 0},
  391. {subscriptions_max, Stats, infinity},
  392. {inflight_cnt, Stats, 0},
  393. {inflight_max, Stats, infinity},
  394. {mqueue_len, Stats, 0},
  395. {mqueue_max, Stats, infinity},
  396. {mqueue_dropped, Stats, 0},
  397. {awaiting_rel_cnt, Stats, 0},
  398. {awaiting_rel_max, Stats, infinity},
  399. {recv_oct, Stats, 0},
  400. {recv_cnt, Stats, 0},
  401. {recv_pkt, Stats, 0},
  402. {recv_msg, Stats, 0},
  403. {send_oct, Stats, 0},
  404. {send_cnt, Stats, 0},
  405. {send_pkt, Stats, 0},
  406. {send_msg, Stats, 0},
  407. {mailbox_len, Stats, 0},
  408. {heap_size, Stats, 0},
  409. {reductions, Stats, 0}
  410. ],
  411. eval(FetchX ++ extra_fields(R)).
  412. extra_fields({_, Infos, _Stats} = R) ->
  413. extra_fields(
  414. maps:get(protocol, maps:get(clientinfo, Infos)),
  415. R
  416. ).
  417. extra_fields(lwm2m, {_, Infos, _Stats}) ->
  418. ClientInfo = maps:get(clientinfo, Infos, #{}),
  419. [
  420. {endpoint_name, ClientInfo},
  421. {lifetime, ClientInfo}
  422. ];
  423. extra_fields(_, _) ->
  424. [].
  425. eval(Ls) ->
  426. eval(Ls, #{}).
  427. eval([], AccMap) ->
  428. AccMap;
  429. eval([{K, Vx} | More], AccMap) ->
  430. case valuex_get(K, Vx) of
  431. undefined -> eval(More, AccMap#{K => null});
  432. Value -> eval(More, AccMap#{K => Value})
  433. end;
  434. eval([{K, Vx, Default} | More], AccMap) ->
  435. case valuex_get(K, Vx) of
  436. undefined -> eval(More, AccMap#{K => Default});
  437. Value -> eval(More, AccMap#{K => Value})
  438. end.
  439. valuex_get(K, Vx) when is_map(Vx); is_list(Vx) ->
  440. key_get(K, Vx);
  441. valuex_get(_K, {InKey, Obj}) when is_map(Obj); is_list(Obj) ->
  442. key_get(InKey, Obj);
  443. valuex_get(_K, {InKey, Obj, MappingFun}) when is_map(Obj); is_list(Obj) ->
  444. case key_get(InKey, Obj) of
  445. undefined -> undefined;
  446. Val -> MappingFun(Val)
  447. end.
  448. key_get(K, M) when is_map(M) ->
  449. maps:get(K, M, undefined);
  450. key_get(K, L) when is_list(L) ->
  451. proplists:get_value(K, L).
  452. -spec peer_to_binary_addr(emqx_types:peername()) -> binary().
  453. peer_to_binary_addr({Addr, _}) ->
  454. list_to_binary(inet:ntoa(Addr)).
  455. -spec peer_to_port(emqx_types:peername()) -> inet:port_number().
  456. peer_to_port({_, Port}) ->
  457. Port.
  458. conn_state_to_connected(connected) -> true;
  459. conn_state_to_connected(_) -> false.
  460. %%--------------------------------------------------------------------
  461. %% Swagger defines
  462. %%--------------------------------------------------------------------
  463. schema("/gateways/:name/clients") ->
  464. #{
  465. 'operationId' => clients,
  466. get =>
  467. #{
  468. tags => ?TAGS,
  469. desc => ?DESC(list_clients),
  470. summary => <<"List gateway's clients">>,
  471. parameters => params_client_query(),
  472. responses =>
  473. ?STANDARD_RESP(#{
  474. 200 => [
  475. {data, schema_client_list()},
  476. {meta, mk(hoconsc:ref(emqx_dashboard_swagger, meta), #{})}
  477. ]
  478. })
  479. }
  480. };
  481. schema("/gateways/:name/clients/:clientid") ->
  482. #{
  483. 'operationId' => clients_insta,
  484. get =>
  485. #{
  486. tags => ?TAGS,
  487. desc => ?DESC(get_client),
  488. summary => <<"Get client info">>,
  489. parameters => params_client_insta(),
  490. responses =>
  491. ?STANDARD_RESP(#{200 => schema_client()})
  492. },
  493. delete =>
  494. #{
  495. tags => ?TAGS,
  496. desc => ?DESC(kick_client),
  497. summary => <<"Kick out client">>,
  498. parameters => params_client_insta(),
  499. responses =>
  500. ?STANDARD_RESP(#{204 => <<"Kicked">>})
  501. }
  502. };
  503. schema("/gateways/:name/clients/:clientid/subscriptions") ->
  504. #{
  505. 'operationId' => subscriptions,
  506. get =>
  507. #{
  508. tags => ?TAGS,
  509. desc => ?DESC(list_subscriptions),
  510. summary => <<"List client's subscription">>,
  511. parameters => params_client_insta(),
  512. responses =>
  513. ?STANDARD_RESP(
  514. #{
  515. 200 => emqx_dashboard_swagger:schema_with_examples(
  516. hoconsc:array(ref(subscription)),
  517. examples_subscription_list()
  518. )
  519. }
  520. )
  521. },
  522. post =>
  523. #{
  524. tags => ?TAGS,
  525. desc => ?DESC(add_subscription),
  526. summary => <<"Add subscription for client">>,
  527. parameters => params_client_insta(),
  528. 'requestBody' => emqx_dashboard_swagger:schema_with_examples(
  529. ref(subscription),
  530. examples_subscription()
  531. ),
  532. responses =>
  533. ?STANDARD_RESP(
  534. #{
  535. 201 => emqx_dashboard_swagger:schema_with_examples(
  536. ref(subscription),
  537. examples_subscription()
  538. )
  539. }
  540. )
  541. }
  542. };
  543. schema("/gateways/:name/clients/:clientid/subscriptions/:topic") ->
  544. #{
  545. 'operationId' => subscriptions,
  546. delete =>
  547. #{
  548. tags => ?TAGS,
  549. desc => ?DESC(delete_subscription),
  550. summary => <<"Delete client's subscription">>,
  551. parameters => params_topic_name_in_path() ++ params_client_insta(),
  552. responses =>
  553. ?STANDARD_RESP(#{204 => <<"Unsubscribed">>})
  554. }
  555. }.
  556. params_client_query() ->
  557. params_gateway_name_in_path() ++
  558. params_client_searching_in_qs() ++
  559. params_paging().
  560. params_client_insta() ->
  561. params_clientid_in_path() ++
  562. params_gateway_name_in_path().
  563. params_client_searching_in_qs() ->
  564. M = #{in => query, required => false, example => <<"">>},
  565. [
  566. {node,
  567. mk(
  568. binary(),
  569. M#{desc => ?DESC(param_node)}
  570. )},
  571. {clientid,
  572. mk(
  573. binary(),
  574. M#{desc => ?DESC(param_clientid)}
  575. )},
  576. {username,
  577. mk(
  578. binary(),
  579. M#{desc => ?DESC(param_username)}
  580. )},
  581. {ip_address,
  582. mk(
  583. binary(),
  584. M#{desc => ?DESC(param_ip_address)}
  585. )},
  586. {conn_state,
  587. mk(
  588. binary(),
  589. M#{desc => ?DESC(param_conn_state)}
  590. )},
  591. {proto_ver,
  592. mk(
  593. binary(),
  594. M#{desc => ?DESC(param_proto_ver)}
  595. )},
  596. {clean_start,
  597. mk(
  598. boolean(),
  599. M#{desc => ?DESC(param_clean_start)}
  600. )},
  601. {like_clientid,
  602. mk(
  603. binary(),
  604. M#{desc => ?DESC(param_like_clientid)}
  605. )},
  606. {like_username,
  607. mk(
  608. binary(),
  609. M#{desc => ?DESC(param_like_username)}
  610. )},
  611. {gte_created_at,
  612. mk(
  613. emqx_utils_calendar:epoch_millisecond(),
  614. M#{
  615. desc => ?DESC(param_gte_created_at)
  616. }
  617. )},
  618. {lte_created_at,
  619. mk(
  620. emqx_utils_calendar:epoch_millisecond(),
  621. M#{
  622. desc => ?DESC(param_lte_created_at)
  623. }
  624. )},
  625. {gte_connected_at,
  626. mk(
  627. emqx_utils_calendar:epoch_millisecond(),
  628. M#{
  629. desc => ?DESC(param_gte_connected_at)
  630. }
  631. )},
  632. {lte_connected_at,
  633. mk(
  634. emqx_utils_calendar:epoch_millisecond(),
  635. M#{
  636. desc => ?DESC(param_lte_connected_at)
  637. }
  638. )},
  639. {endpoint_name,
  640. mk(
  641. binary(),
  642. M#{desc => ?DESC(param_endpoint_name)}
  643. )},
  644. {like_endpoint_name,
  645. mk(
  646. binary(),
  647. M#{desc => ?DESC(param_like_endpoint_name)}
  648. )},
  649. {gte_lifetime,
  650. mk(
  651. binary(),
  652. M#{
  653. desc => ?DESC(param_gte_lifetime)
  654. }
  655. )},
  656. {lte_lifetime,
  657. mk(
  658. binary(),
  659. M#{
  660. desc => ?DESC(param_lte_lifetime)
  661. }
  662. )}
  663. ].
  664. params_paging() ->
  665. emqx_dashboard_swagger:fields(page) ++
  666. emqx_dashboard_swagger:fields(limit).
  667. params_gateway_name_in_path() ->
  668. [
  669. {name,
  670. mk(
  671. hoconsc:enum(emqx_gateway_schema:gateway_names()),
  672. #{
  673. in => path,
  674. desc => ?DESC(emqx_gateway_api, gateway_name)
  675. }
  676. )}
  677. ].
  678. params_clientid_in_path() ->
  679. [
  680. {clientid,
  681. mk(
  682. binary(),
  683. #{
  684. in => path,
  685. desc => ?DESC(clientid)
  686. }
  687. )}
  688. ].
  689. params_topic_name_in_path() ->
  690. [
  691. {topic,
  692. mk(
  693. binary(),
  694. #{
  695. in => path,
  696. desc => ?DESC(topic)
  697. }
  698. )}
  699. ].
  700. %%--------------------------------------------------------------------
  701. %% schemas
  702. schema_client_list() ->
  703. emqx_dashboard_swagger:schema_with_examples(
  704. hoconsc:union([
  705. hoconsc:array(ref(?MODULE, stomp_client)),
  706. hoconsc:array(ref(?MODULE, mqttsn_client)),
  707. hoconsc:array(ref(?MODULE, coap_client)),
  708. hoconsc:array(ref(?MODULE, lwm2m_client)),
  709. hoconsc:array(ref(?MODULE, exproto_client))
  710. ]),
  711. examples_client_list()
  712. ).
  713. schema_client() ->
  714. emqx_dashboard_swagger:schema_with_examples(
  715. hoconsc:union([
  716. ref(?MODULE, stomp_client),
  717. ref(?MODULE, mqttsn_client),
  718. ref(?MODULE, coap_client),
  719. ref(?MODULE, lwm2m_client),
  720. ref(?MODULE, exproto_client)
  721. ]),
  722. examples_client()
  723. ).
  724. namespace() -> undefined.
  725. roots() ->
  726. [
  727. stomp_client,
  728. mqttsn_client,
  729. coap_client,
  730. lwm2m_client,
  731. exproto_client,
  732. subscription
  733. ].
  734. fields(stomp_client) ->
  735. common_client_props();
  736. fields(mqttsn_client) ->
  737. common_client_props();
  738. fields(coap_client) ->
  739. common_client_props();
  740. fields(lwm2m_client) ->
  741. [
  742. {endpoint_name,
  743. mk(
  744. binary(),
  745. #{desc => ?DESC(endpoint_name)}
  746. )},
  747. {lifetime,
  748. mk(
  749. integer(),
  750. #{desc => ?DESC(lifetime)}
  751. )}
  752. ] ++ common_client_props();
  753. fields(exproto_client) ->
  754. common_client_props();
  755. fields(subscription) ->
  756. [
  757. {topic,
  758. mk(
  759. binary(),
  760. #{desc => ?DESC(topic)}
  761. )},
  762. {qos,
  763. mk(
  764. integer(),
  765. #{desc => ?DESC(qos)}
  766. )},
  767. {nl,
  768. %% FIXME: why not boolean?
  769. mk(
  770. integer(),
  771. #{desc => ?DESC(nl)}
  772. )},
  773. {rap,
  774. mk(
  775. integer(),
  776. #{desc => ?DESC(rap)}
  777. )},
  778. {rh,
  779. mk(
  780. integer(),
  781. #{desc => ?DESC(rh)}
  782. )},
  783. {sub_props,
  784. mk(
  785. ref(extra_sub_props),
  786. #{desc => ?DESC(sub_props)}
  787. )}
  788. ];
  789. fields(extra_sub_props) ->
  790. [
  791. {subid,
  792. mk(
  793. binary(),
  794. #{
  795. desc => ?DESC(subid)
  796. }
  797. )}
  798. ].
  799. common_client_props() ->
  800. [
  801. {node,
  802. mk(
  803. binary(),
  804. #{
  805. desc => ?DESC(node)
  806. }
  807. )},
  808. {clientid,
  809. mk(
  810. binary(),
  811. #{desc => ?DESC(clientid)}
  812. )},
  813. {username,
  814. mk(
  815. binary(),
  816. #{desc => ?DESC(username)}
  817. )},
  818. {mountpoint,
  819. mk(
  820. binary(),
  821. #{desc => ?DESC(mountpoint)}
  822. )},
  823. {proto_name,
  824. mk(
  825. binary(),
  826. #{desc => ?DESC(proto_name)}
  827. )},
  828. {proto_ver,
  829. mk(
  830. binary(),
  831. #{desc => ?DESC(proto_ver)}
  832. )},
  833. {ip_address,
  834. mk(
  835. binary(),
  836. #{desc => ?DESC(ip_address)}
  837. )},
  838. {port,
  839. mk(
  840. integer(),
  841. #{desc => ?DESC(port)}
  842. )},
  843. {is_bridge,
  844. mk(
  845. boolean(),
  846. #{
  847. desc => ?DESC(is_bridge)
  848. }
  849. )},
  850. {connected_at,
  851. mk(
  852. emqx_utils_calendar:epoch_millisecond(),
  853. #{desc => ?DESC(connected_at)}
  854. )},
  855. {disconnected_at,
  856. mk(
  857. emqx_utils_calendar:epoch_millisecond(),
  858. #{
  859. desc => ?DESC(disconnected_at)
  860. }
  861. )},
  862. {connected,
  863. mk(
  864. boolean(),
  865. #{desc => ?DESC(connected)}
  866. )},
  867. %% FIXME: the will_msg attribute is not a general attribute
  868. %% for every protocol. But it should be returned to frontend if someone
  869. %% want it
  870. %%
  871. %, {will_msg,
  872. % mk(binary(),
  873. % #{ desc => ?DESC(will_msg)})}
  874. {keepalive,
  875. mk(
  876. integer(),
  877. #{desc => ?DESC(keepalive)}
  878. )},
  879. {clean_start,
  880. mk(
  881. boolean(),
  882. #{
  883. desc => ?DESC(clean_start)
  884. }
  885. )},
  886. {expiry_interval,
  887. mk(
  888. integer(),
  889. #{
  890. desc => ?DESC(expiry_interval)
  891. }
  892. )},
  893. {created_at,
  894. mk(
  895. emqx_utils_calendar:epoch_millisecond(),
  896. #{desc => ?DESC(created_at)}
  897. )},
  898. {subscriptions_cnt,
  899. mk(
  900. integer(),
  901. #{
  902. desc => ?DESC(subscriptions_cnt)
  903. }
  904. )},
  905. {subscriptions_max,
  906. mk(
  907. integer(),
  908. #{
  909. desc => ?DESC(subscriptions_max)
  910. }
  911. )},
  912. {inflight_cnt,
  913. mk(
  914. integer(),
  915. #{desc => ?DESC(inflight_cnt)}
  916. )},
  917. {inflight_max,
  918. mk(
  919. integer(),
  920. #{desc => ?DESC(inflight_max)}
  921. )},
  922. {mqueue_len,
  923. mk(
  924. integer(),
  925. #{desc => ?DESC(mqueue_len)}
  926. )},
  927. {mqueue_max,
  928. mk(
  929. integer(),
  930. #{desc => ?DESC(mqueue_max)}
  931. )},
  932. {mqueue_dropped,
  933. mk(
  934. integer(),
  935. #{
  936. desc => ?DESC(mqueue_dropped)
  937. }
  938. )},
  939. {awaiting_rel_cnt,
  940. mk(
  941. integer(),
  942. %% FIXME: PUBREC ??
  943. #{desc => ?DESC(awaiting_rel_cnt)}
  944. )},
  945. {awaiting_rel_max,
  946. mk(
  947. integer(),
  948. #{
  949. desc => ?DESC(awaiting_rel_max)
  950. }
  951. )},
  952. {recv_oct,
  953. mk(
  954. integer(),
  955. #{desc => ?DESC(recv_oct)}
  956. )},
  957. {recv_cnt,
  958. mk(
  959. integer(),
  960. #{desc => ?DESC(recv_cnt)}
  961. )},
  962. {recv_pkt,
  963. mk(
  964. integer(),
  965. #{desc => ?DESC(recv_pkt)}
  966. )},
  967. {recv_msg,
  968. mk(
  969. integer(),
  970. #{desc => ?DESC(recv_msg)}
  971. )},
  972. {send_oct,
  973. mk(
  974. integer(),
  975. #{desc => ?DESC(send_oct)}
  976. )},
  977. {send_cnt,
  978. mk(
  979. integer(),
  980. #{desc => ?DESC(send_cnt)}
  981. )},
  982. {send_pkt,
  983. mk(
  984. integer(),
  985. #{desc => ?DESC(send_pkt)}
  986. )},
  987. {send_msg,
  988. mk(
  989. integer(),
  990. #{desc => ?DESC(send_msg)}
  991. )},
  992. {mailbox_len,
  993. mk(
  994. integer(),
  995. #{desc => ?DESC(mailbox_len)}
  996. )},
  997. {heap_size,
  998. mk(
  999. integer(),
  1000. #{desc => ?DESC(heap_size)}
  1001. )},
  1002. {reductions,
  1003. mk(
  1004. integer(),
  1005. #{desc => ?DESC(reductions)}
  1006. )}
  1007. ].
  1008. %%--------------------------------------------------------------------
  1009. %% examples
  1010. examples_client_list() ->
  1011. #{
  1012. general_client_list =>
  1013. #{
  1014. summary => <<"General client list">>,
  1015. value => [example_general_client()]
  1016. },
  1017. lwm2m_client_list =>
  1018. #{
  1019. summary => <<"LwM2M client list">>,
  1020. value => [example_lwm2m_client()]
  1021. }
  1022. }.
  1023. examples_client() ->
  1024. #{
  1025. general_client =>
  1026. #{
  1027. summary => <<"General client info">>,
  1028. value => example_general_client()
  1029. },
  1030. lwm2m_client =>
  1031. #{
  1032. summary => <<"LwM2M client info">>,
  1033. value => example_lwm2m_client()
  1034. }
  1035. }.
  1036. examples_subscription_list() ->
  1037. #{
  1038. general_subscription_list =>
  1039. #{
  1040. summary => <<"A general subscription list">>,
  1041. value => [example_general_subscription()]
  1042. },
  1043. stomp_subscription_list =>
  1044. #{
  1045. summary => <<"The STOMP subscription list">>,
  1046. value => [example_stomp_subscription]
  1047. }
  1048. }.
  1049. examples_subscription() ->
  1050. #{
  1051. general_subscription =>
  1052. #{
  1053. summary => <<"A general subscription">>,
  1054. value => example_general_subscription()
  1055. },
  1056. stomp_subscription =>
  1057. #{
  1058. summary => <<"A STOMP subscription">>,
  1059. value => example_stomp_subscription()
  1060. }
  1061. }.
  1062. example_lwm2m_client() ->
  1063. maps:merge(
  1064. example_general_client(),
  1065. #{
  1066. proto_name => <<"LwM2M">>,
  1067. proto_ver => <<"1.0">>,
  1068. endpoint_name => <<"urn:imei:154928475237123">>,
  1069. lifetime => 86400
  1070. }
  1071. ).
  1072. example_general_client() ->
  1073. #{
  1074. clientid => <<"MzAyMzEzNTUwNzk1NDA1MzYyMzIwNzUxNjQwMTY1NzQ0NjE">>,
  1075. username => <<"guest">>,
  1076. node => <<"emqx@127.0.0.1">>,
  1077. proto_name => "STOMP",
  1078. proto_ver => <<"1.0">>,
  1079. ip_address => <<"127.0.0.1">>,
  1080. port => 50675,
  1081. clean_start => true,
  1082. connected => true,
  1083. is_bridge => false,
  1084. keepalive => 0,
  1085. expiry_interval => 0,
  1086. subscriptions_cnt => 0,
  1087. subscriptions_max => <<"infinity">>,
  1088. awaiting_rel_cnt => 0,
  1089. awaiting_rel_max => <<"infinity">>,
  1090. mqueue_len => 0,
  1091. mqueue_max => <<"infinity">>,
  1092. mqueue_dropped => 0,
  1093. inflight_cnt => 0,
  1094. inflight_max => <<"infinity">>,
  1095. heap_size => 4185,
  1096. recv_oct => 56,
  1097. recv_cnt => 1,
  1098. recv_pkt => 1,
  1099. recv_msg => 0,
  1100. send_oct => 61,
  1101. send_cnt => 1,
  1102. send_pkt => 1,
  1103. send_msg => 0,
  1104. reductions => 72022,
  1105. mailbox_len => 0,
  1106. created_at => <<"2021-12-07T10:44:02.721+08:00">>,
  1107. connected_at => <<"2021-12-07T10:44:02.721+08:00">>,
  1108. disconnected_at => null
  1109. }.
  1110. example_stomp_subscription() ->
  1111. maps:merge(
  1112. example_general_subscription(),
  1113. #{
  1114. topic => <<"stomp/topic">>,
  1115. sub_props => #{subid => <<"10">>}
  1116. }
  1117. ).
  1118. example_general_subscription() ->
  1119. #{
  1120. topic => <<"test/topic">>,
  1121. qos => 1,
  1122. nl => 0,
  1123. rap => 0,
  1124. rh => 0
  1125. }.