| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337 |
- %%--------------------------------------------------------------------
- %% Copyright (c) 2024 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_auth_utils).
- -include_lib("emqx/include/emqx_placeholder.hrl").
- -include_lib("snabbkaffe/include/trace.hrl").
- %% Template parsing/rendering
- -export([
- parse_deep/2,
- parse_str/2,
- parse_sql/3,
- render_deep_for_json/2,
- render_deep_for_url/2,
- render_deep_for_raw/2,
- render_str/2,
- render_urlencoded_str/2,
- render_sql_params/2
- ]).
- %% URL parsing
- -export([parse_url/1]).
- %% HTTP request/response helpers
- -export([generate_request/2]).
- -define(DEFAULT_HTTP_REQUEST_CONTENT_TYPE, <<"application/json">>).
- %%--------------------------------------------------------------------
- %% API
- %%--------------------------------------------------------------------
- %%--------------------------------------------------------------------
- %% Template parsing/rendering
- parse_deep(Template, AllowedVars) ->
- Result = emqx_template:parse_deep(Template),
- handle_disallowed_placeholders(Result, AllowedVars, {deep, Template}).
- parse_str(Template, AllowedVars) ->
- Result = emqx_template:parse(Template),
- handle_disallowed_placeholders(Result, AllowedVars, {string, Template}).
- parse_sql(Template, ReplaceWith, AllowedVars) ->
- {Statement, Result} = emqx_template_sql:parse_prepstmt(
- Template,
- #{parameters => ReplaceWith, strip_double_quote => true}
- ),
- {Statement, handle_disallowed_placeholders(Result, AllowedVars, {string, Template})}.
- handle_disallowed_placeholders(Template, AllowedVars, Source) ->
- case emqx_template:validate(AllowedVars, Template) of
- ok ->
- Template;
- {error, Disallowed} ->
- ?tp(warning, "auth_template_invalid", #{
- template => Source,
- reason => Disallowed,
- allowed => #{placeholders => AllowedVars},
- notice =>
- "Disallowed placeholders will be rendered as is."
- " However, consider using `${$}` escaping for literal `$` where"
- " needed to avoid unexpected results."
- }),
- Result = prerender_disallowed_placeholders(Template, AllowedVars),
- case Source of
- {string, _} ->
- emqx_template:parse(Result);
- {deep, _} ->
- emqx_template:parse_deep(Result)
- end
- end.
- prerender_disallowed_placeholders(Template, AllowedVars) ->
- {Result, _} = emqx_template:render(Template, #{}, #{
- var_trans => fun(Name, _) ->
- % NOTE
- % Rendering disallowed placeholders in escaped form, which will then
- % parse as a literal string.
- case lists:member(Name, AllowedVars) of
- true -> "${" ++ Name ++ "}";
- false -> "${$}{" ++ Name ++ "}"
- end
- end
- }),
- Result.
- render_deep_for_json(Template, Credential) ->
- % NOTE
- % Ignoring errors here, undefined bindings will be replaced with empty string.
- {Term, _Errors} = emqx_template:render(
- Template,
- rename_client_info_vars(Credential),
- #{var_trans => fun to_string_for_json/2}
- ),
- Term.
- render_deep_for_raw(Template, Credential) ->
- % NOTE
- % Ignoring errors here, undefined bindings will be replaced with empty string.
- {Term, _Errors} = emqx_template:render(
- Template,
- rename_client_info_vars(Credential),
- #{var_trans => fun to_string_for_raw/2}
- ),
- Term.
- render_deep_for_url(Template, Credential) ->
- render_deep_for_raw(Template, Credential).
- render_str(Template, Credential) ->
- % NOTE
- % Ignoring errors here, undefined bindings will be replaced with empty string.
- {String, _Errors} = emqx_template:render(
- Template,
- rename_client_info_vars(Credential),
- #{var_trans => fun to_string/2}
- ),
- unicode:characters_to_binary(String).
- render_urlencoded_str(Template, Credential) ->
- % NOTE
- % Ignoring errors here, undefined bindings will be replaced with empty string.
- {String, _Errors} = emqx_template:render(
- Template,
- rename_client_info_vars(Credential),
- #{var_trans => fun to_urlencoded_string/2}
- ),
- unicode:characters_to_binary(String).
- render_sql_params(ParamList, Credential) ->
- % NOTE
- % Ignoring errors here, undefined bindings will be replaced with empty string.
- {Row, _Errors} = emqx_template:render(
- ParamList,
- rename_client_info_vars(Credential),
- #{var_trans => fun to_sql_value/2}
- ),
- Row.
- to_urlencoded_string(Name, Value) ->
- case uri_string:compose_query([{<<"q">>, to_string(Name, Value)}]) of
- <<"q=", EncodedBin/binary>> ->
- EncodedBin;
- "q=" ++ EncodedStr ->
- list_to_binary(EncodedStr)
- end.
- to_string(Name, Value) ->
- emqx_template:to_string(render_var(Name, Value)).
- %% This converter is to generate data structure possibly with non-utf8 strings.
- %% It converts to unicode only strings (character lists).
- to_string_for_raw(Name, Value) ->
- strings_to_unicode(Name, render_var(Name, Value)).
- %% This converter is to generate data structure suitable for JSON serialization.
- %% JSON strings are sequences of unicode characters, not bytes.
- %% So we force all rendered data to be unicode, not only character lists.
- to_string_for_json(Name, Value) ->
- all_to_unicode(Name, render_var(Name, Value)).
- strings_to_unicode(_Name, Value) when is_binary(Value) ->
- Value;
- strings_to_unicode(Name, Value) when is_list(Value) ->
- to_unicode_binary(Name, Value);
- strings_to_unicode(_Name, Value) ->
- emqx_template:to_string(Value).
- all_to_unicode(Name, Value) when is_list(Value) orelse is_binary(Value) ->
- to_unicode_binary(Name, Value);
- all_to_unicode(_Name, Value) ->
- emqx_template:to_string(Value).
- to_unicode_binary(Name, Value) when is_list(Value) orelse is_binary(Value) ->
- try unicode:characters_to_binary(Value) of
- Encoded when is_binary(Encoded) ->
- Encoded;
- _ ->
- error({encode_error, {non_unicode_data, Name}})
- catch
- error:badarg ->
- error({encode_error, {non_unicode_data, Name}})
- end.
- to_sql_value(Name, Value) ->
- emqx_utils_sql:to_sql_value(render_var(Name, Value)).
- render_var(_, undefined) ->
- % NOTE
- % Any allowed but undefined binding will be replaced with empty string, even when
- % rendering SQL values.
- <<>>;
- render_var(?VAR_PEERHOST, Value) ->
- inet:ntoa(Value);
- render_var(?VAR_PASSWORD, Value) ->
- iolist_to_binary(Value);
- render_var(_Name, Value) ->
- Value.
- rename_client_info_vars(ClientInfo) ->
- Renames = [
- {cn, cert_common_name},
- {dn, cert_subject},
- {protocol, proto_name}
- ],
- lists:foldl(
- fun({Old, New}, Acc) ->
- emqx_utils_maps:rename(Old, New, Acc)
- end,
- ClientInfo,
- Renames
- ).
- %%--------------------------------------------------------------------
- %% URL parsing
- -spec parse_url(binary()) ->
- {_Base :: emqx_utils_uri:request_base(), _Path :: binary(), _Query :: binary()}.
- parse_url(Url) ->
- Parsed = emqx_utils_uri:parse(Url),
- case Parsed of
- #{scheme := undefined} ->
- throw({invalid_url, {no_scheme, Url}});
- #{authority := undefined} ->
- throw({invalid_url, {no_host, Url}});
- #{authority := #{userinfo := Userinfo}} when Userinfo =/= undefined ->
- throw({invalid_url, {userinfo_not_supported, Url}});
- #{fragment := Fragment} when Fragment =/= undefined ->
- throw({invalid_url, {fragments_not_supported, Url}});
- _ ->
- case emqx_utils_uri:request_base(Parsed) of
- {ok, Base} ->
- {Base, emqx_utils_uri:path(Parsed),
- emqx_maybe:define(emqx_utils_uri:query(Parsed), <<>>)};
- {error, Reason} ->
- throw({invalid_url, {invalid_base, Reason, Url}})
- end
- end.
- %%--------------------------------------------------------------------
- %% HTTP request/response helpers
- generate_request(
- #{
- method := Method,
- headers := Headers,
- base_path_template := BasePathTemplate,
- base_query_template := BaseQueryTemplate,
- body_template := BodyTemplate
- },
- Values
- ) ->
- Path = render_urlencoded_str(BasePathTemplate, Values),
- Query = render_deep_for_url(BaseQueryTemplate, Values),
- case Method of
- get ->
- Body = render_deep_for_url(BodyTemplate, Values),
- NPath = append_query(Path, Query, Body),
- {ok, {NPath, Headers}};
- _ ->
- try
- ContentType = post_request_content_type(Headers),
- Body = serialize_body(ContentType, BodyTemplate, Values),
- NPathQuery = append_query(Path, Query),
- {ok, {NPathQuery, Headers, Body}}
- catch
- error:{encode_error, _} = Reason ->
- {error, Reason}
- end
- end.
- post_request_content_type(Headers) ->
- proplists:get_value(<<"content-type">>, Headers, ?DEFAULT_HTTP_REQUEST_CONTENT_TYPE).
- append_query(Path, []) ->
- Path;
- append_query(Path, Query) ->
- [Path, $?, uri_string:compose_query(Query)].
- append_query(Path, Query, Body) ->
- append_query(Path, Query ++ maps:to_list(Body)).
- serialize_body(<<"application/json">>, BodyTemplate, ClientInfo) ->
- Body = emqx_auth_utils:render_deep_for_json(BodyTemplate, ClientInfo),
- emqx_utils_json:encode(Body);
- serialize_body(<<"application/x-www-form-urlencoded">>, BodyTemplate, ClientInfo) ->
- Body = emqx_auth_utils:render_deep_for_url(BodyTemplate, ClientInfo),
- uri_string:compose_query(maps:to_list(Body));
- serialize_body(undefined, _BodyTemplate, _ClientInfo) ->
- throw(missing_content_type_header);
- serialize_body(ContentType, _BodyTemplate, _ClientInfo) ->
- throw({unknown_content_type_header_value, ContentType}).
- -ifdef(TEST).
- -include_lib("eunit/include/eunit.hrl").
- templates_test_() ->
- [
- ?_assertEqual(
- {
- #{port => 80, scheme => http, host => "example.com"},
- <<"">>,
- <<"client=${clientid}">>
- },
- parse_url(<<"http://example.com?client=${clientid}">>)
- ),
- ?_assertEqual(
- {
- #{port => 80, scheme => http, host => "example.com"},
- <<"/path">>,
- <<"client=${clientid}">>
- },
- parse_url(<<"http://example.com/path?client=${clientid}">>)
- ),
- ?_assertEqual(
- {#{port => 80, scheme => http, host => "example.com"}, <<"/path">>, <<>>},
- parse_url(<<"http://example.com/path">>)
- )
- ].
- -endif.
|