diff --git a/apps/emqx_gateway_coap/include/emqx_coap.hrl b/apps/emqx_gateway_coap/include/emqx_coap.hrl index 8e7bd4046..a0cce0680 100644 --- a/apps/emqx_gateway_coap/include/emqx_coap.hrl +++ b/apps/emqx_gateway_coap/include/emqx_coap.hrl @@ -14,6 +14,8 @@ %% limitations under the License. %%-------------------------------------------------------------------- +-ifndef(EMQX_COAP_HRL). + -define(APP, emqx_coap). -define(DEFAULT_COAP_PORT, 5683). -define(DEFAULT_COAPS_PORT, 5684). @@ -79,3 +81,14 @@ }). -type coap_message() :: #coap_message{}. + +-define(QUERY_PARAMS_MAPPING, [ + {<<"c">>, <<"clientid">>}, + {<<"t">>, <<"token">>}, + {<<"u">>, <<"username">>}, + {<<"p">>, <<"password">>}, + {<<"q">>, <<"qos">>}, + {<<"r">>, <<"retain">>} +]). + +-endif. diff --git a/apps/emqx_gateway_coap/src/emqx_coap_channel.erl b/apps/emqx_gateway_coap/src/emqx_coap_channel.erl index 5e3461c52..f2f09a5b7 100644 --- a/apps/emqx_gateway_coap/src/emqx_coap_channel.erl +++ b/apps/emqx_gateway_coap/src/emqx_coap_channel.erl @@ -386,7 +386,7 @@ check_auth_state(Msg, #channel{connection_required = true} = Channel) -> true -> call_session(handle_request, Msg, Channel); false -> - URIQuery = emqx_coap_message:get_option(uri_query, Msg, #{}), + URIQuery = emqx_coap_message:extract_uri_query(Msg), case maps:get(<<"token">>, URIQuery, undefined) of undefined -> ?SLOG(debug, #{msg => "token_required_in_conn_mode", message => Msg}); @@ -430,7 +430,7 @@ check_token( ) -> IsDeleteConn = is_delete_connection_request(Msg), #{clientid := ClientId} = ClientInfo, - case emqx_coap_message:get_option(uri_query, Msg) of + case emqx_coap_message:extract_uri_query(Msg) of #{ <<"clientid">> := ClientId, <<"token">> := Token @@ -742,7 +742,7 @@ process_connection( Channel = #channel{conn_state = idle}, Iter ) -> - Queries = emqx_coap_message:get_option(uri_query, Req), + Queries = emqx_coap_message:extract_uri_query(Req), case emqx_utils:pipeline( [ @@ -775,7 +775,7 @@ process_connection( ) when ConnState == connected -> - Queries = emqx_coap_message:get_option(uri_query, Req), + Queries = emqx_coap_message:extract_uri_query(Req), ErrMsg0 = case Queries of #{<<"clientid">> := ClientId} -> @@ -793,7 +793,7 @@ process_connection( Channel ); process_connection({close, Msg}, _, Channel, _) -> - Queries = emqx_coap_message:get_option(uri_query, Msg), + Queries = emqx_coap_message:extract_uri_query(Msg), case maps:get(<<"clientid">>, Queries, undefined) of undefined -> ok; diff --git a/apps/emqx_gateway_coap/src/emqx_coap_message.erl b/apps/emqx_gateway_coap/src/emqx_coap_message.erl index ee17231a7..f35f54394 100644 --- a/apps/emqx_gateway_coap/src/emqx_coap_message.erl +++ b/apps/emqx_gateway_coap/src/emqx_coap_message.erl @@ -6,7 +6,7 @@ %% Copyright (c) 2015 Petr Gotthard %%-------------------------------------------------------------------- -%% Copyright (c) 2017-2023 EMQ Technologies Co., Ltd. All Rights Reserved. +%% Copyright (c) 2017-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. @@ -43,6 +43,11 @@ set_payload_block/3, set_payload_block/4 ]). +-export([ + extract_uri_query/1, + query_params_mapping_table/0 +]). + -include("emqx_coap.hrl"). request(Type, Method) -> @@ -111,6 +116,12 @@ get_option(Option, Msg) -> get_option(Option, #coap_message{options = Options}, Def) -> maps:get(Option, Options, Def). +extract_uri_query(Msg = #coap_message{}) -> + expand_short_param_name(get_option(uri_query, Msg, #{})). + +query_params_mapping_table() -> + ?QUERY_PARAMS_MAPPING. + set_payload(Payload, Msg) when is_binary(Payload) -> Msg#coap_message{payload = Payload}; set_payload(Payload, Msg) when is_list(Payload) -> @@ -149,3 +160,17 @@ to_options(Opts) when is_map(Opts) -> Opts; to_options(Opts) -> maps:from_list(Opts). + +expand_short_param_name(Queries) when is_map(Queries) -> + lists:foldl( + fun({Short, Long}, Acc) -> + case maps:take(Short, Acc) of + error -> + Acc; + {Value, Acc1} -> + maps:put(Long, Value, Acc1) + end + end, + Queries, + ?QUERY_PARAMS_MAPPING + ). diff --git a/apps/emqx_gateway_coap/src/emqx_coap_pubsub_handler.erl b/apps/emqx_gateway_coap/src/emqx_coap_pubsub_handler.erl index cbb5d7c94..3127b756c 100644 --- a/apps/emqx_gateway_coap/src/emqx_coap_pubsub_handler.erl +++ b/apps/emqx_gateway_coap/src/emqx_coap_pubsub_handler.erl @@ -76,7 +76,7 @@ check_topic(Path) -> get_sub_opts(Msg) -> SubOpts = maps:fold( - fun parse_sub_opts/3, #{}, emqx_coap_message:get_option(uri_query, Msg, #{}) + fun parse_sub_opts/3, #{}, emqx_coap_message:extract_uri_query(Msg) ), case SubOpts of #{qos := _} -> @@ -110,28 +110,24 @@ type_to_qos(coap, #coap_message{type = Type}) -> end. get_publish_opts(Msg) -> - case emqx_coap_message:get_option(uri_query, Msg) of - undefined -> - #{}; - Qs -> - maps:fold( - fun - (<<"retain">>, V, Acc) -> - Val = V =:= <<"true">>, - Acc#{retain => Val}; - (<<"expiry">>, V, Acc) -> - Val = erlang:binary_to_integer(V), - Acc#{expiry_interval => Val}; - (<<"qos">>, V, Acc) -> - Val = erlang:binary_to_integer(V), - Acc#{qos => Val}; - (_, _, Acc) -> - Acc - end, - #{}, - Qs - ) - end. + Qs = emqx_coap_message:extract_uri_query(Msg), + maps:fold( + fun + (<<"retain">>, V, Acc) -> + Val = V =:= <<"true">>, + Acc#{retain => Val}; + (<<"expiry">>, V, Acc) -> + Val = erlang:binary_to_integer(V), + Acc#{expiry_interval => Val}; + (<<"qos">>, V, Acc) -> + Val = erlang:binary_to_integer(V), + Acc#{qos => Val}; + (_, _, Acc) -> + Acc + end, + #{}, + Qs + ). get_publish_qos(Msg, PublishOpts) -> case PublishOpts of diff --git a/apps/emqx_gateway_coap/src/emqx_gateway_coap.app.src b/apps/emqx_gateway_coap/src/emqx_gateway_coap.app.src index ba43cabc8..5f17360a7 100644 --- a/apps/emqx_gateway_coap/src/emqx_gateway_coap.app.src +++ b/apps/emqx_gateway_coap/src/emqx_gateway_coap.app.src @@ -1,7 +1,7 @@ %% -*- mode: erlang -*- {application, emqx_gateway_coap, [ {description, "CoAP Gateway"}, - {vsn, "0.1.6"}, + {vsn, "0.1.7"}, {registered, []}, {applications, [kernel, stdlib, emqx, emqx_gateway]}, {env, []}, diff --git a/apps/emqx_gateway_coap/test/emqx_coap_SUITE.erl b/apps/emqx_gateway_coap/test/emqx_coap_SUITE.erl index 8f93164b5..9ae50a569 100644 --- a/apps/emqx_gateway_coap/test/emqx_coap_SUITE.erl +++ b/apps/emqx_gateway_coap/test/emqx_coap_SUITE.erl @@ -151,6 +151,30 @@ t_connection(_) -> end, do(Action). +t_connection_with_short_param_name(_) -> + Action = fun(Channel) -> + %% connection + Token = connection(Channel, true), + + timer:sleep(100), + ?assertNotEqual( + [], + emqx_gateway_cm_registry:lookup_channels(coap, <<"client1">>) + ), + + %% heartbeat + {ok, changed, _} = send_heartbeat(Token, true), + + disconnection(Channel, Token, true), + + timer:sleep(100), + ?assertEqual( + [], + emqx_gateway_cm_registry:lookup_channels(coap, <<"client1">>) + ) + end, + do(Action). + t_heartbeat(Config) -> Heartbeat = ?config(new_heartbeat, Config), Action = fun(Channel) -> @@ -583,36 +607,67 @@ t_connectionless_pubsub(_) -> %% helpers send_heartbeat(Token) -> - HeartURI = - ?MQTT_PREFIX ++ - "/connection?clientid=client1&token=" ++ - Token, + send_heartbeat(Token, false). - ?LOGT("send heartbeat request:~ts~n", [HeartURI]), - er_coap_client:request(put, HeartURI). +send_heartbeat(Token, ShortenParamName) -> + Prefix = ?MQTT_PREFIX ++ "/connection", + Queries = #{ + "clientid" => <<"client1">>, + "token" => Token + }, + URI = compose_uri(Prefix, Queries, ShortenParamName), + ?LOGT("send heartbeat request:~ts~n", [URI]), + er_coap_client:request(put, URI). connection(Channel) -> - URI = - ?MQTT_PREFIX ++ - "/connection?clientid=client1&username=admin&password=public", + connection(Channel, false). + +connection(Channel, ShortenParamName) -> + Prefix = ?MQTT_PREFIX ++ "/connection", + Queries = #{ + "clientid" => <<"client1">>, + "username" => <<"admin">>, + "password" => <<"public">> + }, + URI = compose_uri(Prefix, Queries, ShortenParamName), Req = make_req(post), {ok, created, Data} = do_request(Channel, URI, Req), #coap_content{payload = BinToken} = Data, binary_to_list(BinToken). disconnection(Channel, Token) -> - %% delete - URI = ?MQTT_PREFIX ++ "/connection?clientid=client1&token=" ++ Token, + disconnection(Channel, Token, false). + +disconnection(Channel, Token, ShortenParamName) -> + Prefix = ?MQTT_PREFIX ++ "/connection", + Queries = #{ + "clientid" => <<"client1">>, + "token" => Token + }, + URI = compose_uri(Prefix, Queries, ShortenParamName), Req = make_req(delete), {ok, deleted, _} = do_request(Channel, URI, Req). -observe(Channel, Token, true) -> - URI = ?PS_PREFIX ++ "/coap/observe?clientid=client1&token=" ++ Token, +observe(Channel, Token, Observe) -> + observe(Channel, Token, Observe, false). + +observe(Channel, Token, true, ShortenParamName) -> + Prefix = ?PS_PREFIX ++ "/coap/observe", + Queries = #{ + "clientid" => <<"client1">>, + "token" => Token + }, + URI = compose_uri(Prefix, Queries, ShortenParamName), Req = make_req(get, <<>>, [{observe, 0}]), {ok, content, _Data} = do_request(Channel, URI, Req), ok; -observe(Channel, Token, false) -> - URI = ?PS_PREFIX ++ "/coap/observe?clientid=client1&token=" ++ Token, +observe(Channel, Token, false, ShortenParamName) -> + Prefix = ?PS_PREFIX ++ "/coap/observe", + Queries = #{ + "clientid" => <<"client1">>, + "token" => Token + }, + URI = compose_uri(Prefix, Queries, ShortenParamName), Req = make_req(get, <<>>, [{observe, 1}]), {ok, nocontent, _Data} = do_request(Channel, URI, Req), ok. @@ -620,8 +675,16 @@ observe(Channel, Token, false) -> pubsub_uri(Topic) when is_list(Topic) -> ?PS_PREFIX ++ "/" ++ Topic. -pubsub_uri(Topic, Token) when is_list(Topic), is_list(Token) -> - ?PS_PREFIX ++ "/" ++ Topic ++ "?clientid=client1&token=" ++ Token. +pubsub_uri(Topic, Token) -> + pubsub_uri(Topic, Token, false). + +pubsub_uri(Topic, Token, ShortenParamName) when is_list(Topic), is_list(Token) -> + Prefix = ?PS_PREFIX ++ "/" ++ Topic, + Queries = #{ + "clientid" => <<"client1">>, + "token" => Token + }, + compose_uri(Prefix, Queries, ShortenParamName). make_req(Method) -> make_req(Method, <<>>). @@ -701,3 +764,28 @@ get_field(type, #coap_message{type = Type}) -> Type; get_field(method, #coap_message{method = Method}) -> Method. + +compose_uri(URI, Queries, ShortenParamName) -> + Queries1 = shorten_param_name(ShortenParamName, Queries), + case maps:size(Queries1) of + 0 -> + URI; + _ -> + URI ++ "?" ++ uri_string:compose_query(maps:to_list(Queries1)) + end. + +shorten_param_name(false, Queries) -> + Queries; +shorten_param_name(true, Queries) -> + lists:foldl( + fun({Short, Long}, Acc) -> + case maps:take(Long, Acc) of + error -> + Acc; + {Value, Acc1} -> + maps:put(Short, Value, Acc1) + end + end, + Queries, + emqx_coap_message:query_params_mapping_table() + ). diff --git a/apps/emqx_gateway_coap/test/emqx_coap_message_tests.erl b/apps/emqx_gateway_coap/test/emqx_coap_message_tests.erl new file mode 100644 index 000000000..7f672a2d1 --- /dev/null +++ b/apps/emqx_gateway_coap/test/emqx_coap_message_tests.erl @@ -0,0 +1,41 @@ +%%-------------------------------------------------------------------- +%% 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_coap_message_tests). + +-include_lib("eunit/include/eunit.hrl"). + +short_name_uri_query_test() -> + UriQueryMap = #{ + <<"c">> => <<"clientid_value">>, + <<"t">> => <<"token_value">>, + <<"u">> => <<"username_value">>, + <<"p">> => <<"password_value">>, + <<"q">> => 1, + <<"r">> => false + }, + ParsedQuery = #{ + <<"clientid">> => <<"clientid_value">>, + <<"token">> => <<"token_value">>, + <<"username">> => <<"username_value">>, + <<"password">> => <<"password_value">>, + <<"qos">> => 1, + <<"retain">> => false + }, + Msg = emqx_coap_message:request( + con, put, <<"payload contents...">>, #{uri_query => UriQueryMap} + ), + ?assertEqual(ParsedQuery, emqx_coap_message:extract_uri_query(Msg)). diff --git a/changes/ce/fix-12285.en.md b/changes/ce/fix-12285.en.md new file mode 100644 index 000000000..8da01afab --- /dev/null +++ b/changes/ce/fix-12285.en.md @@ -0,0 +1,2 @@ +The CoAP gateway supports short parameter names for slight savings in datagram size. +For example, `clientid=bar` can be written as `c=bar`.