From 761294ef1df30aa610ed0e6bf061153ae0cde8c5 Mon Sep 17 00:00:00 2001 From: turtled Date: Thu, 9 May 2019 17:39:40 +0800 Subject: [PATCH] Fix bugs Rm reuseaddr config Migrate the erlang MQTT v5.0 client from emqx Update .travis.yml Fix the 'no function clause matching' error --- .gitignore | 31 +- .travis.yml | 6 +- CHANGELOG.md | 127 -- LICENSE | 211 ++- Makefile | 48 +- README.md | 343 +---- TODO | 14 +- doc/Design.md | 41 - doc/Socket.graphml | 228 --- doc/Socket.png | Bin 13327 -> 0 bytes examples/gen_server/run | 6 - .../gen_server/src/gen_server_example.app.src | 14 - .../gen_server/src/gen_server_example.erl | 88 -- examples/simple/simple_example.erl | 17 - include/emqtt.hrl | 529 +++++++ include/emqttc_packet.hrl | 258 ---- rebar | Bin 182326 -> 0 bytes rebar.config | 30 +- run | 6 - src/emqtt.app.src | 15 + src/emqtt.erl | 1343 +++++++++++++++++ src/emqtt_frame.erl | 655 ++++++++ src/emqtt_props.erl | 154 ++ src/emqtt_sock.erl | 119 ++ src/emqttc.app.src | 10 - src/emqttc.erl | 1118 -------------- src/emqttc_keepalive.erl | 108 -- src/emqttc_message.erl | 137 -- src/emqttc_opts.erl | 46 - src/emqttc_packet.erl | 150 -- src/emqttc_parser.erl | 223 --- src/emqttc_protocol.erl | 328 ---- src/emqttc_reconnector.erl | 120 -- src/emqttc_serialiser.erl | 158 -- src/emqttc_socket.erl | 258 ---- src/emqttc_topic.erl | 160 -- test/.placeholder | 0 test/emqtt_frame_SUITE.erl | 440 ++++++ test/emqttc_test.erl | 75 - 39 files changed, 3504 insertions(+), 4110 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 doc/Design.md delete mode 100644 doc/Socket.graphml delete mode 100644 doc/Socket.png delete mode 100755 examples/gen_server/run delete mode 100644 examples/gen_server/src/gen_server_example.app.src delete mode 100644 examples/gen_server/src/gen_server_example.erl delete mode 100644 examples/simple/simple_example.erl create mode 100644 include/emqtt.hrl delete mode 100644 include/emqttc_packet.hrl delete mode 100755 rebar delete mode 100755 run create mode 100644 src/emqtt.app.src create mode 100644 src/emqtt.erl create mode 100644 src/emqtt_frame.erl create mode 100644 src/emqtt_props.erl create mode 100644 src/emqtt_sock.erl delete mode 100644 src/emqttc.app.src delete mode 100644 src/emqttc.erl delete mode 100644 src/emqttc_keepalive.erl delete mode 100644 src/emqttc_message.erl delete mode 100644 src/emqttc_opts.erl delete mode 100644 src/emqttc_packet.erl delete mode 100644 src/emqttc_parser.erl delete mode 100644 src/emqttc_protocol.erl delete mode 100644 src/emqttc_reconnector.erl delete mode 100644 src/emqttc_serialiser.erl delete mode 100644 src/emqttc_socket.erl delete mode 100644 src/emqttc_topic.erl delete mode 100644 test/.placeholder create mode 100644 test/emqtt_frame_SUITE.erl delete mode 100644 test/emqttc_test.erl diff --git a/.gitignore b/.gitignore index f3abf439..432fa62b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,21 @@ -*.beam -ebin .eunit -*~ -erl_crash.dump deps -.DS_Store -log -eunit.coverage.xml +*.o +*.beam +*.plt +erl_crash.dump +ebin +rel/example_project +.concrete/DEV_MODE .rebar -*.swp -oldsrc/ -doc/*.html -doc/*.css -doc/edoc-info -doc/erlang.png +.idea .idea/ -emqttc.iml +test/ct.cover.spec +ct.coverdata +eunit.coverdata +logs/ +cover/ +.DS_Store +_build/ +rebar3.crashdump +*.swp diff --git a/.travis.yml b/.travis.yml index 6da28de0..e2a3233d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,8 @@ language: erlang sudo: false otp_release: - - 17.0 - - 18.0 - - 18.2.1 + - 21.3 + - 22.0 script: - make - diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 9d49e591..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,127 +0,0 @@ - -ChangeLog -================== - -0.8.0-beta (2016-01-29) ------------------------- - -Fully SSL Options Support (#27) - - -0.7.1-beta (2016-01-07) ------------------------- - -Merge PR #25 - - -0.7.0-beta (2015-11-08) ------------------------- - -Hibernate emqttc and emqttc_socket to reduce memory usage - -Bugfix: emqttc send willmsg (#23) - - -0.6.0-alpha (2015-10-08) ------------------------- - -Feature: Support to specify 'local ipaddress' (#20) - -Improve: add emqttc:start_link(MqttOpts, TcpOpts) api - - -0.5.0-alpha (2015-06-05) ------------------------- - -Support synchronous subscribe/publish APIs - -Feature: emqttc - add sync_publish/4, sync_subscribe/2, sync_subscribe/3 apis - -Feature: mqttc_opt() - add 'puback_timeout', 'suback_timeout' options - -Bugfix: default keepalive bug - - -0.4.1-beta (2015-05-27) ------------------------- - -Bugfix: fix critical issue #11 - - -0.4.0-alpha (2015-05-22) ------------------------- - -Support 'wait_for_connack' timeout and Auto Resubscribe Topics. - -Feature: issue #10 - Should send message to client on (re-)connect or handle re-subscribes also - -Feature: send '{mqttc, Pid, connected}' message to parent process when emqttc is connected successfully - -Feature: send '{mqttc, Pid, disconnected}' message to parent process when emqttc is disconnected from broker and prepare to reconnect. - -Improve: issue #12 - support 'CONNACK' timeout - -Improve: issue #13 - Improve subscribe/publish api with atoms: qos0/1/2 - - -v0.3.1-beta (2015-04-28) ------------------------- - -format comments - -emqttc_message.erl: fix spec - - -v0.3.0-beta (2015-02-20) ------------------------- - -add examples/benchmark - - -v0.2.4-beta (2015-02-18) ------------------------- - -emqttc_socket.erl: handle tcp_error. - -emqttc.erl: change log level from 'info' to 'debug' for SEND/RECV packets. - - -v0.2.3-beta (2015-02-15) ------------------------- - -Upgrade gen_logger to 0.3-beta - -v0.2.2-beta (2015-02-15) ------------------------- - -Improve: handle {'DOWN', MonRef, process, Pid, _Why} from subscriber - -Improve: handle 'SUBACK', 'UNSUBACK' - -v0.2.1-beta (2015-02-14) ------------------------- - -Feature: SSL Socket Support - -v0.2.0-beta (2015-02-12) ------------------------- - -Notice: The API is not compatible with 0.1! - -Feature: Both MQTT V3.1/V3.1.1 Protocol Support - -Feature: QoS0, QoS1, QoS2 Publish and Subscribe - -Feature: KeepAlive and Reconnect support - -Feature: gen_logger support - -Change: Redesign the whole project - -Change: Rewrite the README.md - -v0.1.0-alpha (2015-02-12) ------------------------- - -Tag the version written by @hiroeorz - diff --git a/LICENSE b/LICENSE index 287c4750..05182702 100644 --- a/LICENSE +++ b/LICENSE @@ -1,22 +1,191 @@ -The MIT License (MIT) - -Copyright (c) 2012-2015, Feng Lee - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2019, Anonymous . + + 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. diff --git a/Makefile b/Makefile index fccf208c..227c288d 100644 --- a/Makefile +++ b/Makefile @@ -1,37 +1,31 @@ -.PHONY: test +REBAR := rebar3 -ERL=erl -BEAMDIR=./deps/*/ebin ./ebin -REBAR=./rebar -REBAR_GEN=../../rebar -DIALYZER=dialyzer +.PHONY: all +all: compile -#update-deps -all: get-deps compile - -get-deps: - @$(REBAR) get-deps +compile: + $(REBAR) compile -update-deps: - @$(REBAR) update-deps +.PHONY: clean +clean: distclean -compile: - @$(REBAR) compile +.PHONY: distclean +distclean: + @rm -rf _build erl_crash.dump rebar3.crashdump rebar.lock +.PHONY: xref xref: - @$(REBAR) xref skip_deps=true - -clean: - @$(REBAR) clean + $(REBAR) xref -test: - @$(REBAR) skip_deps=true eunit +.PHONY: eunit +eunit: compile + $(REBAR) eunit verbose=truen -edoc: - @$(REBAR) doc +.PHONY: ct +ct: compile + $(REBAR) ct -v -dialyzer: compile - @$(DIALYZER) ebin deps/ossp_uuid/ebin +.PHONY: dialyzer +dialyzer: + $(REBAR) dialyzer -setup-dialyzer: - @$(DIALYZER) --build_plt --apps kernel stdlib mnesia eunit erts crypto diff --git a/README.md b/README.md index 3fd3ef70..3d0a37b9 100644 --- a/README.md +++ b/README.md @@ -1,343 +1,18 @@ -# Erlang MQTT Client [![Build Status](https://travis-ci.org/emqtt/emqttc.svg?branch=master)](https://travis-ci.org/emqtt/emqttc) +emqtt +===== -emqttc support MQTT V3.1/V3.1.1 Protocol Specification, and support parallel connections and auto reconnect to broker. +Erlang MQTT v5.0 Client. -emqttc requires Erlang R17+. +Build +----- -## Features - -* Both MQTT V3.1/V3.1.1 Protocol Support -* QoS0, QoS1, QoS2 Publish and Subscribe -* TCP/SSL Socket Support -* Reconnect automatically -* Keepalive and ping/pong - -## Usage - -### simple - -examples/simple_example.erl - -```erlang -%% connect to broker -{ok, C} = emqttc:start_link([{host, "localhost"}, {client_id, <<"simpleClient">>}]), - -%% subscribe -emqttc:subscribe(C, <<"TopicA">>, qos0), - -%% publish -emqttc:publish(C, <<"TopicA">>, <<"Payload...">>), - -%% receive message -receive - {publish, Topic, Payload} -> - io:format("Message Received from ~s: ~p~n", [Topic, Payload]) -after - 1000 -> - io:format("Error: receive timeout!~n") -end, - -%% disconnect from broker -emqttc:disconnect(C). - -``` - -### gen_server - -examples/gen_server_example.erl - -```erlang - -... - -%% Connect to broker when init -init(_Args) -> - {ok, C} = emqttc:start_link([{host, "localhost"}, - {client_id, <<"simpleClient">>}, - {logger, info}]), - emqttc:subscribe(C, <<"TopicA">>, qos1), - self() ! publish, - {ok, #state{mqttc = C, seq = 1}}. - -%% Receive Publish Message from TopicA... -handle_info({publish, Topic, Payload}, State) -> - io:format("Message from ~s: ~p~n", [Topic, Payload]), - {noreply, State}; - -%% Client connected -handle_info({mqttc, C, connected}, State = #state{mqttc = C}) -> - io:format("Client ~p is connected~n", [C]), - emqttc:subscribe(C, <<"TopicA">>, 1), - emqttc:subscribe(C, <<"TopicB">>, 2), - self() ! publish, - {noreply, State}; - -%% Client disconnected -handle_info({mqttc, C, disconnected}, State = #state{mqttc = C}) -> - io:format("Client ~p is disconnected~n", [C]), - {noreply, State}; - -``` - -## Build - -``` - -$ make - -``` - -## Connect to Broker - -Connect to MQTT Broker: - -```erlang - -{ok, C1} = emqttc:start_link([{host, "t.emqtt.io"}]). - -%% with name 'emqttclient' -{ok, C2} = emqttc:start_link(emqttclient, [{host, "t.emqtt.io"}]). - -``` -### Connect Options - -```erlang - --type mqttc_opt() :: {host, inet:ip_address() | string()} - | {port, inet:port_number()} - | {client_id, binary()} - | {clean_sess, boolean()} - | {keepalive, non_neg_integer()} - | {proto_ver, mqtt_vsn()} - | {username, binary()} - | {password, binary()} - | {will, list(tuple())} - | {connack_timeout, pos_integer()} - | {puback_timeout, pos_integer()} - | {suback_timeout, pos_integer()} - | ssl | {ssl, [ssl:ssloption()]} - | force_ping | {force_ping, boolean()} - | auto_resub | {auto_resub, boolean()} - | {logger, atom() | {atom(), atom()}} - | {reconnect, non_neg_integer() | {non_neg_integer(), non_neg_integer()} | false}. -``` - -Option | Value | Default | Description | Example --------|-------|---------|-------------|--------- -host | inet:ip_address() or string() | "locahost" | Broker Address | "locahost" -port | inet:port_number() | 1883 | Broker Port | -client_id | binary() | random clientId | MQTT ClientId | <<"slimpleClientId">> -clean_sess | boolean() | true | MQTT CleanSession | -keepalive | non_neg_integer() | 60 | MQTT KeepAlive(secs) -proto_ver | mqtt_vsn() | 4 | MQTT Protocol Version | 3,4 -username | binary() -password | binary() -will | list(tuple()) | undefined | MQTT Will Message | [{qos, 1}, {retain, false}, {topic, <<"WillTopic">>}, {payload, <<"I die">>}] -connack_timeout | pos_integer() | 30 | ConnAck Timeout | {connack_timeout, 10} -ssl | list(ssl:ssloption()) | [] | SSL Options | [{certfile, "path/to/ssl.crt"}, {keyfile, "path/to/ssl.key"}]}] -auto_resub | -logger | atom() or {atom(), atom()} | info | Client Logger | error, {opt, info}, {lager, error} -reconnect | false, or integer() | false | Client Reconnect | false, 4, {4, 60} - -### Clean Session - -Default Clean Session value is true, If you want to set Clean Session = false, add option {clean_sess, false}. - -```erlang - -emqttc:start_link([{host, "t.emqtt.io"}, {clean_sess, false}]). - -``` - -### KeepAlive - -Default KeepAlive value is 60(secs), If you want to change KeepAlive, add option {keepalive, 300}. No KeepAlive to use {keepalive, 0}. - -```erlang - -emqttc:start_link([{host, "t.emqtt.io"}, {keepalive, 60}]). - -``` - -### SSL Socket - -Connect to broker with SSL Socket: - -```erlang - -emqttc:start_link([{host, "t.emqtt.io"}, {port, 8883}, ssl]). - -emqttc:start_link([{host, "t.emqtt.io"}, {port, 8883}, {ssl, [ - {certfile, "path/to/ssl.crt"}, - {keyfile, "path/to/ssl.key"}]} -]). - -More SSL Options: http://erlang.org/doc/man/ssl.html - -``` - -### Logger - -Use 'logger' option to configure emqttc log mechanism. Default log to stdout with 'info' level. - -```erlang - -%% log to stdout with info level -emqttc:start_link([{logger, info}]). - -%% log to otp standard error_logger with warning level -emqttc:start_link([{logger, {error_logger, warning}}]). - -%% log to lager with error level -emqttc:start_link([{logger, {lager, error}}]). - -``` - -#### Logger modules - -Module | Description -----------------|------------ -stdout | io:format -error_logger | error_logger -lager | lager - -#### Logger Levels - -``` -all -debug -info -warning -error -critical -none -``` - -### Reconnect - -Use 'reconnect' option to configure emqttc reconnect policy. Default is 'false'. - -Reconnect Policy: {MinInterval, MaxInterval, MaxRetries}. - -```erlang - -%% reconnect with 4(secs) min interval, 60 max interval -emqttc:start_link([{reconnect, 4}]). - -%% reconnect with 3(secs) min interval, 120(secs) max interval and 10 max retries. -emqttc:start_link([{reconnect, {3, 120, 10}}]). - -``` - -### Auto Resubscribe - -'auto_resub' option to let emqttc resubscribe topics when reconnected. - -```erlang - -%% Resubscribe topics automatically when reconnected. -emqttc:start_link([auto_resub, {reconnect, 4}]). - -``` - -## Subscribe and Publish - -### Publish API - -```erlang - -%% publish(Client, Topic, Payload) with Qos0 -emqttc:publish(Client, <<"/test/TopicA">>, <<"Payload...">>). - -%% publish(Client, Topic, Payload, Qos) -emqttc:publish(Client, <<"Topic">>, <<"Payload">>, 1). -emqttc:publish(Client, <<"Topic">>, <<"Payload">>, qos1). - -%% publish(Client, Topic, Payload, PubOpts) with options -emqttc:publish(Client, <<"/test/TopicA">>, <<"Payload...">>, [{qos, 1}, {retain, true}]). - -``` - -### Synchronous Publish API - -Publish qos1, qos2 messages and wait until puback, pubrel received. - -``` --spec sync_publish(Client, Topic, Payload, PubOpts) -> {ok, MsgId} | {error, timeout} when - Client :: pid() | atom(), - Topic :: binary(), - Payload :: binary(), - PubOpts :: mqtt_qosopt() | [mqtt_pubopt()], - MsgId :: mqtt_packet_id(). -``` - -### Subscribe API - -```erlang - -%% subscribe topic with Qos0 -emqttc:subscribe(Client, <<"Topic">>). - -%% subscribe topic with qos -emqttc:subscribe(Client, {<<"Topic">>, 1}). -emqttc:subscribe(Client, {<<"Topic">>, qos1}). - -%% or -emqttc:subscribe(Client, <<"Topic">>, 1). -emqttc:subscribe(Client, <<"Topic">>, qos2). - - -%% subscribe topics -emqttc:subscribe(Client, [{<<"Topic1">>, 1}, {<<"Topic2">>, qos2}]). - -%% unsubscribe -emqttc:unsubscribe(Client, <<"Topic">>). -emqttc:unsubscribe(Client, [<<"Topic1">>, <<"Topic2">>]). - -``` - -### Synchronous Subscribe API - -Subscribe topics and wait for suback. - -``` --spec sync_subscribe(Client, Topics) -> {ok, mqtt_qos() | [mqtt_qos()]} when - Client :: pid() | atom(), - Topics :: [{binary(), mqtt_qos()}] | {binary(), mqtt_qos()} | binary(). -``` - -## Ping and Pong - -```erlang -pong = emqttc:ping(Client). -``` - -## Disconnect - -```erlang -emqttc:disconnect(Client). -``` - -## Topics Subscribed - -``` -emqttc:topics(Client). -``` - -## Design - -![Design](https://raw.githubusercontent.com/emqtt/emqttc/master/doc/Socket.png) + $ rebar3 compile ## License -The MIT License (MIT) - -## Contributors +Apache License Version 2.0 -[@hiroeorz](https://github.com/hiroeorz) -[@desoulter](https://github.com/desoulter) -[@witeman](https://github.com/witeman) +## Author -## Contact +EMQ X Team. -feng@emqtt.io diff --git a/TODO b/TODO index e5d144c5..d8a39f08 100644 --- a/TODO +++ b/TODO @@ -1,12 +1,4 @@ +1. test +2. README +3. docs -autoConfirm option? - -handle 'SUBACK', 'UNSUBACK' - -Inflight Window and Queue to guarantee message order - -Session State of client: - -QoS 1 and QoS 2 messages which have been sent to the Server, but have not been completely acknowledged. - -QoS 2 messages which have been received from the Server, but have not been completely acknowledged. diff --git a/doc/Design.md b/doc/Design.md deleted file mode 100644 index 1c2d7055..00000000 --- a/doc/Design.md +++ /dev/null @@ -1,41 +0,0 @@ -# Design Guide - -## Modules - -Module | Description ------- | ------------ -emqttc | main api module -emqttc_protocol | mqtt protocol process -....... - -## API Design - -### Connect - -``` -{ok, C} = emqttc:start_link(MQTTOpts). - -``` - -### Publish - -### Subscribe - -## Message Dispatch - -``` -Sub ---------- - \|/ -Sub -----> Client<------Pub------Broker - /|\ -Sub ---------- -``` - -``` -Pub------>Client------->Broker -``` - - - - - diff --git a/doc/Socket.graphml b/doc/Socket.graphml deleted file mode 100644 index e4bb7d33..00000000 --- a/doc/Socket.graphml +++ /dev/null @@ -1,228 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - Client -(FSM) - - - - - - - - - - - - - - - - - - Socket -(Serializer) - - - - - - - - - - - - - - - - - - Receiver -(Parser) - - - - - - - - - - - - - - - - - - User - - - - - - - - - - - - - - - - - - Broker - - - - - - - - - - - - - - - - - - send(Data or Packet) - - - - - - - - - - - - - - - - - - - received(Data) - - - - - - - - - - - - - - - - - - - received(Packet) - - - - - - - - - - - - - - - - - - - Publish - - - - - - - - - - - - - - - - - - - Publish/Subscribe - - - - - - - - - - - - - - - - - - - TCP(SSL) - - - - - - - - - - - - - - - - diff --git a/doc/Socket.png b/doc/Socket.png deleted file mode 100644 index 5f0229acc9f0c98570e38244b0f126a418c771ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13327 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 z zCS#IHmUFDKmzOB|zG&yYOo-a^`@Wl!%9ePS6O}kmWo3iLBVG40bBc@ASp;Dsbd1P& zVldNqfgR-+sEP4HTV|_|$R06BzQMVX=m1|CV4L8)++YSQn5QQoFHkP7^&vwiE-*x4 zM$+-;4{&mM%V;j528)0i^q1$#%Fzz&y`7!P<$rHnUK#7OWyTrUw4#MbJ772FXl*A1 z;$A^LK9;DE=lfqCU%B@`;M~`*-p98WtU0kpMJ)4C6Q4Zl!MeQsCJl-LsJ4>og8&WJ znS|p*F9ZM0x%mUIaUYsx0KY6E5WLLIryig(f*8B59+a<>*KYYM?6NUn`DRvC>F4WI zR8-WgH*8d|&Vm%Ru(0lo-vW>Yf8OA~G0y(;9T*JuTJrYkK8M8yJiQ zT)#1qt7~i==4o>E^iHJ}6&x{&eZtR_kLbia*52aZuS91iR-SqS*arp`hKGja?t<}5 z$T|?c6Cmy@U=$S$KCN3Pln1sLLF+cXw_2!{Iun}qdiMbTt&2uJ@!F2%GE_lIa2dGG zmtD=``hLsIWDyk9#+Wj}Vj%R!vGod1GYdZXhU9YpYNIcX#f~5ncay$;ec@IvS?uS;)5svNFh1B2YR+eied%{ z@htrQOR);4>HAmXfBS0M+S;Hw&q3eZ;#!=2?xv!oWZ2*bcXQhT2(-|s3T5r3e(4?C zQeK8$1RIEq_Qh8mVwg{|7%4*|23oQCCVV3wyK zkHT?81@#DMKcEeko~~C+e*`cz0c3=%tZdj_;f>9ireu}ztP8PC74X~VxR)uJ4mjEc z5;8s_^Qb{YD{5*|IM82B&aua^OGum*)NGhvznZTL1ha|&-d#-CvA4$YTa}2iv4zwa zAs2$c9PTm~p7gue?8}E3{u;5(eJ62>O zc@WMn{$&&~z}5UO^9HQxzqbOY&~9**BoV%N$|zUS&Ry)w<@z>t5b;Rl=@weO`l}Dw z{~s=}^IvwWig8mgHTUmj(XY$?hP6q;;{q1}*tycOoxHqDr*?mDZ)2>CC&} z;Ja>o{YuMoePwkO%+&i=AJRlL5l}9Vb!hvNhWR2o)GQPE-}&9RTu} z04)jPBhxL8#aI{k>QFwQW@5L6fGx-YN^wk!wgAH0NJ#}`ebm;~O;l$2A4Sbx$PiS*I2_Ljm$tXp@MSW3YW~%Wfl^*8y^SHJqStgO zL0XdbTdLQ=j7kyM9CftY;r0S3e7Rb=^YinU6HZ++kKG*x>lMvetb|`3Ob-}It`O~ToL^X2pR5_~_a7e{8)*ecA|8|CZ(QZt;*b^h zWIg?D?f0o42KiIg*4M5o_QMN-l#R`VD8df>anB1KG?=75&z4=E0pj#>6FR@}IrElamg`dO+GaU0++HtGqF~?hz&zg+OpEeM4RT$y2?*&LW_Js$>C4o9LiD zFE0;l?5;N)4i{Ix+T7nkm(pZJQc|tsWc68_&c^a`kwHEXTfuCi%7@zY%L~1s%gN2< zPwb)%V-XgPD~At0)v5<@o>B%LNbvUoBBvILBgV WJKP|ASc<-QLqS$Wra - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). - -stop() -> - gen_server:call(?SERVER, stop). - -%% ------------------------------------------------------------------ -%% gen_server Function Definitions -%% ------------------------------------------------------------------ - -init(_Args) -> - {ok, C} = emqttc:start_link([{host, "localhost"}, - {client_id, <<"simpleClient">>}, - {reconnect, 3}, - {logger, {console, info}}]), - %% The pending subscribe - emqttc:subscribe(C, <<"TopicA">>, 1), - {ok, #state{mqttc = C, seq = 1}}. - -handle_call(stop, _From, State) -> - {stop, normal, ok, State}; - -handle_call(_Request, _From, State) -> - {reply, ok, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -%% Publish Messages -handle_info(publish, State = #state{mqttc = C, seq = I}) -> - Payload = list_to_binary(["hello...", integer_to_list(I)]), - emqttc:publish(C, <<"TopicA">>, Payload, [{qos, 1}]), - emqttc:publish(C, <<"TopicB">>, Payload, [{qos, 2}]), - erlang:send_after(3000, self(), publish), - {noreply, State#state{seq = I+1}}; - -%% Receive Messages -handle_info({publish, Topic, Payload}, State) -> - io:format("Message from ~s: ~p~n", [Topic, Payload]), - {noreply, State}; - -%% Client connected -handle_info({mqttc, C, connected}, State = #state{mqttc = C}) -> - io:format("Client ~p is connected~n", [C]), - emqttc:subscribe(C, <<"TopicA">>, 1), - emqttc:subscribe(C, <<"TopicB">>, 2), - self() ! publish, - {noreply, State}; - -%% Client disconnected -handle_info({mqttc, C, disconnected}, State = #state{mqttc = C}) -> - io:format("Client ~p is disconnected~n", [C]), - {noreply, State}; - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - diff --git a/examples/simple/simple_example.erl b/examples/simple/simple_example.erl deleted file mode 100644 index 7d243076..00000000 --- a/examples/simple/simple_example.erl +++ /dev/null @@ -1,17 +0,0 @@ --module(simple_example). - --export([start/0]). - -start() -> - {ok, C} = emqttc:start_link([{host, "localhost"}, {client_id, <<"simpleClient">>}]), - emqttc:subscribe(C, <<"TopicA">>, 0), - emqttc:publish(C, <<"TopicA">>, <<"hello">>), - receive - {publish, Topic, Payload} -> - io:format("Message Received from ~s: ~p~n", [Topic, Payload]) - after - 1000 -> - io:format("Error: receive timeout!~n") - end, - emqttc:disconnect(C). - diff --git a/include/emqtt.hrl b/include/emqtt.hrl new file mode 100644 index 00000000..61a5738f --- /dev/null +++ b/include/emqtt.hrl @@ -0,0 +1,529 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2019 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. +%%-------------------------------------------------------------------- + +-ifndef(EMQTT_HRL). +-define(EMQTT_HRL, true). + +%%-------------------------------------------------------------------- +%% MQTT SockOpts +%%-------------------------------------------------------------------- + +-define(MQTT_SOCKOPTS, [binary, {packet, raw}, {reuseaddr, true}, + {backlog, 512}, {nodelay, true}]). + +%%-------------------------------------------------------------------- +%% MQTT Protocol Version and Names +%%-------------------------------------------------------------------- + +-define(MQTT_PROTO_V3, 3). +-define(MQTT_PROTO_V4, 4). +-define(MQTT_PROTO_V5, 5). + +-define(PROTOCOL_NAMES, [ + {?MQTT_PROTO_V3, <<"MQIsdp">>}, + {?MQTT_PROTO_V4, <<"MQTT">>}, + {?MQTT_PROTO_V5, <<"MQTT">>}]). + +%%-------------------------------------------------------------------- +%% MQTT QoS Levels +%%-------------------------------------------------------------------- + +-define(QOS_0, 0). %% At most once +-define(QOS_1, 1). %% At least once +-define(QOS_2, 2). %% Exactly once + +-define(IS_QOS(I), (I >= ?QOS_0 andalso I =< ?QOS_2)). + +-define(QOS_I(Name), + begin + (case Name of + ?QOS_0 -> ?QOS_0; + qos0 -> ?QOS_0; + at_most_once -> ?QOS_0; + ?QOS_1 -> ?QOS_1; + qos1 -> ?QOS_1; + at_least_once -> ?QOS_1; + ?QOS_2 -> ?QOS_2; + qos2 -> ?QOS_2; + exactly_once -> ?QOS_2 + end) + end). + +-define(IS_QOS_NAME(I), + (I =:= qos0 orelse I =:= at_most_once orelse + I =:= qos1 orelse I =:= at_least_once orelse + I =:= qos2 orelse I =:= exactly_once)). + +%%-------------------------------------------------------------------- +%% Maximum ClientId Length. +%%-------------------------------------------------------------------- + +-define(MAX_CLIENTID_LEN, 65535). + +%%-------------------------------------------------------------------- +%% MQTT Control Packet Types +%%-------------------------------------------------------------------- + +-define(RESERVED, 0). %% Reserved +-define(CONNECT, 1). %% Client request to connect to Server +-define(CONNACK, 2). %% Server to Client: Connect acknowledgment +-define(PUBLISH, 3). %% Publish message +-define(PUBACK, 4). %% Publish acknowledgment +-define(PUBREC, 5). %% Publish received (assured delivery part 1) +-define(PUBREL, 6). %% Publish release (assured delivery part 2) +-define(PUBCOMP, 7). %% Publish complete (assured delivery part 3) +-define(SUBSCRIBE, 8). %% Client subscribe request +-define(SUBACK, 9). %% Server Subscribe acknowledgment +-define(UNSUBSCRIBE, 10). %% Unsubscribe request +-define(UNSUBACK, 11). %% Unsubscribe acknowledgment +-define(PINGREQ, 12). %% PING request +-define(PINGRESP, 13). %% PING response +-define(DISCONNECT, 14). %% Client or Server is disconnecting +-define(AUTH, 15). %% Authentication exchange + +-define(TYPE_NAMES, [ + 'CONNECT', + 'CONNACK', + 'PUBLISH', + 'PUBACK', + 'PUBREC', + 'PUBREL', + 'PUBCOMP', + 'SUBSCRIBE', + 'SUBACK', + 'UNSUBSCRIBE', + 'UNSUBACK', + 'PINGREQ', + 'PINGRESP', + 'DISCONNECT', + 'AUTH']). + +%%-------------------------------------------------------------------- +%% MQTT V3.1.1 Connect Return Codes +%%-------------------------------------------------------------------- + +-define(CONNACK_ACCEPT, 0). %% Connection accepted +-define(CONNACK_PROTO_VER, 1). %% Unacceptable protocol version +-define(CONNACK_INVALID_ID, 2). %% Client Identifier is correct UTF-8 but not allowed by the Server +-define(CONNACK_SERVER, 3). %% Server unavailable +-define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed +-define(CONNACK_AUTH, 5). %% Client is not authorized to connect + +%%-------------------------------------------------------------------- +%% MQTT V5.0 Reason Codes +%%-------------------------------------------------------------------- + +-define(RC_SUCCESS, 16#00). +-define(RC_NORMAL_DISCONNECTION, 16#00). +-define(RC_GRANTED_QOS_0, 16#00). +-define(RC_GRANTED_QOS_1, 16#01). +-define(RC_GRANTED_QOS_2, 16#02). +-define(RC_DISCONNECT_WITH_WILL_MESSAGE, 16#04). +-define(RC_NO_MATCHING_SUBSCRIBERS, 16#10). +-define(RC_NO_SUBSCRIPTION_EXISTED, 16#11). +-define(RC_CONTINUE_AUTHENTICATION, 16#18). +-define(RC_RE_AUTHENTICATE, 16#19). +-define(RC_UNSPECIFIED_ERROR, 16#80). +-define(RC_MALFORMED_PACKET, 16#81). +-define(RC_PROTOCOL_ERROR, 16#82). +-define(RC_IMPLEMENTATION_SPECIFIC_ERROR, 16#83). +-define(RC_UNSUPPORTED_PROTOCOL_VERSION, 16#84). +-define(RC_CLIENT_IDENTIFIER_NOT_VALID, 16#85). +-define(RC_BAD_USER_NAME_OR_PASSWORD, 16#86). +-define(RC_NOT_AUTHORIZED, 16#87). +-define(RC_SERVER_UNAVAILABLE, 16#88). +-define(RC_SERVER_BUSY, 16#89). +-define(RC_BANNED, 16#8A). +-define(RC_SERVER_SHUTTING_DOWN, 16#8B). +-define(RC_BAD_AUTHENTICATION_METHOD, 16#8C). +-define(RC_KEEP_ALIVE_TIMEOUT, 16#8D). +-define(RC_SESSION_TAKEN_OVER, 16#8E). +-define(RC_TOPIC_FILTER_INVALID, 16#8F). +-define(RC_TOPIC_NAME_INVALID, 16#90). +-define(RC_PACKET_IDENTIFIER_IN_USE, 16#91). +-define(RC_PACKET_IDENTIFIER_NOT_FOUND, 16#92). +-define(RC_RECEIVE_MAXIMUM_EXCEEDED, 16#93). +-define(RC_TOPIC_ALIAS_INVALID, 16#94). +-define(RC_PACKET_TOO_LARGE, 16#95). +-define(RC_MESSAGE_RATE_TOO_HIGH, 16#96). +-define(RC_QUOTA_EXCEEDED, 16#97). +-define(RC_ADMINISTRATIVE_ACTION, 16#98). +-define(RC_PAYLOAD_FORMAT_INVALID, 16#99). +-define(RC_RETAIN_NOT_SUPPORTED, 16#9A). +-define(RC_QOS_NOT_SUPPORTED, 16#9B). +-define(RC_USE_ANOTHER_SERVER, 16#9C). +-define(RC_SERVER_MOVED, 16#9D). +-define(RC_SHARED_SUBSCRIPTIONS_NOT_SUPPORTED, 16#9E). +-define(RC_CONNECTION_RATE_EXCEEDED, 16#9F). +-define(RC_MAXIMUM_CONNECT_TIME, 16#A0). +-define(RC_SUBSCRIPTION_IDENTIFIERS_NOT_SUPPORTED, 16#A1). +-define(RC_WILDCARD_SUBSCRIPTIONS_NOT_SUPPORTED, 16#A2). + +%%-------------------------------------------------------------------- +%% Maximum MQTT Packet ID and Length +%%-------------------------------------------------------------------- + +-define(MAX_PACKET_ID, 16#ffff). +-define(MAX_PACKET_SIZE, 16#fffffff). + +%%-------------------------------------------------------------------- +%% MQTT Frame Mask +%%-------------------------------------------------------------------- + +-define(HIGHBIT, 2#10000000). +-define(LOWBITS, 2#01111111). + +%%-------------------------------------------------------------------- +%% MQTT Packet Fixed Header +%%-------------------------------------------------------------------- + +-record(mqtt_packet_header, { + type = ?RESERVED, + dup = false, + qos = ?QOS_0, + retain = false + }). + +%%-------------------------------------------------------------------- +%% MQTT Packets +%%-------------------------------------------------------------------- + +-define(DEFAULT_SUBOPTS, #{rh => 0, %% Retain Handling + rap => 0, %% Retain as Publish + nl => 0, %% No Local + qos => 0, %% QoS + rc => 0 %% Reason Code + }). + +-record(mqtt_packet_connect, { + proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V4, + is_bridge = false, + clean_start = true, + will_flag = false, + will_qos = ?QOS_0, + will_retain = false, + keepalive = 0, + properties = undefined, + client_id = <<>>, + will_props = undefined, + will_topic = undefined, + will_payload = undefined, + username = undefined, + password = undefined + }). + +-record(mqtt_packet_connack, { + ack_flags, + reason_code, + properties + }). + +-record(mqtt_packet_publish, { + topic_name, + packet_id, + properties + }). + +-record(mqtt_packet_puback, { + packet_id, + reason_code, + properties + }). + +-record(mqtt_packet_subscribe, { + packet_id, + properties, + topic_filters + }). + +-record(mqtt_packet_suback, { + packet_id, + properties, + reason_codes + }). + +-record(mqtt_packet_unsubscribe, { + packet_id, + properties, + topic_filters + }). + +-record(mqtt_packet_unsuback, { + packet_id, + properties, + reason_codes + }). + +-record(mqtt_packet_disconnect, { + reason_code, + properties + }). + +-record(mqtt_packet_auth, { + reason_code, + properties + }). + +%%-------------------------------------------------------------------- +%% MQTT Control Packet +%%-------------------------------------------------------------------- + +-record(mqtt_packet, { + header :: #mqtt_packet_header{}, + variable :: #mqtt_packet_connect{} + | #mqtt_packet_connack{} + | #mqtt_packet_publish{} + | #mqtt_packet_puback{} + | #mqtt_packet_subscribe{} + | #mqtt_packet_suback{} + | #mqtt_packet_unsubscribe{} + | #mqtt_packet_unsuback{} + | #mqtt_packet_disconnect{} + | #mqtt_packet_auth{} + | pos_integer() + | undefined, + payload :: binary() | undefined + }). + +%%-------------------------------------------------------------------- +%% MQTT Packet Match +%%-------------------------------------------------------------------- + +-define(CONNECT_PACKET(Var), + #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, + variable = Var}). + +-define(CONNACK_PACKET(ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, + variable = #mqtt_packet_connack{ack_flags = 0, + reason_code = ReasonCode} + }). + +-define(CONNACK_PACKET(ReasonCode, SessPresent), + #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, + variable = #mqtt_packet_connack{ack_flags = SessPresent, + reason_code = ReasonCode} + }). + +-define(CONNACK_PACKET(ReasonCode, SessPresent, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, + variable = #mqtt_packet_connack{ack_flags = SessPresent, + reason_code = ReasonCode, + properties = Properties} + }). + +-define(AUTH_PACKET(), + #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, + variable = #mqtt_packet_auth{reason_code = 0} + }). + +-define(AUTH_PACKET(ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, + variable = #mqtt_packet_auth{reason_code = ReasonCode} + }). + +-define(AUTH_PACKET(ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?AUTH}, + variable = #mqtt_packet_auth{reason_code = ReasonCode, + properties = Properties} + }). + +-define(PUBLISH_PACKET(QoS), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, qos = QoS}}). + +-define(PUBLISH_PACKET(QoS, PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = QoS}, + variable = #mqtt_packet_publish{packet_id = PacketId} + }). + +-define(PUBLISH_PACKET(QoS, Topic, PacketId, Payload), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = QoS}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId}, + payload = Payload + }). + +-define(PUBLISH_PACKET(QoS, Topic, PacketId, Properties, Payload), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = QoS}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + properties = Properties}, + payload = Payload + }). + +-define(PUBACK_PACKET(PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0} + }). + +-define(PUBACK_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode} + }). + +-define(PUBACK_PACKET(PacketId, ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBACK}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties} + }). + +-define(PUBREC_PACKET(PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0} + }). + +-define(PUBREC_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode} + }). + +-define(PUBREC_PACKET(PacketId, ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREC}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties} + }). + +-define(PUBREL_PACKET(PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, + qos = ?QOS_1}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0} + }). + +-define(PUBREL_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, + qos = ?QOS_1}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode} + }). + +-define(PUBREL_PACKET(PacketId, ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, + qos = ?QOS_1}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties} + }). + +-define(PUBCOMP_PACKET(PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = 0} + }). + +-define(PUBCOMP_PACKET(PacketId, ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode} + }). + +-define(PUBCOMP_PACKET(PacketId, ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBCOMP}, + variable = #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties} + }). + +-define(SUBSCRIBE_PACKET(PacketId, TopicFilters), + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, + qos = ?QOS_1}, + variable = #mqtt_packet_subscribe{packet_id = PacketId, + topic_filters = TopicFilters} + }). + +-define(SUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, + qos = ?QOS_1}, + variable = #mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters} + }). + +-define(SUBACK_PACKET(PacketId, ReasonCodes), + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, + variable = #mqtt_packet_suback{packet_id = PacketId, + reason_codes = ReasonCodes} + }). + +-define(SUBACK_PACKET(PacketId, Properties, ReasonCodes), + #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, + variable = #mqtt_packet_suback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes} + }). + +-define(UNSUBSCRIBE_PACKET(PacketId, TopicFilters), + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, + qos = ?QOS_1}, + variable = #mqtt_packet_unsubscribe{packet_id = PacketId, + topic_filters = TopicFilters} + }). + +-define(UNSUBSCRIBE_PACKET(PacketId, Properties, TopicFilters), + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, + qos = ?QOS_1}, + variable = #mqtt_packet_unsubscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters} + }). + +-define(UNSUBACK_PACKET(PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, + variable = #mqtt_packet_unsuback{packet_id = PacketId} + }). + +-define(UNSUBACK_PACKET(PacketId, ReasonCodes), + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, + variable = #mqtt_packet_unsuback{packet_id = PacketId, + reason_codes = ReasonCodes} + }). + +-define(UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), + #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, + variable = #mqtt_packet_unsuback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes} + }). + +-define(DISCONNECT_PACKET(), + #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, + variable = #mqtt_packet_disconnect{reason_code = 0} + }). + +-define(DISCONNECT_PACKET(ReasonCode), + #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, + variable = #mqtt_packet_disconnect{reason_code = ReasonCode} + }). + +-define(DISCONNECT_PACKET(ReasonCode, Properties), + #mqtt_packet{header = #mqtt_packet_header{type = ?DISCONNECT}, + variable = #mqtt_packet_disconnect{reason_code = ReasonCode, + properties = Properties} + }). + +-define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). + +-endif. diff --git a/include/emqttc_packet.hrl b/include/emqttc_packet.hrl deleted file mode 100644 index 2dd38af7..00000000 --- a/include/emqttc_packet.hrl +++ /dev/null @@ -1,258 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% @Copyright (C) 2012-2015, Feng Lee -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc packet header. -%%% @end -%%%----------------------------------------------------------------------------- - -%%------------------------------------------------------------------------------ -%% Logging wrappers -%%------------------------------------------------------------------------------ - -%% this macro only exists in OTP21 and above where logger already exists --ifdef(OTP_RELEASE). --define(debug(Message, Opts), logger:debug(Message, Opts)). --define(info(Message, Opts), logger:info(Message, Opts)). --define(warn(Message, Opts), logger:warning(Message, Opts)). --define(error(Message, Opts), logger:error(Message, Opts)). --else. --define(debug(Message, Opts), error_logger:info_msg(Message, Opts)). --define(info(Message, Opts), error_logger:info_msg(Message, Opts)). --define(warn(Message, Opts), error_logger:warning_msg(Message, Opts)). --define(error(Message, Opts), error_logger:error_msg(Message, Opts)). --endif. - -%%------------------------------------------------------------------------------ -%% MQTT Protocol Version and Levels -%%------------------------------------------------------------------------------ --define(MQTT_PROTO_V31, 3). --define(MQTT_PROTO_V311, 4). - --define(PROTOCOL_NAMES, [ - {?MQTT_PROTO_V31, <<"MQIsdp">>}, - {?MQTT_PROTO_V311, <<"MQTT">>}]). - --type mqtt_vsn() :: ?MQTT_PROTO_V31 | ?MQTT_PROTO_V311. - -%%------------------------------------------------------------------------------ -%% QoS Levels -%%------------------------------------------------------------------------------ - --define(QOS_0, 0). --define(QOS_1, 1). --define(QOS_2, 2). --define(QOS_UNAUTHORIZED, 128). - --define(IS_QOS(I), (I >= ?QOS_0 andalso I =< ?QOS_2)). - --type mqtt_qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2. - -%%------------------------------------------------------------------------------ -%% Default Keepalive Timeout(secs) -%%------------------------------------------------------------------------------ --define(KEEPALIVE, 90). - -%%------------------------------------------------------------------------------ -%% Max ClientId Length. Why 1024? NiDongDe! -%%------------------------------------------------------------------------------ --define(MAX_CLIENTID_LEN, 1024). - -%%------------------------------------------------------------------------------ -%% MQTT Control Packet Types -%%------------------------------------------------------------------------------ --define(RESERVED, 0). %% Reserved --define(CONNECT, 1). %% Client request to connect to Server --define(CONNACK, 2). %% Server to Client: Connect acknowledgment --define(PUBLISH, 3). %% Publish message --define(PUBACK, 4). %% Publish acknowledgment --define(PUBREC, 5). %% Publish received (assured delivery part 1) --define(PUBREL, 6). %% Publish release (assured delivery part 2) --define(PUBCOMP, 7). %% Publish complete (assured delivery part 3) --define(SUBSCRIBE, 8). %% Client subscribe request --define(SUBACK, 9). %% Server Subscribe acknowledgment --define(UNSUBSCRIBE, 10). %% Unsubscribe request --define(UNSUBACK, 11). %% Unsubscribe acknowledgment --define(PINGREQ, 12). %% PING request --define(PINGRESP, 13). %% PING response --define(DISCONNECT, 14). %% Client is disconnecting - --define(TYPE_NAMES, [ - 'CONNECT', - 'CONNACK', - 'PUBLISH', - 'PUBACK', - 'PUBREC', - 'PUBREL', - 'PUBCOMP', - 'SUBSCRIBE', - 'SUBACK', - 'UNSUBSCRIBE', - 'UNSUBACK', - 'PINGREQ', - 'PINGRESP', - 'DISCONNECT']). - --type mqtt_packet_type() :: ?RESERVED..?DISCONNECT. - -%%------------------------------------------------------------------------------ -%% MQTT Connect Return Codes -%%------------------------------------------------------------------------------ --define(CONNACK_ACCEPT, 0). %% Connection accepted --define(CONNACK_PROTO_VER, 1). %% Unacceptable protocol version --define(CONNACK_INVALID_ID, 2). %% Client Identifier is correct UTF-8 but not allowed by the Server --define(CONNACK_SERVER, 3). %% Server unavailable --define(CONNACK_CREDENTIALS, 4). %% Username or password is malformed --define(CONNACK_AUTH, 5). %% Client is not authorized to connect - --type mqtt_connack() :: ?CONNACK_ACCEPT..?CONNACK_AUTH. - -%%------------------------------------------------------------------------------ -%% MQTT Parser and Serialiser -%%------------------------------------------------------------------------------ --define(MAX_LEN, 16#fffffff). --define(HIGHBIT, 2#10000000). --define(LOWBITS, 2#01111111). - -%%------------------------------------------------------------------------------ -%% MQTT Packet Fixed Header -%%------------------------------------------------------------------------------ --record(mqtt_packet_header, { - type = ?RESERVED :: mqtt_packet_type(), - dup = false :: boolean(), - qos = ?QOS_0 :: mqtt_qos(), - retain = false :: boolean()}). - -%%------------------------------------------------------------------------------ -%% MQTT Packets -%%------------------------------------------------------------------------------ --type mqtt_packet_id() :: 1..16#ffff | undefined. - --record(mqtt_packet_connect, { - client_id = <<>> :: binary(), - proto_ver = ?MQTT_PROTO_V311 :: mqtt_vsn(), - proto_name = <<"MQTT">> :: binary(), - will_retain = false :: boolean(), - will_qos = ?QOS_0 :: mqtt_qos(), - will_flag = false :: boolean(), - clean_sess = false :: boolean(), - keep_alive = 60 :: non_neg_integer(), - will_topic = undefined :: undefined | binary(), - will_msg = undefined :: undefined | binary(), - username = undefined :: undefined | binary(), - password = undefined :: undefined | binary()}). - --record(mqtt_packet_connack, { - ack_flags = ?RESERVED :: 0 | 1, - return_code :: mqtt_connack() }). - --record(mqtt_packet_publish, { - topic_name :: binary(), - packet_id :: mqtt_packet_id() }). - --record(mqtt_packet_puback, { - packet_id :: mqtt_packet_id() }). - --record(mqtt_packet_subscribe, { - packet_id :: mqtt_packet_id(), - topic_table :: list({binary(), mqtt_qos()}) }). - --record(mqtt_packet_unsubscribe, { - packet_id :: mqtt_packet_id(), - topics :: list(binary()) }). - --record(mqtt_packet_suback, { - packet_id :: mqtt_packet_id(), - qos_table :: list(mqtt_qos() | 128) }). - --record(mqtt_packet_unsuback, { - packet_id :: mqtt_packet_id() }). - -%%------------------------------------------------------------------------------ -%% MQTT Control Packet -%%------------------------------------------------------------------------------ --record(mqtt_packet, { - header :: #mqtt_packet_header{}, - variable :: #mqtt_packet_connect{} | #mqtt_packet_connack{} - | #mqtt_packet_publish{} | #mqtt_packet_puback{} - | #mqtt_packet_subscribe{} | #mqtt_packet_suback{} - | #mqtt_packet_unsubscribe{} | #mqtt_packet_unsuback{} - | mqtt_packet_id() | undefined, - payload :: binary() | undefined }). - --type mqtt_packet() :: #mqtt_packet{}. - -%%------------------------------------------------------------------------------ -%% MQTT Packet Match -%%------------------------------------------------------------------------------ --define(CONNECT_PACKET(Packet), - #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, variable = Packet}). - --define(CONNACK_PACKET(ReturnCode), - #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, - variable = #mqtt_packet_connack{return_code = ReturnCode}}). - --define(PUBLISH_PACKET(Qos, Topic, PacketId, Payload), - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, - qos = Qos}, - variable = #mqtt_packet_publish{topic_name = Topic, - packet_id = PacketId}, - payload = Payload}). - --define(PUBACK_PACKET(Type, PacketId), - #mqtt_packet{header = #mqtt_packet_header{type = Type}, - variable = #mqtt_packet_puback{packet_id = PacketId}}). - --define(PUBREL_PACKET(PacketId), - #mqtt_packet{header = #mqtt_packet_header{type = ?PUBREL, qos = ?QOS_1}, - variable = #mqtt_packet_puback{packet_id = PacketId}}). - --define(SUBSCRIBE_PACKET(PacketId, TopicTable), - #mqtt_packet{header = #mqtt_packet_header{type = ?SUBSCRIBE, qos = ?QOS_1}, - variable = #mqtt_packet_subscribe{packet_id = PacketId, - topic_table = TopicTable}}). --define(SUBACK_PACKET(PacketId, QosTable), - #mqtt_packet{header = #mqtt_packet_header{type = ?SUBACK}, - variable = #mqtt_packet_suback{packet_id = PacketId, - qos_table = QosTable}}). --define(UNSUBSCRIBE_PACKET(PacketId, Topics), - #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBSCRIBE, qos = ?QOS_1}, - variable = #mqtt_packet_unsubscribe{packet_id = PacketId, - topics = Topics}}). --define(UNSUBACK_PACKET(PacketId), - #mqtt_packet{header = #mqtt_packet_header{type = ?UNSUBACK}, - variable = #mqtt_packet_unsuback{packet_id = PacketId}}). - --define(PACKET(Type), - #mqtt_packet{header = #mqtt_packet_header{type = Type}}). - -%%------------------------------------------------------------------------------ -%% MQTT Message -%%------------------------------------------------------------------------------ --record(mqtt_message, { - qos = ?QOS_0 :: mqtt_qos(), - retain = false :: boolean(), - dup = false :: boolean(), - msgid :: mqtt_packet_id(), - topic :: binary(), - payload :: binary()}). - --type mqtt_message() :: #mqtt_message{}. diff --git a/rebar b/rebar deleted file mode 100755 index c2b7e20224dff58e53a9ed3f149efbef8586b9c4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 182326 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%0AcqG2JHpOn!G;(!qD1ktteX${pHvVNZ~~I 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�@K+#^FuA;X-b&2l7tp@D$jHch1q;iW1T{AXeu$} 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 diff --git a/rebar.config b/rebar.config index c29cd84e..2101cbd4 100644 --- a/rebar.config +++ b/rebar.config @@ -1,25 +1,9 @@ -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- -%% ex: ts=4 sw=4 ft=erlang et - -{require_min_otp_vsn, "R17"}. - -%warnings_as_errors, warn_untyped_record, -{erl_opts, [ +{minimum_otp_vsn, "21.0"}. +{erl_opts, [debug_info, warn_export_all, - warn_unused_import, - {i, "include"}, - {src_dirs, ["src"]} - ]}. - + warn_unused_import + ]}. +{cover_enabled, true}. {xref_checks, [undefined_function_calls]}. -{cover_enabled, false}. - -{edoc_opts, [{dialyzer_specs, all}, - {report_missing_type, true}, - {report_type_mismatch, true}, - {pretty_print, erl_pp}, - {preprocess, true}]}. - -{validate_app_modules, true}. - -{deps, []}. +{eunit_opts, [verbose, {report,{eunit_surefire,[{dir,"."}]}}]}. +{dialyzer_base_plt_apps, [kernel, stdlib, erts, sasl, ssl, syntax_tools, compiler, crypto]}. diff --git a/run b/run deleted file mode 100755 index f3e39d46..00000000 --- a/run +++ /dev/null @@ -1,6 +0,0 @@ - -#!/bin/sh -# -*- tab-width:4;indent-tabs-mode:nil -*- -# ex: ts=4 sw=4 et - -erl -pa ebin -pa deps/*/ebin -s emqttc diff --git a/src/emqtt.app.src b/src/emqtt.app.src new file mode 100644 index 00000000..ec2cf85a --- /dev/null +++ b/src/emqtt.app.src @@ -0,0 +1,15 @@ +{application, emqtt, + [{id, "emqtt"}, + {description, "Erlang MQTT v5.0 Client"}, + {vsn, "2.0.0"}, + {registered, []}, + {applications, + [kernel, + stdlib + ]}, + {env,[]}, + {modules, []}, + {maintainers,["Feng Lee "]}, + {licenses, ["Apache-2.0"]}, + {links, [{"Github", "https://github.com/emqx/emqtt"}]} + ]}. diff --git a/src/emqtt.erl b/src/emqtt.erl new file mode 100644 index 00000000..01fc8bf9 --- /dev/null +++ b/src/emqtt.erl @@ -0,0 +1,1343 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2019 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(emqtt). + +-behaviour(gen_statem). + +-include("emqtt.hrl"). + +-export([ start_link/0 + , start_link/1 + ]). + +-export([ connect/1 + , disconnect/1 + , disconnect/2 + , disconnect/3 + ]). + +-export([ping/1]). + +%% PubSub +-export([ subscribe/2 + , subscribe/3 + , subscribe/4 + , publish/2 + , publish/3 + , publish/4 + , publish/5 + , unsubscribe/2 + , unsubscribe/3 + ]). + +%% Puback... +-export([ puback/2 + , puback/3 + , puback/4 + , pubrec/2 + , pubrec/3 + , pubrec/4 + , pubrel/2 + , pubrel/3 + , pubrel/4 + , pubcomp/2 + , pubcomp/3 + , pubcomp/4 + ]). + +-export([subscriptions/1]). + +-export([info/1, stop/1]). + +%% For test cases +-export([ pause/1 + , resume/1 + ]). + +-export([ initialized/3 + , waiting_for_connack/3 + , connected/3 + , inflight_full/3 + ]). + +-export([ init/1 + , callback_mode/0 + , handle_event/4 + , terminate/3 + , code_change/4 + ]). + +-export_type([ host/0 + , option/0 + , properties/0 + , payload/0 + , pubopt/0 + , subopt/0 + , mqtt_msg/0 + , client/0 + ]). + +-type(host() :: inet:ip_address() | inet:hostname()). + +%% Message handler is a set of callbacks defined to handle MQTT messages +%% as well as the disconnect event. +-define(NO_MSG_HDLR, undefined). +-type(msg_handler() :: #{puback := fun((_) -> any()), + publish := fun((emqx_types:message()) -> any()), + disconnected := fun(({reason_code(), _Properties :: term()}) -> any()) + }). + +-type(option() :: {name, atom()} + | {owner, pid()} + | {msg_handler, msg_handler()} + | {host, host()} + | {hosts, [{host(), inet:port_number()}]} + | {port, inet:port_number()} + | {tcp_opts, [gen_tcp:option()]} + | {ssl, boolean()} + | {ssl_opts, [ssl:ssl_option()]} + | {connect_timeout, pos_integer()} + | {bridge_mode, boolean()} + | {client_id, iodata()} + | {clean_start, boolean()} + | {username, iodata()} + | {password, iodata()} + | {proto_ver, v3 | v4 | v5} + | {keepalive, non_neg_integer()} + | {max_inflight, pos_integer()} + | {retry_interval, timeout()} + | {will_topic, iodata()} + | {will_payload, iodata()} + | {will_retain, boolean()} + | {will_qos, qos()} + | {will_props, properties()} + | {auto_ack, boolean()} + | {ack_timeout, pos_integer()} + | {force_ping, boolean()} + | {properties, properties()}). + +-type(maybe(T) :: undefined | T). +-type(topic() :: binary()). +-type(payload() :: iodata()). +-type(packet_id() :: 0..16#FF). +-type(reason_code() :: 0..16#FF). +-type(properties() :: #{atom() => term()}). +-type(version() :: ?MQTT_PROTO_V3 + | ?MQTT_PROTO_V4 + | ?MQTT_PROTO_V5). +-type(qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2). +-type(qos_name() :: qos0 | at_most_once | + qos1 | at_least_once | + qos2 | exactly_once). +-type(pubopt() :: {retain, boolean()} + | {qos, qos() | qos_name()} + | {timeout, timeout()}). +-type(subopt() :: {rh, 0 | 1 | 2} + | {rap, boolean()} + | {nl, boolean()} + | {qos, qos() | qos_name()}). + +-type(subscribe_ret() :: + {ok, properties(), [reason_code()]} | {error, term()}). + +-type(client() :: pid() | atom()). + +-record(mqtt_msg, { + qos = ?QOS_0, + retain = false, + dup = false, + packet_id, + topic, + props, + payload + }). + +-opaque(mqtt_msg() :: #mqtt_msg{}). + +-record(state, {name :: atom(), + owner :: pid(), + msg_handler :: ?NO_MSG_HDLR | msg_handler(), + host :: host(), + port :: inet:port_number(), + hosts :: [{host(), inet:port_number()}], + socket :: inet:socket(), + sock_opts :: [emqtt_sock:option()], + connect_timeout :: pos_integer(), + bridge_mode :: boolean(), + client_id :: binary(), + clean_start :: boolean(), + username :: maybe(binary()), + password :: maybe(binary()), + proto_ver :: version(), + proto_name :: iodata(), + keepalive :: non_neg_integer(), + keepalive_timer :: maybe(reference()), + force_ping :: boolean(), + paused :: boolean(), + will_flag :: boolean(), + will_msg :: mqtt_msg(), + properties :: properties(), + pending_calls :: list(), + subscriptions :: map(), + max_inflight :: infinity | pos_integer(), + inflight :: #{packet_id() => term()}, + awaiting_rel :: map(), + auto_ack :: boolean(), + ack_timeout :: pos_integer(), + ack_timer :: reference(), + retry_interval :: pos_integer(), + retry_timer :: reference(), + session_present :: boolean(), + last_packet_id :: packet_id(), + parse_state :: emqtt_frame:parse_state() + }). + +-record(call, {id, from, req, ts}). + + +%% Default timeout +-define(DEFAULT_KEEPALIVE, 60). +-define(DEFAULT_ACK_TIMEOUT, 30000). +-define(DEFAULT_CONNECT_TIMEOUT, 60000). + +-define(PROPERTY(Name, Val), #state{properties = #{Name := Val}}). + +-define(WILL_MSG(QoS, Retain, Topic, Props, Payload), + #mqtt_msg{qos = QoS, + retain = Retain, + topic = Topic, + props = Props, + payload = Payload + }). + +-define(NO_CLIENT_ID, <<>>). + +-define(LOG(Level, Format, Args, State), + begin + (logger:log(Level, #{}, #{report_cb => fun(_) -> {"emqtt(~s): "++(Format), ([State#state.client_id|Args])} end})) + end). + +%%-------------------------------------------------------------------- +%% API +%%-------------------------------------------------------------------- + +-spec(start_link() -> gen_statem:start_ret()). +start_link() -> start_link([]). + +-spec(start_link(map() | [option()]) -> gen_statem:start_ret()). +start_link(Options) when is_map(Options) -> + start_link(maps:to_list(Options)); +start_link(Options) when is_list(Options) -> + ok = emqtt_props:validate( + proplists:get_value(properties, Options, #{})), + case proplists:get_value(name, Options) of + undefined -> + gen_statem:start_link(?MODULE, [with_owner(Options)], []); + Name when is_atom(Name) -> + gen_statem:start_link({local, Name}, ?MODULE, [with_owner(Options)], []) + end. + +with_owner(Options) -> + case proplists:get_value(owner, Options) of + Owner when is_pid(Owner) -> Options; + undefined -> [{owner, self()} | Options] + end. + +-spec(connect(client()) -> {ok, properties()} | {error, term()}). +connect(Client) -> + gen_statem:call(Client, connect, infinity). + +-spec(subscribe(client(), topic() | {topic(), qos() | qos_name() | [subopt()]} | [{topic(), qos()}]) + -> subscribe_ret()). +subscribe(Client, Topic) when is_binary(Topic) -> + subscribe(Client, {Topic, ?QOS_0}); +subscribe(Client, {Topic, QoS}) when is_binary(Topic), is_atom(QoS) -> + subscribe(Client, {Topic, ?QOS_I(QoS)}); +subscribe(Client, {Topic, QoS}) when is_binary(Topic), ?IS_QOS(QoS) -> + subscribe(Client, [{Topic, ?QOS_I(QoS)}]); +subscribe(Client, Topics) when is_list(Topics) -> + subscribe(Client, #{}, lists:map( + fun({Topic, QoS}) when is_binary(Topic), is_atom(QoS) -> + {Topic, [{qos, ?QOS_I(QoS)}]}; + ({Topic, QoS}) when is_binary(Topic), ?IS_QOS(QoS) -> + {Topic, [{qos, ?QOS_I(QoS)}]}; + ({Topic, Opts}) when is_binary(Topic), is_list(Opts) -> + {Topic, Opts} + end, Topics)). + +-spec(subscribe(client(), topic(), qos() | qos_name() | [subopt()]) -> + subscribe_ret(); + (client(), properties(), [{topic(), qos() | [subopt()]}]) -> + subscribe_ret()). +subscribe(Client, Topic, QoS) when is_binary(Topic), is_atom(QoS) -> + subscribe(Client, Topic, ?QOS_I(QoS)); +subscribe(Client, Topic, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> + subscribe(Client, Topic, [{qos, QoS}]); +subscribe(Client, Topic, Opts) when is_binary(Topic), is_list(Opts) -> + subscribe(Client, #{}, [{Topic, Opts}]); +subscribe(Client, Properties, Topics) when is_map(Properties), is_list(Topics) -> + Topics1 = [{Topic, parse_subopt(Opts)} || {Topic, Opts} <- Topics], + gen_statem:call(Client, {subscribe, Properties, Topics1}). + +-spec(subscribe(client(), properties(), topic(), qos() | qos_name() | [subopt()]) + -> subscribe_ret()). +subscribe(Client, Properties, Topic, QoS) + when is_map(Properties), is_binary(Topic), is_atom(QoS) -> + subscribe(Client, Properties, Topic, ?QOS_I(QoS)); +subscribe(Client, Properties, Topic, QoS) + when is_map(Properties), is_binary(Topic), ?IS_QOS(QoS) -> + subscribe(Client, Properties, Topic, [{qos, QoS}]); +subscribe(Client, Properties, Topic, Opts) + when is_map(Properties), is_binary(Topic), is_list(Opts) -> + subscribe(Client, Properties, [{Topic, Opts}]). + +parse_subopt(Opts) -> + parse_subopt(Opts, #{rh => 0, rap => 0, nl => 0, qos => ?QOS_0}). + +parse_subopt([], Result) -> + Result; +parse_subopt([{rh, I} | Opts], Result) when I >= 0, I =< 2 -> + parse_subopt(Opts, Result#{rh := I}); +parse_subopt([{rap, true} | Opts], Result) -> + parse_subopt(Opts, Result#{rap := 1}); +parse_subopt([{rap, false} | Opts], Result) -> + parse_subopt(Opts, Result#{rap := 0}); +parse_subopt([{nl, true} | Opts], Result) -> + parse_subopt(Opts, Result#{nl := 1}); +parse_subopt([{nl, false} | Opts], Result) -> + parse_subopt(Opts, Result#{nl := 0}); +parse_subopt([{qos, QoS} | Opts], Result) -> + parse_subopt(Opts, Result#{qos := ?QOS_I(QoS)}). + +-spec(publish(client(), topic(), payload()) -> ok | {error, term()}). +publish(Client, Topic, Payload) when is_binary(Topic) -> + publish(Client, #mqtt_msg{topic = Topic, qos = ?QOS_0, payload = iolist_to_binary(Payload)}). + +-spec(publish(client(), topic(), payload(), qos() | qos_name() | [pubopt()]) + -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Topic, Payload, QoS) when is_binary(Topic), is_atom(QoS) -> + publish(Client, Topic, Payload, [{qos, ?QOS_I(QoS)}]); +publish(Client, Topic, Payload, QoS) when is_binary(Topic), ?IS_QOS(QoS) -> + publish(Client, Topic, Payload, [{qos, QoS}]); +publish(Client, Topic, Payload, Opts) when is_binary(Topic), is_list(Opts) -> + publish(Client, Topic, #{}, Payload, Opts). + +-spec(publish(client(), topic(), properties(), payload(), [pubopt()]) + -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Topic, Properties, Payload, Opts) + when is_binary(Topic), is_map(Properties), is_list(Opts) -> + ok = emqtt_props:validate(Properties), + Retain = proplists:get_bool(retain, Opts), + QoS = ?QOS_I(proplists:get_value(qos, Opts, ?QOS_0)), + publish(Client, #mqtt_msg{qos = QoS, + retain = Retain, + topic = Topic, + props = Properties, + payload = iolist_to_binary(Payload)}). + +-spec(publish(client(), #mqtt_msg{}) -> ok | {ok, packet_id()} | {error, term()}). +publish(Client, Msg) -> + gen_statem:call(Client, {publish, Msg}). + +-spec(unsubscribe(client(), topic() | [topic()]) -> subscribe_ret()). +unsubscribe(Client, Topic) when is_binary(Topic) -> + unsubscribe(Client, [Topic]); +unsubscribe(Client, Topics) when is_list(Topics) -> + unsubscribe(Client, #{}, Topics). + +-spec(unsubscribe(client(), properties(), topic() | [topic()]) -> subscribe_ret()). +unsubscribe(Client, Properties, Topic) when is_map(Properties), is_binary(Topic) -> + unsubscribe(Client, Properties, [Topic]); +unsubscribe(Client, Properties, Topics) when is_map(Properties), is_list(Topics) -> + gen_statem:call(Client, {unsubscribe, Properties, Topics}). + +-spec(ping(client()) -> pong). +ping(Client) -> + gen_statem:call(Client, ping). + +-spec(disconnect(client()) -> ok). +disconnect(Client) -> + disconnect(Client, ?RC_SUCCESS). + +-spec(disconnect(client(), reason_code()) -> ok). +disconnect(Client, ReasonCode) -> + disconnect(Client, ReasonCode, #{}). + +-spec(disconnect(client(), reason_code(), properties()) -> ok). +disconnect(Client, ReasonCode, Properties) -> + gen_statem:call(Client, {disconnect, ReasonCode, Properties}). + +%%-------------------------------------------------------------------- +%% For test cases +%%-------------------------------------------------------------------- + +puback(Client, PacketId) when is_integer(PacketId) -> + puback(Client, PacketId, ?RC_SUCCESS). +puback(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> + puback(Client, PacketId, ReasonCode, #{}). +puback(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> + gen_statem:cast(Client, {puback, PacketId, ReasonCode, Properties}). + +pubrec(Client, PacketId) when is_integer(PacketId) -> + pubrec(Client, PacketId, ?RC_SUCCESS). +pubrec(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> + pubrec(Client, PacketId, ReasonCode, #{}). +pubrec(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> + gen_statem:cast(Client, {pubrec, PacketId, ReasonCode, Properties}). + +pubrel(Client, PacketId) when is_integer(PacketId) -> + pubrel(Client, PacketId, ?RC_SUCCESS). +pubrel(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> + pubrel(Client, PacketId, ReasonCode, #{}). +pubrel(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> + gen_statem:cast(Client, {pubrel, PacketId, ReasonCode, Properties}). + +pubcomp(Client, PacketId) when is_integer(PacketId) -> + pubcomp(Client, PacketId, ?RC_SUCCESS). +pubcomp(Client, PacketId, ReasonCode) + when is_integer(PacketId), is_integer(ReasonCode) -> + pubcomp(Client, PacketId, ReasonCode, #{}). +pubcomp(Client, PacketId, ReasonCode, Properties) + when is_integer(PacketId), is_integer(ReasonCode), is_map(Properties) -> + gen_statem:cast(Client, {pubcomp, PacketId, ReasonCode, Properties}). + +subscriptions(Client) -> + gen_statem:call(Client, subscriptions). + +info(Client) -> + gen_statem:call(Client, info). + +stop(Client) -> + gen_statem:call(Client, stop). + +pause(Client) -> + gen_statem:call(Client, pause). + +resume(Client) -> + gen_statem:call(Client, resume). + +%%-------------------------------------------------------------------- +%% gen_statem callbacks +%%-------------------------------------------------------------------- + +init([Options]) -> + process_flag(trap_exit, true), + ClientId = case {proplists:get_value(proto_ver, Options, v4), + proplists:get_value(client_id, Options)} of + {v5, undefined} -> ?NO_CLIENT_ID; + {_ver, undefined} -> random_client_id(); + {_ver, Id} -> iolist_to_binary(Id) + end, + State = init(Options, #state{host = {127,0,0,1}, + port = 1883, + hosts = [], + sock_opts = [], + bridge_mode = false, + client_id = ClientId, + clean_start = true, + proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>, + keepalive = ?DEFAULT_KEEPALIVE, + force_ping = false, + paused = false, + will_flag = false, + will_msg = #mqtt_msg{}, + pending_calls = [], + subscriptions = #{}, + max_inflight = infinity, + inflight = #{}, + awaiting_rel = #{}, + properties = #{}, + auto_ack = true, + ack_timeout = ?DEFAULT_ACK_TIMEOUT, + retry_interval = 0, + connect_timeout = ?DEFAULT_CONNECT_TIMEOUT, + last_packet_id = 1 + }), + {ok, initialized, init_parse_state(State)}. + +random_client_id() -> + rand:seed(exsplus, erlang:timestamp()), + I1 = rand:uniform(round(math:pow(2, 48))) - 1, + I2 = rand:uniform(round(math:pow(2, 32))) - 1, + {ok, Host} = inet:gethostname(), + RandId = io_lib:format("~12.16.0b~8.16.0b", [I1, I2]), + iolist_to_binary(["emqtt-", Host, "-", RandId]). + +init([], State) -> + State; +init([{name, Name} | Opts], State) -> + init(Opts, State#state{name = Name}); +init([{owner, Owner} | Opts], State) when is_pid(Owner) -> + link(Owner), + init(Opts, State#state{owner = Owner}); +init([{msg_handler, Hdlr} | Opts], State) -> + init(Opts, State#state{msg_handler = Hdlr}); +init([{host, Host} | Opts], State) -> + init(Opts, State#state{host = Host}); +init([{port, Port} | Opts], State) -> + init(Opts, State#state{port = Port}); +init([{hosts, Hosts} | Opts], State) -> + Hosts1 = + lists:foldl(fun({Host, Port}, Acc) -> + [{Host, Port}|Acc]; + (Host, Acc) -> + [{Host, 1883}|Acc] + end, [], Hosts), + init(Opts, State#state{hosts = Hosts1}); +init([{tcp_opts, TcpOpts} | Opts], State = #state{sock_opts = SockOpts}) -> + init(Opts, State#state{sock_opts = merge_opts(SockOpts, TcpOpts)}); +init([{ssl, EnableSsl} | Opts], State) -> + case lists:keytake(ssl_opts, 1, Opts) of + {value, SslOpts, WithOutSslOpts} -> + init([SslOpts, {ssl, EnableSsl}| WithOutSslOpts], State); + false -> + init([{ssl_opts, []}, {ssl, EnableSsl}| Opts], State) + end; +init([{ssl_opts, SslOpts} | Opts], State = #state{sock_opts = SockOpts}) -> + case lists:keytake(ssl, 1, Opts) of + {value, {ssl, true}, WithOutEnableSsl} -> + ok = ssl:start(), + SockOpts1 = merge_opts(SockOpts, [{ssl_opts, SslOpts}]), + init(WithOutEnableSsl, State#state{sock_opts = SockOpts1}); + {value, {ssl, false}, WithOutEnableSsl} -> + init(WithOutEnableSsl, State); + false -> + init(Opts, State) + end; +init([{client_id, ClientId} | Opts], State) -> + init(Opts, State#state{client_id = iolist_to_binary(ClientId)}); +init([{clean_start, CleanStart} | Opts], State) when is_boolean(CleanStart) -> + init(Opts, State#state{clean_start = CleanStart}); +init([{username, Username} | Opts], State) -> + init(Opts, State#state{username = iolist_to_binary(Username)}); +init([{password, Password} | Opts], State) -> + init(Opts, State#state{password = iolist_to_binary(Password)}); +init([{keepalive, Secs} | Opts], State) -> + init(Opts, State#state{keepalive = Secs}); +init([{proto_ver, v3} | Opts], State) -> + init(Opts, State#state{proto_ver = ?MQTT_PROTO_V3, + proto_name = <<"MQIsdp">>}); +init([{proto_ver, v4} | Opts], State) -> + init(Opts, State#state{proto_ver = ?MQTT_PROTO_V4, + proto_name = <<"MQTT">>}); +init([{proto_ver, v5} | Opts], State) -> + init(Opts, State#state{proto_ver = ?MQTT_PROTO_V5, + proto_name = <<"MQTT">>}); +init([{will_topic, Topic} | Opts], State = #state{will_msg = WillMsg}) -> + WillMsg1 = init_will_msg({topic, Topic}, WillMsg), + init(Opts, State#state{will_flag = true, will_msg = WillMsg1}); +init([{will_props, Properties} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({props, Properties}, WillMsg)}); +init([{will_payload, Payload} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({payload, Payload}, WillMsg)}); +init([{will_retain, Retain} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({retain, Retain}, WillMsg)}); +init([{will_qos, QoS} | Opts], State = #state{will_msg = WillMsg}) -> + init(Opts, State#state{will_msg = init_will_msg({qos, QoS}, WillMsg)}); +init([{connect_timeout, Timeout}| Opts], State) -> + init(Opts, State#state{connect_timeout = timer:seconds(Timeout)}); +init([{ack_timeout, Timeout}| Opts], State) -> + init(Opts, State#state{ack_timeout = timer:seconds(Timeout)}); +init([force_ping | Opts], State) -> + init(Opts, State#state{force_ping = true}); +init([{force_ping, ForcePing} | Opts], State) when is_boolean(ForcePing) -> + init(Opts, State#state{force_ping = ForcePing}); +init([{properties, Properties} | Opts], State = #state{properties = InitProps}) -> + init(Opts, State#state{properties = maps:merge(InitProps, Properties)}); +init([{max_inflight, infinity} | Opts], State) -> + init(Opts, State#state{max_inflight = infinity, + inflight = #{}}); +init([{max_inflight, I} | Opts], State) when is_integer(I) -> + init(Opts, State#state{max_inflight = I, + inflight = #{}}); +init([auto_ack | Opts], State) -> + init(Opts, State#state{auto_ack = true}); +init([{auto_ack, AutoAck} | Opts], State) when is_boolean(AutoAck) -> + init(Opts, State#state{auto_ack = AutoAck}); +init([{retry_interval, I} | Opts], State) -> + init(Opts, State#state{retry_interval = timer:seconds(I)}); +init([{bridge_mode, Mode} | Opts], State) when is_boolean(Mode) -> + init(Opts, State#state{bridge_mode = Mode}); +init([_Opt | Opts], State) -> + init(Opts, State). + +init_will_msg({topic, Topic}, WillMsg) -> + WillMsg#mqtt_msg{topic = iolist_to_binary(Topic)}; +init_will_msg({props, Props}, WillMsg) -> + WillMsg#mqtt_msg{props = Props}; +init_will_msg({payload, Payload}, WillMsg) -> + WillMsg#mqtt_msg{payload = iolist_to_binary(Payload)}; +init_will_msg({retain, Retain}, WillMsg) when is_boolean(Retain) -> + WillMsg#mqtt_msg{retain = Retain}; +init_will_msg({qos, QoS}, WillMsg) -> + WillMsg#mqtt_msg{qos = ?QOS_I(QoS)}. + +init_parse_state(State = #state{proto_ver = Ver, properties = Properties}) -> + MaxSize = maps:get('Maximum-Packet-Size', Properties, ?MAX_PACKET_SIZE), + ParseState = emqtt_frame:initial_parse_state( + #{max_size => MaxSize, version => Ver}), + State#state{parse_state = ParseState}. + +merge_opts(Defaults, Options) -> + lists:foldl( + fun({Opt, Val}, Acc) -> + lists:keystore(Opt, 1, Acc, {Opt, Val}); + (Opt, Acc) -> + lists:usort([Opt | Acc]) + end, Defaults, Options). + +callback_mode() -> state_functions. + +initialized({call, From}, connect, State = #state{sock_opts = SockOpts, + connect_timeout = Timeout}) -> + case sock_connect(hosts(State), SockOpts, Timeout) of + {ok, Sock} -> + case mqtt_connect(run_sock(State#state{socket = Sock})) of + {ok, NewState} -> + {next_state, waiting_for_connack, + add_call(new_call(connect, From), NewState), [Timeout]}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +initialized(EventType, EventContent, State) -> + handle_event(EventType, EventContent, initialized, State). + +mqtt_connect(State = #state{client_id = ClientId, + clean_start = CleanStart, + bridge_mode = IsBridge, + username = Username, + password = Password, + proto_ver = ProtoVer, + proto_name = ProtoName, + keepalive = KeepAlive, + will_flag = WillFlag, + will_msg = WillMsg, + properties = Properties}) -> + ?WILL_MSG(WillQoS, WillRetain, WillTopic, WillProps, WillPayload) = WillMsg, + ConnProps = emqtt_props:filter(?CONNECT, Properties), + send(?CONNECT_PACKET( + #mqtt_packet_connect{proto_ver = ProtoVer, + proto_name = ProtoName, + is_bridge = IsBridge, + clean_start = CleanStart, + will_flag = WillFlag, + will_qos = WillQoS, + will_retain = WillRetain, + keepalive = KeepAlive, + properties = ConnProps, + client_id = ClientId, + will_props = WillProps, + will_topic = WillTopic, + will_payload = WillPayload, + username = Username, + password = Password}), State). + +waiting_for_connack(cast, ?CONNACK_PACKET(?RC_SUCCESS, + SessPresent, + Properties), + State = #state{properties = AllProps, + client_id = ClientId}) -> + case take_call(connect, State) of + {value, #call{from = From}, State1} -> + AllProps1 = case Properties of + undefined -> AllProps; + _ -> maps:merge(AllProps, Properties) + end, + Reply = {ok, Properties}, + State2 = State1#state{client_id = assign_id(ClientId, AllProps1), + properties = AllProps1, + session_present = SessPresent}, + {next_state, connected, ensure_keepalive_timer(State2), + [{reply, From, Reply}]}; + false -> + {stop, bad_connack} + end; + +waiting_for_connack(cast, ?CONNACK_PACKET(ReasonCode, + _SessPresent, + Properties), + State = #state{proto_ver = ProtoVer}) -> + Reason = reason_code_name(ReasonCode, ProtoVer), + case take_call(connect, State) of + {value, #call{from = From}, _State} -> + Reply = {error, {Reason, Properties}}, + {stop_and_reply, {shutdown, Reason}, [{reply, From, Reply}]}; + false -> {stop, connack_error} + end; + +waiting_for_connack(timeout, _Timeout, State) -> + case take_call(connect, State) of + {value, #call{from = From}, _State} -> + Reply = {error, connack_timeout}, + {stop_and_reply, connack_timeout, [{reply, From, Reply}]}; + false -> {stop, connack_timeout} + end; + +waiting_for_connack(EventType, EventContent, State) -> + case take_call(connect, State) of + {value, #call{from = From}, _State} -> + case handle_event(EventType, EventContent, waiting_for_connack, State) of + {stop, Reason, State} -> + Reply = {error, {Reason, EventContent}}, + {stop_and_reply, Reason, [{reply, From, Reply}]}; + StateCallbackResult -> + StateCallbackResult + end; + false -> {stop, connack_timeout} + end. + +connected({call, From}, subscriptions, #state{subscriptions = Subscriptions}) -> + {keep_state_and_data, [{reply, From, maps:to_list(Subscriptions)}]}; + +connected({call, From}, info, State) -> + Info = lists:zip(record_info(fields, state), tl(tuple_to_list(State))), + {keep_state_and_data, [{reply, From, Info}]}; + +connected({call, From}, pause, State) -> + {keep_state, State#state{paused = true}, [{reply, From, ok}]}; + +connected({call, From}, resume, State) -> + {keep_state, State#state{paused = false}, [{reply, From, ok}]}; + +connected({call, From}, client_id, #state{client_id = ClientId}) -> + {keep_state_and_data, [{reply, From, ClientId}]}; + +connected({call, From}, SubReq = {subscribe, Properties, Topics}, + State = #state{last_packet_id = PacketId, subscriptions = Subscriptions}) -> + case send(?SUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of + {ok, NewState} -> + Call = new_call({subscribe, PacketId}, From, SubReq), + Subscriptions1 = + lists:foldl(fun({Topic, Opts}, Acc) -> + maps:put(Topic, Opts, Acc) + end, Subscriptions, Topics), + {keep_state, ensure_ack_timer(add_call(Call,NewState#state{subscriptions = Subscriptions1}))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, {publish, Msg = #mqtt_msg{qos = ?QOS_0}}, State) -> + case send(Msg, State) of + {ok, NewState} -> + {keep_state, NewState, [{reply, From, ok}]}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, {publish, Msg = #mqtt_msg{qos = QoS}}, + State = #state{inflight = Inflight, last_packet_id = PacketId}) + when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) -> + Msg1 = Msg#mqtt_msg{packet_id = PacketId}, + case send(Msg1, State) of + {ok, NewState} -> + Inflight1 = maps:put(PacketId, {publish, Msg1, os:timestamp()}, Inflight), + State1 = ensure_retry_timer(NewState#state{inflight = Inflight1}), + Actions = [{reply, From, {ok, PacketId}}], + case is_inflight_full(State1) of + true -> {next_state, inflight_full, State1, Actions}; + false -> {keep_state, State1, Actions} + end; + {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, {error, {PacketId, Reason}}}]} + end; + +connected({call, From}, UnsubReq = {unsubscribe, Properties, Topics}, + State = #state{last_packet_id = PacketId}) -> + case send(?UNSUBSCRIBE_PACKET(PacketId, Properties, Topics), State) of + {ok, NewState} -> + Call = new_call({unsubscribe, PacketId}, From, UnsubReq), + {keep_state, ensure_ack_timer(add_call(Call, NewState))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, ping, State) -> + case send(?PACKET(?PINGREQ), State) of + {ok, NewState} -> + Call = new_call(ping, From), + {keep_state, ensure_ack_timer(add_call(Call, NewState))}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected({call, From}, {disconnect, ReasonCode, Properties}, State) -> + case send(?DISCONNECT_PACKET(ReasonCode, Properties), State) of + {ok, NewState} -> + {stop_and_reply, normal, [{reply, From, ok}], NewState}; + Error = {error, Reason} -> + {stop_and_reply, Reason, [{reply, From, Error}]} + end; + +connected(cast, {puback, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBACK_PACKET(PacketId, ReasonCode, Properties), State); + +connected(cast, {pubrec, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBREC_PACKET(PacketId, ReasonCode, Properties), State); + +connected(cast, {pubrel, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBREL_PACKET(PacketId, ReasonCode, Properties), State); + +connected(cast, {pubcomp, PacketId, ReasonCode, Properties}, State) -> + send_puback(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), State); + +connected(cast, ?PUBLISH_PACKET(_QoS, _PacketId), #state{paused = true}) -> + keep_state_and_data; + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_0, _PacketId), State) -> + {keep_state, deliver(packet_to_msg(Packet), State)}; + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_1, _PacketId), State) -> + publish_process(?QOS_1, Packet, State); + +connected(cast, Packet = ?PUBLISH_PACKET(?QOS_2, _PacketId), State) -> + publish_process(?QOS_2, Packet, State); + +connected(cast, ?PUBACK_PACKET(_PacketId, _ReasonCode, _Properties) = PubAck, State) -> + {keep_state, delete_inflight(PubAck, State)}; + +connected(cast, ?PUBREC_PACKET(PacketId), State = #state{inflight = Inflight}) -> + NState = case maps:find(PacketId, Inflight) of + {ok, {publish, _Msg, _Ts}} -> + Inflight1 = maps:put(PacketId, {pubrel, PacketId, os:timestamp()}, Inflight), + State#state{inflight = Inflight1}; + {ok, {pubrel, _Ref, _Ts}} -> + ?LOG(notice, "Duplicated PUBREC Packet: ~p", [PacketId], State), + State; + error -> + ?LOG(warning, "Unexpected PUBREC Packet: ~p", [PacketId], State), + State + end, + send_puback(?PUBREL_PACKET(PacketId), NState); + +%%TODO::... if auto_ack is false, should we take PacketId from the map? +connected(cast, ?PUBREL_PACKET(PacketId), + State = #state{awaiting_rel = AwaitingRel, auto_ack = AutoAck}) -> + case maps:take(PacketId, AwaitingRel) of + {Packet, AwaitingRel1} -> + NewState = deliver(packet_to_msg(Packet), State#state{awaiting_rel = AwaitingRel1}), + case AutoAck of + true -> send_puback(?PUBCOMP_PACKET(PacketId), NewState); + false -> {keep_state, NewState} + end; + error -> + ?LOG(warning, "Unexpected PUBREL: ~p", [PacketId], State), + keep_state_and_data + end; + +connected(cast, ?PUBCOMP_PACKET(_PacketId, _ReasonCode, _Properties) = PubComp, State) -> + {keep_state, delete_inflight(PubComp, State)}; + +connected(cast, ?SUBACK_PACKET(PacketId, Properties, ReasonCodes), + State = #state{subscriptions = _Subscriptions}) -> + case take_call({subscribe, PacketId}, State) of + {value, #call{from = From}, NewState} -> + %%TODO: Merge reason codes to subscriptions? + Reply = {ok, Properties, ReasonCodes}, + {keep_state, NewState, [{reply, From, Reply}]}; + false -> + keep_state_and_data + end; + +connected(cast, ?UNSUBACK_PACKET(PacketId, Properties, ReasonCodes), + State = #state{subscriptions = Subscriptions}) -> + case take_call({unsubscribe, PacketId}, State) of + {value, #call{from = From, req = {_, _, Topics}}, NewState} -> + Subscriptions1 = + lists:foldl(fun(Topic, Acc) -> + maps:remove(Topic, Acc) + end, Subscriptions, Topics), + {keep_state, NewState#state{subscriptions = Subscriptions1}, + [{reply, From, {ok, Properties, ReasonCodes}}]}; + false -> + keep_state_and_data + end; + +connected(cast, ?PACKET(?PINGRESP), #state{pending_calls = []}) -> + keep_state_and_data; +connected(cast, ?PACKET(?PINGRESP), State) -> + case take_call(ping, State) of + {value, #call{from = From}, NewState} -> + {keep_state, NewState, [{reply, From, pong}]}; + false -> + keep_state_and_data + end; + +connected(cast, ?DISCONNECT_PACKET(ReasonCode, Properties), State) -> + {stop, {disconnected, ReasonCode, Properties}, State}; + +connected(info, {timeout, _TRef, keepalive}, State = #state{force_ping = true}) -> + case send(?PACKET(?PINGREQ), State) of + {ok, NewState} -> + {keep_state, ensure_keepalive_timer(NewState)}; + Error -> {stop, Error} + end; + +connected(info, {timeout, TRef, keepalive}, + State = #state{socket = Sock, paused = Paused, keepalive_timer = TRef}) -> + case (not Paused) andalso should_ping(Sock) of + true -> + case send(?PACKET(?PINGREQ), State) of + {ok, NewState} -> + {keep_state, ensure_keepalive_timer(NewState), [hibernate]}; + Error -> {stop, Error} + end; + false -> + {keep_state, ensure_keepalive_timer(State), [hibernate]}; + {error, Reason} -> + {stop, Reason} + end; + +connected(info, {timeout, TRef, ack}, State = #state{ack_timer = TRef, + ack_timeout = Timeout, + pending_calls = Calls}) -> + NewState = State#state{ack_timer = undefined, + pending_calls = timeout_calls(Timeout, Calls)}, + {keep_state, ensure_ack_timer(NewState)}; + +connected(info, {timeout, TRef, retry}, State = #state{retry_timer = TRef, + inflight = Inflight}) -> + case maps:size(Inflight) == 0 of + true -> {keep_state, State#state{retry_timer = undefined}}; + false -> retry_send(State) + end; + +connected(EventType, EventContent, Data) -> + handle_event(EventType, EventContent, connected, Data). + +inflight_full({call, _From}, {publish, #mqtt_msg{qos = QoS}}, _State) when (QoS =:= ?QOS_1); (QoS =:= ?QOS_2) -> + {keep_state_and_data, [postpone]}; +inflight_full(cast, ?PUBACK_PACKET(_PacketId, _ReasonCode, _Properties) = PubAck, State) -> + delete_inflight_when_full(PubAck, State); +inflight_full(cast, ?PUBCOMP_PACKET(_PacketId, _ReasonCode, _Properties) = PubComp, State) -> + delete_inflight_when_full(PubComp, State); +inflight_full(EventType, EventContent, Data) -> + %% inflight_full is a sub-state of connected state, + %% delegate all other events to connected state. + connected(EventType, EventContent, Data). + +handle_event({call, From}, stop, _StateName, _State) -> + {stop_and_reply, normal, [{reply, From, ok}]}; +handle_event(info, {TcpOrSsL, _Sock, Data}, _StateName, State) + when TcpOrSsL =:= tcp; TcpOrSsL =:= ssl -> + ?LOG(debug, "RECV Data: ~p", [Data], State), + process_incoming(Data, [], run_sock(State)); + +handle_event(info, {Error, _Sock, Reason}, _StateName, State) + when Error =:= tcp_error; Error =:= ssl_error -> + ?LOG(error, "The connection error occured ~p, reason:~p", + [Error, Reason], State), + {stop, {shutdown, Reason}, State}; + +handle_event(info, {Closed, _Sock}, _StateName, State) + when Closed =:= tcp_closed; Closed =:= ssl_closed -> + ?LOG(debug, "~p", [Closed], State), + {stop, {shutdown, Closed}, State}; + +handle_event(info, {'EXIT', Owner, Reason}, _, State = #state{owner = Owner}) -> + ?LOG(debug, "Got EXIT from owner, Reason: ~p", [Reason], State), + {stop, {shutdown, Reason}, State}; + +handle_event(info, {inet_reply, _Sock, ok}, _, _State) -> + keep_state_and_data; + +handle_event(info, {inet_reply, _Sock, {error, Reason}}, _, State) -> + ?LOG(error, "Got tcp error: ~p", [Reason], State), + {stop, {shutdown, Reason}, State}; + +handle_event(info, EventContent = {'EXIT', _Pid, normal}, StateName, State) -> + ?LOG(info, "State: ~s, Unexpected Event: (info, ~p)", + [StateName, EventContent], State), + keep_state_and_data; + +handle_event(EventType, EventContent, StateName, State) -> + ?LOG(error, "State: ~s, Unexpected Event: (~p, ~p)", + [StateName, EventType, EventContent], State), + keep_state_and_data. + +%% Mandatory callback functions +terminate(Reason, _StateName, State = #state{socket = Socket}) -> + case Reason of + {disconnected, ReasonCode, Properties} -> + %% backward compatible + ok = eval_msg_handler(State, disconnected, {ReasonCode, Properties}); + _ -> + ok = eval_msg_handler(State, disconnected, Reason) + end, + case Socket =:= undefined of + true -> ok; + _ -> emqtt_sock:close(Socket) + end. + +code_change(_Vsn, State, Data, _Extra) -> + {ok, State, Data}. + +%%-------------------------------------------------------------------- +%% Internal functions +%%-------------------------------------------------------------------- + +should_ping(Sock) -> + case emqtt_sock:getstat(Sock, [send_oct]) of + {ok, [{send_oct, Val}]} -> + OldVal = get(send_oct), put(send_oct, Val), + OldVal == undefined orelse OldVal == Val; + Error = {error, _Reason} -> + Error + end. + +is_inflight_full(#state{max_inflight = infinity}) -> + false; +is_inflight_full(#state{max_inflight = MaxLimit, inflight = Inflight}) -> + maps:size(Inflight) >= MaxLimit. + +delete_inflight(?PUBACK_PACKET(PacketId, ReasonCode, Properties), + State = #state{inflight = Inflight}) -> + case maps:find(PacketId, Inflight) of + {ok, {publish, #mqtt_msg{packet_id = PacketId}, _Ts}} -> + ok = eval_msg_handler(State, puback, #{packet_id => PacketId, + reason_code => ReasonCode, + properties => Properties}), + State#state{inflight = maps:remove(PacketId, Inflight)}; + error -> + ?LOG(warning, "Unexpected PUBACK: ~p", [PacketId], State), + State + end; +delete_inflight(?PUBCOMP_PACKET(PacketId, ReasonCode, Properties), + State = #state{inflight = Inflight}) -> + case maps:find(PacketId, Inflight) of + {ok, {pubrel, _PacketId, _Ts}} -> + ok = eval_msg_handler(State, puback, #{packet_id => PacketId, + reason_code => ReasonCode, + properties => Properties}), + State#state{inflight = maps:remove(PacketId, Inflight)}; + error -> + ?LOG(warning, "Unexpected PUBCOMP Packet: ~p", [PacketId], State), + State + end. + +delete_inflight_when_full(Packet, State) -> + State1 = delete_inflight(Packet, State), + case is_inflight_full(State1) of + true -> {keep_state, State1}; + false -> {next_state, connected, State1} + end. + +assign_id(?NO_CLIENT_ID, Props) -> + case maps:find('Assigned-Client-Identifier', Props) of + {ok, Value} -> + Value; + _ -> + error(bad_client_id) + end; +assign_id(Id, _Props) -> + Id. + +publish_process(?QOS_1, Packet = ?PUBLISH_PACKET(?QOS_1, PacketId), + State0 = #state{auto_ack = AutoAck}) -> + State = deliver(packet_to_msg(Packet), State0), + case AutoAck of + true -> send_puback(?PUBACK_PACKET(PacketId), State); + false -> {keep_state, State} + end; +publish_process(?QOS_2, Packet = ?PUBLISH_PACKET(?QOS_2, PacketId), + State = #state{awaiting_rel = AwaitingRel}) -> + case send_puback(?PUBREC_PACKET(PacketId), State) of + {keep_state, NewState} -> + AwaitingRel1 = maps:put(PacketId, Packet, AwaitingRel), + {keep_state, NewState#state{awaiting_rel = AwaitingRel1}}; + Stop -> Stop + end. + +ensure_keepalive_timer(State = ?PROPERTY('Server-Keep-Alive', Secs)) -> + ensure_keepalive_timer(timer:seconds(Secs), State#state{keepalive = Secs}); +ensure_keepalive_timer(State = #state{keepalive = 0}) -> + State; +ensure_keepalive_timer(State = #state{keepalive = I}) -> + ensure_keepalive_timer(timer:seconds(I), State). +ensure_keepalive_timer(I, State) when is_integer(I) -> + State#state{keepalive_timer = erlang:start_timer(I, self(), keepalive)}. + +new_call(Id, From) -> + new_call(Id, From, undefined). +new_call(Id, From, Req) -> + #call{id = Id, from = From, req = Req, ts = os:timestamp()}. + +add_call(Call, Data = #state{pending_calls = Calls}) -> + Data#state{pending_calls = [Call | Calls]}. + +take_call(Id, Data = #state{pending_calls = Calls}) -> + case lists:keytake(Id, #call.id, Calls) of + {value, Call, Left} -> + {value, Call, Data#state{pending_calls = Left}}; + false -> false + end. + +timeout_calls(Timeout, Calls) -> + timeout_calls(os:timestamp(), Timeout, Calls). +timeout_calls(Now, Timeout, Calls) -> + lists:foldl(fun(C = #call{from = From, ts = Ts}, Acc) -> + case (timer:now_diff(Now, Ts) div 1000) >= Timeout of + true -> From ! {error, ack_timeout}, + Acc; + false -> [C | Acc] + end + end, [], Calls). + +ensure_ack_timer(State = #state{ack_timer = undefined, + ack_timeout = Timeout, + pending_calls = Calls}) when length(Calls) > 0 -> + State#state{ack_timer = erlang:start_timer(Timeout, self(), ack)}; +ensure_ack_timer(State) -> State. + +ensure_retry_timer(State = #state{retry_interval = Interval}) -> + do_ensure_retry_timer(Interval, State). + +do_ensure_retry_timer(Interval, State = #state{retry_timer = undefined}) + when Interval > 0 -> + State#state{retry_timer = erlang:start_timer(Interval, self(), retry)}; +do_ensure_retry_timer(_Interval, State) -> + State. + +retry_send(State = #state{inflight = Inflight}) -> + SortFun = fun({_, _, Ts1}, {_, _, Ts2}) -> Ts1 < Ts2 end, + Msgs = lists:sort(SortFun, maps:values(Inflight)), + retry_send(Msgs, os:timestamp(), State ). + +retry_send([], _Now, State) -> + {keep_state, ensure_retry_timer(State)}; +retry_send([{Type, Msg, Ts} | Msgs], Now, State = #state{retry_interval = Interval}) -> + Diff = timer:now_diff(Now, Ts) div 1000, %% micro -> ms + case (Diff >= Interval) of + true -> case retry_send(Type, Msg, Now, State) of + {ok, NewState} -> retry_send(Msgs, Now, NewState); + {error, Error} -> {stop, Error} + end; + false -> {keep_state, do_ensure_retry_timer(Interval - Diff, State)} + end. + +retry_send(publish, Msg = #mqtt_msg{qos = QoS, packet_id = PacketId}, + Now, State = #state{inflight = Inflight}) -> + Msg1 = Msg#mqtt_msg{dup = (QoS =:= ?QOS_1)}, + case send(Msg1, State) of + {ok, NewState} -> + Inflight1 = maps:put(PacketId, {publish, Msg1, Now}, Inflight), + {ok, NewState#state{inflight = Inflight1}}; + Error = {error, _Reason} -> + Error + end; +retry_send(pubrel, PacketId, Now, State = #state{inflight = Inflight}) -> + case send(?PUBREL_PACKET(PacketId), State) of + {ok, NewState} -> + Inflight1 = maps:put(PacketId, {pubrel, PacketId, Now}, Inflight), + {ok, NewState#state{inflight = Inflight1}}; + Error = {error, _Reason} -> + Error + end. + +deliver(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}, + State) -> + Msg = #{qos => QoS, dup => Dup, retain => Retain, packet_id => PacketId, + topic => Topic, properties => Props, payload => Payload, + client_pid => self()}, + ok = eval_msg_handler(State, publish, Msg), + State. + +eval_msg_handler(#state{msg_handler = ?NO_MSG_HDLR, + owner = Owner}, + disconnected, {ReasonCode, Properties}) -> + %% Special handling for disconnected message when there is no handler callback + Owner ! {disconnected, ReasonCode, Properties}, + ok; +eval_msg_handler(#state{msg_handler = ?NO_MSG_HDLR}, + disconnected, _OtherReason) -> + %% do nothing to be backward compatible + ok; +eval_msg_handler(#state{msg_handler = ?NO_MSG_HDLR, + owner = Owner}, Kind, Msg) -> + Owner ! {Kind, Msg}, + ok; +eval_msg_handler(#state{msg_handler = Handler}, Kind, Msg) -> + F = maps:get(Kind, Handler), + _ = F(Msg), + ok. + +packet_to_msg(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + dup = Dup, + qos = QoS, + retain = R}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + properties = Props}, + payload = Payload}) -> + #mqtt_msg{qos = QoS, retain = R, dup = Dup, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}. + +msg_to_packet(#mqtt_msg{qos = QoS, dup = Dup, retain = Retain, packet_id = PacketId, + topic = Topic, props = Props, payload = Payload}) -> + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = QoS, + retain = Retain, + dup = Dup}, + variable = #mqtt_packet_publish{topic_name = Topic, + packet_id = PacketId, + properties = Props}, + payload = Payload}. + +%%-------------------------------------------------------------------- +%% Socket Connect/Send + +sock_connect(Hosts, SockOpts, Timeout) -> + sock_connect(Hosts, SockOpts, Timeout, {error, no_hosts}). + +sock_connect([], _SockOpts, _Timeout, LastErr) -> + LastErr; +sock_connect([{Host, Port} | Hosts], SockOpts, Timeout, _LastErr) -> + case emqtt_sock:connect(Host, Port, SockOpts, Timeout) of + {ok, Socket} -> {ok, Socket}; + Err = {error, _Reason} -> + sock_connect(Hosts, SockOpts, Timeout, Err) + end. + +hosts(#state{hosts = [], host = Host, port = Port}) -> + [{Host, Port}]; +hosts(#state{hosts = Hosts}) -> Hosts. + +send_puback(Packet, State) -> + case send(Packet, State) of + {ok, NewState} -> {keep_state, NewState}; + {error, Reason} -> {stop, {shutdown, Reason}} + end. + +send(Msg, State) when is_record(Msg, mqtt_msg) -> + send(msg_to_packet(Msg), State); + +send(Packet, State = #state{socket = Sock, proto_ver = Ver}) + when is_record(Packet, mqtt_packet) -> + Data = emqtt_frame:serialize(Packet, Ver), + ?LOG(debug, "SEND Data: ~1000p", [Packet], State), + case emqtt_sock:send(Sock, Data) of + ok -> {ok, bump_last_packet_id(State)}; + Error -> Error + end. + +run_sock(State = #state{socket = Sock}) -> + emqtt_sock:setopts(Sock, [{active, once}]), State. + +%%-------------------------------------------------------------------- +%% Process incomming + +process_incoming(<<>>, Packets, State) -> + {keep_state, State, next_events(Packets)}; + +process_incoming(Bytes, Packets, State = #state{parse_state = ParseState}) -> + try emqtt_frame:parse(Bytes, ParseState) of + {ok, Packet, Rest, NParseState} -> + process_incoming(Rest, [Packet|Packets], State#state{parse_state = NParseState}); + {ok, NParseState} -> + {keep_state, State#state{parse_state = NParseState}, next_events(Packets)}; + {error, Reason} -> + {stop, Reason} + catch + error:Error -> + {stop, Error} + end. + +next_events([]) -> + []; +next_events([Packet]) -> + {next_event, cast, Packet}; +next_events(Packets) -> + [{next_event, cast, Packet} || Packet <- lists:reverse(Packets)]. + +%%-------------------------------------------------------------------- +%% packet_id generation + +bump_last_packet_id(State = #state{last_packet_id = Id}) -> + State#state{last_packet_id = next_packet_id(Id)}. + +-spec next_packet_id(packet_id()) -> packet_id(). +next_packet_id(?MAX_PACKET_ID) -> 1; +next_packet_id(Id) -> Id + 1. + +%%-------------------------------------------------------------------- +%% ReasonCode Name + +reason_code_name(I, Ver) when Ver >= ?MQTT_PROTO_V5 -> + reason_code_name(I); +reason_code_name(0, _Ver) -> connection_acceptd; +reason_code_name(1, _Ver) -> unacceptable_protocol_version; +reason_code_name(2, _Ver) -> client_identifier_not_valid; +reason_code_name(3, _Ver) -> server_unavaliable; +reason_code_name(4, _Ver) -> malformed_username_or_password; +reason_code_name(5, _Ver) -> unauthorized_client; +reason_code_name(_, _Ver) -> unknown_error. + +reason_code_name(16#00) -> success; +reason_code_name(16#01) -> granted_qos1; +reason_code_name(16#02) -> granted_qos2; +reason_code_name(16#04) -> disconnect_with_will_message; +reason_code_name(16#10) -> no_matching_subscribers; +reason_code_name(16#11) -> no_subscription_existed; +reason_code_name(16#18) -> continue_authentication; +reason_code_name(16#19) -> re_authenticate; +reason_code_name(16#80) -> unspecified_error; +reason_code_name(16#81) -> malformed_Packet; +reason_code_name(16#82) -> protocol_error; +reason_code_name(16#83) -> implementation_specific_error; +reason_code_name(16#84) -> unsupported_protocol_version; +reason_code_name(16#85) -> client_identifier_not_valid; +reason_code_name(16#86) -> bad_username_or_password; +reason_code_name(16#87) -> not_authorized; +reason_code_name(16#88) -> server_unavailable; +reason_code_name(16#89) -> server_busy; +reason_code_name(16#8A) -> banned; +reason_code_name(16#8B) -> server_shutting_down; +reason_code_name(16#8C) -> bad_authentication_method; +reason_code_name(16#8D) -> keepalive_timeout; +reason_code_name(16#8E) -> session_taken_over; +reason_code_name(16#8F) -> topic_filter_invalid; +reason_code_name(16#90) -> topic_name_invalid; +reason_code_name(16#91) -> packet_identifier_inuse; +reason_code_name(16#92) -> packet_identifier_not_found; +reason_code_name(16#93) -> receive_maximum_exceeded; +reason_code_name(16#94) -> topic_alias_invalid; +reason_code_name(16#95) -> packet_too_large; +reason_code_name(16#96) -> message_rate_too_high; +reason_code_name(16#97) -> quota_exceeded; +reason_code_name(16#98) -> administrative_action; +reason_code_name(16#99) -> payload_format_invalid; +reason_code_name(16#9A) -> retain_not_supported; +reason_code_name(16#9B) -> qos_not_supported; +reason_code_name(16#9C) -> use_another_server; +reason_code_name(16#9D) -> server_moved; +reason_code_name(16#9E) -> shared_subscriptions_not_supported; +reason_code_name(16#9F) -> connection_rate_exceeded; +reason_code_name(16#A0) -> maximum_connect_time; +reason_code_name(16#A1) -> subscription_identifiers_not_supported; +reason_code_name(16#A2) -> wildcard_subscriptions_not_supported; +reason_code_name(_Code) -> unknown_error. diff --git a/src/emqtt_frame.erl b/src/emqtt_frame.erl new file mode 100644 index 00000000..63e9e0e9 --- /dev/null +++ b/src/emqtt_frame.erl @@ -0,0 +1,655 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2019 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(emqtt_frame). + +-include("emqtt.hrl"). + +-export([ initial_parse_state/0 + , initial_parse_state/1 + ]). + +-export([ parse/1 + , parse/2 + , serialize/1 + , serialize/2 + ]). + +-export_type([ options/0 + , parse_state/0 + , parse_result/0 + ]). + +-type(version() :: ?MQTT_PROTO_V3 + | ?MQTT_PROTO_V4 + | ?MQTT_PROTO_V5). + +-type(options() :: #{max_size => 1..?MAX_PACKET_SIZE, + version => version()}). + +-opaque(parse_state() :: {none, options()} | {more, cont_fun()}). + +-opaque(parse_result() :: {ok, parse_state()} + | {ok, #mqtt_packet{}, binary(), parse_state()}). + +-type(cont_fun() :: fun((binary()) -> parse_result())). + +-define(none(Opts), {none, Opts}). +-define(more(Cont), {more, Cont}). +-define(DEFAULT_OPTIONS, + #{max_size => ?MAX_PACKET_SIZE, + version => ?MQTT_PROTO_V4 + }). + +%%-------------------------------------------------------------------- +%% Init Parse State +%%-------------------------------------------------------------------- + +-spec(initial_parse_state() -> {none, options()}). +initial_parse_state() -> + initial_parse_state(#{}). + +-spec(initial_parse_state(options()) -> {none, options()}). +initial_parse_state(Options) when is_map(Options) -> + ?none(merge_opts(Options)). + +%% @pivate +merge_opts(Options) -> + maps:merge(?DEFAULT_OPTIONS, Options). + +%%-------------------------------------------------------------------- +%% Parse MQTT Frame +%%-------------------------------------------------------------------- + +-spec(parse(binary()) -> parse_result()). +parse(Bin) -> + parse(Bin, initial_parse_state()). + +-spec(parse(binary(), parse_state()) -> parse_result()). +parse(<<>>, {none, Options}) -> + {ok, ?more(fun(Bin) -> parse(Bin, {none, Options}) end)}; +parse(<>, {none, Options}) -> + parse_remaining_len(Rest, #mqtt_packet_header{type = Type, + dup = bool(Dup), + qos = fixqos(Type, QoS), + retain = bool(Retain)}, Options); +parse(Bin, {more, Cont}) when is_binary(Bin), is_function(Cont) -> + Cont(Bin). + +parse_remaining_len(<<>>, Header, Options) -> + {ok, ?more(fun(Bin) -> parse_remaining_len(Bin, Header, Options) end)}; +parse_remaining_len(Rest, Header, Options) -> + parse_remaining_len(Rest, Header, 1, 0, Options). + +parse_remaining_len(_Bin, _Header, _Multiplier, Length, #{max_size := MaxSize}) + when Length > MaxSize -> + error(mqtt_frame_too_large); +parse_remaining_len(<<>>, Header, Multiplier, Length, Options) -> + {ok, ?more(fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length, Options) end)}; +%% Match DISCONNECT without payload +parse_remaining_len(<<0:8, Rest/binary>>, Header = #mqtt_packet_header{type = ?DISCONNECT}, 1, 0, Options) -> + Packet = packet(Header, #mqtt_packet_disconnect{reason_code = ?RC_SUCCESS}), + {ok, Packet, Rest, ?none(Options)}; +%% Match PINGREQ. +parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0, Options) -> + parse_frame(Rest, Header, 0, Options); +%% Match PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK... +parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0, Options) -> + parse_frame(Rest, Header, 2, Options); +parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value, Options) -> + parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier, Options); +parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value, + Options = #{max_size := MaxSize}) -> + FrameLen = Value + Len * Multiplier, + if + FrameLen > MaxSize -> error(mqtt_frame_too_large); + true -> parse_frame(Rest, Header, FrameLen, Options) + end. + +parse_frame(Bin, Header, 0, Options) -> + {ok, packet(Header), Bin, ?none(Options)}; + +parse_frame(Bin, Header, Length, Options) -> + case Bin of + <> -> + case parse_packet(Header, FrameBin, Options) of + {Variable, Payload} -> + {ok, packet(Header, Variable, Payload), Rest, ?none(Options)}; + Variable = #mqtt_packet_connect{proto_ver = Ver} -> + {ok, packet(Header, Variable), Rest, ?none(Options#{version := Ver})}; + Variable -> + {ok, packet(Header, Variable), Rest, ?none(Options)} + end; + TooShortBin -> + {ok, ?more(fun(BinMore) -> + parse_frame(<>, Header, Length, Options) + end)} + end. + +packet(Header) -> + #mqtt_packet{header = Header}. +packet(Header, Variable) -> + #mqtt_packet{header = Header, variable = Variable}. +packet(Header, Variable, Payload) -> + #mqtt_packet{header = Header, variable = Variable, payload = Payload}. + +parse_packet(#mqtt_packet_header{type = ?CONNECT}, FrameBin, _Options) -> + {ProtoName, Rest} = parse_utf8_string(FrameBin), + <> = Rest, + % Note: Crash when reserved flag doesn't equal to 0, there is no strict compliance with the MQTT5.0. + <> = Rest1, + + {Properties, Rest3} = parse_properties(Rest2, ProtoVer), + {ClientId, Rest4} = parse_utf8_string(Rest3), + ConnPacket = #mqtt_packet_connect{proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = (BridgeTag =:= 8), + clean_start = bool(CleanStart), + will_flag = bool(WillFlag), + will_qos = WillQoS, + will_retain = bool(WillRetain), + keepalive = KeepAlive, + properties = Properties, + client_id = ClientId}, + {ConnPacket1, Rest5} = parse_will_message(ConnPacket, Rest4), + {Username, Rest6} = parse_utf8_string(Rest5, bool(UsernameFlag)), + {Passsword, <<>>} = parse_utf8_string(Rest6, bool(PasswordFlag)), + ConnPacket1#mqtt_packet_connect{username = Username, password = Passsword}; + +parse_packet(#mqtt_packet_header{type = ?CONNACK}, + <>, #{version := Ver}) -> + {Properties, <<>>} = parse_properties(Rest, Ver), + #mqtt_packet_connack{ack_flags = AckFlags, + reason_code = ReasonCode, + properties = Properties}; + +parse_packet(#mqtt_packet_header{type = ?PUBLISH, qos = QoS}, Bin, + #{version := Ver}) -> + {TopicName, Rest} = parse_utf8_string(Bin), + {PacketId, Rest1} = case QoS of + ?QOS_0 -> {undefined, Rest}; + _ -> parse_packet_id(Rest) + end, + {Properties, Payload} = parse_properties(Rest1, Ver), + {#mqtt_packet_publish{topic_name = TopicName, + packet_id = PacketId, + properties = Properties}, Payload}; + +parse_packet(#mqtt_packet_header{type = PubAck}, <>, _Options) + when ?PUBACK =< PubAck, PubAck =< ?PUBCOMP -> + #mqtt_packet_puback{packet_id = PacketId, reason_code = 0}; +parse_packet(#mqtt_packet_header{type = PubAck}, <>, + #{version := Ver = ?MQTT_PROTO_V5}) + when ?PUBACK =< PubAck, PubAck =< ?PUBCOMP -> + {Properties, <<>>} = parse_properties(Rest, Ver), + #mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties}; + +parse_packet(#mqtt_packet_header{type = ?SUBSCRIBE}, <>, + #{version := Ver}) -> + {Properties, Rest1} = parse_properties(Rest, Ver), + TopicFilters = parse_topic_filters(subscribe, Rest1), + #mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}; + +parse_packet(#mqtt_packet_header{type = ?SUBACK}, <>, + #{version := Ver}) -> + {Properties, Rest1} = parse_properties(Rest, Ver), + #mqtt_packet_suback{packet_id = PacketId, + properties = Properties, + reason_codes = parse_reason_codes(Rest1)}; + +parse_packet(#mqtt_packet_header{type = ?UNSUBSCRIBE}, <>, + #{version := Ver}) -> + {Properties, Rest1} = parse_properties(Rest, Ver), + TopicFilters = parse_topic_filters(unsubscribe, Rest1), + #mqtt_packet_unsubscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}; + +parse_packet(#mqtt_packet_header{type = ?UNSUBACK}, <>, _Options) -> + #mqtt_packet_unsuback{packet_id = PacketId}; +parse_packet(#mqtt_packet_header{type = ?UNSUBACK}, <>, + #{version := Ver}) -> + {Properties, Rest1} = parse_properties(Rest, Ver), + ReasonCodes = parse_reason_codes(Rest1), + #mqtt_packet_unsuback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}; + +parse_packet(#mqtt_packet_header{type = ?DISCONNECT}, <>, + #{version := ?MQTT_PROTO_V5}) -> + {Properties, <<>>} = parse_properties(Rest, ?MQTT_PROTO_V5), + #mqtt_packet_disconnect{reason_code = ReasonCode, + properties = Properties}; + +parse_packet(#mqtt_packet_header{type = ?AUTH}, <>, + #{version := ?MQTT_PROTO_V5}) -> + {Properties, <<>>} = parse_properties(Rest, ?MQTT_PROTO_V5), + #mqtt_packet_auth{reason_code = ReasonCode, properties = Properties}. + +parse_will_message(Packet = #mqtt_packet_connect{will_flag = true, + proto_ver = Ver}, Bin) -> + {Props, Rest} = parse_properties(Bin, Ver), + {Topic, Rest1} = parse_utf8_string(Rest), + {Payload, Rest2} = parse_binary_data(Rest1), + {Packet#mqtt_packet_connect{will_props = Props, + will_topic = Topic, + will_payload = Payload}, Rest2}; +parse_will_message(Packet, Bin) -> + {Packet, Bin}. + +% protocol_approved(Ver, Name) -> +% lists:member({Ver, Name}, ?PROTOCOL_NAMES). + +parse_packet_id(<>) -> + {PacketId, Rest}. + +parse_properties(Bin, Ver) when Ver =/= ?MQTT_PROTO_V5 -> + {undefined, Bin}; +%% TODO: version mess? +parse_properties(<<>>, ?MQTT_PROTO_V5) -> + {#{}, <<>>}; +parse_properties(<<0, Rest/binary>>, ?MQTT_PROTO_V5) -> + {#{}, Rest}; +parse_properties(Bin, ?MQTT_PROTO_V5) -> + {Len, Rest} = parse_variable_byte_integer(Bin), + <> = Rest, + {parse_property(PropsBin, #{}), Rest1}. + +parse_property(<<>>, Props) -> + Props; +parse_property(<<16#01, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Payload-Format-Indicator' => Val}); +parse_property(<<16#02, Val:32/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Message-Expiry-Interval' => Val}); +parse_property(<<16#03, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf8_string(Bin), + parse_property(Rest, Props#{'Content-Type' => Val}); +parse_property(<<16#08, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf8_string(Bin), + parse_property(Rest, Props#{'Response-Topic' => Val}); +parse_property(<<16#09, Len:16/big, Val:Len/binary, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Correlation-Data' => Val}); +parse_property(<<16#0B, Bin/binary>>, Props) -> + {Val, Rest} = parse_variable_byte_integer(Bin), + parse_property(Rest, Props#{'Subscription-Identifier' => Val}); +parse_property(<<16#11, Val:32/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Session-Expiry-Interval' => Val}); +parse_property(<<16#12, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf8_string(Bin), + parse_property(Rest, Props#{'Assigned-Client-Identifier' => Val}); +parse_property(<<16#13, Val:16, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Server-Keep-Alive' => Val}); +parse_property(<<16#15, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf8_string(Bin), + parse_property(Rest, Props#{'Authentication-Method' => Val}); +parse_property(<<16#16, Len:16/big, Val:Len/binary, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Authentication-Data' => Val}); +parse_property(<<16#17, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Request-Problem-Information' => Val}); +parse_property(<<16#18, Val:32, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Will-Delay-Interval' => Val}); +parse_property(<<16#19, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Request-Response-Information' => Val}); +parse_property(<<16#1A, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf8_string(Bin), + parse_property(Rest, Props#{'Response-Information' => Val}); +parse_property(<<16#1C, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf8_string(Bin), + parse_property(Rest, Props#{'Server-Reference' => Val}); +parse_property(<<16#1F, Bin/binary>>, Props) -> + {Val, Rest} = parse_utf8_string(Bin), + parse_property(Rest, Props#{'Reason-String' => Val}); +parse_property(<<16#21, Val:16/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Receive-Maximum' => Val}); +parse_property(<<16#22, Val:16/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Topic-Alias-Maximum' => Val}); +parse_property(<<16#23, Val:16/big, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Topic-Alias' => Val}); +parse_property(<<16#24, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Maximum-QoS' => Val}); +parse_property(<<16#25, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Retain-Available' => Val}); +parse_property(<<16#26, Bin/binary>>, Props) -> + {Pair, Rest} = parse_utf8_pair(Bin), + case maps:find('User-Property', Props) of + {ok, UserProps} -> + parse_property(Rest,Props#{'User-Property' := [Pair|UserProps]}); + error -> + parse_property(Rest, Props#{'User-Property' => [Pair]}) + end; +parse_property(<<16#27, Val:32, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Maximum-Packet-Size' => Val}); +parse_property(<<16#28, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Wildcard-Subscription-Available' => Val}); +parse_property(<<16#29, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Subscription-Identifier-Available' => Val}); +parse_property(<<16#2A, Val, Bin/binary>>, Props) -> + parse_property(Bin, Props#{'Shared-Subscription-Available' => Val}). + +parse_variable_byte_integer(Bin) -> + parse_variable_byte_integer(Bin, 1, 0). +parse_variable_byte_integer(<<1:1, Len:7, Rest/binary>>, Multiplier, Value) -> + parse_variable_byte_integer(Rest, Multiplier * ?HIGHBIT, Value + Len * Multiplier); +parse_variable_byte_integer(<<0:1, Len:7, Rest/binary>>, Multiplier, Value) -> + {Value + Len * Multiplier, Rest}. + +parse_topic_filters(subscribe, Bin) -> + [{Topic, #{rh => Rh, rap => Rap, nl => Nl, qos => QoS, rc => 0}} + || <> <= Bin]; + +parse_topic_filters(unsubscribe, Bin) -> + [Topic || <> <= Bin]. + +parse_reason_codes(Bin) -> + [Code || <> <= Bin]. + +parse_utf8_pair(<>) -> + {{Key, Val}, Rest}. + +parse_utf8_string(Bin, false) -> + {undefined, Bin}; +parse_utf8_string(Bin, true) -> + parse_utf8_string(Bin). + +parse_utf8_string(<>) -> + {Str, Rest}. + +parse_binary_data(<>) -> + {Data, Rest}. + +%%-------------------------------------------------------------------- +%% Serialize MQTT Packet +%%-------------------------------------------------------------------- + +-spec(serialize(#mqtt_packet{}) -> iodata()). +serialize(Packet) -> + serialize(Packet, ?MQTT_PROTO_V4). + +-spec(serialize(#mqtt_packet{}, version()) -> iodata()). +serialize(#mqtt_packet{header = Header, + variable = Variable, + payload = Payload}, Ver) -> + serialize(Header, serialize_variable(Variable, Ver), serialize_payload(Payload)). + +serialize(#mqtt_packet_header{type = Type, + dup = Dup, + qos = QoS, + retain = Retain}, VariableBin, PayloadBin) + when ?CONNECT =< Type andalso Type =< ?AUTH -> + Len = iolist_size(VariableBin) + iolist_size(PayloadBin), + (Len =< ?MAX_PACKET_SIZE) orelse error(mqtt_frame_too_large), + [<>, + serialize_remaining_len(Len), VariableBin, PayloadBin]. + +serialize_variable(#mqtt_packet_connect{ + proto_name = ProtoName, + proto_ver = ProtoVer, + is_bridge = IsBridge, + clean_start = CleanStart, + will_flag = WillFlag, + will_qos = WillQoS, + will_retain = WillRetain, + keepalive = KeepAlive, + properties = Properties, + client_id = ClientId, + will_props = WillProps, + will_topic = WillTopic, + will_payload = WillPayload, + username = Username, + password = Password}, _Ver) -> + [serialize_binary_data(ProtoName), + <<(case IsBridge of + true -> 16#80 + ProtoVer; + false -> ProtoVer + end):8, + (flag(Username)):1, + (flag(Password)):1, + (flag(WillRetain)):1, + WillQoS:2, + (flag(WillFlag)):1, + (flag(CleanStart)):1, + 0:1, + KeepAlive:16/big-unsigned-integer>>, + serialize_properties(Properties, ProtoVer), + serialize_utf8_string(ClientId), + case WillFlag of + true -> [serialize_properties(WillProps, ProtoVer), + serialize_utf8_string(WillTopic), + serialize_binary_data(WillPayload)]; + false -> <<>> + end, + serialize_utf8_string(Username, true), + serialize_utf8_string(Password, true)]; + +serialize_variable(#mqtt_packet_connack{ack_flags = AckFlags, + reason_code = ReasonCode, + properties = Properties}, Ver) -> + [AckFlags, ReasonCode, serialize_properties(Properties, Ver)]; + +serialize_variable(#mqtt_packet_publish{topic_name = TopicName, + packet_id = PacketId, + properties = Properties}, Ver) -> + [serialize_utf8_string(TopicName), + if + PacketId =:= undefined -> <<>>; + true -> <> + end, + serialize_properties(Properties, Ver)]; + +serialize_variable(#mqtt_packet_puback{packet_id = PacketId}, Ver) + when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 -> + <>; +serialize_variable(#mqtt_packet_puback{packet_id = PacketId, + reason_code = ReasonCode, + properties = Properties}, + Ver = ?MQTT_PROTO_V5) -> + [<>, ReasonCode, + serialize_properties(Properties, Ver)]; + +serialize_variable(#mqtt_packet_subscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}, Ver) -> + [<>, serialize_properties(Properties, Ver), + serialize_topic_filters(subscribe, TopicFilters, Ver)]; + +serialize_variable(#mqtt_packet_suback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}, Ver) -> + [<>, serialize_properties(Properties, Ver), + serialize_reason_codes(ReasonCodes)]; + +serialize_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, + properties = Properties, + topic_filters = TopicFilters}, Ver) -> + [<>, serialize_properties(Properties, Ver), + serialize_topic_filters(unsubscribe, TopicFilters, Ver)]; + +serialize_variable(#mqtt_packet_unsuback{packet_id = PacketId, + properties = Properties, + reason_codes = ReasonCodes}, Ver) -> + [<>, serialize_properties(Properties, Ver), + serialize_reason_codes(ReasonCodes)]; + +serialize_variable(#mqtt_packet_disconnect{}, Ver) + when Ver == ?MQTT_PROTO_V3; Ver == ?MQTT_PROTO_V4 -> + <<>>; + +serialize_variable(#mqtt_packet_disconnect{reason_code = ReasonCode, + properties = Properties}, + Ver = ?MQTT_PROTO_V5) -> + [ReasonCode, serialize_properties(Properties, Ver)]; +serialize_variable(#mqtt_packet_disconnect{}, _Ver) -> + <<>>; + +serialize_variable(#mqtt_packet_auth{reason_code = ReasonCode, + properties = Properties}, + Ver = ?MQTT_PROTO_V5) -> + [ReasonCode, serialize_properties(Properties, Ver)]; + +serialize_variable(PacketId, ?MQTT_PROTO_V3) when is_integer(PacketId) -> + <>; +serialize_variable(PacketId, ?MQTT_PROTO_V4) when is_integer(PacketId) -> + <>; +serialize_variable(undefined, _Ver) -> + <<>>. + +serialize_payload(undefined) -> <<>>; +serialize_payload(Bin) -> Bin. + +serialize_properties(_Props, Ver) when Ver =/= ?MQTT_PROTO_V5 -> + <<>>; +serialize_properties(Props, ?MQTT_PROTO_V5) -> + serialize_properties(Props). + +serialize_properties(undefined) -> + <<0>>; +serialize_properties(Props) when map_size(Props) == 0 -> + <<0>>; +serialize_properties(Props) when is_map(Props) -> + Bin = << <<(serialize_property(Prop, Val))/binary>> || {Prop, Val} <- maps:to_list(Props) >>, + [serialize_variable_byte_integer(byte_size(Bin)), Bin]. + +serialize_property(_, undefined) -> + <<>>; +serialize_property('Payload-Format-Indicator', Val) -> + <<16#01, Val>>; +serialize_property('Message-Expiry-Interval', Val) -> + <<16#02, Val:32/big>>; +serialize_property('Content-Type', Val) -> + <<16#03, (serialize_utf8_string(Val))/binary>>; +serialize_property('Response-Topic', Val) -> + <<16#08, (serialize_utf8_string(Val))/binary>>; +serialize_property('Correlation-Data', Val) -> + <<16#09, (byte_size(Val)):16, Val/binary>>; +serialize_property('Subscription-Identifier', Val) -> + <<16#0B, (serialize_variable_byte_integer(Val))/binary>>; +serialize_property('Session-Expiry-Interval', Val) -> + <<16#11, Val:32/big>>; +serialize_property('Assigned-Client-Identifier', Val) -> + <<16#12, (serialize_utf8_string(Val))/binary>>; +serialize_property('Server-Keep-Alive', Val) -> + <<16#13, Val:16/big>>; +serialize_property('Authentication-Method', Val) -> + <<16#15, (serialize_utf8_string(Val))/binary>>; +serialize_property('Authentication-Data', Val) -> + <<16#16, (iolist_size(Val)):16, Val/binary>>; +serialize_property('Request-Problem-Information', Val) -> + <<16#17, Val>>; +serialize_property('Will-Delay-Interval', Val) -> + <<16#18, Val:32/big>>; +serialize_property('Request-Response-Information', Val) -> + <<16#19, Val>>; +serialize_property('Response-Information', Val) -> + <<16#1A, (serialize_utf8_string(Val))/binary>>; +serialize_property('Server-Reference', Val) -> + <<16#1C, (serialize_utf8_string(Val))/binary>>; +serialize_property('Reason-String', Val) -> + <<16#1F, (serialize_utf8_string(Val))/binary>>; +serialize_property('Receive-Maximum', Val) -> + <<16#21, Val:16/big>>; +serialize_property('Topic-Alias-Maximum', Val) -> + <<16#22, Val:16/big>>; +serialize_property('Topic-Alias', Val) -> + <<16#23, Val:16/big>>; +serialize_property('Maximum-QoS', Val) -> + <<16#24, Val>>; +serialize_property('Retain-Available', Val) -> + <<16#25, Val>>; +serialize_property('User-Property', {Key, Val}) -> + <<16#26, (serialize_utf8_pair({Key, Val}))/binary>>; +serialize_property('User-Property', Props) when is_list(Props) -> + << <<(serialize_property('User-Property', {Key, Val}))/binary>> + || {Key, Val} <- Props >>; +serialize_property('Maximum-Packet-Size', Val) -> + <<16#27, Val:32/big>>; +serialize_property('Wildcard-Subscription-Available', Val) -> + <<16#28, Val>>; +serialize_property('Subscription-Identifier-Available', Val) -> + <<16#29, Val>>; +serialize_property('Shared-Subscription-Available', Val) -> + <<16#2A, Val>>. + +serialize_topic_filters(subscribe, TopicFilters, ?MQTT_PROTO_V5) -> + << <<(serialize_utf8_string(Topic))/binary, + ?RESERVED:2, Rh:2, (flag(Rap)):1,(flag(Nl)):1, QoS:2 >> + || {Topic, #{rh := Rh, rap := Rap, nl := Nl, qos := QoS}} + <- TopicFilters >>; + +serialize_topic_filters(subscribe, TopicFilters, _Ver) -> + << <<(serialize_utf8_string(Topic))/binary, ?RESERVED:6, QoS:2>> + || {Topic, #{qos := QoS}} <- TopicFilters >>; + +serialize_topic_filters(unsubscribe, TopicFilters, _Ver) -> + << <<(serialize_utf8_string(Topic))/binary>> || Topic <- TopicFilters >>. + +serialize_reason_codes(undefined) -> + <<>>; +serialize_reason_codes(ReasonCodes) when is_list(ReasonCodes) -> + << <> || Code <- ReasonCodes >>. + +serialize_utf8_pair({Name, Value}) -> + << (serialize_utf8_string(Name))/binary, (serialize_utf8_string(Value))/binary >>. + +serialize_binary_data(Bin) -> + [<<(byte_size(Bin)):16/big-unsigned-integer>>, Bin]. + +serialize_utf8_string(undefined, false) -> + error(utf8_string_undefined); +serialize_utf8_string(undefined, true) -> + <<>>; +serialize_utf8_string(String, _AllowNull) -> + serialize_utf8_string(String). + +serialize_utf8_string(String) -> + StringBin = unicode:characters_to_binary(String), + Len = byte_size(StringBin), + true = (Len =< 16#ffff), + <>. + +serialize_remaining_len(I) -> + serialize_variable_byte_integer(I). + +serialize_variable_byte_integer(N) when N =< ?LOWBITS -> + <<0:1, N:7>>; +serialize_variable_byte_integer(N) -> + <<1:1, (N rem ?HIGHBIT):7, (serialize_variable_byte_integer(N div ?HIGHBIT))/binary>>. + +bool(0) -> false; +bool(1) -> true. + +flag(undefined) -> ?RESERVED; +flag(false) -> 0; +flag(true) -> 1; +flag(X) when is_integer(X) -> X; +flag(B) when is_binary(B) -> 1. + +fixqos(?PUBREL, 0) -> 1; +fixqos(?SUBSCRIBE, 0) -> 1; +fixqos(?UNSUBSCRIBE, 0) -> 1; +fixqos(_Type, QoS) -> QoS. diff --git a/src/emqtt_props.erl b/src/emqtt_props.erl new file mode 100644 index 00000000..61c6025a --- /dev/null +++ b/src/emqtt_props.erl @@ -0,0 +1,154 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2019 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. +%%-------------------------------------------------------------------- + +%% @doc MQTT5 Properties +-module(emqtt_props). + +-include("emqtt.hrl"). + +-export([ id/1 + , name/1 + , filter/2 + , validate/1 + ]). + +-define(PROPS_TABLE, + #{16#01 => {'Payload-Format-Indicator', 'Byte', [?PUBLISH]}, + 16#02 => {'Message-Expiry-Interval', 'Four-Byte-Integer', [?PUBLISH]}, + 16#03 => {'Content-Type', 'UTF8-Encoded-String', [?PUBLISH]}, + 16#08 => {'Response-Topic', 'UTF8-Encoded-String', [?PUBLISH]}, + 16#09 => {'Correlation-Data', 'Binary-Data', [?PUBLISH]}, + 16#0B => {'Subscription-Identifier', 'Variable-Byte-Integer', [?PUBLISH, ?SUBSCRIBE]}, + 16#11 => {'Session-Expiry-Interval', 'Four-Byte-Integer', [?CONNECT, ?CONNACK, ?DISCONNECT]}, + 16#12 => {'Assigned-Client-Identifier', 'UTF8-Encoded-String', [?CONNACK]}, + 16#13 => {'Server-Keep-Alive', 'Two-Byte-Integer', [?CONNACK]}, + 16#15 => {'Authentication-Method', 'UTF8-Encoded-String', [?CONNECT, ?CONNACK, ?AUTH]}, + 16#16 => {'Authentication-Data', 'Binary-Data', [?CONNECT, ?CONNACK, ?AUTH]}, + 16#17 => {'Request-Problem-Information', 'Byte', [?CONNECT]}, + 16#18 => {'Will-Delay-Interval', 'Four-Byte-Integer', ['WILL']}, + 16#19 => {'Request-Response-Information', 'Byte', [?CONNECT]}, + 16#1A => {'Response-Information', 'UTF8-Encoded-String', [?CONNACK]}, + 16#1C => {'Server-Reference', 'UTF8-Encoded-String', [?CONNACK, ?DISCONNECT]}, + 16#1F => {'Reason-String', 'UTF8-Encoded-String', 'ALL'}, + 16#21 => {'Receive-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]}, + 16#22 => {'Topic-Alias-Maximum', 'Two-Byte-Integer', [?CONNECT, ?CONNACK]}, + 16#23 => {'Topic-Alias', 'Two-Byte-Integer', [?PUBLISH]}, + 16#24 => {'Maximum-QoS', 'Byte', [?CONNACK]}, + 16#25 => {'Retain-Available', 'Byte', [?CONNACK]}, + 16#26 => {'User-Property', 'UTF8-String-Pair', 'ALL'}, + 16#27 => {'Maximum-Packet-Size', 'Four-Byte-Integer', [?CONNECT, ?CONNACK]}, + 16#28 => {'Wildcard-Subscription-Available', 'Byte', [?CONNACK]}, + 16#29 => {'Subscription-Identifier-Available', 'Byte', [?CONNACK]}, + 16#2A => {'Shared-Subscription-Available', 'Byte', [?CONNACK]}}). + +name(16#01) -> 'Payload-Format-Indicator'; +name(16#02) -> 'Message-Expiry-Interval'; +name(16#03) -> 'Content-Type'; +name(16#08) -> 'Response-Topic'; +name(16#09) -> 'Correlation-Data'; +name(16#0B) -> 'Subscription-Identifier'; +name(16#11) -> 'Session-Expiry-Interval'; +name(16#12) -> 'Assigned-Client-Identifier'; +name(16#13) -> 'Server-Keep-Alive'; +name(16#15) -> 'Authentication-Method'; +name(16#16) -> 'Authentication-Data'; +name(16#17) -> 'Request-Problem-Information'; +name(16#18) -> 'Will-Delay-Interval'; +name(16#19) -> 'Request-Response-Information'; +name(16#1A) -> 'Response-Information'; +name(16#1C) -> 'Server-Reference'; +name(16#1F) -> 'Reason-String'; +name(16#21) -> 'Receive-Maximum'; +name(16#22) -> 'Topic-Alias-Maximum'; +name(16#23) -> 'Topic-Alias'; +name(16#24) -> 'Maximum-QoS'; +name(16#25) -> 'Retain-Available'; +name(16#26) -> 'User-Property'; +name(16#27) -> 'Maximum-Packet-Size'; +name(16#28) -> 'Wildcard-Subscription-Available'; +name(16#29) -> 'Subscription-Identifier-Available'; +name(16#2A) -> 'Shared-Subscription-Available'. + +id('Payload-Format-Indicator') -> 16#01; +id('Message-Expiry-Interval') -> 16#02; +id('Content-Type') -> 16#03; +id('Response-Topic') -> 16#08; +id('Correlation-Data') -> 16#09; +id('Subscription-Identifier') -> 16#0B; +id('Session-Expiry-Interval') -> 16#11; +id('Assigned-Client-Identifier') -> 16#12; +id('Server-Keep-Alive') -> 16#13; +id('Authentication-Method') -> 16#15; +id('Authentication-Data') -> 16#16; +id('Request-Problem-Information') -> 16#17; +id('Will-Delay-Interval') -> 16#18; +id('Request-Response-Information') -> 16#19; +id('Response-Information') -> 16#1A; +id('Server-Reference') -> 16#1C; +id('Reason-String') -> 16#1F; +id('Receive-Maximum') -> 16#21; +id('Topic-Alias-Maximum') -> 16#22; +id('Topic-Alias') -> 16#23; +id('Maximum-QoS') -> 16#24; +id('Retain-Available') -> 16#25; +id('User-Property') -> 16#26; +id('Maximum-Packet-Size') -> 16#27; +id('Wildcard-Subscription-Available') -> 16#28; +id('Subscription-Identifier-Available') -> 16#29; +id('Shared-Subscription-Available') -> 16#2A. + +filter(PacketType, Props) when is_map(Props) -> + maps:from_list(filter(PacketType, maps:to_list(Props))); + +filter(PacketType, Props) when ?CONNECT =< PacketType, PacketType =< ?AUTH, is_list(Props) -> + Filter = fun(Name) -> + case maps:find(id(Name), ?PROPS_TABLE) of + {ok, {Name, _Type, 'ALL'}} -> + true; + {ok, {Name, _Type, AllowedTypes}} -> + lists:member(PacketType, AllowedTypes); + error -> false + end + end, + [Prop || Prop = {Name, _} <- Props, Filter(Name)]. + +validate(Props) when is_map(Props) -> + lists:foreach(fun validate_prop/1, maps:to_list(Props)). + +validate_prop(Prop = {Name, Val}) -> + case maps:find(id(Name), ?PROPS_TABLE) of + {ok, {Name, Type, _}} -> + validate_value(Type, Val) + orelse error(bad_property, Prop); + error -> + error({bad_property, Prop}) + end. + +validate_value('Byte', Val) -> + is_integer(Val); +validate_value('Two-Byte-Integer', Val) -> + is_integer(Val); +validate_value('Four-Byte-Integer', Val) -> + is_integer(Val); +validate_value('Variable-Byte-Integer', Val) -> + is_integer(Val); +validate_value('UTF8-Encoded-String', Val) -> + is_binary(Val); +validate_value('Binary-Data', Val) -> + is_binary(Val); +validate_value('UTF8-String-Pair', Val) -> + is_tuple(Val) orelse is_list(Val). + diff --git a/src/emqtt_sock.erl b/src/emqtt_sock.erl new file mode 100644 index 00000000..32358e10 --- /dev/null +++ b/src/emqtt_sock.erl @@ -0,0 +1,119 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2019 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(emqtt_sock). + +-export([ connect/4 + , send/2 + , close/1 + ]). + +-export([ sockname/1 + , setopts/2 + , getstat/2 + ]). + +-record(ssl_socket, {tcp, ssl}). + +-type(socket() :: inet:socket() | #ssl_socket{}). + +-type(sockname() :: {inet:ip_address(), inet:port_number()}). + +-type(option() :: gen_tcp:connect_option() | {ssl_opts, [ssl:ssl_option()]}). + +-export_type([socket/0, option/0]). + +-define(DEFAULT_TCP_OPTIONS, [binary, {packet, raw}, {active, false}, + {nodelay, true}, {reuseaddr, true}]). + +-spec(connect(inet:ip_address() | inet:hostname(), + inet:port_number(), [option()], timeout()) + -> {ok, socket()} | {error, term()}). +connect(Host, Port, SockOpts, Timeout) -> + TcpOpts = merge_opts(?DEFAULT_TCP_OPTIONS, + lists:keydelete(ssl_opts, 1, SockOpts)), + case gen_tcp:connect(Host, Port, TcpOpts, Timeout) of + {ok, Sock} -> + case lists:keyfind(ssl_opts, 1, SockOpts) of + {ssl_opts, SslOpts} -> + ssl_upgrade(Sock, SslOpts, Timeout); + false -> {ok, Sock} + end; + {error, Reason} -> + {error, Reason} + end. + +ssl_upgrade(Sock, SslOpts, Timeout) -> + TlsVersions = proplists:get_value(versions, SslOpts, []), + Ciphers = proplists:get_value(ciphers, SslOpts, default_ciphers(TlsVersions)), + SslOpts2 = merge_opts(SslOpts, [{ciphers, Ciphers}]), + case ssl:connect(Sock, SslOpts2, Timeout) of + {ok, SslSock} -> + ok = ssl:controlling_process(SslSock, self()), + {ok, #ssl_socket{tcp = Sock, ssl = SslSock}}; + {error, Reason} -> {error, Reason} + end. + +-spec(send(socket(), iodata()) -> ok | {error, einval | closed}). +send(Sock, Data) when is_port(Sock) -> + try erlang:port_command(Sock, Data) of + true -> ok + catch + error:badarg -> {error, einval} + end; +send(#ssl_socket{ssl = SslSock}, Data) -> + ssl:send(SslSock, Data). + +-spec(close(socket()) -> ok). +close(Sock) when is_port(Sock) -> + gen_tcp:close(Sock); +close(#ssl_socket{ssl = SslSock}) -> + ssl:close(SslSock). + +-spec(setopts(socket(), [gen_tcp:option() | ssl:socketoption()]) -> ok). +setopts(Sock, Opts) when is_port(Sock) -> + inet:setopts(Sock, Opts); +setopts(#ssl_socket{ssl = SslSock}, Opts) -> + ssl:setopts(SslSock, Opts). + +-spec(getstat(socket(), [atom()]) + -> {ok, [{atom(), integer()}]} | {error, term()}). +getstat(Sock, Options) when is_port(Sock) -> + inet:getstat(Sock, Options); +getstat(#ssl_socket{tcp = Sock}, Options) -> + inet:getstat(Sock, Options). + +-spec(sockname(socket()) -> {ok, sockname()} | {error, term()}). +sockname(Sock) when is_port(Sock) -> + inet:sockname(Sock); +sockname(#ssl_socket{ssl = SslSock}) -> + ssl:sockname(SslSock). + +-spec(merge_opts(list(), list()) -> list()). +merge_opts(Defaults, Options) -> + lists:foldl( + fun({Opt, Val}, Acc) -> + lists:keystore(Opt, 1, Acc, {Opt, Val}); + (Opt, Acc) -> + lists:usort([Opt | Acc]) + end, Defaults, Options). + +default_ciphers(TlsVersions) -> + lists:foldl( + fun(TlsVer, Ciphers) -> + Ciphers ++ ssl:cipher_suites(all, TlsVer) + end, [], TlsVersions). + diff --git a/src/emqttc.app.src b/src/emqttc.app.src deleted file mode 100644 index 6895913e..00000000 --- a/src/emqttc.app.src +++ /dev/null @@ -1,10 +0,0 @@ -{application, emqttc, [ - {id, "emqttc"}, - {vsn, "0.8.0"}, - {description, "Erlang MQTT Client"}, - {registered, []}, - {applications, [kernel, - stdlib]}, - {included_applications, []}, - {env, []} -]}. diff --git a/src/emqttc.erl b/src/emqttc.erl deleted file mode 100644 index 1ca6abd8..00000000 --- a/src/emqttc.erl +++ /dev/null @@ -1,1118 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2015-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc main client api. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttc). - --author("hiroe.orz@gmail.com"). - --author("Feng Lee "). - --include("emqttc_packet.hrl"). - --import(proplists, [get_value/2, get_value/3]). - -%% Start application --export([start/0]). - -%% Start emqttc client --export([start_link/0, start_link/1, start_link/2, start_link/3, start_link/4]). - -%% Lookup topics --export([topics/1]). - -%% Publish, Subscribe API --export([publish/3, publish/4, - sync_publish/4, - subscribe/2, subscribe/3, - sync_subscribe/2, sync_subscribe/3, - unsubscribe/2, - ping/1, - disconnect/1]). - --behaviour(gen_fsm). - -%% gen_fsm callbacks --export([init/1, handle_event/3, handle_sync_event/4, handle_info/3, - terminate/3, code_change/4]). - -%% FSM state --export([connecting/2, - waiting_for_connack/2, waiting_for_connack/3, - connected/2, connected/3, - disconnected/2, disconnected/3]). - --ifdef(TEST). - --export([qos_opt/1]). - --endif. - --type mqttc_opt() :: {host, inet:ip_address() | string()} - | {port, inet:port_number()} - | {client_id, binary()} - | {clean_sess, boolean()} - | {keepalive, non_neg_integer()} - | {proto_ver, mqtt_vsn()} - | {username, binary()} - | {password, binary()} - | {will, list(tuple())} - | {connack_timeout, pos_integer()} - | {puback_timeout, pos_integer()} - | {suback_timeout, pos_integer()} - | ssl | {ssl, [ssl:ssloption()]} - | force_ping | {force_ping, boolean()} - | auto_resub | {auto_resub, boolean()} - | {reconnect, non_neg_integer() | {non_neg_integer(), non_neg_integer()} | false}. - --type mqtt_qosopt() :: qos0 | qos1 | qos2 | mqtt_qos(). - --type mqtt_pubopt() :: mqtt_qosopt() | {qos, mqtt_qos()} | {retain, boolean()}. - --record(state, {recipient :: pid(), - name :: atom(), - host = "localhost" :: inet:ip_address() | string(), - port = 1883 :: inet:port_number(), - socket :: inet:socket(), - receiver :: pid(), - proto_state :: emqttc_protocol:proto_state(), - subscribers = [] :: list(), - pubsub_map = #{} :: map(), - ping_reqs = [] :: list(), - pending_pubsub = [] :: list(), - inflight_reqs = #{} :: map(), - inflight_msgid :: pos_integer(), - auto_resub = false :: boolean(), - force_ping = false :: boolean(), - keepalive :: emqttc_keepalive:keepalive() | undefined, - keepalive_after :: non_neg_integer(), - connack_timeout :: pos_integer(), - puback_timeout :: pos_integer(), - suback_timeout :: pos_integer(), - connack_tref :: reference(), - transport = tcp :: tcp | ssl, - reconnector :: emqttc_reconnector:reconnector() | undefined, - tcp_opts :: [gen_tcp:connect_option()], - ssl_opts :: [ssl:ssloption()]}). - -%% 60 secs --define(CONNACK_TIMEOUT, 60). - -%% 10 secs --define(SYNC_SEND_TIMEOUT, 10). - -%% 8 secs --define(SUBACK_TIMEOUT, 8). - -%% 4 secs --define(PUBACK_TIMEOUT, 4). - -%%%============================================================================= -%%% API -%%%============================================================================= - -start() -> - application:start(emqttc). - -%%------------------------------------------------------------------------------ -%% @doc Start emqttc client with default options. -%% @end -%%------------------------------------------------------------------------------ --spec start_link() -> {ok, Client :: pid()} | ignore | {error, term()}. -start_link() -> - start_link([]). - -%%------------------------------------------------------------------------------ -%% @doc Start emqttc client with options. -%% @end -%%------------------------------------------------------------------------------ --spec start_link(MqttOpts) -> {ok, Client} | ignore | {error, any()} when - MqttOpts :: [mqttc_opt()], - Client :: pid(). -start_link(MqttOpts) when is_list(MqttOpts) -> - start_link(MqttOpts, []). - -%%------------------------------------------------------------------------------ -%% @doc Start emqttc client with name, options. -%% @end -%%------------------------------------------------------------------------------ --spec start_link(Name | MqttOpts, TcpOpts) -> {ok, pid()} | ignore | {error, any()} when - Name :: atom(), - MqttOpts :: [mqttc_opt()], - TcpOpts :: [gen_tcp:connect_option()]. -start_link(Name, MqttOpts) when is_atom(Name), is_list(MqttOpts) -> - start_link(Name, MqttOpts, []); -start_link(MqttOpts, TcpOpts) when is_list(MqttOpts), is_list(TcpOpts) -> - gen_fsm:start_link(?MODULE, [undefined, self(), MqttOpts, TcpOpts], []). - -%%------------------------------------------------------------------------------ -%% @doc Start emqttc client with name, options, tcp options. -%% @end -%%------------------------------------------------------------------------------ --spec start_link(Name, MqttOpts, TcpOpts) -> {ok, pid()} | ignore | {error, any()} when - Name :: atom(), - MqttOpts :: [mqttc_opt()], - TcpOpts :: [gen_tcp:connect_option()]. -start_link(Name, MqttOpts, TcpOpts) when is_atom(Name), is_list(MqttOpts), is_list(TcpOpts) -> - start_link(Name, self(), MqttOpts, TcpOpts). - -%%------------------------------------------------------------------------------ -%% @doc Start emqttc client with Recipient, name, options, tcp options. -%% @end -%%------------------------------------------------------------------------------ --spec start_link(Name, Recipient, MqttOpts, TcpOpts) -> {ok, pid()} | ignore | {error, any()} when - Name :: atom(), - Recipient :: pid() | atom(), - MqttOpts :: [mqttc_opt()], - TcpOpts :: [gen_tcp:connect_option()]. -start_link(Name, Recipient, MqttOpts, TcpOpts) when is_pid(Recipient), is_atom(Name), is_list(MqttOpts), is_list(TcpOpts) -> - gen_fsm:start_link({local, Name}, ?MODULE, [Name, Recipient, MqttOpts, TcpOpts], []). - -%%------------------------------------------------------------------------------ -%% @doc Lookup topics subscribed -%% @end -%%------------------------------------------------------------------------------ --spec topics(Client :: pid()) -> [{binary(), mqtt_qos()}]. -topics(Client) -> - gen_fsm:sync_send_all_state_event(Client, topics). - -%%------------------------------------------------------------------------------ -%% @doc Publish message to broker with QoS0. -%% @end -%%------------------------------------------------------------------------------ --spec publish(Client, Topic, Payload) -> ok when - Client :: pid() | atom(), - Topic :: binary(), - Payload :: binary(). -publish(Client, Topic, Payload) when is_binary(Topic), is_binary(Payload) -> - publish(Client, #mqtt_message{topic = Topic, payload = Payload}). - -%%------------------------------------------------------------------------------ -%% @doc Publish message to broker with Qos, retain options. -%% @end -%%------------------------------------------------------------------------------ --spec publish(Client, Topic, Payload, PubOpts) -> ok when - Client :: pid() | atom(), - Topic :: binary(), - Payload :: binary(), - PubOpts :: mqtt_qosopt() | [mqtt_pubopt()]. -publish(Client, Topic, Payload, QosOpt) when ?IS_QOS(QosOpt); is_atom(QosOpt) -> - publish(Client, message(Topic, Payload, QosOpt)); - -publish(Client, Topic, Payload, PubOpts) when is_list(PubOpts) -> - publish(Client, message(Topic, Payload, PubOpts)). - -%%------------------------------------------------------------------------------ -%% @doc Publish message to broker and return until Puback received. -%% @end -%%------------------------------------------------------------------------------ --spec sync_publish(Client, Topic, Payload, PubOpts) -> {ok, MsgId} | {error, timeout} when - Client :: pid() | atom(), - Topic :: binary(), - Payload :: binary(), - PubOpts :: mqtt_qosopt() | [mqtt_pubopt()], - MsgId :: mqtt_packet_id(). -sync_publish(Client, Topic, Payload, QosOpt) when ?IS_QOS(QosOpt); is_atom(QosOpt) -> - sync_publish(Client, message(Topic, Payload, QosOpt)); - -sync_publish(Client, Topic, Payload, PubOpts) when is_list(PubOpts) -> - sync_publish(Client, message(Topic, Payload, PubOpts)). - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Publish MQTT Message. -%% @end -%%------------------------------------------------------------------------------ --spec publish(Client, Message) -> ok when - Client :: pid() | atom(), - Message :: mqtt_message(). -publish(Client, Msg) when is_record(Msg, mqtt_message) -> - gen_fsm:send_event(Client, {publish, Msg}). - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Publish MQTT Message and waits until Puback received. -%% @end -%%------------------------------------------------------------------------------ -sync_publish(Client, Msg) when is_record(Msg, mqtt_message) -> - gen_fsm:sync_send_event(Client, {publish, Msg}). - -%% make mqtt message -message(Topic, Payload, QosOpt) when ?IS_QOS(QosOpt); is_atom(QosOpt) -> - #mqtt_message{qos = qos_opt(QosOpt), - topic = Topic, - payload = Payload}; - -message(Topic, Payload, PubOpts) when is_list(PubOpts) -> - #mqtt_message{qos = qos_opt(PubOpts), - retain = get_value(retain, PubOpts, false), - topic = Topic, - payload = Payload}. - -%%------------------------------------------------------------------------------ -%% @doc Subscribe topic or topics. -%% @end -%%------------------------------------------------------------------------------ --spec subscribe(Client, Topics) -> ok when - Client :: pid() | atom(), - Topics :: [{binary(), mqtt_qos()}] | {binary(), mqtt_qos()} | binary(). -subscribe(Client, Topic) when is_binary(Topic) -> - subscribe(Client, {Topic, ?QOS_0}); -subscribe(Client, {Topic, Qos}) when is_binary(Topic), (?IS_QOS(Qos) orelse is_atom(Qos)) -> - subscribe(Client, [{Topic, qos_opt(Qos)}]); -subscribe(Client, [{_Topic, _Qos} | _] = Topics) -> - send_subscribe(Client, [{Topic, qos_opt(Qos)} || {Topic, Qos} <- Topics]). - -%%------------------------------------------------------------------------------ -%% @doc Subscribe topic or topics and wait until suback received. -%% @end -%%------------------------------------------------------------------------------ --spec sync_subscribe(Client, Topics) -> {ok, (mqtt_qos() | ?QOS_UNAUTHORIZED) | [mqtt_qos() | ?QOS_UNAUTHORIZED]} when - Client :: pid() | atom(), - Topics :: [{binary(), mqtt_qos()}] | {binary(), mqtt_qos()} | binary(). -sync_subscribe(Client, Topic) when is_binary(Topic) -> - sync_subscribe(Client, {Topic, ?QOS_0}); -sync_subscribe(Client, {Topic, Qos}) when is_binary(Topic), (?IS_QOS(Qos) orelse is_atom(Qos)) -> - case sync_subscribe(Client, [{Topic, qos_opt(Qos)}]) of - {ok, [GrantedQos]} -> - {ok, GrantedQos}; - {error, Error} -> - {error, Error} - end; -sync_subscribe(Client, [{_Topic, _Qos} | _] = Topics) -> - sync_send_subscribe(Client, [{Topic, qos_opt(Qos)} || {Topic, Qos} <- Topics]). - -%%------------------------------------------------------------------------------ -%% @doc Subscribe Topic with Qos. -%% @end -%%------------------------------------------------------------------------------ --spec subscribe(Client, Topic, Qos) -> ok when - Client :: pid() | atom(), - Topic :: binary(), - Qos :: qos0 | qos1 | qos2 | mqtt_qos(). -subscribe(Client, Topic, Qos) when is_binary(Topic), (?IS_QOS(Qos) orelse is_atom(Qos)) -> - subscribe(Client, [{Topic, qos_opt(Qos)}]). - -%%------------------------------------------------------------------------------ -%% @doc Subscribe Topic with QoS and wait until suback received. -%% @end -%%------------------------------------------------------------------------------ --spec sync_subscribe(Client, Topic, Qos) -> {ok, mqtt_qos() | ?QOS_UNAUTHORIZED} when - Client :: pid() | atom(), - Topic :: binary(), - Qos :: qos0 | qos1 | qos2 | mqtt_qos(). -sync_subscribe(Client, Topic, Qos) when is_binary(Topic), (?IS_QOS(Qos) orelse is_atom(Qos)) -> - case sync_send_subscribe(Client, [{Topic, qos_opt(Qos)}]) of - {ok, [GrantedQos]} -> {ok, GrantedQos}; - {error, Error} -> {error, Error} - end. - -send_subscribe(Client, TopicTable) -> - gen_fsm:send_event(Client, {subscribe, self(), TopicTable}). - -sync_send_subscribe(Client, TopicTable) -> - gen_fsm:sync_send_event(Client, {subscribe, self(), TopicTable}, ?SYNC_SEND_TIMEOUT*1000). - -%%------------------------------------------------------------------------------ -%% @doc Unsubscribe Topics -%% @end -%%------------------------------------------------------------------------------ --spec unsubscribe(Client, Topics) -> ok when - Client :: pid() | atom(), - Topics :: [binary()] | binary(). -unsubscribe(Client, Topic) when is_binary(Topic) -> - unsubscribe(Client, [Topic]); -unsubscribe(Client, [Topic | _] = Topics) when is_binary(Topic) -> - gen_fsm:send_event(Client, {unsubscribe, self(), Topics}). - -%%------------------------------------------------------------------------------ -%% @doc Sync Send ping to broker. -%% @end -%%------------------------------------------------------------------------------ --spec ping(Client) -> pong when Client :: pid() | atom(). -ping(Client) -> - gen_fsm:sync_send_event(Client, {self(), ping}, ?SYNC_SEND_TIMEOUT*1000). - -%%------------------------------------------------------------------------------ -%% @doc Disconnect from broker. -%% @end -%%------------------------------------------------------------------------------ --spec disconnect(Client) -> ok when Client :: pid() | atom(). -disconnect(Client) -> - gen_fsm:send_event(Client, disconnect). - -%%%============================================================================= -%%% gen_fsm callbacks -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @private -%% @doc -%% Whenever a gen_fsm is started using gen_fsm:start/[3,4] or -%% gen_fsm:start_link/[3,4], this function is called by the new -%% process to initialize. -%% -%% @end -%%------------------------------------------------------------------------------ --spec(init(Args :: term()) -> - {ok, StateName :: atom(), StateData :: #state{}} | - {ok, StateName :: atom(), StateData :: #state{}, timeout() | hibernate} | - {stop, Reason :: term()} | ignore). -init([undefined, Recipient, MqttOpts, TcpOpts]) -> - init([pid_to_list(Recipient), Recipient, MqttOpts, TcpOpts]); - -init([Name, Recipient, MqttOpts, TcpOpts]) -> - - process_flag(trap_exit, true), - - case get_value(client_id, MqttOpts) of - undefined -> ?warn("ClientId is NULL!", []); - _ -> ok - end, - - ProtoState = emqttc_protocol:init( - emqttc_opts:merge([{keepalive, ?KEEPALIVE}], MqttOpts)), - - State = init(MqttOpts, #state{name = Name, - recipient = Recipient, - host = "127.0.0.1", - port = 1883, - proto_state = ProtoState, - keepalive_after = ?KEEPALIVE, - connack_timeout = ?CONNACK_TIMEOUT, - puback_timeout = ?PUBACK_TIMEOUT, - suback_timeout = ?SUBACK_TIMEOUT, - tcp_opts = TcpOpts, - ssl_opts = []}), - - {ok, connecting, State, 0}. - -init([], State) -> - State; -init([{host, Host} | Opts], State) -> - init(Opts, State#state{host = Host}); -init([{port, Port} | Opts], State) -> - init(Opts, State#state{port = Port}); -init([ssl | Opts], State) -> - ssl:start(), % ok? - init(Opts, State#state{transport = ssl}); -init([{ssl, SslOpts} | Opts], State) -> - ssl:start(), % ok? - init(Opts, State#state{transport = ssl, ssl_opts = SslOpts}); -init([{auto_resub, Cfg} | Opts], State) when is_boolean(Cfg) -> - init(Opts, State#state{auto_resub= Cfg}); -init([auto_resub | Opts], State) -> - init(Opts, State#state{auto_resub= true}); -init([{force_ping, Cfg} | Opts], State) when is_boolean(Cfg) -> - init(Opts, State#state{force_ping = Cfg}); -init([force_ping | Opts], State) -> - init(Opts, State#state{force_ping = true}); -init([{keepalive, Time} | Opts], State) -> - init(Opts, State#state{keepalive_after = Time}); -init([{connack_timeout, Timeout}| Opts], State) -> - init(Opts, State#state{connack_timeout = Timeout}); -init([{puback_timeout, Timeout}| Opts], State) -> - init(Opts, State#state{puback_timeout = Timeout}); -init([{suback_timeout, Timeout}| Opts], State) -> - init(Opts, State#state{suback_timeout = Timeout}); -init([{reconnect, ReconnOpt} | Opts], State) -> - init(Opts, State#state{reconnector = init_reconnector(ReconnOpt)}); -init([_Opt | Opts], State) -> - init(Opts, State). - -init_reconnector(false) -> - undefined; -init_reconnector(Params) when is_integer(Params) orelse is_tuple(Params) -> - emqttc_reconnector:new(Params). - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Event Handler for state that connecting to MQTT broker. -%% @end -%%------------------------------------------------------------------------------ -connecting(timeout, State) -> - connect(State). - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Event Handler for state that waiting_for_connack from MQTT broker. -%% @end -%%------------------------------------------------------------------------------ -waiting_for_connack(?CONNACK_PACKET(?CONNACK_ACCEPT), State = #state{ - recipient = Recipient, - name = Name, - pending_pubsub = Pending, - auto_resub = AutoResub, - pubsub_map = PubsubMap, - proto_state = ProtoState, - keepalive = KeepAlive, - connack_tref = TRef}) -> - ?info("[Client ~s] RECV: CONNACK_ACCEPT", [Name]), - - %% Cancel connack timer - if - TRef =:= undefined -> ok; - true -> gen_fsm:cancel_timer(TRef) - end, - - {ok, ProtoState1} = emqttc_protocol:received('CONNACK', ProtoState), - - %% Resubscribe automatically - case AutoResub of - true -> - case [{Topic, Qos} || {Topic, {Qos, _Subs}} <- maps:to_list(PubsubMap)] of - [] -> ok; - TopicTable -> subscribe(self(), TopicTable) - end; - false -> - ok - end, - - %% Send the pending pubsub - [gen_fsm:send_event(self(), Event) || Event <- lists:reverse(Pending)], - - %% Start keepalive - case emqttc_keepalive:start(KeepAlive) of - {ok, KeepAlive1} -> - %% Tell recipient to subscribe - Recipient ! {mqttc, self(), connected}, - - {next_state, connected, State#state{proto_state = ProtoState1, - keepalive = KeepAlive1, - connack_tref = undefined, - pending_pubsub = []}}; - {error, Error} -> - {stop, {shutdown, Error}, State} - end; - -waiting_for_connack(?CONNACK_PACKET(ReturnCode), State = #state{name = Name}) -> - ErrConnAck = emqttc_packet:connack_name(ReturnCode), - ?debug("[Client ~s] RECV: ~s", [Name, ErrConnAck]), - {stop, {shutdown, {connack_error, ErrConnAck}}, State}; - -waiting_for_connack(Packet = ?PACKET(_Type), State = #state{name = Name}) -> - ?error("[Client ~s] RECV: ~s, when waiting for connack!", [Name, emqttc_packet:dump(Packet)]), - next_state(waiting_for_connack, State); - -waiting_for_connack(Event = {publish, _Msg}, State) -> - next_state(waiting_for_connack, pending(Event, State)); - -waiting_for_connack(Event = {Tag, _From, _Topics}, State) - when Tag =:= subscribe orelse Tag =:= unsubscribe -> - next_state(waiting_for_connack, pending(Event, State)); - -waiting_for_connack(disconnect, State=#state{receiver = Receiver, proto_state = ProtoState}) -> - emqttc_protocol:disconnect(ProtoState), - emqttc_socket:stop(Receiver), - {stop, normal, State#state{socket = undefined, receiver = undefined}}; - -waiting_for_connack({timeout, TRef, connack}, State = #state{name = Name, connack_tref = TRef}) -> - ?error("[Client ~s] CONNACK Timeout!", [Name]), - {stop, {shutdown, connack_timeout}, State}; - -waiting_for_connack(Event, State = #state{name = Name}) -> - ?warn("[Client ~s] Unexpected Event: ~p, when waiting for connack!", [Name, Event]), - {next_state, waiting_for_connack, State}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Sync Event Handler for state that waiting_for_connack from MQTT broker. -%% @end -%%------------------------------------------------------------------------------ -waiting_for_connack(Event, _From, State = #state{name = Name}) -> - ?error("[Client ~s] Event when waiting_for_connack: ~p", [Name, Event]), - {reply, {error, waiting_for_connack}, waiting_for_connack, State}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Event Handler for state that connected to MQTT broker. -%% @end -%%------------------------------------------------------------------------------ -connected({publish, Msg}, State=#state{proto_state = ProtoState}) -> - {ok, _, ProtoState1} = emqttc_protocol:publish(Msg, ProtoState), - next_state(connected, State#state{proto_state = ProtoState1}); - -connected({subscribe, SubPid, Topics}, State = #state{subscribers = Subscribers, - pubsub_map = PubSubMap, - proto_state = ProtoState}) -> - - {ok, MsgId, ProtoState1} = emqttc_protocol:subscribe(Topics, ProtoState), - - %% monitor subscriber - Subscribers1 = - case lists:keyfind(SubPid, 1, Subscribers) of - {SubPid, _MonRef} -> - Subscribers; - false -> - [{SubPid, erlang:monitor(process, SubPid)} | Subscribers] - end, - - %% register to pubsub - PubSubMap1 = lists:foldl( - fun({Topic, Qos}, Map) -> - case maps:find(Topic, Map) of - {ok, {OldQos, Subs}} -> - case lists:member(SubPid, Subs) of - true -> - if - Qos =:= OldQos -> - Map; - true -> - ?error("Subscribe topic '~s' with different qos: old=~p, new=~p", [Topic, OldQos, Qos]), - maps:put(Topic, {Qos, Subs}, Map) - end; - false -> - maps:put(Topic, {Qos, [SubPid| Subs]}, Map) - end; - error -> - maps:put(Topic, {Qos, [SubPid]}, Map) - end - end, PubSubMap, Topics), - - next_state(connected, State#state{subscribers = Subscribers1, - pubsub_map = PubSubMap1, - inflight_msgid = MsgId, - proto_state = ProtoState1}); - -connected({unsubscribe, From, Topics}, State=#state{subscribers = Subscribers, - pubsub_map = PubSubMap, - proto_state = ProtoState}) -> - - {ok, ProtoState1} = emqttc_protocol:unsubscribe(Topics, ProtoState), - - %% unregister from pubsub - PubSubMap1 = - lists:foldl( - fun(Topic, Map) -> - case maps:find(Topic, Map) of - {ok, {Qos, Subs}} -> - case lists:member(From, Subs) of - true -> - maps:put(Topic, {Qos, lists:delete(From, Subs)}, Map); - false -> - Map - end; - error -> - Map - end - end, PubSubMap, Topics), - - %% demonitor - Subscribers1 = - case lists:keyfind(From, 1, Subscribers) of - {From, MonRef} -> - case lists:member(From, lists:append([Subs || {_Qos, Subs} <- maps:values(PubSubMap1)])) of - true -> - Subscribers; - false -> - erlang:demonitor(MonRef, [flush]), - lists:keydelete(From, 1, Subscribers) - end; - false -> - Subscribers - end, - - next_state(connected, State#state{subscribers = Subscribers1, - pubsub_map = PubSubMap1, - proto_state = ProtoState1}); - -connected(disconnect, State=#state{receiver = Receiver, proto_state = ProtoState}) -> - emqttc_protocol:disconnect(ProtoState), - emqttc_socket:stop(Receiver), - {stop, normal, State#state{socket = undefined, receiver = undefined}}; - -connected(Packet = ?PACKET(_Type), State = #state{name = Name}) -> - % ?debug("[Client ~s] RECV: ~s", [Name, emqttc_packet:dump(Packet)]), - {ok, NewState} = received(Packet, State), - next_state(connected, NewState); - -connected(Event, State = #state{name = Name}) -> - ?warn("[Client ~s] Unexpected Event: ~p, when broker connected!", [Name, Event]), - next_state(connected, State). - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Sync Event Handler for state that connected to MQTT broker. -%% @end -%%------------------------------------------------------------------------------ - -connected({publish, Msg = #mqtt_message{qos = ?QOS_0}}, _From, State=#state{proto_state = ProtoState}) -> - {ok, _, ProtoState1} = emqttc_protocol:publish(Msg, ProtoState), - {reply, ok, connected, State#state{proto_state = ProtoState1}}; - -connected({publish, Msg = #mqtt_message{qos = _Qos}}, From, State=#state{inflight_reqs = InflightReqs, - puback_timeout = AckTimeout, - proto_state = ProtoState}) -> - {ok, MsgId, ProtoState1} = emqttc_protocol:publish(Msg, ProtoState), - - MRef = erlang:send_after(AckTimeout*1000, self(), {timeout, puback, MsgId}), - - InflightReqs1 = maps:put(MsgId, {publish, From, MRef}, InflightReqs), - - {next_state, connected, State#state{proto_state = ProtoState1, inflight_reqs = InflightReqs1}}; - -connected(Event = {subscribe, _SubPid, _Topics}, From, State = #state{inflight_reqs = InflightReqs, - suback_timeout = AckTimeout}) -> - - {next_state, _, State1 = #state{inflight_msgid = MsgId}, _} = connected(Event, State), - - MRef = erlang:send_after(AckTimeout*1000, self(), {timeout, suback, MsgId}), - - InflightReqs1 = maps:put(MsgId, {subscribe, From, MRef}, InflightReqs), - - {next_state, connected, State1#state{inflight_reqs = InflightReqs1}}; - -connected({Pid, ping}, From, State = #state{ping_reqs = PingReqs, proto_state = ProtoState}) -> - emqttc_protocol:ping(ProtoState), - PingReqs1 = - case lists:keyfind(From, 1, PingReqs) of - {From, _MonRef} -> - PingReqs; - false -> - [{From, erlang:monitor(process, Pid)} | PingReqs] - end, - {next_state, connected, State#state{ping_reqs = PingReqs1}}; - -connected(Event, _From, State = #state{name = Name}) -> - ?error("[Client ~s] Unexpected Sync Event when connected: ~p", [Name, Event]), - {reply, {error, unexpected_event}, connected, State}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Event Handler for state that disconnected from MQTT broker. -%% @end -%%------------------------------------------------------------------------------ -disconnected(Event = {publish, _Msg}, State) -> - next_state(disconnected, pending(Event, State)); - -disconnected(Event = {Tag, _From, _Topics}, State) when - Tag =:= subscribe orelse Tag =:= unsubscribe -> - next_state(disconnected, pending(Event, State)); - -disconnected(disconnect, State) -> - {stop, normal, State}; - -disconnected(Event, State = #state{name = Name}) -> - ?error("[Client ~s] Unexpected Event: ~p, when disconnected from broker!", [Name, Event]), - next_state(disconnected, State). - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Sync Event Handler for state that disconnected from MQTT broker. -%% @end -%%------------------------------------------------------------------------------ -disconnected(Event, _From, State = #state{name = Name}) -> - ?error("Client ~s] Unexpected Sync Event: ~p, when disconnected from broker!", [Name, Event]), - {reply, {error, disonnected}, disconnected, State}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc -%% Whenever a gen_fsm receives an event sent using -%% gen_fsm:send_all_state_event/2, this function is called to handle -%% the event. -%% -%% @end -%%------------------------------------------------------------------------------ --spec(handle_event(Event :: term(), StateName :: atom(), - StateData :: #state{}) -> - {next_state, NextStateName :: atom(), NewStateData :: #state{}} | - {next_state, NextStateName :: atom(), NewStateData :: #state{}, - timeout() | hibernate} | - {stop, Reason :: term(), NewStateData :: #state{}}). - -handle_event({frame_error, Error}, _StateName, State = #state{name = Name}) -> - ?error("[Client ~s] Frame Error: ~p", [Name, Error]), - {stop, {shutdown, {frame_error, Error}}, State}; - -handle_event({connection_lost, Reason}, StateName, State = #state{recipient = Recipient, name = Name, keepalive = KeepAlive, connack_tref = TRef}) - when StateName =:= connected; StateName =:= waiting_for_connack -> - - ?warn("[Client ~s] Connection lost for: ~p", [Name, Reason]), - - %% cancel connack timer first, if connection lost when waiting for connack. - case {StateName, TRef} of - {waiting_for_connack, undefined} -> ok; - {waiting_for_connack, TRef} -> gen_fsm:cancel_timer(TRef); - _ -> ok - end, - - %% cancel keepalive - emqttc_keepalive:cancel(KeepAlive), - - %% tell recipient - Recipient ! {mqttc, self(), disconnected}, - - try_reconnect(Reason, State#state{socket = undefined, connack_tref = TRef}); - -handle_event(Event, StateName, State = #state{name = Name}) -> - ?warn("[Client ~s] Unexpected Event when ~s: ~p", [Name, StateName, Event]), - {next_state, StateName, State}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc -%% Whenever a gen_fsm receives an event sent using -%% gen_fsm:sync_send_all_state_event/[2,3], this function is called -%% to handle the event. -%% -%% @end -%%------------------------------------------------------------------------------ --spec(handle_sync_event(Event :: term(), From :: {pid(), Tag :: term()}, - StateName :: atom(), StateData :: term()) -> - {reply, Reply :: term(), NextStateName :: atom(), NewStateData :: term()} | - {reply, Reply :: term(), NextStateName :: atom(), NewStateData :: term(), - timeout() | hibernate} | - {next_state, NextStateName :: atom(), NewStateData :: term()} | - {next_state, NextStateName :: atom(), NewStateData :: term(), - timeout() | hibernate} | - {stop, Reason :: term(), Reply :: term(), NewStateData :: term()} | - {stop, Reason :: term(), NewStateData :: term()}). -handle_sync_event(topics, _From, StateName, State = #state{pubsub_map = PubsubMap}) -> - TopicTable = [{Topic, Qos} || {Topic, {Qos, _Subs}} <- maps:to_list(PubsubMap)], - {reply, TopicTable, StateName, State}; - -handle_sync_event(_Event, _From, StateName, State) -> - Reply = ok, - {reply, Reply, StateName, State}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc -%% This function is called by a gen_fsm when it receives any -%% message other than a synchronous or asynchronous event -%% (or a system message). -%% -%% @end -%%------------------------------------------------------------------------------ - --spec(handle_info(Info :: term(), StateName :: atom(), StateData :: term()) -> - {next_state, NextStateName :: atom(), NewStateData :: term()} | - {next_state, NextStateName :: atom(), NewStateData :: term(), timeout() | hibernate} | - {stop, Reason :: normal | term(), NewStateData :: term()}). -handle_info({timeout, suback, MsgId}, StateName, State) -> - {next_state, StateName, reply_timeout({suback, MsgId}, State)}; - -handle_info({timeout, puback, MsgId}, StateName, State) -> - {next_state, StateName, reply_timeout({puback, MsgId}, State)}; - -handle_info({reconnect, timeout}, disconnected, State) -> - connect(State); - -handle_info({keepalive, timeout}, connected, State = - #state{proto_state = ProtoState, keepalive = KeepAlive, force_ping = ForcePing}) -> - case emqttc_keepalive:resume(KeepAlive) of - timeout -> - emqttc_protocol:ping(ProtoState), - case emqttc_keepalive:restart(KeepAlive) of - {ok, NewKeepAlive} -> - next_state(connected, State#state{keepalive = NewKeepAlive}); - {error, Error} -> - {stop, {shutdown, Error}, State} - end; - {resumed, NewKeepAlive} -> - case ForcePing of - true -> emqttc_protocol:ping(ProtoState); - false -> ignore - end, - next_state(connected, State#state{keepalive = NewKeepAlive}); - {error, Error} -> - {stop, {shutdown, Error}, State} - end; - -handle_info({'EXIT', Receiver, normal}, StateName, State = #state{receiver = Receiver}) -> - {next_state, StateName, State#state{receiver = undefined}}; - -handle_info({'EXIT', Receiver, Reason}, _StateName, - State = #state{name = Name, receiver = Receiver, - keepalive = KeepAlive}) -> - %% event occured when receiver error - ?error("[Client ~s] receiver exit: ~p", [Name, Reason]), - emqttc_keepalive:cancel(KeepAlive), - try_reconnect({receiver, Reason}, State#state{receiver = undefined, socket = undefined}); - -handle_info(Down = {'DOWN', MonRef, process, Pid, _Why}, StateName, - State = #state{name = Name, - subscribers = Subscribers, - pubsub_map = PubSubMap, - ping_reqs = PingReqs}) -> - ?warn("[Client ~s] Process DOWN: ~p", [Name, Down]), - - %% ping? - PingReqs1 = lists:keydelete(MonRef, 2, PingReqs), - - %% clear pubsub - {Subscribers1, PubSubMap1} = - case lists:keyfind(MonRef, 2, Subscribers) of - {Pid, MonRef} -> - {lists:delete({Pid, MonRef}, Subscribers), - maps:fold(fun(Topic, {Qos, Subs}, Map) -> - case lists:member(Pid, Subs) of - true -> maps:put(Topic, {Qos, lists:delete(Pid, Subs)}, Map); - false -> Map - end - end, PubSubMap, PubSubMap)}; - false -> - {Subscribers, PubSubMap} - end, - - next_state(StateName, State#state{subscribers = Subscribers1, - pubsub_map = PubSubMap1, - ping_reqs = PingReqs1}); - -handle_info({inet_reply, Socket, ok}, StateName, State = #state{socket = Socket}) -> - %socket send reply. - next_state(StateName, State); - -handle_info(Info, StateName, State = #state{name = Name}) -> - ?error("[Client ~s] Unexpected Info when ~s: ~p", [Name, StateName, Info]), - {next_state, StateName, State}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc -%% This function is called by a gen_fsm when it is about to -%% terminate. It should be the opposite of Module:init/1 and do any -%% necessary cleaning up. When it returns, the gen_fsm terminates with -%% Reason. The return value is ignored. -%% -%% @end -%%------------------------------------------------------------------------------ --spec(terminate(Reason :: normal | shutdown | {shutdown, term()} -| term(), StateName :: atom(), StateData :: term()) -> term()). -terminate(_Reason, _StateName, #state{keepalive = KeepAlive, reconnector = Reconnector}) -> - emqttc_keepalive:cancel(KeepAlive), - if - Reconnector =:= undefined -> ok; - true -> emqttc_reconnector:reset(Reconnector) - end, - ok. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Convert process state when code is changed -%% @end -%%------------------------------------------------------------------------------ --spec(code_change(OldVsn :: term() | {down, term()}, StateName :: atom(), - StateData :: #state{}, Extra :: term()) -> - {ok, NextStateName :: atom(), NewStateData :: #state{}}). -code_change(_OldVsn, StateName, State, _Extra) -> - {ok, StateName, State}. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -next_state(StateName, State) -> - {next_state, StateName, State, hibernate}. - -connect(State = #state{name = Name, - host = Host, - port = Port, - socket = undefined, - receiver = undefined, - proto_state = ProtoState, - keepalive_after = KeepAliveTime, - connack_timeout = ConnAckTimeout, - transport = Transport, - tcp_opts = TcpOpts, - ssl_opts = SslOpts}) -> - ?info("[Client ~s]: connecting to ~s:~p", [Name, Host, Port]), - case emqttc_socket:connect(self(), Transport, Host, Port, TcpOpts, SslOpts) of - {ok, Socket, Receiver} -> - ProtoState1 = emqttc_protocol:set_socket(ProtoState, Socket), - emqttc_protocol:connect(ProtoState1), - KeepAlive = emqttc_keepalive:new({Socket, send_oct}, KeepAliveTime, {keepalive, timeout}), - TRef = gen_fsm:start_timer(ConnAckTimeout*1000, connack), - ?info("[Client ~s] connected with ~s:~p", [Name, Host, Port]), - {next_state, waiting_for_connack, State#state{socket = Socket, - receiver = Receiver, - keepalive = KeepAlive, - connack_tref = TRef, - proto_state = ProtoState1}}; - {error, Reason} -> - ?info("[Client ~s] connection failure: ~p", [Name, Reason]), - try_reconnect(Reason, State) - end. - -try_reconnect(Reason, State = #state{reconnector = undefined}) -> - {stop, {shutdown, Reason}, State}; - -try_reconnect(Reason, State = #state{name = Name, reconnector = Reconnector}) -> - ?info("[Client ~s] try reconnecting...", [Name]), - case emqttc_reconnector:execute(Reconnector, {reconnect, timeout}) of - {ok, Reconnector1} -> - {next_state, disconnected, State#state{reconnector = Reconnector1}}; - {stop, Error} -> - ?error("[Client ~s] reconect error: ~p", [Name, Error]), - {stop, {shutdown, Reason}, State} - end. - -pending(Event, State = #state{pending_pubsub = Pending}) -> - State#state{pending_pubsub = [Event | Pending]}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Handle Received Packet -%% @end -%%------------------------------------------------------------------------------ -received(?PUBLISH_PACKET(?QOS_0, Topic, undefined, Payload), State) -> - dispatch({publish, Topic, Payload}, State), - {ok, State}; - -received(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, _PacketId, Payload), State = #state{proto_state = ProtoState}) -> - - emqttc_protocol:received({'PUBLISH', Packet}, ProtoState), - - dispatch({publish, Topic, Payload}, State), - - {ok, State}; - -received(Packet = ?PUBLISH_PACKET(?QOS_2, _Topic, _PacketId, _Payload), State = #state{proto_state = ProtoState}) -> - - {ok, ProtoState1} = emqttc_protocol:received({'PUBLISH', Packet}, ProtoState), - - {ok, State#state{proto_state = ProtoState1}}; - -received(?PUBACK_PACKET(?PUBACK, PacketId), State = #state{proto_state = ProtoState}) -> - - {ok, ProtoState1} = emqttc_protocol:received({'PUBACK', PacketId}, ProtoState), - - {ok, reply({publish, PacketId}, {ok, PacketId}, State#state{proto_state = ProtoState1})}; - -received(?PUBACK_PACKET(?PUBREC, PacketId), State = #state{proto_state = ProtoState}) -> - - {ok, ProtoState1} = emqttc_protocol:received({'PUBREC', PacketId}, ProtoState), - - {ok, State#state{proto_state = ProtoState1}}; - -received(?PUBACK_PACKET(?PUBREL, PacketId), State = #state{proto_state = ProtoState}) -> - ProtoState2 = - case emqttc_protocol:received({'PUBREL', PacketId}, ProtoState) of - {ok, ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, Payload), ProtoState1} -> - dispatch({publish, Topic, Payload}, State), ProtoState1; - {ok, ProtoState1} -> ProtoState1 - end, - emqttc_protocol:pubcomp(PacketId, ProtoState2), - {ok, State#state{proto_state = ProtoState2}}; - -received(?PUBACK_PACKET(?PUBCOMP, PacketId), State = #state{proto_state = ProtoState}) -> - - {ok, ProtoState1} = emqttc_protocol:received({'PUBCOMP', PacketId}, ProtoState), - - {ok, reply({publish, PacketId}, {ok, PacketId}, State#state{proto_state = ProtoState1})}; - -received(?SUBACK_PACKET(PacketId, QosTable), State = #state{proto_state = ProtoState}) -> - - {ok, ProtoState1} = emqttc_protocol:received({'SUBACK', PacketId, QosTable}, ProtoState), - - {ok, reply({subscribe, PacketId}, {ok, QosTable}, State#state{proto_state = ProtoState1})}; - -received(?UNSUBACK_PACKET(PacketId), State = #state{proto_state = ProtoState}) -> - {ok, ProtoState1} = emqttc_protocol:received({'UNSUBACK', PacketId}, ProtoState), - {ok, State#state{proto_state = ProtoState1}}; - -received(?PACKET(?PINGRESP), State= #state{ping_reqs = PingReqs}) -> - [begin erlang:demonitor(Mon), gen_fsm:reply(Caller, pong) end || {Caller, Mon} <- PingReqs], - {ok, State#state{ping_reqs = []}}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Reply to synchronous request. -%% @end -%%------------------------------------------------------------------------------ -reply({PubSub, ReqId}, Reply, State = #state{inflight_reqs = InflightReqs}) -> - InflightReqs1 = - case maps:find(ReqId, InflightReqs) of - {ok, {PubSub, From, MRef}} -> - erlang:cancel_timer(MRef), - gen_fsm:reply(From, Reply), - maps:remove(ReqId, InflightReqs); - error -> - InflightReqs - end, - State#state{inflight_reqs = InflightReqs1}. - -reply_timeout({Ack, ReqId}, State=#state{inflight_reqs = InflightReqs}) -> - InflightReqs1 = - case maps:find(ReqId, InflightReqs) of - {ok, {_Pubsub, From, _MRef}} -> - gen_fsm:reply(From, {error, ack_timeout}), - maps:remove(ReqId, InflightReqs); - error -> - ?error("~s timeout, cannot find inflight reqid: ~p", [Ack, ReqId]), - InflightReqs - end, - State#state{inflight_reqs = InflightReqs1}. - -%%------------------------------------------------------------------------------ -%% @private -%% @doc Dispatch Publish Message to subscribers. -%% @end -%%------------------------------------------------------------------------------ -dispatch(Publish = {publish, TopicName, _Payload}, #state{recipient = Recipient, - pubsub_map = PubSubMap}) -> - Matched = - lists:foldl( - fun(TopicFilter, Acc) -> - case emqttc_topic:match(TopicName, TopicFilter) of - true -> - {_Qos, Subs} = maps:get(TopicFilter, PubSubMap), - lists:append(Subs, Acc); - false -> - Acc - end - end, [], maps:keys(PubSubMap)), - if - length(Matched) =:= 0 -> - %% Dispath to Recipient if no subscription matched. - Recipient ! Publish; - true -> - [Sub ! Publish || Sub <- unique(Matched)], ok - end. - -qos_opt(qos2) -> - ?QOS_2; -qos_opt(qos1) -> - ?QOS_1; -qos_opt(qos0) -> - ?QOS_0; -qos_opt(Qos) when is_integer(Qos), ?QOS_0 =< Qos, Qos =< ?QOS_2 -> - Qos; -qos_opt([]) -> - ?QOS_0; -qos_opt([qos2|_PubOpts]) -> - ?QOS_2; -qos_opt([qos1|_PubOpts]) -> - ?QOS_1; -qos_opt([qos0|_PubOpts]) -> - ?QOS_0; -qos_opt([{qos, Qos}|_PubOpts]) -> - Qos; -qos_opt([_|PubOpts]) -> - qos_opt(PubOpts). - -unique(L) -> - sets:to_list(sets:from_list(L)). diff --git a/src/emqttc_keepalive.erl b/src/emqttc_keepalive.erl deleted file mode 100644 index 642b63a7..00000000 --- a/src/emqttc_keepalive.erl +++ /dev/null @@ -1,108 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc emqttc socket keepalive. -%%% -%%% @author Feng Lee -%%%----------------------------------------------------------------------------- - --module(emqttc_keepalive). - --record(keepalive, {socket, - stat_name, - stat_val = 0, - timeout_sec, - timeout_msg, - timer_ref}). - --opaque keepalive() :: #keepalive{} | undefined. - --export_type([keepalive/0]). - -%% API --export([new/3, start/1, restart/1, resume/1, cancel/1]). - -%% @doc Create a KeepAlive. --spec new({Socket, StatName}, TimeoutSec, TimeoutMsg) -> KeepAlive when - Socket :: inet:socket() | ssl:sslsocket(), - StatName :: recv_oct | send_oct, - TimeoutSec :: non_neg_integer(), - TimeoutMsg :: tuple(), - KeepAlive :: keepalive(). -new({_Socket, _StatName}, 0, _TimeoutMsg) -> - undefined; -new({Socket, StatName}, TimeoutSec, TimeoutMsg) when TimeoutSec > 0 -> - #keepalive{socket = Socket, - stat_name = StatName, - timeout_sec = TimeoutSec, - timeout_msg = TimeoutMsg}. - -%% @doc Start KeepAlive --spec start(keepalive()) -> {ok, keepalive()} | {error, any()}. -start(undefined) -> - {ok, undefined}; -start(KeepAlive = #keepalive{socket = Socket, stat_name = StatName, - timeout_sec = TimeoutSec, - timeout_msg = TimeoutMsg}) -> - case emqttc_socket:getstat(Socket, [StatName]) of - {ok, [{StatName, StatVal}]} -> - Ref = erlang:send_after(TimeoutSec*1000, self(), TimeoutMsg), - {ok, KeepAlive#keepalive{stat_val = StatVal, timer_ref = Ref}}; - {error, Error} -> - {error, Error} - end. - -%% @doc Restart KeepAlive --spec restart(keepalive()) -> {ok, keepalive()} | {error, any()}. -restart(KeepAlive) -> start(KeepAlive). - -%% @doc Resume KeepAlive, called when timeout. --spec resume(keepalive()) -> timeout | {resumed, keepalive()} | {error, any()}. -resume(undefined) -> {resumed, undefined}; -resume(KeepAlive = #keepalive{socket = Socket, - stat_name = StatName, - stat_val = StatVal, - timeout_sec = TimeoutSec, - timeout_msg = TimeoutMsg, - timer_ref = Ref}) -> - case emqttc_socket:getstat(Socket, [StatName]) of - {ok, [{StatName, NewStatVal}]} -> - if - NewStatVal =:= StatVal -> - timeout; - true -> - cancel(Ref), %need? - NewRef = erlang:send_after(TimeoutSec*1000, self(), TimeoutMsg), - {resumed, KeepAlive#keepalive{stat_val = NewStatVal, timer_ref = NewRef}} - end; - {error, Error} -> - {error, Error} - end. - -%% @doc Cancel KeepAlive. --spec cancel(keepalive() | reference()) -> any(). -cancel(undefined) -> - ok; -cancel(#keepalive{timer_ref = Ref}) -> - cancel(Ref); -cancel(Ref) when is_reference(Ref)-> - catch erlang:cancel_timer(Ref). - diff --git a/src/emqttc_message.erl b/src/emqttc_message.erl deleted file mode 100644 index 8a7ae5b4..00000000 --- a/src/emqttc_message.erl +++ /dev/null @@ -1,137 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc message handler. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --module(emqttc_message). - --author("feng@emqtt.io"). - -%% API --export([]). - --include("emqttc_packet.hrl"). - --export([from_packet/1, to_packet/1]). - --export([set_flag/1, set_flag/2, unset_flag/1, unset_flag/2]). - --export([dump/1]). - -%%------------------------------------------------------------------------------ -%% @doc message from packet -%% @end -%%------------------------------------------------------------------------------ --spec from_packet(mqtt_packet()) -> mqtt_message() | undefined. -from_packet(#mqtt_packet{ header = #mqtt_packet_header{ type = ?PUBLISH, - retain = Retain, - qos = Qos, - dup = Dup }, - variable = #mqtt_packet_publish{ topic_name = Topic, - packet_id = PacketId }, - payload = Payload }) -> - #mqtt_message{ msgid = PacketId, - qos = Qos, - retain = Retain, - dup = Dup, - topic = Topic, - payload = Payload }; - -from_packet(#mqtt_packet_connect{ will_flag = false }) -> - undefined; - -from_packet(#mqtt_packet_connect{ will_retain = Retain, - will_qos = Qos, - will_topic = Topic, - will_msg = Msg }) -> - #mqtt_message{ retain = Retain, - qos = Qos, - topic = Topic, - dup = false, - payload = Msg }. - -%%------------------------------------------------------------------------------ -%% @doc message to packet -%% @end -%%------------------------------------------------------------------------------ --spec to_packet(mqtt_message()) -> mqtt_packet(). -to_packet(#mqtt_message{ msgid = MsgId, - qos = Qos, - retain = Retain, - dup = Dup, - topic = Topic, - payload = Payload }) -> - - PacketId = if - Qos =:= ?QOS_0 -> undefined; - true -> MsgId - end, - - #mqtt_packet{ header = #mqtt_packet_header { type = ?PUBLISH, - qos = Qos, - retain = Retain, - dup = Dup }, - variable = #mqtt_packet_publish { topic_name = Topic, - packet_id = PacketId }, - payload = Payload }. - -%%------------------------------------------------------------------------------ -%% @doc set dup, retain flag -%% @end -%%------------------------------------------------------------------------------ --spec set_flag(mqtt_message() ) -> mqtt_message(). -set_flag(Msg) -> - Msg#mqtt_message{dup = true, retain = true}. --spec set_flag(atom(), mqtt_message() ) -> mqtt_message(). -set_flag(dup, Msg = #mqtt_message{dup = false}) -> - Msg#mqtt_message{dup = true}; -set_flag(retain, Msg = #mqtt_message{retain = false}) -> - Msg#mqtt_message{retain = true}; -set_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. - - -%%------------------------------------------------------------------------------ -%% @doc unset dup, retain flag -%% @end -%%------------------------------------------------------------------------------ --spec unset_flag(mqtt_message() ) -> mqtt_message(). -unset_flag(Msg) -> - Msg#mqtt_message{dup = false, retain = false}. --spec unset_flag(atom(), mqtt_message() ) -> mqtt_message(). -unset_flag(dup, Msg = #mqtt_message{dup = true}) -> - Msg#mqtt_message{dup = false}; -unset_flag(retain, Msg = #mqtt_message{retain = true}) -> - Msg#mqtt_message{retain = false}; -unset_flag(Flag, Msg) when Flag =:= dup orelse Flag =:= retain -> Msg. - - -%%------------------------------------------------------------------------------ -%% @doc dump message -%% @end -%%------------------------------------------------------------------------------ -dump(#mqtt_message{msgid= MsgId, qos = Qos, retain = Retain, dup = Dup, topic = Topic}) -> - io_lib:format("Message(MsgId=~p, Qos=~p, Retain=~s, Dup=~s, Topic=~s)", - [ MsgId, Qos, Retain, Dup, Topic ]). - diff --git a/src/emqttc_opts.erl b/src/emqttc_opts.erl deleted file mode 100644 index a799c1e3..00000000 --- a/src/emqttc_opts.erl +++ /dev/null @@ -1,46 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- --module(emqttc_opts). - --export([merge/2]). - -%%------------------------------------------------------------------------------ -%% @doc Merge Options -%% @end -%%------------------------------------------------------------------------------ -merge(Defaults, Options) -> - lists:foldl( - fun({Opt, Val}, Acc) -> - case lists:keymember(Opt, 1, Acc) of - true -> - lists:keyreplace(Opt, 1, Acc, {Opt, Val}); - false -> - [{Opt, Val}|Acc] - end; - (Opt, Acc) -> - case lists:member(Opt, Acc) of - true -> Acc; - false -> [Opt | Acc] - end - end, Defaults, Options). - - diff --git a/src/emqttc_packet.erl b/src/emqttc_packet.erl deleted file mode 100644 index 24d3c6d0..00000000 --- a/src/emqttc_packet.erl +++ /dev/null @@ -1,150 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc packet. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --module(emqttc_packet). - --author("Feng Lee "). - --include("emqttc_packet.hrl"). - -%% API --export([protocol_name/1, type_name/1, connack_name/1]). - --export([dump/1]). - -%%------------------------------------------------------------------------------ -%% @doc Protocol name of version -%% @end -%%------------------------------------------------------------------------------ --spec protocol_name(mqtt_vsn()) -> binary(). -protocol_name(Ver) when Ver =:= ?MQTT_PROTO_V31; Ver =:= ?MQTT_PROTO_V311-> - proplists:get_value(Ver, ?PROTOCOL_NAMES). - -%%------------------------------------------------------------------------------ -%% @doc Name of MQTT packet type -%% @end -%%------------------------------------------------------------------------------ --spec type_name(mqtt_packet_type()) -> atom(). -type_name(Type) when Type > ?RESERVED andalso Type =< ?DISCONNECT -> - lists:nth(Type, ?TYPE_NAMES). - -%%------------------------------------------------------------------------------ -%% @doc Connack Name -%% @end -%%------------------------------------------------------------------------------ --spec connack_name(mqtt_connack()) -> atom(). -connack_name(?CONNACK_ACCEPT) -> 'CONNACK_ACCEPT'; -connack_name(?CONNACK_PROTO_VER) -> 'CONNACK_PROTO_VER'; -connack_name(?CONNACK_INVALID_ID ) -> 'CONNACK_INVALID_ID'; -connack_name(?CONNACK_SERVER) -> 'CONNACK_SERVER'; -connack_name(?CONNACK_CREDENTIALS) -> 'CONNACK_CREDENTIALS'; -connack_name(?CONNACK_AUTH) -> 'CONNACK_AUTH'. - -%%------------------------------------------------------------------------------ -%% @doc Dump packet -%% @end -%%------------------------------------------------------------------------------ --spec dump(mqtt_packet()) -> iolist(). -dump(#mqtt_packet{header = Header, variable = Variable, payload = Payload}) -> - dump_header(Header, dump_variable(Variable, Payload)). - -dump_header(#mqtt_packet_header{type = Type, - dup = Dup, - qos = QoS, - retain = Retain}, S) -> - S1 = if - S == undefined -> <<>>; - true -> [", ", S] - end, - io_lib:format("~s(Q~p, R~p, D~p~s)", [type_name(Type), QoS, i(Retain), i(Dup), S1]). - -dump_variable(undefined, _) -> - undefined; -dump_variable(Variable, undefined) -> - dump_variable(Variable); -dump_variable(Variable, Payload) -> - io_lib:format("~s, Payload=~p", [dump_variable(Variable), Payload]). - -dump_variable(#mqtt_packet_connect{ - proto_ver = ProtoVer, - proto_name = ProtoName, - will_retain = WillRetain, - will_qos = WillQoS, - will_flag = WillFlag, - clean_sess = CleanSess, - keep_alive = KeepAlive, - client_id = ClientId, - will_topic = WillTopic, - will_msg = WillMsg, - username = Username, - password = Password}) -> - Format = "ClientId=~s, ProtoName=~s, ProtoVsn=~p, CleanSess=~s, KeepAlive=~p, Username=~s, Password=~s", - Args = [ClientId, ProtoName, ProtoVer, CleanSess, KeepAlive, Username, dump_password(Password)], - {Format1, Args1} = if - WillFlag -> { Format ++ ", Will(Q~p, R~p, Topic=~s, Msg=~s)", - Args ++ [WillQoS, i(WillRetain), WillTopic, WillMsg] }; - true -> {Format, Args} - end, - io_lib:format(Format1, Args1); - -dump_variable(#mqtt_packet_connack{ack_flags = AckFlags, - return_code = ReturnCode}) -> - io_lib:format("AckFlags=~p, RetainCode=~p", [AckFlags, ReturnCode]); - -dump_variable(#mqtt_packet_publish{topic_name = TopicName, - packet_id = PacketId}) -> - io_lib:format("TopicName=~s, PacketId=~p", [TopicName, PacketId]); - -dump_variable(#mqtt_packet_puback{packet_id = PacketId}) -> - io_lib:format("PacketId=~p", [PacketId]); - -dump_variable(#mqtt_packet_subscribe{packet_id = PacketId, - topic_table = TopicTable}) -> - io_lib:format("PacketId=~p, TopicTable=~p", [PacketId, TopicTable]); - -dump_variable(#mqtt_packet_unsubscribe{packet_id = PacketId, - topics = Topics}) -> - io_lib:format("PacketId=~p, Topics=~p", [PacketId, Topics]); - -dump_variable(#mqtt_packet_suback{packet_id = PacketId, - qos_table = QosTable}) -> - io_lib:format("PacketId=~p, QosTable=~p", [PacketId, QosTable]); - -dump_variable(#mqtt_packet_unsuback{packet_id = PacketId}) -> - io_lib:format("PacketId=~p", [PacketId]); - -dump_variable(PacketId) when is_integer(PacketId) -> - io_lib:format("PacketId=~p", [PacketId]); - -dump_variable(undefined) -> undefined. - -dump_password(undefined) -> undefined; -dump_password(_) -> <<"******">>. - -i(true) -> 1; -i(false) -> 0; -i(I) when is_integer(I) -> I. diff --git a/src/emqttc_parser.erl b/src/emqttc_parser.erl deleted file mode 100644 index 36839cca..00000000 --- a/src/emqttc_parser.erl +++ /dev/null @@ -1,223 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc received packet parser. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --module(emqttc_parser). - --author("Feng Lee "). - --include("emqttc_packet.hrl"). - -%% API --export([new/0, parse/2]). - -%%%----------------------------------------------------------------------------- -%% @doc Initialize a parser. -%% @end -%%%----------------------------------------------------------------------------- --spec new() -> none. -new() -> none. - -%%%----------------------------------------------------------------------------- -%% @doc Parse MQTT Packet. -%% @end -%%%----------------------------------------------------------------------------- --spec parse(binary(), none | fun()) -> {ok, mqtt_packet()} | {error, any()} | {more, fun()}. -parse(<<>>, none) -> - {more, fun(Bin) -> parse(Bin, none) end}; -parse(<>, none) -> - parse_remaining_len(Rest, #mqtt_packet_header{type = PacketType, - dup = bool(Dup), - qos = QoS, - retain = bool(Retain)}); -parse(Bin, Cont) -> Cont(Bin). - -parse_remaining_len(<<>>, Header) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Header) end}; -parse_remaining_len(Rest, Header) -> - parse_remaining_len(Rest, Header, 1, 0). - -parse_remaining_len(_Bin, _Header, _Multiplier, Length) - when Length > ?MAX_LEN -> - {error, invalid_mqtt_frame_len}; -parse_remaining_len(<<>>, Header, Multiplier, Length) -> - {more, fun(Bin) -> parse_remaining_len(Bin, Header, Multiplier, Length) end}; -parse_remaining_len(<<0:1, 2:7, Rest/binary>>, Header, 1, 0) -> - parse_frame(Rest, Header, 2); -parse_remaining_len(<<0:8, Rest/binary>>, Header, 1, 0) -> - parse_frame(Rest, Header, 0); -parse_remaining_len(<<1:1, Len:7, Rest/binary>>, Header, Multiplier, Value) -> - parse_remaining_len(Rest, Header, Multiplier * ?HIGHBIT, Value + Len * Multiplier); -parse_remaining_len(<<0:1, Len:7, Rest/binary>>, Header, Multiplier, Value) -> - FrameLen = Value + Len * Multiplier, - if - FrameLen > ?MAX_LEN -> {error, invalid_mqtt_frame_len}; - true -> parse_frame(Rest, Header, FrameLen) - end. - -parse_frame(Bin, #mqtt_packet_header{type = Type, - qos = Qos} = Header, Length) -> - case {Type, Bin} of - %{?CONNECT, <>} -> - % {ProtoName, Rest1} = parse_utf(FrameBin), - % <> = Rest1, - % <> = Rest2, - % {ClientId, Rest4} = parse_utf(Rest3), - % {WillTopic, Rest5} = parse_utf(Rest4, WillFlag), - % {WillMsg, Rest6} = parse_msg(Rest5, WillFlag), - % {UserName, Rest7} = parse_utf(Rest6, UsernameFlag), - % {PasssWord, <<>>} = parse_utf(Rest7, PasswordFlag), - % case protocol_name_approved(ProtoVersion, ProtoName) of - % true -> - % wrap(Header, - % #mqtt_packet_connect{ - % proto_ver = ProtoVersion, - % proto_name = ProtoName, - % will_retain = bool(WillRetain), - % will_qos = WillQos, - % will_flag = bool(WillFlag), - % clean_sess = bool(CleanSession), - % keep_alive = KeepAlive, - % client_id = ClientId, - % will_topic = WillTopic, - % will_msg = WillMsg, - % username = UserName, - % password = PasssWord}, Rest); - % false -> - % {error, protocol_header_corrupt} - % end; - {?CONNACK, <>} -> - <<_Reserved:7, SP:1, ReturnCode:8>> = FrameBin, - wrap(Header, #mqtt_packet_connack{ack_flags = SP, - return_code = ReturnCode }, Rest); - {?PUBLISH, <>} -> - {TopicName, Rest1} = parse_utf(FrameBin), - {PacketId, Payload} = case Qos of - 0 -> {undefined, Rest1}; - _ -> <> = Rest1, - {Id, R} - end, - wrap(Header, #mqtt_packet_publish{topic_name = TopicName, - packet_id = PacketId}, - Payload, Rest); - {?PUBACK, <>} -> - <> = FrameBin, - wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); - {?PUBREC, <>} -> - <> = FrameBin, - wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); - {?PUBREL, <>} -> - 1 = Qos, - <> = FrameBin, - wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); - {?PUBCOMP, <>} -> - <> = FrameBin, - wrap(Header, #mqtt_packet_puback{packet_id = PacketId}, Rest); - %{?SUBSCRIBE, <>} -> - % 1 = Qos, - % <> = FrameBin, - % TopicTable = parse_topics(?SUBSCRIBE, Rest1, []), - % wrap(Header, #mqtt_packet_subscribe{packet_id = PacketId, - % topic_table = TopicTable}, Rest); - {?SUBACK, <>} -> - <> = FrameBin, - wrap(Header, #mqtt_packet_suback{packet_id = PacketId, - qos_table = parse_qos(Rest1, [])}, Rest); - %{?UNSUBSCRIBE, <>} -> - % 1 = Qos, - % <> = FrameBin, - % Topics = parse_topics(?UNSUBSCRIBE, Rest1, []), - % wrap(Header, #mqtt_packet_unsubscribe{packet_id = PacketId, - % topics = Topics}, Rest); - {?UNSUBACK, <>} -> - <> = FrameBin, - wrap(Header, #mqtt_packet_unsuback{packet_id = PacketId}, Rest); - %{?PINGREQ, Rest} -> - % Length = 0, - % wrap(Header, Rest); - {?PINGRESP, Rest} -> - Length = 0, - wrap(Header, Rest); - %{?DISCONNECT, Rest} -> - % Length = 0, - % wrap(Header, Rest); - {_, TooShortBin} -> - {more, fun(BinMore) -> - parse_frame(<>, - Header, Length) - end} - end. - -wrap(Header, Variable, Payload, Rest) -> - {ok, #mqtt_packet{header = Header, variable = Variable, payload = Payload}, Rest}. -wrap(Header, Variable, Rest) -> - {ok, #mqtt_packet{header = Header, variable = Variable}, Rest}. -wrap(Header, Rest) -> - {ok, #mqtt_packet{header = Header}, Rest}. - -parse_qos(<<>>, Acc) -> - lists:reverse(Acc); -parse_qos(<>, Acc) -> - parse_qos(Rest, [QoS | Acc]). - -% server function -%parse_topics(_, <<>>, Topics) -> -% Topics; -%parse_topics(?SUBSCRIBE = Sub, Bin, Topics) -> -% {Name, <<_:6, QoS:2, Rest/binary>>} = parse_utf(Bin), -% parse_topics(Sub, Rest, [{Name, QoS}| Topics]); -%parse_topics(?UNSUBSCRIBE = Sub, Bin, Topics) -> -% {Name, <>} = parse_utf(Bin), -% parse_topics(Sub, Rest, [Name | Topics]). - -%parse_utf(Bin, 0) -> -% {undefined, Bin}; -%parse_utf(Bin, _) -> -% parse_utf(Bin). - -parse_utf(<>) -> - {Str, Rest}. - -% server function -%parse_msg(Bin, 0) -> -% {undefined, Bin}; -%parse_msg(<>, _) -> -% {Msg, Rest}. - -bool(0) -> false; -bool(1) -> true. - -%protocol_name_approved(Ver, Name) -> -% lists:member({Ver, Name}, ?PROTOCOL_NAMES). - diff --git a/src/emqttc_protocol.erl b/src/emqttc_protocol.erl deleted file mode 100644 index 42021bcd..00000000 --- a/src/emqttc_protocol.erl +++ /dev/null @@ -1,328 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc client-side protocol handler. -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttc_protocol). - --author("Feng Lee "). - --include("emqttc_packet.hrl"). - --compile(nowarn_deprecated_function). - -%% State API --export([init/1, set_socket/2]). - -%% Protocol API --export([connect/1, - publish/2, - puback/2, - pubrec/2, - pubrel/2, - pubcomp/2, - subscribe/2, - unsubscribe/2, - ping/1, - disconnect/1, - received/2]). - --record(proto_state, { - socket :: inet:socket(), - socket_name :: list() | binary(), - proto_ver = 4 :: mqtt_vsn(), - proto_name = <<"MQTT">> :: binary(), - client_id :: binary(), - clean_sess = true :: boolean(), - keepalive = ?KEEPALIVE :: non_neg_integer(), - will_flag = false :: boolean(), - will_msg :: mqtt_message(), - username :: binary() | undefined, - password :: binary() | undefined, - packet_id = 1 :: mqtt_packet_id(), - subscriptions = #{} :: map(), - awaiting_ack = #{} :: map(), - awaiting_rel = #{} :: map(), - awaiting_comp = #{} :: map()}). - --type proto_state() :: #proto_state{}. - --export_type([proto_state/0]). - -%%%============================================================================= -%%% API -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc Init protocol with MQTT options. -%% @end -%%------------------------------------------------------------------------------ --spec init(MqttOpts) -> State when - MqttOpts :: list(tuple()), - State :: proto_state(). -init(MqttOpts) -> - init(MqttOpts, #proto_state{client_id = random_id(), - will_msg = #mqtt_message{}}). - -init([], State) -> - State; -init([{client_id, ClientId} | Opts], State) when is_binary(ClientId) -> - init(Opts, State#proto_state{client_id = ClientId}); -init([{proto_ver, ?MQTT_PROTO_V31} | Opts], State) -> - init(Opts, State#proto_state{proto_ver = ?MQTT_PROTO_V31, proto_name = <<"MQIsdp">>}); -init([{proto_ver, ?MQTT_PROTO_V311} | Opts], State) -> - init(Opts, State#proto_state{proto_ver = ?MQTT_PROTO_V311, proto_name = <<"MQTT">>}); -init([{clean_sess, CleanSess} | Opts], State) when is_boolean(CleanSess) -> - init(Opts, State#proto_state{clean_sess = CleanSess}); -init([{keepalive, KeepAlive} | Opts], State) when is_integer(KeepAlive) -> - init(Opts, State#proto_state{keepalive = KeepAlive}); -init([{username, Username} | Opts], State) when is_binary(Username)-> - init(Opts, State#proto_state{username = Username}); -init([{password, Password} | Opts], State) when is_binary(Password) -> - init(Opts, State#proto_state{password = Password}); -init([{will, WillOpts} | Opts], State = #proto_state{will_msg = WillMsg}) -> - init(Opts, State#proto_state{will_flag = true, - will_msg = init_willmsg(WillOpts, WillMsg)}); -init([_Opt | Opts], State) -> - init(Opts, State). - -init_willmsg([], WillMsg) -> - WillMsg; -init_willmsg([{topic, Topic} | Opts], WillMsg) when is_binary(Topic) -> - init_willmsg(Opts, WillMsg#mqtt_message{topic = Topic}); -init_willmsg([{payload, Payload} | Opts], WillMsg) when is_binary(Payload) -> - init_willmsg(Opts, WillMsg#mqtt_message{payload = Payload}); -init_willmsg([{qos, Qos} | Opts], WillMsg) when ?IS_QOS(Qos) -> - init_willmsg(Opts, WillMsg#mqtt_message{qos = Qos}); -init_willmsg([{retain, Retain} | Opts], WillMsg) when is_boolean(Retain) -> - init_willmsg(Opts, WillMsg#mqtt_message{retain = Retain}); -init_willmsg([_Opt | Opts], State) -> - init_willmsg(Opts, State). - -random_id() -> - random:seed(case erlang:function_exported(erlang, timestamp, 0) of - true -> %% R18 - erlang:timestamp(); - false -> %% R17 - erlang:now() - end), - I1 = random:uniform(round(math:pow(2, 48))) - 1, - I2 = random:uniform(round(math:pow(2, 32))) - 1, - {ok, Host} = inet:gethostname(), - list_to_binary(["emqttc_", Host, "_" | io_lib:format("~12.16.0b~8.16.0b", [I1, I2])]). - -%%------------------------------------------------------------------------------ -%% @doc Set socket -%% @end -%%0----------------------------------------------------------------------------- -set_socket(State, Socket) -> - {ok, SockName} = emqttc_socket:sockname_s(Socket), - State#proto_state{ - socket = Socket, - socket_name = SockName - }. - -%%------------------------------------------------------------------------------ -%% @doc Send CONNECT Packet -%% @end -%%------------------------------------------------------------------------------ -connect(State = #proto_state{client_id = ClientId, - proto_ver = ProtoVer, - proto_name = ProtoName, - clean_sess = CleanSess, - keepalive = KeepAlive, - will_flag = WillFlag, - will_msg = #mqtt_message{qos = WillQos, - retain = WillRetain, - topic = WillTopic, - payload = WillMsg}, - username = Username, - password = Password}) -> - - - Connect = #mqtt_packet_connect{client_id = ClientId, - proto_ver = ProtoVer, - proto_name = ProtoName, - will_flag = WillFlag, - will_retain = WillRetain, - will_qos = WillQos, - clean_sess = CleanSess, - keep_alive = KeepAlive, - will_topic = WillTopic, - will_msg = WillMsg, - username = Username, - password = Password}, - - send(?CONNECT_PACKET(Connect), State). - -%%------------------------------------------------------------------------------ -%% @doc -%% Publish Message to Broker: -%% -%% Qos0 message sent directly. -%% Qos1, Qos2 messages should be stored first. -%% -%% @end -%%------------------------------------------------------------------------------ -publish(Message = #mqtt_message{qos = ?QOS_0}, State) -> - {ok, NewState} = send(emqttc_message:to_packet(Message), State), - {ok, undefined, NewState}; - -publish(Message = #mqtt_message{qos = Qos}, State = #proto_state{ - packet_id = PacketId, awaiting_ack = AwaitingAck}) - when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - Message1 = Message#mqtt_message{msgid = PacketId}, - Message2 = - if - Qos =:= ?QOS_2 -> Message1#mqtt_message{dup = false}; - true -> Message1 - end, - Awaiting1 = maps:put(PacketId, Message2, AwaitingAck), - {ok, NewState} = send(emqttc_message:to_packet(Message2), - next_packet_id(State#proto_state{awaiting_ack = Awaiting1})), - {ok, PacketId, NewState}. - -puback(PacketId, State) when is_integer(PacketId) -> - send(?PUBACK_PACKET(?PUBACK, PacketId), State). - -pubrec(PacketId, State) when is_integer(PacketId) -> - send(?PUBACK_PACKET(?PUBREC, PacketId), State). - -pubrel(PacketId, State) when is_integer(PacketId) -> - send(?PUBREL_PACKET(PacketId), State). %% qos = 1 - -pubcomp(PacketId, State) when is_integer(PacketId) -> - send(?PUBACK_PACKET(?PUBCOMP, PacketId), State). - -subscribe(Topics, State = #proto_state{packet_id = PacketId, - subscriptions = SubMap}) -> - Resubs = [Topic || {Name, _Qos} = Topic <- Topics, maps:is_key(Name, SubMap)], - case Resubs of - [] -> ok; - _ -> ?warn("[~s] resubscribe ~p", [logtag(State), Resubs]) - end, - SubMap1 = lists:foldl(fun({Name, Qos}, Acc) -> maps:put(Name, Qos, Acc) end, SubMap, Topics), - %% send packet - {ok, NewState} = send(?SUBSCRIBE_PACKET(PacketId, Topics), next_packet_id(State#proto_state{subscriptions = SubMap1})), - {ok, PacketId, NewState}. - -unsubscribe(Topics, State = #proto_state{subscriptions = SubMap, packet_id = PacketId}) -> - case Topics -- maps:keys(SubMap) of - [] -> ok; - BadUnsubs -> ?warn("[~s] should not unsubscribe ~p", [logtag(State), BadUnsubs]) - end, - %% unsubscribe from topic tree - SubMap1 = lists:foldl(fun(Topic, Acc) -> maps:remove(Topic, Acc) end, SubMap, Topics), - %% send packet - send(?UNSUBSCRIBE_PACKET(PacketId, Topics), next_packet_id(State#proto_state{subscriptions = SubMap1})). - -ping(State) -> - send(?PACKET(?PINGREQ), State). - -disconnect(State) -> - send(?PACKET(?DISCONNECT), State). - -received('CONNACK', State = #proto_state{clean_sess = true}) -> - %%TODO: Send awaiting... - {ok, State}; - -received('CONNACK', State = #proto_state{clean_sess = false}) -> - %%TODO: Resume Session... - {ok, State}; - -received({'PUBLISH', ?PUBLISH_PACKET(?QOS_1, _Topic, PacketId, _Payload)}, State) -> - puback(PacketId, State); - -received({'PUBLISH', Packet = ?PUBLISH_PACKET(?QOS_2, _Topic, PacketId, _Payload)}, - State = #proto_state{awaiting_rel = AwaitingRel}) -> - pubrec(PacketId, State), - {ok, State#proto_state{awaiting_rel = maps:put(PacketId, Packet, AwaitingRel)}}; - -received({'PUBACK', PacketId}, State = #proto_state{awaiting_ack = AwaitingAck}) -> - case maps:is_key(PacketId, AwaitingAck) of - true -> ok; - false -> ?warn("[~s] PUBACK PacketId '~p' not found!", [logtag(State), PacketId]) - end, - {ok, State#proto_state{awaiting_ack = maps:remove(PacketId, AwaitingAck)}}; - -received({'PUBREC', PacketId}, State = #proto_state{awaiting_ack = AwaitingAck, - awaiting_comp = AwaitingComp}) -> - case maps:is_key(PacketId, AwaitingAck) of - true -> ok; - false -> ?warn("[~s] PUBREC PacketId '~p' not found!", [logtag(State), PacketId]) - end, - pubrel(PacketId, State), - {ok, State#proto_state{awaiting_ack = maps:remove(PacketId, AwaitingAck), - awaiting_comp = maps:put(PacketId, true, AwaitingComp)}}; - -received({'PUBREL', PacketId}, State = #proto_state{awaiting_rel = AwaitingRel}) -> - case maps:find(PacketId, AwaitingRel) of - {ok, Publish} -> - {ok, Publish, State#proto_state{awaiting_rel = maps:remove(PacketId, AwaitingRel)}}; - error -> - ?warn("[~s] PUBREL PacketId '~p' not found!", [logtag(State), PacketId]), - {ok, State} - end; - -received({'PUBCOMP', PacketId}, State = #proto_state{awaiting_comp = AwaitingComp}) -> - case maps:is_key(PacketId, AwaitingComp) of - true -> ok; - false -> ?warn("[~s] PUBREC PacketId '~p' not exist", [logtag(State), PacketId]) - end, - {ok, State#proto_state{ awaiting_comp = maps:remove(PacketId, AwaitingComp)}}; - -received({'SUBACK', _PacketId, _QosTable}, State) -> - %% TODO... - {ok, State}; - -received({'UNSUBACK', _PacketId}, State) -> - %% TODO... - {ok, State}. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @private -%% @doc -%% Send Packet to broker. -%% -%% @end -%%------------------------------------------------------------------------------ -send(Packet, State = #proto_state{socket = Socket}) -> - LogTag = logtag(State), - ?debug("[~s] SENT: ~s", [LogTag, emqttc_packet:dump(Packet)]), - Data = emqttc_serialiser:serialise(Packet), - ?debug("[~s] SENT: ~p", [LogTag, Data]), - emqttc_socket:send(Socket, Data), - {ok, State}. - -next_packet_id(State = #proto_state{packet_id = 16#ffff}) -> - State#proto_state{packet_id = 1}; - -next_packet_id(State = #proto_state{packet_id = Id }) -> - State#proto_state{packet_id = Id + 1}. - -logtag(#proto_state{socket_name = SocketName, client_id = ClientId}) -> - io_lib:format("~s@~s", [ClientId, SocketName]). diff --git a/src/emqttc_reconnector.erl b/src/emqttc_reconnector.erl deleted file mode 100644 index 3ad4a0cd..00000000 --- a/src/emqttc_reconnector.erl +++ /dev/null @@ -1,120 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc client reconnector. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttc_reconnector). - --author('feng@emqtt.io'). - --export([new/0, new/1, execute/2, reset/1]). - -%% 4 seconds --define(MIN_INTERVAL, 4). - -%% 1 minute --define(MAX_INTERVAL, 60). - --define(IS_MAX_RETRIES(Max), (is_integer(Max) orelse Max =:= infinity)). - --record(reconnector, { - min_interval = ?MIN_INTERVAL, - max_interval = ?MAX_INTERVAL, - max_retries = infinity, - interval = ?MIN_INTERVAL, - retries = 0, - timer = undefined}). - --opaque reconnector() :: #reconnector{}. - --export_type([reconnector/0]). - -%%------------------------------------------------------------------------------ -%% @doc Create a reconnector. -%% @end -%%------------------------------------------------------------------------------ --spec new() -> reconnector(). -new() -> - new({?MIN_INTERVAL, ?MAX_INTERVAL}). - -%%------------------------------------------------------------------------------ -%% @doc Create a reconnector with min_interval, max_interval seconds and max retries. -%% @end -%%------------------------------------------------------------------------------ --spec new(MinInterval) -> reconnector() when - MinInterval :: non_neg_integer() | {non_neg_integer(), non_neg_integer()}. -new(MinInterval) when is_integer(MinInterval), MinInterval =< ?MAX_INTERVAL -> - new({MinInterval, ?MAX_INTERVAL}); - -new({MinInterval, MaxInterval}) when is_integer(MinInterval), is_integer(MaxInterval), MinInterval =< MaxInterval -> - new({MinInterval, MaxInterval, infinity}); -new({_MinInterval, _MaxInterval}) -> - new({?MIN_INTERVAL, ?MAX_INTERVAL, infinity}); -new({MinInterval, MaxInterval, MaxRetries}) when is_integer(MinInterval), - is_integer(MaxInterval), ?IS_MAX_RETRIES(MaxRetries) -> - #reconnector{min_interval = MinInterval, - interval = MinInterval, - max_interval = MaxInterval, - max_retries = MaxRetries}. - -%%------------------------------------------------------------------------------ -%% @doc Execute reconnector. -%% @end -%%------------------------------------------------------------------------------ --spec execute(Reconntor, TimeoutMsg) -> {stop, any()} | {ok, reconnector()} when - Reconntor :: reconnector(), - TimeoutMsg :: tuple(). -execute(#reconnector{retries = Retries, max_retries = MaxRetries}, _TimoutMsg) when - MaxRetries =/= infinity andalso (Retries > MaxRetries) -> - {stop, retries_exhausted}; - -execute(Reconnector=#reconnector{min_interval = MinInterval, - max_interval = MaxInterval, - interval = Interval, - retries = Retries, - timer = Timer}, TimeoutMsg) -> - % cancel timer first... - cancel(Timer), - % power - Interval1 = Interval * 2, - Interval2 = - if - Interval1 > MaxInterval -> MinInterval; - true -> Interval1 - end, - NewTimer = erlang:send_after(Interval2*1000, self(), TimeoutMsg), - {ok, Reconnector#reconnector{interval = Interval2, retries = Retries+1, timer = NewTimer }}. - -%%------------------------------------------------------------------------------ -%% @doc Reset reconnector -%% @end -%%------------------------------------------------------------------------------ --spec reset(reconnector()) -> reconnector(). -reset(Reconnector = #reconnector{min_interval = MinInterval, timer = Timer}) -> - cancel(Timer), - Reconnector#reconnector{interval = MinInterval, retries = 0, timer = undefined}. - -cancel(undefined) ->ok; -cancel(Timer) when is_reference(Timer) -> erlang:cancel_timer(Timer). - diff --git a/src/emqttc_serialiser.erl b/src/emqttc_serialiser.erl deleted file mode 100644 index ff2d6b0e..00000000 --- a/src/emqttc_serialiser.erl +++ /dev/null @@ -1,158 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc packet serialiser. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --module(emqttc_serialiser). - --author("feng@emqtt.io"). - --include("emqttc_packet.hrl"). - -%% API --export([serialise/1]). - - -serialise(#mqtt_packet{header = Header = #mqtt_packet_header{type = Type}, - variable = Variable, - payload = Payload}) -> - serialise_header(Header, - serialise_variable(Type, Variable, - serialise_payload(Payload))). - -serialise_header(#mqtt_packet_header{type = Type, - dup = Dup, - qos = Qos, - retain = Retain}, - {VariableBin, PayloadBin}) - when is_integer(Type) andalso ?CONNECT =< Type andalso Type =< ?DISCONNECT -> - Len = size(VariableBin) + size(PayloadBin), - true = (Len =< ?MAX_LEN), - LenBin = serialise_len(Len), - <>. - -serialise_variable(?CONNECT, #mqtt_packet_connect{client_id = ClientId, - proto_ver = ProtoVer, - proto_name = ProtoName, - will_retain = WillRetain, - will_qos = WillQos, - will_flag = WillFlag, - clean_sess = CleanSess, - keep_alive = KeepAlive, - will_topic = WillTopic, - will_msg = WillMsg, - username = Username, - password = Password }, undefined) -> - VariableBin = <<(size(ProtoName)):16/big-unsigned-integer, - ProtoName/binary, - ProtoVer:8, - (opt(Username)):1, - (opt(Password)):1, - (opt(WillRetain)):1, - WillQos:2, - (opt(WillFlag)):1, - (opt(CleanSess)):1, - 0:1, - KeepAlive:16/big-unsigned-integer>>, - PayloadBin = serialise_utf(ClientId), - PayloadBin1 = case WillFlag of - true -> <>; - false -> PayloadBin - end, - UserPasswd = << <<(serialise_utf(B))/binary>> || B <- [Username, Password], B =/= undefined >>, - {VariableBin, <>}; - -serialise_variable(?CONNACK, #mqtt_packet_connack{ack_flags = AckFlags, - return_code = ReturnCode}, - undefined) -> - {<>, <<>>}; - -serialise_variable(?SUBSCRIBE, #mqtt_packet_subscribe{packet_id = PacketId, - topic_table = Topics }, undefined) -> - {<>, serialise_topics(Topics)}; - -serialise_variable(?SUBACK, #mqtt_packet_suback {packet_id = PacketId, - qos_table = QosTable}, - undefined) -> - {<>, << <> || Q <- QosTable >>}; - -serialise_variable(?UNSUBSCRIBE, #mqtt_packet_unsubscribe{ - packet_id = PacketId, topics = Topics }, undefined) -> - {<>, serialise_topics(Topics)}; - -serialise_variable(?UNSUBACK, #mqtt_packet_suback {packet_id = PacketId}, - undefined) -> - {<>, <<>>}; - -serialise_variable(?PUBLISH, #mqtt_packet_publish { topic_name = TopicName, - packet_id = PacketId }, PayloadBin) -> - TopicBin = serialise_utf(TopicName), - PacketIdBin = if - PacketId =:= undefined -> <<>>; - true -> <> - end, - {<>, PayloadBin}; - -serialise_variable(PubAck, #mqtt_packet_puback { packet_id = PacketId }, _Payload) - when PubAck =:= ?PUBACK; PubAck =:= ?PUBREC; PubAck =:= ?PUBREL; PubAck =:= ?PUBCOMP -> - {<>, <<>>}; - -serialise_variable(?PINGREQ, undefined, undefined) -> - {<<>>, <<>>}; - -serialise_variable(?DISCONNECT, undefined, undefined) -> - {<<>>, <<>>}. - -serialise_payload(undefined) -> - undefined; -serialise_payload(Bin) when is_binary(Bin) -> - Bin. - -serialise_topics([{_Topic, _Qos}|_] = Topics) -> - << <<(serialise_utf(Topic))/binary, ?RESERVED:6, Qos:2>> || {Topic, Qos} <- Topics >>; - -serialise_topics([H|_] = Topics) when is_binary(H) -> - << <<(serialise_utf(Topic))/binary>> || Topic <- Topics >>. - -serialise_utf(String) -> - StringBin = unicode:characters_to_binary(String), - Len = size(StringBin), - true = (Len =< 16#ffff), - <>. - -serialise_len(N) when N =< ?LOWBITS -> - <<0:1, N:7>>; -serialise_len(N) -> - <<1:1, (N rem ?HIGHBIT):7, (serialise_len(N div ?HIGHBIT))/binary>>. - -opt(undefined) -> ?RESERVED; -opt(false) -> 0; -opt(true) -> 1; -opt(X) when is_integer(X) -> X; -opt(B) when is_binary(B) -> 1. diff --git a/src/emqttc_socket.erl b/src/emqttc_socket.erl deleted file mode 100644 index 5ec932a9..00000000 --- a/src/emqttc_socket.erl +++ /dev/null @@ -1,258 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc socket and receiver. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --module(emqttc_socket). - --author("feng@emqtt.io"). - --include("emqttc_packet.hrl"). - -%% API --export([connect/6, controlling_process/2, send/2, close/1, stop/1]). - --export([sockname/1, sockname_s/1, setopts/2, getstat/2]). - -%% Internal export --export([receiver/2, receiver_loop/3]). - -%% 30 (secs) --define(TIMEOUT, 90000). - --define(TCPOPTIONS, [ - binary, - {packet, raw}, - {reuseaddr, true}, - {nodelay, true}, - {active, false}, - {send_timeout, ?TIMEOUT}]). - --define(SSLOPTIONS, [{depth, 0}]). - --record(ssl_socket, {tcp, ssl}). - --type ssl_socket() :: #ssl_socket{}. - --define(IS_SSL(Socket), is_record(Socket, ssl_socket)). - -%%------------------------------------------------------------------------------ -%% @doc Connect to broker with TCP or SSL transport -%% @end -%%------------------------------------------------------------------------------ --spec connect(ClientPid, Transport, Host, Port, TcpOpts, SslOpts) -> {ok, Socket, Receiver} | {error, term()} when - ClientPid :: pid(), - Transport :: tcp | ssl, - Host :: inet:ip_address() | string(), - Port :: inet:port_number(), - TcpOpts :: [gen_tcp:connect_option()], - SslOpts :: [ssl:ssloption()], - Socket :: inet:socket() | ssl_socket(), - Receiver :: pid(). -connect(ClientPid, Transport, Host, Port, TcpOpts, SslOpts) when is_pid(ClientPid) -> - case connect(Transport, Host, Port, TcpOpts, SslOpts) of - {ok, Socket} -> - ReceiverPid = spawn_link(?MODULE, receiver, [ClientPid, Socket]), - controlling_process(Socket, ReceiverPid), - {ok, Socket, ReceiverPid}; - {error, Reason} -> - {error, Reason} - end. - --spec connect(Transport, Host, Port, TcpOpts, SslOpts) -> {ok, Socket} | {error, any()} when - Transport :: tcp | ssl, - Host :: inet:ip_address() | string(), - Port :: inet:port_number(), - TcpOpts :: [gen_tcp:connect_option()], - SslOpts :: [ssl:ssloption()], - Socket :: inet:socket() | ssl_socket(). -connect(tcp, Host, Port, TcpOpts, _SslOpts) -> - gen_tcp:connect(Host, Port, emqttc_opts:merge(?TCPOPTIONS, TcpOpts), ?TIMEOUT); -connect(ssl, Host, Port, TcpOpts, SslOpts) -> - case gen_tcp:connect(Host, Port, emqttc_opts:merge(?TCPOPTIONS, TcpOpts), ?TIMEOUT) of - {ok, Socket} -> - case ssl:connect(Socket, emqttc_opts:merge(?SSLOPTIONS, SslOpts), ?TIMEOUT) of - {ok, SslSocket} -> {ok, #ssl_socket{tcp = Socket, ssl = SslSocket}}; - {error, SslReason} -> {error, SslReason} - end; - {error, Reason} -> - {error, Reason} - end. - -%%------------------------------------------------------------------------------ -%% @doc Socket controlling process -%% @end -%%------------------------------------------------------------------------------ -controlling_process(Socket, Pid) when is_port(Socket) -> - gen_tcp:controlling_process(Socket, Pid); -controlling_process(#ssl_socket{ssl = SslSocket}, Pid) -> - ssl:controlling_process(SslSocket, Pid). - -%%------------------------------------------------------------------------------ -%% @doc Send Packet and Data -%% @end -%%------------------------------------------------------------------------------ --spec send(Socket, Data) -> ok when - Socket :: inet:socket() | ssl_socket(), - Data :: binary(). -send(Socket, Data) when is_port(Socket) -> - gen_tcp:send(Socket, Data); -send(#ssl_socket{ssl = SslSocket}, Data) -> - ssl:send(SslSocket, Data). - -%%------------------------------------------------------------------------------ -%% @doc Close Socket. -%% @end -%%------------------------------------------------------------------------------ --spec close(Socket :: inet:socket() | ssl_socket()) -> ok. -close(Socket) when is_port(Socket) -> - gen_tcp:close(Socket); -close(#ssl_socket{ssl = SslSocket}) -> - ssl:close(SslSocket). - -%%------------------------------------------------------------------------------ -%% @doc Stop Receiver. -%% @end -%%------------------------------------------------------------------------------ --spec stop(Receiver :: pid()) -> ok. -stop(Receiver) -> - Receiver ! stop. - -%%------------------------------------------------------------------------------ -%% @doc Set socket options. -%% @end -%%------------------------------------------------------------------------------ -setopts(Socket, Opts) when is_port(Socket) -> - inet:setopts(Socket, Opts); -setopts(#ssl_socket{ssl = SslSocket}, Opts) -> - ssl:setopts(SslSocket, Opts). - -%%------------------------------------------------------------------------------ -%% @doc Get socket stats. -%% @end -%%------------------------------------------------------------------------------ --spec getstat(Socket, Stats) -> {ok, Values} | {error, any()} when - Socket :: inet:socket() | ssl_socket(), - Stats :: list(), - Values :: list(). -getstat(Socket, Stats) when is_port(Socket) -> - inet:getstat(Socket, Stats); -getstat(#ssl_socket{tcp = Socket}, Stats) -> - inet:getstat(Socket, Stats). - -%%------------------------------------------------------------------------------ -%% @doc Socket name. -%% @end -%%------------------------------------------------------------------------------ --spec sockname(Socket) -> {ok, {Address, Port}} | {error, any()} when - Socket :: inet:socket() | ssl_socket(), - Address :: inet:ip_address(), - Port :: inet:port_number(). -sockname(Socket) when is_port(Socket) -> - inet:sockname(Socket); -sockname(#ssl_socket{ssl = SslSocket}) -> - ssl:sockname(SslSocket). - -sockname_s(Sock) -> - case sockname(Sock) of - {ok, {Addr, Port}} -> - {ok, lists:flatten(io_lib:format("~s:~p", [maybe_ntoab(Addr), Port]))}; - Error -> - Error - end. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -receiver(ClientPid, Socket) -> - receiver_activate(ClientPid, Socket, emqttc_parser:new()). - -receiver_activate(ClientPid, Socket, ParseState) -> - setopts(Socket, [{active, once}]), - erlang:hibernate(?MODULE, receiver_loop, [ClientPid, Socket, ParseState]). - -receiver_loop(ClientPid, Socket, ParseState) -> - receive - {tcp, Socket, Data} -> - case parse_received_bytes(ClientPid, Data, ParseState) of - {ok, NewParserState} -> - receiver_activate(ClientPid, Socket, NewParserState); - {error, Error} -> - gen_fsm:send_all_state_event(ClientPid, {frame_error, Error}) - end; - {tcp_error, Socket, Reason} -> - connection_lost(ClientPid, {tcp_error, Reason}); - {tcp_closed, Socket} -> - connection_lost(ClientPid, tcp_closed); - {ssl, _SslSocket, Data} -> - case parse_received_bytes(ClientPid, Data, ParseState) of - {ok, NewParserState} -> - receiver_activate(ClientPid, Socket, NewParserState); - {error, Error} -> - gen_fsm:send_all_state_event(ClientPid, {frame_error, Error}) - end; - {ssl_error, _SslSocket, Reason} -> - connection_lost(ClientPid, {ssl_error, Reason}); - {ssl_closed, _SslSocket} -> - connection_lost(ClientPid, ssl_closed); - stop -> - close(Socket) - end. - -parse_received_bytes(_ClientPid, <<>>, ParseState) -> - {ok, ParseState}; - -parse_received_bytes(ClientPid, Data, ParseState) -> - case catch emqttc_parser:parse(Data, ParseState) of - {more, ParseState1} -> - {ok, ParseState1}; - {ok, Packet, Rest} -> - gen_fsm:send_event(ClientPid, Packet), - parse_received_bytes(ClientPid, Rest, emqttc_parser:new()); - {error, Error} -> - {error, Error}; - {'EXIT', Reason} -> - {error, Reason} - end. - -connection_lost(ClientPid, Reason) -> - gen_fsm:send_all_state_event(ClientPid, {connection_lost, Reason}). - -maybe_ntoab(Addr) when is_tuple(Addr) -> ntoab(Addr); -maybe_ntoab(Host) -> Host. - -ntoa({0,0,0,0,0,16#ffff,AB,CD}) -> - inet_parse:ntoa({AB bsr 8, AB rem 256, CD bsr 8, CD rem 256}); -ntoa(IP) -> - inet_parse:ntoa(IP). - -ntoab(IP) -> - Str = ntoa(IP), - case string:str(Str, ":") of - 0 -> Str; - _ -> "[" ++ Str ++ "]" - end. - diff --git a/src/emqttc_topic.erl b/src/emqttc_topic.erl deleted file mode 100644 index e4ac352e..00000000 --- a/src/emqttc_topic.erl +++ /dev/null @@ -1,160 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2016 eMQTT.IO, All Rights Reserved. -%%% -%%% Permission is hereby granted, free of charge, to any person obtaining a copy -%%% of this software and associated documentation files (the "Software"), to deal -%%% in the Software without restriction, including without limitation the rights -%%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -%%% copies of the Software, and to permit persons to whom the Software is -%%% furnished to do so, subject to the following conditions: -%%% -%%% The above copyright notice and this permission notice shall be included in all -%%% copies or substantial portions of the Software. -%%% -%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -%%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -%%% SOFTWARE. -%%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttc topic handler. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --module(emqttc_topic). - --author("Feng Lee "). - --import(lists, [reverse/1]). - -%% API --export([type/1, match/2, validate/1, triples/1, words/1]). - --define(MAX_TOPIC_LEN, 65535). - -%%------------------------------------------------------------------------------ -%% @doc Topic Type: direct or wildcard -%% @end -%%%----------------------------------------------------------------------------- --spec type(binary()) -> direct | wildcard. -type(Topic) when is_binary(Topic) -> - type2(words(Topic)). - -type2([]) -> - direct; -type2(['#'|_]) -> - wildcard; -type2(['+'|_]) -> - wildcard; -type2([_H |T]) -> - type2(T). - -%%------------------------------------------------------------------------------ -%% @doc Match Topic name with filter. -%% @end -%%------------------------------------------------------------------------------ --spec match(binary(), binary()) -> boolean(). -match(Name, Filter) when is_binary(Name) and is_binary(Filter) -> - match(words(Name), words(Filter)); -match([], []) -> - true; -match([H|T1], [H|T2]) -> - match(T1, T2); -match([<<$$, _/binary>>|_], ['+'|_]) -> - false; -match([_H|T1], ['+'|T2]) -> - match(T1, T2); -match([<<$$, _/binary>>|_], ['#']) -> - false; -match(_, ['#']) -> - true; -match([_H1|_], [_H2|_]) -> - false; -match([_H1|_], []) -> - false; -match([], [_H|_T2]) -> - false. - -%%------------------------------------------------------------------------------ -%% @doc Validate Topic name and filter. -%% @end -%%------------------------------------------------------------------------------ --spec validate({name | filter, binary()}) -> boolean(). -validate({_, <<>>}) -> - false; -validate({_, Topic}) when is_binary(Topic) and (size(Topic) > ?MAX_TOPIC_LEN) -> - false; -validate({filter, Topic}) when is_binary(Topic) -> - validate2(words(Topic)); -validate({name, Topic}) when is_binary(Topic) -> - Words = words(Topic), - validate2(Words) and (not include_wildcard(Words)). - -validate2([]) -> - true; -validate2(['#']) -> % end with '#' - true; -validate2(['#'|Words]) when length(Words) > 0 -> - false; -validate2([''|Words]) -> - validate2(Words); -validate2(['+'|Words]) -> - validate2(Words); -validate2([W|Words]) -> - case validate3(W) of - true -> validate2(Words); - false -> false - end. - -validate3(<<>>) -> - true; -validate3(<>) when C == $#; C == $+; C == 0 -> - false; -validate3(<<_/utf8, Rest/binary>>) -> - validate3(Rest). - -include_wildcard([]) -> false; -include_wildcard(['#'|_T]) -> true; -include_wildcard(['+'|_T]) -> true; -include_wildcard([ _ | T]) -> include_wildcard(T). - -%%------------------------------------------------------------------------------ -%% @doc Topic to Triples. -%% @end -%%------------------------------------------------------------------------------ -triples(Topic) when is_binary(Topic) -> - triples(words(Topic), root, []). - -triples([], _Parent, Acc) -> - reverse(Acc); - -triples([W|Words], Parent, Acc) -> - Node = join(Parent, W), - triples(Words, Node, [{Parent, W, Node}|Acc]). - -join(root, W) -> - W; -join(Parent, W) -> - <<(bin(Parent))/binary, $/, (bin(W))/binary>>. - -bin('') -> <<>>; -bin('+') -> <<"+">>; -bin('#') -> <<"#">>; -bin(Bin) when is_binary(Bin) -> Bin. - -%%------------------------------------------------------------------------------ -%% @doc Split Topic into Words. -%% @end -%%------------------------------------------------------------------------------ -words(Topic) when is_binary(Topic) -> - [word(W) || W <- binary:split(Topic, <<"/">>, [global])]. - -word(<<>>) -> ''; -word(<<"+">>) -> '+'; -word(<<"#">>) -> '#'; -word(Bin) -> Bin. - diff --git a/test/.placeholder b/test/.placeholder deleted file mode 100644 index e69de29b..00000000 diff --git a/test/emqtt_frame_SUITE.erl b/test/emqtt_frame_SUITE.erl new file mode 100644 index 00000000..0407ffc3 --- /dev/null +++ b/test/emqtt_frame_SUITE.erl @@ -0,0 +1,440 @@ +%%-------------------------------------------------------------------- +%% Copyright (c) 2019 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(emqtt_frame_SUITE). + +-compile(export_all). +-compile(nowarn_export_all). + +-include("emqtt.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +all() -> + [{group, connect}, + {group, connack}, + {group, publish}, + {group, puback}, + {group, subscribe}, + {group, suback}, + {group, unsubscribe}, + {group, unsuback}, + {group, ping}, + {group, disconnect}, + {group, auth}]. + +groups() -> + [{connect, [parallel], + [serialize_parse_connect, + serialize_parse_v3_connect, + serialize_parse_v4_connect, + serialize_parse_v5_connect, + serialize_parse_connect_without_clientid, + serialize_parse_connect_with_will, + serialize_parse_bridge_connect + ]}, + {connack, [parallel], + [serialize_parse_connack, + serialize_parse_connack_v5 + ]}, + {publish, [parallel], + [serialize_parse_qos0_publish, + serialize_parse_qos1_publish, + serialize_parse_qos2_publish, + serialize_parse_publish_v5 + ]}, + {puback, [parallel], + [serialize_parse_puback, + serialize_parse_puback_v5, + serialize_parse_pubrec, + serialize_parse_pubrec_v5, + serialize_parse_pubrel, + serialize_parse_pubrel_v5, + serialize_parse_pubcomp, + serialize_parse_pubcomp_v5 + ]}, + {subscribe, [parallel], + [serialize_parse_subscribe, + serialize_parse_subscribe_v5 + ]}, + {suback, [parallel], + [serialize_parse_suback, + serialize_parse_suback_v5 + ]}, + {unsubscribe, [parallel], + [serialize_parse_unsubscribe, + serialize_parse_unsubscribe_v5 + ]}, + {unsuback, [parallel], + [serialize_parse_unsuback, + serialize_parse_unsuback_v5 + ]}, + {ping, [parallel], + [serialize_parse_pingreq, + serialize_parse_pingresp + ]}, + {disconnect, [parallel], + [serialize_parse_disconnect, + serialize_parse_disconnect_v5 + ]}, + {auth, [parallel], + [serialize_parse_auth_v5] + }]. + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +init_per_group(_Group, Config) -> + Config. + +end_per_group(_Group, _Config) -> + ok. + +serialize_parse_connect(_) -> + Packet1 = ?CONNECT_PACKET(#mqtt_packet_connect{}), + ?assertEqual(Packet1, parse_serialize(Packet1)), + Packet2 = ?CONNECT_PACKET(#mqtt_packet_connect{ + client_id = <<"clientId">>, + will_qos = ?QOS_1, + will_flag = true, + will_retain = true, + will_topic = <<"will">>, + will_payload = <<"bye">>, + clean_start = true + }), + ?assertEqual(Packet2, parse_serialize(Packet2)). + +serialize_parse_v3_connect(_) -> + Bin = <<16,37,0,6,77,81,73,115,100,112,3,2,0,60,0,23,109,111,115, + 113,112,117, 98,47,49,48,52,53,49,45,105,77,97,99,46,108, + 111,99,97>>, + Packet = ?CONNECT_PACKET( + #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, + proto_name = <<"MQIsdp">>, + client_id = <<"mosqpub/10451-iMac.loca">>, + clean_start = true, + keepalive = 60 + }), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(Bin)). + +serialize_parse_v4_connect(_) -> + Bin = <<16,35,0,4,77,81,84,84,4,2,0,60,0,23,109,111,115,113,112,117, + 98,47,49,48,52,53,49,45,105,77,97,99,46,108,111,99,97>>, + Packet = ?CONNECT_PACKET(#mqtt_packet_connect{proto_ver = 4, + proto_name = <<"MQTT">>, + client_id = <<"mosqpub/10451-iMac.loca">>, + clean_start = true, + keepalive = 60}), + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(Bin)). + +serialize_parse_v5_connect(_) -> + Props = #{'Session-Expiry-Interval' => 60, + 'Receive-Maximum' => 100, + 'Maximum-QoS' => ?QOS_2, + 'Retain-Available' => 1, + 'Maximum-Packet-Size' => 1024, + 'Topic-Alias-Maximum' => 10, + 'Request-Response-Information' => 1, + 'Request-Problem-Information' => 1, + 'Authentication-Method' => <<"oauth2">>, + 'Authentication-Data' => <<"33kx93k">> + }, + + WillProps = #{'Will-Delay-Interval' => 60, + 'Payload-Format-Indicator' => 1, + 'Message-Expiry-Interval' => 60, + 'Content-Type' => <<"text/json">>, + 'Response-Topic' => <<"topic">>, + 'Correlation-Data' => <<"correlateid">>, + 'User-Property' => [{<<"k">>, <<"v">>}] + }, + Packet = ?CONNECT_PACKET( + #mqtt_packet_connect{proto_name = <<"MQTT">>, + proto_ver = ?MQTT_PROTO_V5, + is_bridge = false, + clean_start = true, + client_id = <<>>, + will_flag = true, + will_qos = ?QOS_1, + will_retain = false, + keepalive = 60, + properties = Props, + will_props = WillProps, + will_topic = <<"topic">>, + will_payload = <<>>, + username = <<"device:1">>, + password = <<"passwd">> + }), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_connect_without_clientid(_) -> + Bin = <<16,12,0,4,77,81,84,84,4,2,0,60,0,0>>, + Packet = ?CONNECT_PACKET( + #mqtt_packet_connect{proto_ver = 4, + proto_name = <<"MQTT">>, + client_id = <<>>, + clean_start = true, + keepalive = 60 + }), + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(Bin)). + +serialize_parse_connect_with_will(_) -> + Bin = <<16,67,0,6,77,81,73,115,100,112,3,206,0,60,0,23,109,111,115,113,112, + 117,98,47,49,48,52,53,50,45,105,77,97,99,46,108,111,99,97,0,5,47,119, + 105,108,108,0,7,119,105,108,108,109,115,103,0,4,116,101,115,116,0,6, + 112,117,98,108,105,99>>, + Packet = #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, + variable = #mqtt_packet_connect{proto_ver = ?MQTT_PROTO_V3, + proto_name = <<"MQIsdp">>, + client_id = <<"mosqpub/10452-iMac.loca">>, + clean_start = true, + keepalive = 60, + will_retain = false, + will_qos = ?QOS_1, + will_flag = true, + will_topic = <<"/will">>, + will_payload = <<"willmsg">>, + username = <<"test">>, + password = <<"public">> + }}, + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(Bin)). + +serialize_parse_bridge_connect(_) -> + Bin = <<16,86,0,6,77,81,73,115,100,112,131,44,0,60,0,19,67,95,48,48,58,48,67, + 58,50,57,58,50,66,58,55,55,58,53,50,0,48,36,83,89,83,47,98,114,111,107, + 101,114,47,99,111,110,110,101,99,116,105,111,110,47,67,95,48,48,58,48, + 67,58,50,57,58,50,66,58,55,55,58,53,50,47,115,116,97,116,101,0,1,48>>, + Topic = <<"$SYS/broker/connection/C_00:0C:29:2B:77:52/state">>, + Packet = #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, + variable = #mqtt_packet_connect{client_id = <<"C_00:0C:29:2B:77:52">>, + proto_ver = 16#03, + proto_name = <<"MQIsdp">>, + is_bridge = true, + will_retain = true, + will_qos = ?QOS_1, + will_flag = true, + clean_start = false, + keepalive = 60, + will_topic = Topic, + will_payload = <<"0">> + }}, + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(Bin)). + +serialize_parse_connack(_) -> + Packet = ?CONNACK_PACKET(?RC_SUCCESS), + ?assertEqual(<<32,2,0,0>>, serialize_to_binary(Packet)), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_connack_v5(_) -> + Props = #{'Session-Expiry-Interval' => 60, + 'Receive-Maximum' => 100, + 'Maximum-QoS' => ?QOS_2, + 'Retain-Available' => 1, + 'Maximum-Packet-Size' => 1024, + 'Assigned-Client-Identifier' => <<"id">>, + 'Topic-Alias-Maximum' => 10, + 'Reason-String' => <<>>, + 'Wildcard-Subscription-Available' => 1, + 'Subscription-Identifier-Available' => 1, + 'Shared-Subscription-Available' => 1, + 'Server-Keep-Alive' => 60, + 'Response-Information' => <<"response">>, + 'Server-Reference' => <<"192.168.1.10">>, + 'Authentication-Method' => <<"oauth2">>, + 'Authentication-Data' => <<"33kx93k">> + }, + Packet = ?CONNACK_PACKET(?RC_SUCCESS, 0, Props), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_qos0_publish(_) -> + Bin = <<48,14,0,7,120,120,120,47,121,121,121,104,101,108,108,111>>, + Packet = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + dup = false, + qos = ?QOS_0, + retain = false}, + variable = #mqtt_packet_publish{topic_name = <<"xxx/yyy">>, + packet_id = undefined}, + payload = <<"hello">>}, + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(Bin)). + +serialize_parse_qos1_publish(_) -> + Bin = <<50,13,0,5,97,47,98,47,99,0,1,104,97,104,97>>, + Packet = #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + dup = false, + qos = ?QOS_1, + retain = false}, + variable = #mqtt_packet_publish{topic_name = <<"a/b/c">>, + packet_id = 1}, + payload = <<"haha">>}, + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(Bin)). + +serialize_parse_qos2_publish(_) -> + Packet = ?PUBLISH_PACKET(?QOS_2, <<"Topic">>, 1, payload()), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_publish_v5(_) -> + Props = #{'Payload-Format-Indicator' => 1, + 'Message-Expiry-Interval' => 60, + 'Topic-Alias' => 16#AB, + 'Response-Topic' => <<"reply">>, + 'Correlation-Data' => <<"correlation-id">>, + 'Subscription-Identifier' => 1, + 'Content-Type' => <<"text/json">>}, + Packet = ?PUBLISH_PACKET(?QOS_1, <<"$share/group/topic">>, 1, Props, <<"payload">>), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_puback(_) -> + Packet = ?PUBACK_PACKET(1), + ?assertEqual(<<64,2,0,1>>, serialize_to_binary(Packet)), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_puback_v5(_) -> + Packet = ?PUBACK_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_pubrec(_) -> + Packet = ?PUBREC_PACKET(1), + ?assertEqual(<<5:4,0:4,2,0,1>>, serialize_to_binary(Packet)), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_pubrec_v5(_) -> + Packet = ?PUBREC_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_pubrel(_) -> + Packet = ?PUBREL_PACKET(1), + Bin = serialize_to_binary(Packet), + ?assertEqual(<<6:4,2:4,2,0,1>>, Bin), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_pubrel_v5(_) -> + Packet = ?PUBREL_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_pubcomp(_) -> + Packet = ?PUBCOMP_PACKET(1), + Bin = serialize_to_binary(Packet), + ?assertEqual(<<7:4,0:4,2,0,1>>, Bin), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_pubcomp_v5(_) -> + Packet = ?PUBCOMP_PACKET(16, ?RC_SUCCESS, #{'Reason-String' => <<"success">>}), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_subscribe(_) -> + %% SUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[{<<"TopicA">>,2}]) + Bin = <<130,11,0,2,0,6,84,111,112,105,99,65,2>>, + TopicOpts = #{nl => 0 , rap => 0, rc => 0, rh => 0, qos => 2}, + TopicFilters = [{<<"TopicA">>, TopicOpts}], + Packet = ?SUBSCRIBE_PACKET(2, TopicFilters), + ?assertEqual(Bin, serialize_to_binary(Packet)), + %%ct:log("Bin: ~p, Packet: ~p ~n", [Packet, parse(Bin)]), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(Bin)). + +serialize_parse_subscribe_v5(_) -> + TopicFilters = [{<<"TopicQos0">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0}}, + {<<"TopicQos1">>, #{rh => 1, qos => ?QOS_2, rap => 0, nl => 0, rc => 0}}], + Packet = ?SUBSCRIBE_PACKET(3, #{'Subscription-Identifier' => 16#FFFFFFF}, TopicFilters), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_suback(_) -> + Packet = ?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128]), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_suback_v5(_) -> + Packet = ?SUBACK_PACKET(1, #{'Reason-String' => <<"success">>, + 'User-Property' => [{<<"key">>, <<"value">>}]}, + [?QOS_0, ?QOS_1, 128]), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_unsubscribe(_) -> + %% UNSUBSCRIBE(Q1, R0, D0, PacketId=2, TopicTable=[<<"TopicA">>]) + Packet = ?UNSUBSCRIBE_PACKET(2, [<<"TopicA">>]), + Bin = <<162,10,0,2,0,6,84,111,112,105,99,65>>, + ?assertEqual(Bin, serialize_to_binary(Packet)), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(Bin)). + +serialize_parse_unsubscribe_v5(_) -> + Props = #{'User-Property' => [{<<"key">>, <<"val">>}]}, + Packet = ?UNSUBSCRIBE_PACKET(10, Props, [<<"Topic1">>, <<"Topic2">>]), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_unsuback(_) -> + Packet = ?UNSUBACK_PACKET(10), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_unsuback_v5(_) -> + Packet = ?UNSUBACK_PACKET(10, #{'Reason-String' => <<"Not authorized">>, + 'User-Property' => [{<<"key">>, <<"val">>}]}, + [16#87, 16#87, 16#87]), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_pingreq(_) -> + PingReq = ?PACKET(?PINGREQ), + ?assertEqual(PingReq, parse_serialize(PingReq)). + +serialize_parse_pingresp(_) -> + PingResp = ?PACKET(?PINGRESP), + ?assertEqual(PingResp, parse_serialize(PingResp)). + +parse_disconnect(_) -> + Packet = ?DISCONNECT_PACKET(?RC_SUCCESS), + ?assertMatch({ok, Packet, <<>>, _}, emqtt_frame:parse(<<224, 0>>)). + +serialize_parse_disconnect(_) -> + Packet = ?DISCONNECT_PACKET(?RC_SUCCESS), + ?assertEqual(Packet, parse_serialize(Packet)). + +serialize_parse_disconnect_v5(_) -> + Packet = ?DISCONNECT_PACKET(?RC_SUCCESS, + #{'Session-Expiry-Interval' => 60, + 'Reason-String' => <<"server_moved">>, + 'Server-Reference' => <<"192.168.1.10">> + }), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +serialize_parse_auth_v5(_) -> + Packet = ?AUTH_PACKET(?RC_SUCCESS, + #{'Authentication-Method' => <<"oauth2">>, + 'Authentication-Data' => <<"3zekkd">>, + 'Reason-String' => <<"success">>, + 'User-Property' => [{<<"key">>, <<"val">>}] + }), + ?assertEqual(Packet, parse_serialize(Packet, #{version => ?MQTT_PROTO_V5})). + +parse_serialize(Packet) -> + parse_serialize(Packet, #{version => ?MQTT_PROTO_V4}). + +parse_serialize(Packet, Opts = #{version := Ver}) when is_map(Opts) -> + Bin = iolist_to_binary(emqtt_frame:serialize(Packet, Ver)), + ParseState = emqtt_frame:initial_parse_state(Opts), + {ok, NPacket, <<>>, _} = emqtt_frame:parse(Bin, ParseState), + NPacket. + +serialize_to_binary(Packet) -> + iolist_to_binary(emqtt_frame:serialize(Packet)). + +payload() -> + iolist_to_binary(["payload." || _I <- lists:seq(1, 1000)]). + diff --git a/test/emqttc_test.erl b/test/emqttc_test.erl deleted file mode 100644 index f8ed0769..00000000 --- a/test/emqttc_test.erl +++ /dev/null @@ -1,75 +0,0 @@ - --module(emqttc_test). - --ifdef(TEST). - --include_lib("eunit/include/eunit.hrl"). - -qos_opt_test() -> - ?assertEqual(0, emqttc:qos_opt(0)), - ?assertEqual(2, emqttc:qos_opt(qos2)), - ?assertEqual(0, emqttc:qos_opt([])), - ?assertEqual(1, emqttc:qos_opt([qos1])), - ?assertEqual(2, emqttc:qos_opt([{qos, 2}])), - ?assertEqual(1, emqttc:qos_opt([qos1, {qos, 2}, {retain, 0}])), - ?assertEqual(0, emqttc:qos_opt([{retain, 0}])). - -subscribe_test() -> - {ok, C} = start_client(), - emqttc:subscribe(C, <<"Topic">>, 1), - emqttc:subscribe(C, <<"Topic">>, 2). - -publish_test() -> - {ok, C} = start_client(), - emqttc:subscribe(C, <<"Topic">>, 2), - emqttc:publish(C, <<"Topic">>, <<"Payload(Qos0)">>), - emqttc:publish(C, <<"Topic">>, <<"Payload(Qos1)">>, [{qos, 1}]), - emqttc:publish(C, <<"Topic">>, <<"Payload(Qos2)">>, [{qos, 2}]). - -unsubscribe_test() -> - {ok, C} = start_client(), - emqttc:subscribe(C, <<"Topic">>, 1), - emqttc:unsubscribe(C, <<"Topic">>). - -ping_test() -> - {ok, C} = start_client([{client_id, <<"pingTestClient">>}]), - timer:sleep(1000), - pong = emqttc:ping(C). - -disconnect_test() -> - {ok, C} = start_client(), - timer:sleep(1000), - emqttc:disconnect(C). - -subscribe_down_test() -> - {ok, C} = start_client(), - _SubPid = spawn(fun() -> - emqttc:subscribe(C, <<"Topic">>), - receive - {publish, _Topic, _Payload} -> ok - after - 1000 -> exit(timeout) - end - end), - timer:sleep(500), - emqttc:publish(C, <<"Topic">>, <<"Payload">>), - timer:sleep(500). - -clean_sess_test() -> - {ok, C} = start_client([{client_id, <<"testClient">>}, {clean_sess, false}]), - emqttc:subscribe(C, <<"Topic">>, 1), - emqttc:publish(C, <<"Topic">>, <<"Playload">>, [{qos, 1}]), - emqttc:disconnect(C), - timer:sleep(100), - {ok, C2} = start_client([{client_id, <<"testClient">>}, {clean_sess, false}]), - emqttc:subscribe(C2, <<"Topic1">>, 1), - emqttc:publish(C2, <<"Topic1">>, <<"Playload">>, [{qos, 1}]), - emqttc:disconnect(C2). - -start_client() -> - emqttc:start_link([{logger, {error_logger, info}}]). - -start_client(Opts) -> - emqttc:start_link([{logger, {error_logger, info}}|Opts]). - --endif.