emqttd_http.erl 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. %%--------------------------------------------------------------------
  2. %% Copyright (c) 2013-2018 EMQ Enterprise, Inc. (http://emqtt.io)
  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. %% @doc HTTP publish API and websocket client.
  17. -module(emqttd_http).
  18. -author("Feng Lee <feng@emqtt.io>").
  19. -include("emqttd.hrl").
  20. -include("emqttd_protocol.hrl").
  21. -import(proplists, [get_value/2, get_value/3]).
  22. -export([http_handler/0, handle_request/2, http_api/0, inner_handle_request/2]).
  23. -include("emqttd_internal.hrl").
  24. -record(state, {dispatch}).
  25. http_handler() ->
  26. APIs = http_api(),
  27. State = #state{dispatch = dispatcher(APIs)},
  28. {?MODULE, handle_request, [State]}.
  29. http_api() ->
  30. Attr = emqttd_rest_api:module_info(attributes),
  31. [{Regexp, Method, Function, Args} || {http_api, [{Regexp, Method, Function, Args}]} <- Attr].
  32. %%--------------------------------------------------------------------
  33. %% Handle HTTP Request
  34. %%--------------------------------------------------------------------
  35. handle_request(Req, State) ->
  36. Path = Req:get(path),
  37. case Path of
  38. "/status" ->
  39. handle_request("/status", Req, Req:get(method));
  40. "/" ->
  41. handle_request("/", Req, Req:get(method));
  42. "/api/v2/auth" ->
  43. handle_request(Path, Req, State);
  44. _ ->
  45. if_authorized(Req, fun() -> handle_request(Path, Req, State) end)
  46. end.
  47. inner_handle_request(Req, State) ->
  48. Path = Req:get(path),
  49. case Path of
  50. "/api/v2/auth" -> handle_request(Path, Req, State);
  51. _ -> if_authorized(Req, fun() -> handle_request(Path, Req, State) end)
  52. end.
  53. handle_request("/api/v2/" ++ Url, Req, #state{dispatch = Dispatch}) ->
  54. Dispatch(Req, Url);
  55. handle_request("/status", Req, Method) when Method =:= 'HEAD'; Method =:= 'GET' ->
  56. {InternalStatus, _ProvidedStatus} = init:get_status(),
  57. AppStatus = case lists:keysearch(emqttd, 1, application:which_applications()) of
  58. false -> not_running;
  59. {value, _Val} -> running
  60. end,
  61. Status = io_lib:format("Node ~s is ~s~nemqttd is ~s",
  62. [node(), InternalStatus, AppStatus]),
  63. Req:ok({"text/plain", iolist_to_binary(Status)});
  64. handle_request("/", Req, Method) when Method =:= 'HEAD'; Method =:= 'GET' ->
  65. respond(Req, 200, api_list());
  66. handle_request(_, Req, #state{}) ->
  67. respond(Req, 404, []).
  68. dispatcher(APIs) ->
  69. fun(Req, Url) ->
  70. Method = Req:get(method),
  71. case filter(APIs, Url, Method) of
  72. [{Regexp, _Method, Function, FilterArgs}] ->
  73. case params(Req) of
  74. {error, Error1} ->
  75. respond(Req, 200, Error1);
  76. Params ->
  77. case {check_params(Params, FilterArgs),
  78. check_params_type(Params, FilterArgs)} of
  79. {true, true} ->
  80. {match, [MatchList]} = re:run(Url, Regexp, [global, {capture, all_but_first, list}]),
  81. Args = lists:append([[Method, Params], MatchList]),
  82. lager:debug("Mod:~p, Fun:~p, Args:~p", [emqttd_rest_api, Function, Args]),
  83. case catch apply(emqttd_rest_api, Function, Args) of
  84. {ok, Data} ->
  85. respond(Req, 200, [{code, ?SUCCESS}, {result, Data}]);
  86. {error, Error} ->
  87. respond(Req, 200, Error);
  88. {'EXIT', Reason} ->
  89. lager:error("Execute API '~s' Error: ~p", [Url, Reason]),
  90. respond(Req, 404, [])
  91. end;
  92. {false, _} ->
  93. respond(Req, 200, [{code, ?ERROR7}, {message, <<"params error">>}]);
  94. {_, false} ->
  95. respond(Req, 200, [{code, ?ERROR8}, {message, <<"params type error">>}])
  96. end
  97. end;
  98. _ ->
  99. lager:error("No match Url:~p", [Url]),
  100. respond(Req, 404, [])
  101. end
  102. end.
  103. % %%--------------------------------------------------------------------
  104. % %% Basic Authorization
  105. % %%--------------------------------------------------------------------
  106. if_authorized(Req, Fun) ->
  107. case authorized(Req) of
  108. true -> Fun();
  109. false -> respond(Req, 401, [])
  110. end.
  111. authorized(Req) ->
  112. case Req:get_header_value("Authorization") of
  113. undefined ->
  114. false;
  115. "Basic " ++ BasicAuth ->
  116. {Username, Password} = user_passwd(BasicAuth),
  117. case emqttd_mgmt:check_user(Username, Password) of
  118. ok ->
  119. true;
  120. {error, Reason} ->
  121. lager:error("HTTP Auth failure: username=~s, reason=~p", [Username, Reason]),
  122. false
  123. end
  124. end.
  125. user_passwd(BasicAuth) ->
  126. list_to_tuple(binary:split(base64:decode(BasicAuth), <<":">>)).
  127. respond(Req, 401, Data) ->
  128. Req:respond({401, [{"WWW-Authenticate", "Basic Realm=\"emqx control center\""}], Data});
  129. respond(Req, 404, Data) ->
  130. Req:respond({404, [{"Content-Type", "text/plain"}], Data});
  131. respond(Req, 200, Data) ->
  132. Req:respond({200, [{"Content-Type", "application/json"}], to_json(Data)});
  133. respond(Req, Code, Data) ->
  134. Req:respond({Code, [{"Content-Type", "text/plain"}], Data}).
  135. filter(APIs, Url, Method) ->
  136. lists:filter(fun({Regexp, Method1, _Function, _Args}) ->
  137. case re:run(Url, Regexp, [global, {capture, all_but_first, list}]) of
  138. {match, _} -> Method =:= Method1;
  139. _ -> false
  140. end
  141. end, APIs).
  142. params(Req) ->
  143. Method = Req:get(method),
  144. case Method of
  145. 'GET' ->
  146. mochiweb_request:parse_qs(Req);
  147. _ ->
  148. case Req:recv_body() of
  149. <<>> -> [];
  150. undefined -> [];
  151. Body ->
  152. case jsx:is_json(Body) of
  153. true -> jsx:decode(Body);
  154. false ->
  155. lager:error("Body:~p", [Body]),
  156. {error, [{code, ?ERROR9}, {message, <<"Body not json">>}]}
  157. end
  158. end
  159. end.
  160. check_params(_Params, Args) when Args =:= [] ->
  161. true;
  162. check_params(Params, Args)->
  163. not lists:any(fun({Item, _Type}) -> undefined =:= proplists:get_value(Item, Params) end, Args).
  164. check_params_type(_Params, Args) when Args =:= [] ->
  165. true;
  166. check_params_type(Params, Args) ->
  167. not lists:any(fun({Item, Type}) ->
  168. Val = proplists:get_value(Item, Params),
  169. case Type of
  170. int -> not is_integer(Val);
  171. binary -> not is_binary(Val);
  172. bool -> not is_boolean(Val)
  173. end
  174. end, Args).
  175. to_json([]) -> <<"[]">>;
  176. to_json(Data) -> iolist_to_binary(mochijson2:encode(Data)).
  177. api_list() ->
  178. [{paths, [<<"api/v2/management/nodes">>,
  179. <<"api/v2/management/nodes/{node_name}">>,
  180. <<"api/v2/monitoring/nodes">>,
  181. <<"api/v2/monitoring/nodes/{node_name}">>,
  182. <<"api/v2/monitoring/listeners">>,
  183. <<"api/v2/monitoring/listeners/{node_name}">>,
  184. <<"api/v2/monitoring/metrics/">>,
  185. <<"api/v2/monitoring/metrics/{node_name}">>,
  186. <<"api/v2/monitoring/stats">>,
  187. <<"api/v2/monitoring/stats/{node_name}">>,
  188. <<"api/v2/nodes/{node_name}/clients">>,
  189. <<"api/v2/nodes/{node_name}/clients/{clientid}">>,
  190. <<"api/v2/clients/{clientid}">>,
  191. <<"api/v2/clients/{clientid}/clean_acl_cache">>,
  192. <<"api/v2/nodes/{node_name}/sessions">>,
  193. <<"api/v2/nodes/{node_name}/sessions/{clientid}">>,
  194. <<"api/v2/sessions/{clientid}">>,
  195. <<"api/v2/nodes/{node_name}/subscriptions">>,
  196. <<"api/v2/nodes/{node_name}/subscriptions/{clientid}">>,
  197. <<"api/v2/subscriptions/{clientid}">>,
  198. <<"api/v2/routes">>,
  199. <<"api/v2/routes/{topic}">>,
  200. <<"api/v2/mqtt/publish">>,
  201. <<"api/v2/mqtt/subscribe">>,
  202. <<"api/v2/mqtt/unsubscribe">>,
  203. <<"api/v2/nodes/{node_name}/plugins">>,
  204. <<"api/v2/nodes/{node_name}/plugins/{plugin_name}">>,
  205. <<"api/v2/configs/{app}">>,
  206. <<"api/v2/nodes/{node_name}/configs/{app}">>]}].