emqx/src/emqx_http_lib.erl

177 lines
6.0 KiB
Erlang

%%--------------------------------------------------------------------
%% Copyright (c) 2021-2022 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_http_lib).
-export([ uri_encode/1
, uri_decode/1
, uri_parse/1
, normalise_headers/1
]).
-export_type([uri_map/0]).
-type uri_map() :: #{scheme := http | https,
host := unicode:chardata(),
port := non_neg_integer(),
path => unicode:chardata(),
query => unicode:chardata(),
fragment => unicode:chardata(),
userinfo => unicode:chardata()}.
-type hex_uri() :: string() | binary().
-type maybe_hex_uri() :: string() | binary(). %% A possibly hexadecimal encoded URI.
-type uri() :: string() | binary().
%% @doc Decode percent-encoded URI.
%% This is copied from http_uri.erl which has been deprecated since OTP-23
%% The recommended replacement uri_string function is not quite equivalent
%% and not backward compatible.
-spec uri_decode(maybe_hex_uri()) -> uri().
uri_decode(String) when is_list(String) ->
do_uri_decode(String);
uri_decode(String) when is_binary(String) ->
do_uri_decode_binary(String).
do_uri_decode([$%,Hex1,Hex2|Rest]) ->
[hex2dec(Hex1)*16+hex2dec(Hex2)|do_uri_decode(Rest)];
do_uri_decode([First|Rest]) ->
[First|do_uri_decode(Rest)];
do_uri_decode([]) ->
[].
do_uri_decode_binary(<<$%, Hex:2/binary, Rest/bits>>) ->
<<(binary_to_integer(Hex, 16)), (do_uri_decode_binary(Rest))/binary>>;
do_uri_decode_binary(<<First:1/binary, Rest/bits>>) ->
<<First/binary, (do_uri_decode_binary(Rest))/binary>>;
do_uri_decode_binary(<<>>) ->
<<>>.
%% @doc Encode URI.
-spec uri_encode(uri()) -> hex_uri().
uri_encode(URI) when is_list(URI) ->
lists:append([do_uri_encode(Char) || Char <- URI]);
uri_encode(URI) when is_binary(URI) ->
<< <<(do_uri_encode_binary(Char))/binary>> || <<Char>> <= URI >>.
%% @doc Parse URI into a map as uri_string:uri_map(), but with two fields
%% normalised: (1): port number is never 'undefined', default ports are used
%% if missing. (2): scheme is always atom.
-spec uri_parse(string() | binary()) -> {ok, uri_map()} | {error, any()}.
uri_parse(URI) ->
try
{ok, do_parse(uri_string:normalize(URI))}
catch
throw : Reason ->
{error, Reason}
end.
do_parse({error, Reason, Which}) -> throw({Reason, Which});
do_parse(URI) ->
%% ensure we return string() instead of binary() in uri_map() values.
Map = uri_string:parse(unicode:characters_to_list(URI)),
case maps:is_key(scheme, Map) of
true ->
normalise_parse_result(Map);
false ->
%% missing scheme, add "http://" and try again
Map2 = uri_string:parse(unicode:characters_to_list(["http://", URI])),
normalise_parse_result(Map2)
end.
%% @doc Return HTTP headers list with keys lower-cased and
%% underscores replaced with hyphens
%% NOTE: assuming the input Headers list is a proplists,
%% that is, when a key is duplicated, list header overrides tail
%% e.g. [{"Content_Type", "applicaiton/binary"}, {"content-type", "applicaiton/json"}]
%% results in: [{<<"content-type">>, "applicaiton/binary"}]
normalise_headers(Headers0) ->
F = fun({K0, V}) ->
K = re:replace(K0, "_", "-", [{return,binary}]),
{string:lowercase(K), V}
end,
Headers = lists:map(F, Headers0),
Keys = proplists:get_keys(Headers),
[{K, proplists:get_value(K, Headers)} || K <- Keys].
normalise_parse_result(#{host := Host, scheme := Scheme0} = Map) ->
{Scheme, DefaultPort} = atom_scheme_and_default_port(Scheme0),
Port = case maps:get(port, Map, undefined) of
N when is_number(N) -> N;
_ -> DefaultPort
end,
Map#{ scheme := Scheme
, host := emqx_misc:maybe_parse_ip(Host)
, port => Port
}.
%% NOTE: so far we only support http/coap schemes.
atom_scheme_and_default_port(Scheme) when is_list(Scheme) ->
atom_scheme_and_default_port(list_to_binary(Scheme));
atom_scheme_and_default_port(<<"http">> ) -> {http, 80};
atom_scheme_and_default_port(<<"https">>) -> {https, 443};
atom_scheme_and_default_port(<<"coap">> ) -> {coap, 5683};
atom_scheme_and_default_port(<<"coaps">>) -> {coaps, 5684};
atom_scheme_and_default_port(Other) -> throw({unsupported_scheme, Other}).
do_uri_encode(Char) ->
case reserved(Char) of
true ->
[ $% | integer_to_hexlist(Char)];
false ->
[Char]
end.
do_uri_encode_binary(Char) ->
case reserved(Char) of
true ->
<< $%, (integer_to_binary(Char, 16))/binary >>;
false ->
<<Char>>
end.
reserved($;) -> true;
reserved($:) -> true;
reserved($@) -> true;
reserved($&) -> true;
reserved($=) -> true;
reserved($+) -> true;
reserved($,) -> true;
reserved($/) -> true;
reserved($?) -> true;
reserved($#) -> true;
reserved($[) -> true;
reserved($]) -> true;
reserved($<) -> true;
reserved($>) -> true;
reserved($\") -> true;
reserved(${) -> true;
reserved($}) -> true;
reserved($|) -> true;
reserved($\\) -> true;
reserved($') -> true;
reserved($^) -> true;
reserved($%) -> true;
reserved($\s) -> true;
reserved(_) -> false.
integer_to_hexlist(Int) ->
integer_to_list(Int, 16).
hex2dec(X) when (X>=$0) andalso (X=<$9) -> X-$0;
hex2dec(X) when (X>=$A) andalso (X=<$F) -> X-$A+10;
hex2dec(X) when (X>=$a) andalso (X=<$f) -> X-$a+10.