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 5f0229ac..00000000 Binary files a/doc/Socket.png and /dev/null differ diff --git a/examples/gen_server/run b/examples/gen_server/run deleted file mode 100755 index 91cd4962..00000000 --- a/examples/gen_server/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 ../../ebin -pa ../../deps/*/ebin -smp true -s gen_server_example start_link - diff --git a/examples/gen_server/src/gen_server_example.app.src b/examples/gen_server/src/gen_server_example.app.src deleted file mode 100644 index bd10c575..00000000 --- a/examples/gen_server/src/gen_server_example.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, gen_server_example, - [ - {description, "emqttc gen_server example"}, - {vsn, "1.0"}, - {modules, [ - gen_server_example - ]}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {env, []} - ]}. diff --git a/examples/gen_server/src/gen_server_example.erl b/examples/gen_server/src/gen_server_example.erl deleted file mode 100644 index 7e31a827..00000000 --- a/examples/gen_server/src/gen_server_example.erl +++ /dev/null @@ -1,88 +0,0 @@ --module(gen_server_example). - --behaviour(gen_server). - --define(SERVER, ?MODULE). - -%% ------------------------------------------------------------------ -%% API Function Exports -%% ------------------------------------------------------------------ - --export([start_link/0, stop/0]). - -%% ------------------------------------------------------------------ -%% gen_server Function Exports -%% ------------------------------------------------------------------ - --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {mqttc, seq}). - -%% ------------------------------------------------------------------ -%% API Function Definitions -%% ------------------------------------------------------------------ - -start_link() -> - 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 c2b7e202..00000000 Binary files a/rebar and /dev/null differ 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.