diff --git a/.gitignore b/.gitignore index 2dcf3c065..15f67b8e0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ plugins/*/ebin log/ *.swp *.so +examples diff --git a/.gitmodules b/.gitmodules index 61767a9fb..093a4a897 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "tests/org.eclipse.paho.mqtt.testing"] - path = tests/org.eclipse.paho.mqtt.testing - url = git://git.eclipse.org/gitroot/paho/org.eclipse.paho.mqtt.testing.git +[submodule "plugins/emqttd_dashboard"] + path = plugins/emqttd_dashboard + url = https://github.com/emqtt/emqttd_dashboard.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 700532931..3c604bf61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,72 @@ emqttd ChangeLog ================== +0.9.0-alpha (2015-06-14) +------------------------- + +Session and Queue + +Merge emqtt, emqttd apps + +Move apps/emqttd/src src + +Session + +Queue + +Alarm + +Protocol Compliant + + +0.8.6-beta (2015-06-17) +------------------------- + +Bugfix: issue #175 - publish Will message when websocket is closed without 'DISCONNECT' packet + + +0.8.5-beta (2015-06-10) +------------------------- + +Bugfix: issue #53 - client will receive duplicate messages when overlapping subscription + + +0.8.4-beta (2015-06-08) +------------------------- + +Bugfix: issue #165 - duplicated message when publish 'retained' message to persistent client + + +0.8.3-beta (2015-06-05) +------------------------- + +Bugfix: issue #158 - should queue:in new message after old one dropped + +Bugfix: issue #155 - emqtt_parser.erl: parse_topics/3 should reverse topics + +Bugfix: issue #149 - Forget to merge plugins/emqttd_auth_mysql from 'dev' branch to 'master' in 0.8.x release + + +0.8.2-alpha (2015-06-01) +------------------------- + +Bugfix: issue #147 - WebSocket client cannot subscribe queue '$Q/queue/${clientId}' + +Bugfix: issue #146 - emqttd_auth_ldap: fill(Username, UserDn) is not right + + 0.8.1-alpha (2015-05-28) ------------------------- +Client [Presence](https://github.com/emqtt/emqttd/wiki/Presence) Support and [$SYS Topics](https://github.com/emqtt/emqttd/wiki/$SYS-Topics) Redesigned! + Bugfix: issue #138 - when client disconnected normally, broker will not publish disconnected $SYS message -Improve: issue #136 - $SYS topics result should not include $SYS messages +Bugfix: fix websocket url in emqttd/priv/www/websocket.html + +Improve: etc/emqttd.config to allow websocket connections from any hosts + +Improve: rel/reltool.config to exclude unnecessary apps. 0.8.0-alpha (2015-05-25) diff --git a/README.md b/README.md index bc86472e7..566961f8f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,16 @@ emqttd is aimed to provide a solid, enterprise grade, extensible open-source MQT * Passed eclipse paho interoperability tests +## Plugins + +* [emqttd_auth_clientid](https://github.com/emqtt/emqttd/wiki/Authentication) - Authentication with ClientIds +* emqttd_auth_mysql - Authentication with MySQL +* emqttd_auth_ldap - Authentication with LDAP +* emqttd_mod_autosub - Subscribe some topics automatically when client connected +* [emqttd_mod_presence](https://github.com/emqtt/emqttd/wiki/Presence) - Publish presence message to $SYS topics when client connected or disconnected +* [emqttd_mod_rewrite](https://github.com/emqtt/emqttd/wiki/Rewrite) - Topics rewrite like HTTP rewrite module + + ## Design ![emqttd architecture](http://emqtt.io/static/img/Architecture.png) @@ -92,12 +102,12 @@ The MIT License (MIT) ## Contributors -[@hejin1026](https://github.com/hejin1026) -[@desoulter](https://github.com/desoulter) -[@turtleDeng](https://github.com/turtleDeng) -[@Hades32](https://github.com/Hades32) -[@huangdan](https://github.com/huangdan) - +* [@callbay](https://github.com/callbay) +* [@hejin1026](https://github.com/hejin1026) +* [@desoulter](https://github.com/desoulter) +* [@turtleDeng](https://github.com/turtleDeng) +* [@Hades32](https://github.com/Hades32) +* [@huangdan](https://github.com/huangdan) ## Author diff --git a/TODO b/TODO deleted file mode 100644 index e43cbfda2..000000000 --- a/TODO +++ /dev/null @@ -1,63 +0,0 @@ - -v0.9.0-alpha (2015-05-30) -------------------------- - -Presence Management.... - -Dashboard - -Presence Management.... - - -v0.8.0-alpha (2015-05-10) -------------------------- - -Force Subscriptions... - -Documents... - -MySQL Auth - -AMQP - -Bridge Test - - -0.8.0 (2015-05-10) -------------------------- - -Force Subscription... - -Point2Point Queue... - -MySQL Auth - -LDAP Auth - -PG Auth - -MySQL ACL - -Retained Message... - -Tsung MQTT Test - - -0.7.1-alpha (2015-05-05) -------------------------- - -Bugfix - -Admin Dashboard - -one million connections test... - -topic match benchmark tests... - -Dialyzer ... - -full test cases... - -README.md: add "Supports and Contact" include IRC, mailling-list, email. - -Document diff --git a/apps/emqtt/include/emqtt.hrl b/apps/emqtt/include/emqtt.hrl deleted file mode 100644 index 1184aab01..000000000 --- a/apps/emqtt/include/emqtt.hrl +++ /dev/null @@ -1,69 +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 -%%% MQTT Common Header. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - -%%------------------------------------------------------------------------------ -%% 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(IS_QOS(I), (I >= ?QOS_0 andalso I =< ?QOS_2)). - --type mqtt_qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2. - -%%------------------------------------------------------------------------------ -%% MQTT Message -%%------------------------------------------------------------------------------ - --type mqtt_msgid() :: undefined | 1..16#ffff. - --record(mqtt_message, { - %% topic is first for message may be retained - topic :: binary(), - qos = ?QOS_0 :: mqtt_qos(), - retain = false :: boolean(), - dup = false :: boolean(), - msgid :: mqtt_msgid(), - payload :: binary() -}). - --type mqtt_message() :: #mqtt_message{}. - diff --git a/apps/emqtt/src/emqtt.app.src b/apps/emqtt/src/emqtt.app.src deleted file mode 100644 index ea5191f92..000000000 --- a/apps/emqtt/src/emqtt.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqtt, - [ - {description, "Erlang MQTT Common Library"}, - {vsn, "0.8.0"}, - {modules, []}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {env, []} - ]}. diff --git a/apps/emqttd/include/emqttd.hrl b/apps/emqttd/include/emqttd.hrl deleted file mode 100644 index 9c2ab934a..000000000 --- a/apps/emqttd/include/emqttd.hrl +++ /dev/null @@ -1,112 +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 -%%% MQTT Broker Header. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - -%%------------------------------------------------------------------------------ -%% Banner -%%------------------------------------------------------------------------------ --define(COPYRIGHT, "Copyright (C) 2012-2015, Feng Lee "). - --define(LICENSE_MESSAGE, "Licensed under MIT"). - --define(PROTOCOL_VERSION, "MQTT/3.1.1"). - --define(ERTS_MINIMUM, "6.0"). - -%%------------------------------------------------------------------------------ -%% PubSub -%%------------------------------------------------------------------------------ --type pubsub() :: publish | subscribe. - -%%------------------------------------------------------------------------------ -%% MQTT Topic -%%------------------------------------------------------------------------------ --record(mqtt_topic, { - topic :: binary(), - node :: node() -}). - --type mqtt_topic() :: #mqtt_topic{}. - -%%------------------------------------------------------------------------------ -%% MQTT Subscriber -%%------------------------------------------------------------------------------ --record(mqtt_subscriber, { - topic :: binary(), - qos = 0 :: 0 | 1 | 2, - pid :: pid() -}). - --type mqtt_subscriber() :: #mqtt_subscriber{}. - -%%------------------------------------------------------------------------------ -%% P2P Queue Subscriber -%%------------------------------------------------------------------------------ --record(mqtt_queue, { - name :: binary(), - subpid :: pid(), - qos = 0 :: 0 | 1 | 2 -}). - --type mqtt_queue() :: #mqtt_queue{}. - -%%------------------------------------------------------------------------------ -%% MQTT Client -%%------------------------------------------------------------------------------ --record(mqtt_client, { - clientid :: binary(), - username :: binary() | undefined, - ipaddr :: inet:ip_address() -}). - --type mqtt_client() :: #mqtt_client{}. - -%%------------------------------------------------------------------------------ -%% MQTT Session -%%------------------------------------------------------------------------------ --record(mqtt_session, { - clientid, - session_pid, - subscriptions = [], - awaiting_ack, - awaiting_rel -}). - --type mqtt_session() :: #mqtt_session{}. - -%%------------------------------------------------------------------------------ -%% MQTT Plugin -%%------------------------------------------------------------------------------ --record(mqtt_plugin, { - name, - version, - attrs, - description -}). - --type mqtt_plugin() :: #mqtt_plugin{}. - - diff --git a/apps/emqttd/include/emqttd_systop.hrl b/apps/emqttd/include/emqttd_systop.hrl deleted file mode 100644 index d2c929273..000000000 --- a/apps/emqttd/include/emqttd_systop.hrl +++ /dev/null @@ -1,108 +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 -%%% eMQTT System Topics. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - --define(SYSTOP, <<"$SYS">>). - -%%------------------------------------------------------------------------------ -%% $SYS Topics of Broker -%%------------------------------------------------------------------------------ --define(SYSTOP_BROKERS, [ - version, % Broker version - uptime, % Broker uptime - datetime, % Broker local datetime - sysdescr % Broker description -]). - -%%------------------------------------------------------------------------------ -%% $SYS Topics for Clients -%%------------------------------------------------------------------------------ --define(SYSTOP_CLIENTS, [ - 'clients/count', % clients connected current - 'clients/max' % max clients connected - %'clients/connected', - %'clients/disconnected', -]). - -%%------------------------------------------------------------------------------ -%% $SYS Topics for Sessions -%%------------------------------------------------------------------------------ --define(SYSTOP_SESSIONS, [ - 'sessions/count', - 'sessions/max' -]). - -%%------------------------------------------------------------------------------ -%% $SYS Topics for Subscribers -%%------------------------------------------------------------------------------ --define(SYSTOP_PUBSUB, [ - 'queues/count', % ... - 'queues/max', % ... - 'topics/count', % ... - 'topics/max', % ... - 'subscribers/count', % ... - 'subscribers/max' % ... -]). - -%%------------------------------------------------------------------------------ -%% Bytes sent and received of Broker -%%------------------------------------------------------------------------------ --define(SYSTOP_BYTES, [ - {counter, 'bytes/received'}, % Total bytes received - {counter, 'bytes/sent'} % Total bytes sent -]). - -%%------------------------------------------------------------------------------ -%% Packets sent and received of Broker -%%------------------------------------------------------------------------------ --define(SYSTOP_PACKETS, [ - {counter, 'packets/received'}, % All Packets received - {counter, 'packets/sent'}, % All Packets sent - {counter, 'packets/connect'}, % CONNECT Packets received - {counter, 'packets/connack'}, % CONNACK Packets sent - {counter, 'packets/publish/received'}, % PUBLISH packets received - {counter, 'packets/publish/sent'}, % PUBLISH packets sent - {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received - {counter, 'packets/suback'}, % SUBACK packets sent - {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received - {counter, 'packets/unsuback'}, % UNSUBACK Packets sent - {counter, 'packets/pingreq'}, % PINGREQ packets received - {counter, 'packets/pingresp'}, % PINGRESP Packets sent - {counter, 'packets/disconnect'} % DISCONNECT Packets received -]). - -%%------------------------------------------------------------------------------ -%% Messages sent and received of broker -%%------------------------------------------------------------------------------ --define(SYSTOP_MESSAGES, [ - {counter, 'messages/received'}, % Messages received - {counter, 'messages/sent'}, % Messages sent - {gauge, 'messages/retained/count'},% Messagea retained - {gauge, 'messages/stored/count'}, % Messages stored - {counter, 'messages/dropped'} % Messages dropped -]). - - diff --git a/apps/emqttd/include/emqttd_topic.hrl b/apps/emqttd/include/emqttd_topic.hrl deleted file mode 100644 index a98dc791a..000000000 --- a/apps/emqttd/include/emqttd_topic.hrl +++ /dev/null @@ -1,48 +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 -%%% emqtt topic header. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - -%%------------------------------------------------------------------------------ -%% MQTT Topic -%%------------------------------------------------------------------------------ --record(topic, { - name :: binary(), - node :: node() -}). - --type topic() :: #topic{}. - -%%------------------------------------------------------------------------------ -%% MQTT Topic Subscriber -%%------------------------------------------------------------------------------ --record(topic_subscriber, { - topic :: binary(), - qos = 0 :: 0 | 1 | 2, - subpid :: pid() -}). - --type topic_subscriber() :: #topic_subscriber{}. - diff --git a/apps/emqttd/priv/www/index.html b/apps/emqttd/priv/www/index.html deleted file mode 100644 index d84f633cd..000000000 --- a/apps/emqttd/priv/www/index.html +++ /dev/null @@ -1,64 +0,0 @@ - - - MQTT over WebSocket - - -

MQTT Over WebSocket

-
- -   State: -
- - - - diff --git a/apps/emqttd/priv/www/mqttws31.js b/apps/emqttd/priv/www/mqttws31.js deleted file mode 100644 index 91dfc2de5..000000000 --- a/apps/emqttd/priv/www/mqttws31.js +++ /dev/null @@ -1,2143 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2013 IBM Corp. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * and Eclipse Distribution License v1.0 which accompany this distribution. - * - * The Eclipse Public License is available at - * http://www.eclipse.org/legal/epl-v10.html - * and the Eclipse Distribution License is available at - * http://www.eclipse.org/org/documents/edl-v10.php. - * - * Contributors: - * Andrew Banks - initial API and implementation and initial documentation - *******************************************************************************/ - - -// Only expose a single object name in the global namespace. -// Everything must go through this module. Global Paho.MQTT module -// only has a single public function, client, which returns -// a Paho.MQTT client object given connection details. - -/** - * Send and receive messages using web browsers. - *

- * This programming interface lets a JavaScript client application use the MQTT V3.1 or - * V3.1.1 protocol to connect to an MQTT-supporting messaging server. - * - * The function supported includes: - *

    - *
  1. Connecting to and disconnecting from a server. The server is identified by its host name and port number. - *
  2. Specifying options that relate to the communications link with the server, - * for example the frequency of keep-alive heartbeats, and whether SSL/TLS is required. - *
  3. Subscribing to and receiving messages from MQTT Topics. - *
  4. Publishing messages to MQTT Topics. - *
- *

- * The API consists of two main objects: - *

- *
{@link Paho.MQTT.Client}
- *
This contains methods that provide the functionality of the API, - * including provision of callbacks that notify the application when a message - * arrives from or is delivered to the messaging server, - * or when the status of its connection to the messaging server changes.
- *
{@link Paho.MQTT.Message}
- *
This encapsulates the payload of the message along with various attributes - * associated with its delivery, in particular the destination to which it has - * been (or is about to be) sent.
- *
- *

- * The programming interface validates parameters passed to it, and will throw - * an Error containing an error message intended for developer use, if it detects - * an error with any parameter. - *

- * Example: - * - *

-client = new Paho.MQTT.Client(location.hostname, Number(location.port), "clientId");
-client.onConnectionLost = onConnectionLost;
-client.onMessageArrived = onMessageArrived;
-client.connect({onSuccess:onConnect});
-
-function onConnect() {
-  // Once a connection has been made, make a subscription and send a message.
-  console.log("onConnect");
-  client.subscribe("/World");
-  message = new Paho.MQTT.Message("Hello");
-  message.destinationName = "/World";
-  client.send(message); 
-};
-function onConnectionLost(responseObject) {
-  if (responseObject.errorCode !== 0)
-	console.log("onConnectionLost:"+responseObject.errorMessage);
-};
-function onMessageArrived(message) {
-  console.log("onMessageArrived:"+message.payloadString);
-  client.disconnect(); 
-};	
- * 
- * @namespace Paho.MQTT - */ - -if (typeof Paho === "undefined") { - Paho = {}; -} - -Paho.MQTT = (function (global) { - - // Private variables below, these are only visible inside the function closure - // which is used to define the module. - - var version = "@VERSION@"; - var buildLevel = "@BUILDLEVEL@"; - - /** - * Unique message type identifiers, with associated - * associated integer values. - * @private - */ - var MESSAGE_TYPE = { - CONNECT: 1, - CONNACK: 2, - PUBLISH: 3, - PUBACK: 4, - PUBREC: 5, - PUBREL: 6, - PUBCOMP: 7, - SUBSCRIBE: 8, - SUBACK: 9, - UNSUBSCRIBE: 10, - UNSUBACK: 11, - PINGREQ: 12, - PINGRESP: 13, - DISCONNECT: 14 - }; - - // Collection of utility methods used to simplify module code - // and promote the DRY pattern. - - /** - * Validate an object's parameter names to ensure they - * match a list of expected variables name for this option - * type. Used to ensure option object passed into the API don't - * contain erroneous parameters. - * @param {Object} obj - User options object - * @param {Object} keys - valid keys and types that may exist in obj. - * @throws {Error} Invalid option parameter found. - * @private - */ - var validate = function(obj, keys) { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - if (keys.hasOwnProperty(key)) { - if (typeof obj[key] !== keys[key]) - throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key])); - } else { - var errorStr = "Unknown property, " + key + ". Valid properties are:"; - for (var key in keys) - if (keys.hasOwnProperty(key)) - errorStr = errorStr+" "+key; - throw new Error(errorStr); - } - } - } - }; - - /** - * Return a new function which runs the user function bound - * to a fixed scope. - * @param {function} User function - * @param {object} Function scope - * @return {function} User function bound to another scope - * @private - */ - var scope = function (f, scope) { - return function () { - return f.apply(scope, arguments); - }; - }; - - /** - * Unique message type identifiers, with associated - * associated integer values. - * @private - */ - var ERROR = { - OK: {code:0, text:"AMQJSC0000I OK."}, - CONNECT_TIMEOUT: {code:1, text:"AMQJSC0001E Connect timed out."}, - SUBSCRIBE_TIMEOUT: {code:2, text:"AMQJS0002E Subscribe timed out."}, - UNSUBSCRIBE_TIMEOUT: {code:3, text:"AMQJS0003E Unsubscribe timed out."}, - PING_TIMEOUT: {code:4, text:"AMQJS0004E Ping timed out."}, - INTERNAL_ERROR: {code:5, text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"}, - CONNACK_RETURNCODE: {code:6, text:"AMQJS0006E Bad Connack return code:{0} {1}."}, - SOCKET_ERROR: {code:7, text:"AMQJS0007E Socket error:{0}."}, - SOCKET_CLOSE: {code:8, text:"AMQJS0008I Socket closed."}, - MALFORMED_UTF: {code:9, text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."}, - UNSUPPORTED: {code:10, text:"AMQJS0010E {0} is not supported by this browser."}, - INVALID_STATE: {code:11, text:"AMQJS0011E Invalid state {0}."}, - INVALID_TYPE: {code:12, text:"AMQJS0012E Invalid type {0} for {1}."}, - INVALID_ARGUMENT: {code:13, text:"AMQJS0013E Invalid argument {0} for {1}."}, - UNSUPPORTED_OPERATION: {code:14, text:"AMQJS0014E Unsupported operation."}, - INVALID_STORED_DATA: {code:15, text:"AMQJS0015E Invalid data in local storage key={0} value={1}."}, - INVALID_MQTT_MESSAGE_TYPE: {code:16, text:"AMQJS0016E Invalid MQTT message type {0}."}, - MALFORMED_UNICODE: {code:17, text:"AMQJS0017E Malformed Unicode string:{0} {1}."}, - }; - - /** CONNACK RC Meaning. */ - var CONNACK_RC = { - 0:"Connection Accepted", - 1:"Connection Refused: unacceptable protocol version", - 2:"Connection Refused: identifier rejected", - 3:"Connection Refused: server unavailable", - 4:"Connection Refused: bad user name or password", - 5:"Connection Refused: not authorized" - }; - - /** - * Format an error message text. - * @private - * @param {error} ERROR.KEY value above. - * @param {substitutions} [array] substituted into the text. - * @return the text with the substitutions made. - */ - var format = function(error, substitutions) { - var text = error.text; - if (substitutions) { - var field,start; - for (var i=0; i 0) { - var part1 = text.substring(0,start); - var part2 = text.substring(start+field.length); - text = part1+substitutions[i]+part2; - } - } - } - return text; - }; - - //MQTT protocol and version 6 M Q I s d p 3 - var MqttProtoIdentifierv3 = [0x00,0x06,0x4d,0x51,0x49,0x73,0x64,0x70,0x03]; - //MQTT proto/version for 311 4 M Q T T 4 - var MqttProtoIdentifierv4 = [0x00,0x04,0x4d,0x51,0x54,0x54,0x04]; - - /** - * Construct an MQTT wire protocol message. - * @param type MQTT packet type. - * @param options optional wire message attributes. - * - * Optional properties - * - * messageIdentifier: message ID in the range [0..65535] - * payloadMessage: Application Message - PUBLISH only - * connectStrings: array of 0 or more Strings to be put into the CONNECT payload - * topics: array of strings (SUBSCRIBE, UNSUBSCRIBE) - * requestQoS: array of QoS values [0..2] - * - * "Flag" properties - * cleanSession: true if present / false if absent (CONNECT) - * willMessage: true if present / false if absent (CONNECT) - * isRetained: true if present / false if absent (CONNECT) - * userName: true if present / false if absent (CONNECT) - * password: true if present / false if absent (CONNECT) - * keepAliveInterval: integer [0..65535] (CONNECT) - * - * @private - * @ignore - */ - var WireMessage = function (type, options) { - this.type = type; - for (var name in options) { - if (options.hasOwnProperty(name)) { - this[name] = options[name]; - } - } - }; - - WireMessage.prototype.encode = function() { - // Compute the first byte of the fixed header - var first = ((this.type & 0x0f) << 4); - - /* - * Now calculate the length of the variable header + payload by adding up the lengths - * of all the component parts - */ - - var remLength = 0; - var topicStrLength = new Array(); - var destinationNameLength = 0; - - // if the message contains a messageIdentifier then we need two bytes for that - if (this.messageIdentifier != undefined) - remLength += 2; - - switch(this.type) { - // If this a Connect then we need to include 12 bytes for its header - case MESSAGE_TYPE.CONNECT: - switch(this.mqttVersion) { - case 3: - remLength += MqttProtoIdentifierv3.length + 3; - break; - case 4: - remLength += MqttProtoIdentifierv4.length + 3; - break; - } - - remLength += UTF8Length(this.clientId) + 2; - if (this.willMessage != undefined) { - remLength += UTF8Length(this.willMessage.destinationName) + 2; - // Will message is always a string, sent as UTF-8 characters with a preceding length. - var willMessagePayloadBytes = this.willMessage.payloadBytes; - if (!(willMessagePayloadBytes instanceof Uint8Array)) - willMessagePayloadBytes = new Uint8Array(payloadBytes); - remLength += willMessagePayloadBytes.byteLength +2; - } - if (this.userName != undefined) - remLength += UTF8Length(this.userName) + 2; - if (this.password != undefined) - remLength += UTF8Length(this.password) + 2; - break; - - // Subscribe, Unsubscribe can both contain topic strings - case MESSAGE_TYPE.SUBSCRIBE: - first |= 0x02; // Qos = 1; - for ( var i = 0; i < this.topics.length; i++) { - topicStrLength[i] = UTF8Length(this.topics[i]); - remLength += topicStrLength[i] + 2; - } - remLength += this.requestedQos.length; // 1 byte for each topic's Qos - // QoS on Subscribe only - break; - - case MESSAGE_TYPE.UNSUBSCRIBE: - first |= 0x02; // Qos = 1; - for ( var i = 0; i < this.topics.length; i++) { - topicStrLength[i] = UTF8Length(this.topics[i]); - remLength += topicStrLength[i] + 2; - } - break; - - case MESSAGE_TYPE.PUBREL: - first |= 0x02; // Qos = 1; - break; - - case MESSAGE_TYPE.PUBLISH: - if (this.payloadMessage.duplicate) first |= 0x08; - first = first |= (this.payloadMessage.qos << 1); - if (this.payloadMessage.retained) first |= 0x01; - destinationNameLength = UTF8Length(this.payloadMessage.destinationName); - remLength += destinationNameLength + 2; - var payloadBytes = this.payloadMessage.payloadBytes; - remLength += payloadBytes.byteLength; - if (payloadBytes instanceof ArrayBuffer) - payloadBytes = new Uint8Array(payloadBytes); - else if (!(payloadBytes instanceof Uint8Array)) - payloadBytes = new Uint8Array(payloadBytes.buffer); - break; - - case MESSAGE_TYPE.DISCONNECT: - break; - - default: - ; - } - - // Now we can allocate a buffer for the message - - var mbi = encodeMBI(remLength); // Convert the length to MQTT MBI format - var pos = mbi.length + 1; // Offset of start of variable header - var buffer = new ArrayBuffer(remLength + pos); - var byteStream = new Uint8Array(buffer); // view it as a sequence of bytes - - //Write the fixed header into the buffer - byteStream[0] = first; - byteStream.set(mbi,1); - - // If this is a PUBLISH then the variable header starts with a topic - if (this.type == MESSAGE_TYPE.PUBLISH) - pos = writeString(this.payloadMessage.destinationName, destinationNameLength, byteStream, pos); - // If this is a CONNECT then the variable header contains the protocol name/version, flags and keepalive time - - else if (this.type == MESSAGE_TYPE.CONNECT) { - switch (this.mqttVersion) { - case 3: - byteStream.set(MqttProtoIdentifierv3, pos); - pos += MqttProtoIdentifierv3.length; - break; - case 4: - byteStream.set(MqttProtoIdentifierv4, pos); - pos += MqttProtoIdentifierv4.length; - break; - } - var connectFlags = 0; - if (this.cleanSession) - connectFlags = 0x02; - if (this.willMessage != undefined ) { - connectFlags |= 0x04; - connectFlags |= (this.willMessage.qos<<3); - if (this.willMessage.retained) { - connectFlags |= 0x20; - } - } - if (this.userName != undefined) - connectFlags |= 0x80; - if (this.password != undefined) - connectFlags |= 0x40; - byteStream[pos++] = connectFlags; - pos = writeUint16 (this.keepAliveInterval, byteStream, pos); - } - - // Output the messageIdentifier - if there is one - if (this.messageIdentifier != undefined) - pos = writeUint16 (this.messageIdentifier, byteStream, pos); - - switch(this.type) { - case MESSAGE_TYPE.CONNECT: - pos = writeString(this.clientId, UTF8Length(this.clientId), byteStream, pos); - if (this.willMessage != undefined) { - pos = writeString(this.willMessage.destinationName, UTF8Length(this.willMessage.destinationName), byteStream, pos); - pos = writeUint16(willMessagePayloadBytes.byteLength, byteStream, pos); - byteStream.set(willMessagePayloadBytes, pos); - pos += willMessagePayloadBytes.byteLength; - - } - if (this.userName != undefined) - pos = writeString(this.userName, UTF8Length(this.userName), byteStream, pos); - if (this.password != undefined) - pos = writeString(this.password, UTF8Length(this.password), byteStream, pos); - break; - - case MESSAGE_TYPE.PUBLISH: - // PUBLISH has a text or binary payload, if text do not add a 2 byte length field, just the UTF characters. - byteStream.set(payloadBytes, pos); - - break; - -// case MESSAGE_TYPE.PUBREC: -// case MESSAGE_TYPE.PUBREL: -// case MESSAGE_TYPE.PUBCOMP: -// break; - - case MESSAGE_TYPE.SUBSCRIBE: - // SUBSCRIBE has a list of topic strings and request QoS - for (var i=0; i> 4; - var messageInfo = first &= 0x0f; - pos += 1; - - - // Decode the remaining length (MBI format) - - var digit; - var remLength = 0; - var multiplier = 1; - do { - if (pos == input.length) { - return [null,startingPos]; - } - digit = input[pos++]; - remLength += ((digit & 0x7F) * multiplier); - multiplier *= 128; - } while ((digit & 0x80) != 0); - - var endPos = pos+remLength; - if (endPos > input.length) { - return [null,startingPos]; - } - - var wireMessage = new WireMessage(type); - switch(type) { - case MESSAGE_TYPE.CONNACK: - var connectAcknowledgeFlags = input[pos++]; - if (connectAcknowledgeFlags & 0x01) - wireMessage.sessionPresent = true; - wireMessage.returnCode = input[pos++]; - break; - - case MESSAGE_TYPE.PUBLISH: - var qos = (messageInfo >> 1) & 0x03; - - var len = readUint16(input, pos); - pos += 2; - var topicName = parseUTF8(input, pos, len); - pos += len; - // If QoS 1 or 2 there will be a messageIdentifier - if (qos > 0) { - wireMessage.messageIdentifier = readUint16(input, pos); - pos += 2; - } - - var message = new Paho.MQTT.Message(input.subarray(pos, endPos)); - if ((messageInfo & 0x01) == 0x01) - message.retained = true; - if ((messageInfo & 0x08) == 0x08) - message.duplicate = true; - message.qos = qos; - message.destinationName = topicName; - wireMessage.payloadMessage = message; - break; - - case MESSAGE_TYPE.PUBACK: - case MESSAGE_TYPE.PUBREC: - case MESSAGE_TYPE.PUBREL: - case MESSAGE_TYPE.PUBCOMP: - case MESSAGE_TYPE.UNSUBACK: - wireMessage.messageIdentifier = readUint16(input, pos); - break; - - case MESSAGE_TYPE.SUBACK: - wireMessage.messageIdentifier = readUint16(input, pos); - pos += 2; - wireMessage.returnCode = input.subarray(pos, endPos); - break; - - default: - ; - } - - return [wireMessage,endPos]; - } - - function writeUint16(input, buffer, offset) { - buffer[offset++] = input >> 8; //MSB - buffer[offset++] = input % 256; //LSB - return offset; - } - - function writeString(input, utf8Length, buffer, offset) { - offset = writeUint16(utf8Length, buffer, offset); - stringToUTF8(input, buffer, offset); - return offset + utf8Length; - } - - function readUint16(buffer, offset) { - return 256*buffer[offset] + buffer[offset+1]; - } - - /** - * Encodes an MQTT Multi-Byte Integer - * @private - */ - function encodeMBI(number) { - var output = new Array(1); - var numBytes = 0; - - do { - var digit = number % 128; - number = number >> 7; - if (number > 0) { - digit |= 0x80; - } - output[numBytes++] = digit; - } while ( (number > 0) && (numBytes<4) ); - - return output; - } - - /** - * Takes a String and calculates its length in bytes when encoded in UTF8. - * @private - */ - function UTF8Length(input) { - var output = 0; - for (var i = 0; i 0x7FF) - { - // Surrogate pair means its a 4 byte character - if (0xD800 <= charCode && charCode <= 0xDBFF) - { - i++; - output++; - } - output +=3; - } - else if (charCode > 0x7F) - output +=2; - else - output++; - } - return output; - } - - /** - * Takes a String and writes it into an array as UTF8 encoded bytes. - * @private - */ - function stringToUTF8(input, output, start) { - var pos = start; - for (var i = 0; i>6 & 0x1F | 0xC0; - output[pos++] = charCode & 0x3F | 0x80; - } else if (charCode <= 0xFFFF) { - output[pos++] = charCode>>12 & 0x0F | 0xE0; - output[pos++] = charCode>>6 & 0x3F | 0x80; - output[pos++] = charCode & 0x3F | 0x80; - } else { - output[pos++] = charCode>>18 & 0x07 | 0xF0; - output[pos++] = charCode>>12 & 0x3F | 0x80; - output[pos++] = charCode>>6 & 0x3F | 0x80; - output[pos++] = charCode & 0x3F | 0x80; - }; - } - return output; - } - - function parseUTF8(input, offset, length) { - var output = ""; - var utf16; - var pos = offset; - - while (pos < offset+length) - { - var byte1 = input[pos++]; - if (byte1 < 128) - utf16 = byte1; - else - { - var byte2 = input[pos++]-128; - if (byte2 < 0) - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16),""])); - if (byte1 < 0xE0) // 2 byte character - utf16 = 64*(byte1-0xC0) + byte2; - else - { - var byte3 = input[pos++]-128; - if (byte3 < 0) - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16)])); - if (byte1 < 0xF0) // 3 byte character - utf16 = 4096*(byte1-0xE0) + 64*byte2 + byte3; - else - { - var byte4 = input[pos++]-128; - if (byte4 < 0) - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); - if (byte1 < 0xF8) // 4 byte character - utf16 = 262144*(byte1-0xF0) + 4096*byte2 + 64*byte3 + byte4; - else // longer encodings are not supported - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); - } - } - } - - if (utf16 > 0xFFFF) // 4 byte character - express as a surrogate pair - { - utf16 -= 0x10000; - output += String.fromCharCode(0xD800 + (utf16 >> 10)); // lead character - utf16 = 0xDC00 + (utf16 & 0x3FF); // trail character - } - output += String.fromCharCode(utf16); - } - return output; - } - - /** - * Repeat keepalive requests, monitor responses. - * @ignore - */ - var Pinger = function(client, window, keepAliveInterval) { - this._client = client; - this._window = window; - this._keepAliveInterval = keepAliveInterval*1000; - this.isReset = false; - - var pingReq = new WireMessage(MESSAGE_TYPE.PINGREQ).encode(); - - var doTimeout = function (pinger) { - return function () { - return doPing.apply(pinger); - }; - }; - - /** @ignore */ - var doPing = function() { - if (!this.isReset) { - this._client._trace("Pinger.doPing", "Timed out"); - this._client._disconnected( ERROR.PING_TIMEOUT.code , format(ERROR.PING_TIMEOUT)); - } else { - this.isReset = false; - this._client._trace("Pinger.doPing", "send PINGREQ"); - this._client.socket.send(pingReq); - this.timeout = this._window.setTimeout(doTimeout(this), this._keepAliveInterval); - } - } - - this.reset = function() { - this.isReset = true; - this._window.clearTimeout(this.timeout); - if (this._keepAliveInterval > 0) - this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval); - } - - this.cancel = function() { - this._window.clearTimeout(this.timeout); - } - }; - - /** - * Monitor request completion. - * @ignore - */ - var Timeout = function(client, window, timeoutSeconds, action, args) { - this._window = window; - if (!timeoutSeconds) - timeoutSeconds = 30; - - var doTimeout = function (action, client, args) { - return function () { - return action.apply(client, args); - }; - }; - this.timeout = setTimeout(doTimeout(action, client, args), timeoutSeconds * 1000); - - this.cancel = function() { - this._window.clearTimeout(this.timeout); - } - }; - - /* - * Internal implementation of the Websockets MQTT V3.1 client. - * - * @name Paho.MQTT.ClientImpl @constructor - * @param {String} host the DNS nameof the webSocket host. - * @param {Number} port the port number for that host. - * @param {String} clientId the MQ client identifier. - */ - var ClientImpl = function (uri, host, port, path, clientId) { - // Check dependencies are satisfied in this browser. - if (!("WebSocket" in global && global["WebSocket"] !== null)) { - throw new Error(format(ERROR.UNSUPPORTED, ["WebSocket"])); - } - if (!("localStorage" in global && global["localStorage"] !== null)) { - throw new Error(format(ERROR.UNSUPPORTED, ["localStorage"])); - } - if (!("ArrayBuffer" in global && global["ArrayBuffer"] !== null)) { - throw new Error(format(ERROR.UNSUPPORTED, ["ArrayBuffer"])); - } - this._trace("Paho.MQTT.Client", uri, host, port, path, clientId); - - this.host = host; - this.port = port; - this.path = path; - this.uri = uri; - this.clientId = clientId; - - // Local storagekeys are qualified with the following string. - // The conditional inclusion of path in the key is for backward - // compatibility to when the path was not configurable and assumed to - // be /mqtt - this._localKey=host+":"+port+(path!="/mqtt"?":"+path:"")+":"+clientId+":"; - - // Create private instance-only message queue - // Internal queue of messages to be sent, in sending order. - this._msg_queue = []; - - // Messages we have sent and are expecting a response for, indexed by their respective message ids. - this._sentMessages = {}; - - // Messages we have received and acknowleged and are expecting a confirm message for - // indexed by their respective message ids. - this._receivedMessages = {}; - - // Internal list of callbacks to be executed when messages - // have been successfully sent over web socket, e.g. disconnect - // when it doesn't have to wait for ACK, just message is dispatched. - this._notify_msg_sent = {}; - - // Unique identifier for SEND messages, incrementing - // counter as messages are sent. - this._message_identifier = 1; - - // Used to determine the transmission sequence of stored sent messages. - this._sequence = 0; - - - // Load the local state, if any, from the saved version, only restore state relevant to this client. - for (var key in localStorage) - if ( key.indexOf("Sent:"+this._localKey) == 0 - || key.indexOf("Received:"+this._localKey) == 0) - this.restore(key); - }; - - // Messaging Client public instance members. - ClientImpl.prototype.host; - ClientImpl.prototype.port; - ClientImpl.prototype.path; - ClientImpl.prototype.uri; - ClientImpl.prototype.clientId; - - // Messaging Client private instance members. - ClientImpl.prototype.socket; - /* true once we have received an acknowledgement to a CONNECT packet. */ - ClientImpl.prototype.connected = false; - /* The largest message identifier allowed, may not be larger than 2**16 but - * if set smaller reduces the maximum number of outbound messages allowed. - */ - ClientImpl.prototype.maxMessageIdentifier = 65536; - ClientImpl.prototype.connectOptions; - ClientImpl.prototype.hostIndex; - ClientImpl.prototype.onConnectionLost; - ClientImpl.prototype.onMessageDelivered; - ClientImpl.prototype.onMessageArrived; - ClientImpl.prototype.traceFunction; - ClientImpl.prototype._msg_queue = null; - ClientImpl.prototype._connectTimeout; - /* The sendPinger monitors how long we allow before we send data to prove to the server that we are alive. */ - ClientImpl.prototype.sendPinger = null; - /* The receivePinger monitors how long we allow before we require evidence that the server is alive. */ - ClientImpl.prototype.receivePinger = null; - - ClientImpl.prototype.receiveBuffer = null; - - ClientImpl.prototype._traceBuffer = null; - ClientImpl.prototype._MAX_TRACE_ENTRIES = 100; - - ClientImpl.prototype.connect = function (connectOptions) { - var connectOptionsMasked = this._traceMask(connectOptions, "password"); - this._trace("Client.connect", connectOptionsMasked, this.socket, this.connected); - - if (this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); - if (this.socket) - throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); - - this.connectOptions = connectOptions; - - if (connectOptions.uris) { - this.hostIndex = 0; - this._doConnect(connectOptions.uris[0]); - } else { - this._doConnect(this.uri); - } - - }; - - ClientImpl.prototype.subscribe = function (filter, subscribeOptions) { - this._trace("Client.subscribe", filter, subscribeOptions); - - if (!this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); - - var wireMessage = new WireMessage(MESSAGE_TYPE.SUBSCRIBE); - wireMessage.topics=[filter]; - if (subscribeOptions.qos != undefined) - wireMessage.requestedQos = [subscribeOptions.qos]; - else - wireMessage.requestedQos = [0]; - - if (subscribeOptions.onSuccess) { - wireMessage.onSuccess = function(grantedQos) {subscribeOptions.onSuccess({invocationContext:subscribeOptions.invocationContext,grantedQos:grantedQos});}; - } - - if (subscribeOptions.onFailure) { - wireMessage.onFailure = function(errorCode) {subscribeOptions.onFailure({invocationContext:subscribeOptions.invocationContext,errorCode:errorCode});}; - } - - if (subscribeOptions.timeout) { - wireMessage.timeOut = new Timeout(this, window, subscribeOptions.timeout, subscribeOptions.onFailure - , [{invocationContext:subscribeOptions.invocationContext, - errorCode:ERROR.SUBSCRIBE_TIMEOUT.code, - errorMessage:format(ERROR.SUBSCRIBE_TIMEOUT)}]); - } - - // All subscriptions return a SUBACK. - this._requires_ack(wireMessage); - this._schedule_message(wireMessage); - }; - - /** @ignore */ - ClientImpl.prototype.unsubscribe = function(filter, unsubscribeOptions) { - this._trace("Client.unsubscribe", filter, unsubscribeOptions); - - if (!this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); - - var wireMessage = new WireMessage(MESSAGE_TYPE.UNSUBSCRIBE); - wireMessage.topics = [filter]; - - if (unsubscribeOptions.onSuccess) { - wireMessage.callback = function() {unsubscribeOptions.onSuccess({invocationContext:unsubscribeOptions.invocationContext});}; - } - if (unsubscribeOptions.timeout) { - wireMessage.timeOut = new Timeout(this, window, unsubscribeOptions.timeout, unsubscribeOptions.onFailure - , [{invocationContext:unsubscribeOptions.invocationContext, - errorCode:ERROR.UNSUBSCRIBE_TIMEOUT.code, - errorMessage:format(ERROR.UNSUBSCRIBE_TIMEOUT)}]); - } - - // All unsubscribes return a SUBACK. - this._requires_ack(wireMessage); - this._schedule_message(wireMessage); - }; - - ClientImpl.prototype.send = function (message) { - this._trace("Client.send", message); - - if (!this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); - - wireMessage = new WireMessage(MESSAGE_TYPE.PUBLISH); - wireMessage.payloadMessage = message; - - if (message.qos > 0) - this._requires_ack(wireMessage); - else if (this.onMessageDelivered) - this._notify_msg_sent[wireMessage] = this.onMessageDelivered(wireMessage.payloadMessage); - this._schedule_message(wireMessage); - }; - - ClientImpl.prototype.disconnect = function () { - this._trace("Client.disconnect"); - - if (!this.socket) - throw new Error(format(ERROR.INVALID_STATE, ["not connecting or connected"])); - - wireMessage = new WireMessage(MESSAGE_TYPE.DISCONNECT); - - // Run the disconnected call back as soon as the message has been sent, - // in case of a failure later on in the disconnect processing. - // as a consequence, the _disconected call back may be run several times. - this._notify_msg_sent[wireMessage] = scope(this._disconnected, this); - - this._schedule_message(wireMessage); - }; - - ClientImpl.prototype.getTraceLog = function () { - if ( this._traceBuffer !== null ) { - this._trace("Client.getTraceLog", new Date()); - this._trace("Client.getTraceLog in flight messages", this._sentMessages.length); - for (var key in this._sentMessages) - this._trace("_sentMessages ",key, this._sentMessages[key]); - for (var key in this._receivedMessages) - this._trace("_receivedMessages ",key, this._receivedMessages[key]); - - return this._traceBuffer; - } - }; - - ClientImpl.prototype.startTrace = function () { - if ( this._traceBuffer === null ) { - this._traceBuffer = []; - } - this._trace("Client.startTrace", new Date(), version); - }; - - ClientImpl.prototype.stopTrace = function () { - delete this._traceBuffer; - }; - - ClientImpl.prototype._doConnect = function (wsurl) { - // When the socket is open, this client will send the CONNECT WireMessage using the saved parameters. - if (this.connectOptions.useSSL) { - var uriParts = wsurl.split(":"); - uriParts[0] = "wss"; - wsurl = uriParts.join(":"); - } - this.connected = false; - if (this.connectOptions.mqttVersion < 4) { - this.socket = new WebSocket(wsurl, ["mqttv3.1"]); - } else { - this.socket = new WebSocket(wsurl, ["mqtt"]); - } - this.socket.binaryType = 'arraybuffer'; - - this.socket.onopen = scope(this._on_socket_open, this); - this.socket.onmessage = scope(this._on_socket_message, this); - this.socket.onerror = scope(this._on_socket_error, this); - this.socket.onclose = scope(this._on_socket_close, this); - - this.sendPinger = new Pinger(this, window, this.connectOptions.keepAliveInterval); - this.receivePinger = new Pinger(this, window, this.connectOptions.keepAliveInterval); - - this._connectTimeout = new Timeout(this, window, this.connectOptions.timeout, this._disconnected, [ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)]); - }; - - - // Schedule a new message to be sent over the WebSockets - // connection. CONNECT messages cause WebSocket connection - // to be started. All other messages are queued internally - // until this has happened. When WS connection starts, process - // all outstanding messages. - ClientImpl.prototype._schedule_message = function (message) { - this._msg_queue.push(message); - // Process outstanding messages in the queue if we have an open socket, and have received CONNACK. - if (this.connected) { - this._process_queue(); - } - }; - - ClientImpl.prototype.store = function(prefix, wireMessage) { - var storedMessage = {type:wireMessage.type, messageIdentifier:wireMessage.messageIdentifier, version:1}; - - switch(wireMessage.type) { - case MESSAGE_TYPE.PUBLISH: - if(wireMessage.pubRecReceived) - storedMessage.pubRecReceived = true; - - // Convert the payload to a hex string. - storedMessage.payloadMessage = {}; - var hex = ""; - var messageBytes = wireMessage.payloadMessage.payloadBytes; - for (var i=0; i= 2) { - var x = parseInt(hex.substring(0, 2), 16); - hex = hex.substring(2, hex.length); - byteStream[i++] = x; - } - var payloadMessage = new Paho.MQTT.Message(byteStream); - - payloadMessage.qos = storedMessage.payloadMessage.qos; - payloadMessage.destinationName = storedMessage.payloadMessage.destinationName; - if (storedMessage.payloadMessage.duplicate) - payloadMessage.duplicate = true; - if (storedMessage.payloadMessage.retained) - payloadMessage.retained = true; - wireMessage.payloadMessage = payloadMessage; - - break; - - default: - throw Error(format(ERROR.INVALID_STORED_DATA, [key, value])); - } - - if (key.indexOf("Sent:"+this._localKey) == 0) { - wireMessage.payloadMessage.duplicate = true; - this._sentMessages[wireMessage.messageIdentifier] = wireMessage; - } else if (key.indexOf("Received:"+this._localKey) == 0) { - this._receivedMessages[wireMessage.messageIdentifier] = wireMessage; - } - }; - - ClientImpl.prototype._process_queue = function () { - var message = null; - // Process messages in order they were added - var fifo = this._msg_queue.reverse(); - - // Send all queued messages down socket connection - while ((message = fifo.pop())) { - this._socket_send(message); - // Notify listeners that message was successfully sent - if (this._notify_msg_sent[message]) { - this._notify_msg_sent[message](); - delete this._notify_msg_sent[message]; - } - } - }; - - /** - * Expect an ACK response for this message. Add message to the set of in progress - * messages and set an unused identifier in this message. - * @ignore - */ - ClientImpl.prototype._requires_ack = function (wireMessage) { - var messageCount = Object.keys(this._sentMessages).length; - if (messageCount > this.maxMessageIdentifier) - throw Error ("Too many messages:"+messageCount); - - while(this._sentMessages[this._message_identifier] !== undefined) { - this._message_identifier++; - } - wireMessage.messageIdentifier = this._message_identifier; - this._sentMessages[wireMessage.messageIdentifier] = wireMessage; - if (wireMessage.type === MESSAGE_TYPE.PUBLISH) { - this.store("Sent:", wireMessage); - } - if (this._message_identifier === this.maxMessageIdentifier) { - this._message_identifier = 1; - } - }; - - /** - * Called when the underlying websocket has been opened. - * @ignore - */ - ClientImpl.prototype._on_socket_open = function () { - // Create the CONNECT message object. - var wireMessage = new WireMessage(MESSAGE_TYPE.CONNECT, this.connectOptions); - wireMessage.clientId = this.clientId; - this._socket_send(wireMessage); - }; - - /** - * Called when the underlying websocket has received a complete packet. - * @ignore - */ - ClientImpl.prototype._on_socket_message = function (event) { - this._trace("Client._on_socket_message", event.data); - // Reset the receive ping timer, we now have evidence the server is alive. - this.receivePinger.reset(); - var messages = this._deframeMessages(event.data); - for (var i = 0; i < messages.length; i+=1) { - this._handleMessage(messages[i]); - } - } - - ClientImpl.prototype._deframeMessages = function(data) { - var byteArray = new Uint8Array(data); - if (this.receiveBuffer) { - var newData = new Uint8Array(this.receiveBuffer.length+byteArray.length); - newData.set(this.receiveBuffer); - newData.set(byteArray,this.receiveBuffer.length); - byteArray = newData; - delete this.receiveBuffer; - } - try { - var offset = 0; - var messages = []; - while(offset < byteArray.length) { - var result = decodeMessage(byteArray,offset); - var wireMessage = result[0]; - offset = result[1]; - if (wireMessage !== null) { - messages.push(wireMessage); - } else { - break; - } - } - if (offset < byteArray.length) { - this.receiveBuffer = byteArray.subarray(offset); - } - } catch (error) { - this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,error.stack.toString()])); - return; - } - return messages; - } - - ClientImpl.prototype._handleMessage = function(wireMessage) { - - this._trace("Client._handleMessage", wireMessage); - - try { - switch(wireMessage.type) { - case MESSAGE_TYPE.CONNACK: - this._connectTimeout.cancel(); - - // If we have started using clean session then clear up the local state. - if (this.connectOptions.cleanSession) { - for (var key in this._sentMessages) { - var sentMessage = this._sentMessages[key]; - localStorage.removeItem("Sent:"+this._localKey+sentMessage.messageIdentifier); - } - this._sentMessages = {}; - - for (var key in this._receivedMessages) { - var receivedMessage = this._receivedMessages[key]; - localStorage.removeItem("Received:"+this._localKey+receivedMessage.messageIdentifier); - } - this._receivedMessages = {}; - } - // Client connected and ready for business. - if (wireMessage.returnCode === 0) { - this.connected = true; - // Jump to the end of the list of uris and stop looking for a good host. - if (this.connectOptions.uris) - this.hostIndex = this.connectOptions.uris.length; - } else { - this._disconnected(ERROR.CONNACK_RETURNCODE.code , format(ERROR.CONNACK_RETURNCODE, [wireMessage.returnCode, CONNACK_RC[wireMessage.returnCode]])); - break; - } - - // Resend messages. - var sequencedMessages = new Array(); - for (var msgId in this._sentMessages) { - if (this._sentMessages.hasOwnProperty(msgId)) - sequencedMessages.push(this._sentMessages[msgId]); - } - - // Sort sentMessages into the original sent order. - var sequencedMessages = sequencedMessages.sort(function(a,b) {return a.sequence - b.sequence;} ); - for (var i=0, len=sequencedMessages.length; i - * Most applications will create just one Client object and then call its connect() method, - * however applications can create more than one Client object if they wish. - * In this case the combination of host, port and clientId attributes must be different for each Client object. - *

- * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods - * (even though the underlying protocol exchange might be synchronous in nature). - * This means they signal their completion by calling back to the application, - * via Success or Failure callback functions provided by the application on the method in question. - * Such callbacks are called at most once per method invocation and do not persist beyond the lifetime - * of the script that made the invocation. - *

- * In contrast there are some callback functions, most notably onMessageArrived, - * that are defined on the {@link Paho.MQTT.Client} object. - * These may get called multiple times, and aren't directly related to specific method invocations made by the client. - * - * @name Paho.MQTT.Client - * - * @constructor - * - * @param {string} host - the address of the messaging server, as a fully qualified WebSocket URI, as a DNS name or dotted decimal IP address. - * @param {number} port - the port number to connect to - only required if host is not a URI - * @param {string} path - the path on the host to connect to - only used if host is not a URI. Default: '/mqtt'. - * @param {string} clientId - the Messaging client identifier, between 1 and 23 characters in length. - * - * @property {string} host - read only the server's DNS hostname or dotted decimal IP address. - * @property {number} port - read only the server's port. - * @property {string} path - read only the server's path. - * @property {string} clientId - read only used when connecting to the server. - * @property {function} onConnectionLost - called when a connection has been lost. - * after a connect() method has succeeded. - * Establish the call back used when a connection has been lost. The connection may be - * lost because the client initiates a disconnect or because the server or network - * cause the client to be disconnected. The disconnect call back may be called without - * the connectionComplete call back being invoked if, for example the client fails to - * connect. - * A single response object parameter is passed to the onConnectionLost callback containing the following fields: - *

    - *
  1. errorCode - *
  2. errorMessage - *
- * @property {function} onMessageDelivered called when a message has been delivered. - * All processing that this Client will ever do has been completed. So, for example, - * in the case of a Qos=2 message sent by this client, the PubComp flow has been received from the server - * and the message has been removed from persistent storage before this callback is invoked. - * Parameters passed to the onMessageDelivered callback are: - *
    - *
  1. {@link Paho.MQTT.Message} that was delivered. - *
- * @property {function} onMessageArrived called when a message has arrived in this Paho.MQTT.client. - * Parameters passed to the onMessageArrived callback are: - *
    - *
  1. {@link Paho.MQTT.Message} that has arrived. - *
- */ - var Client = function (host, port, path, clientId) { - - var uri; - - if (typeof host !== "string") - throw new Error(format(ERROR.INVALID_TYPE, [typeof host, "host"])); - - if (arguments.length == 2) { - // host: must be full ws:// uri - // port: clientId - clientId = port; - uri = host; - var match = uri.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/); - if (match) { - host = match[4]||match[2]; - port = parseInt(match[7]); - path = match[8]; - } else { - throw new Error(format(ERROR.INVALID_ARGUMENT,[host,"host"])); - } - } else { - if (arguments.length == 3) { - clientId = path; - path = "/mqtt"; - } - if (typeof port !== "number" || port < 0) - throw new Error(format(ERROR.INVALID_TYPE, [typeof port, "port"])); - if (typeof path !== "string") - throw new Error(format(ERROR.INVALID_TYPE, [typeof path, "path"])); - - var ipv6AddSBracket = (host.indexOf(":") != -1 && host.slice(0,1) != "[" && host.slice(-1) != "]"); - uri = "ws://"+(ipv6AddSBracket?"["+host+"]":host)+":"+port+path; - } - - var clientIdLength = 0; - for (var i = 0; i 65535) - throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, "clientId"])); - - var client = new ClientImpl(uri, host, port, path, clientId); - this._getHost = function() { return host; }; - this._setHost = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getPort = function() { return port; }; - this._setPort = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getPath = function() { return path; }; - this._setPath = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getURI = function() { return uri; }; - this._setURI = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getClientId = function() { return client.clientId; }; - this._setClientId = function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); }; - - this._getOnConnectionLost = function() { return client.onConnectionLost; }; - this._setOnConnectionLost = function(newOnConnectionLost) { - if (typeof newOnConnectionLost === "function") - client.onConnectionLost = newOnConnectionLost; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnectionLost, "onConnectionLost"])); - }; - - this._getOnMessageDelivered = function() { return client.onMessageDelivered; }; - this._setOnMessageDelivered = function(newOnMessageDelivered) { - if (typeof newOnMessageDelivered === "function") - client.onMessageDelivered = newOnMessageDelivered; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageDelivered, "onMessageDelivered"])); - }; - - this._getOnMessageArrived = function() { return client.onMessageArrived; }; - this._setOnMessageArrived = function(newOnMessageArrived) { - if (typeof newOnMessageArrived === "function") - client.onMessageArrived = newOnMessageArrived; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageArrived, "onMessageArrived"])); - }; - - this._getTrace = function() { return client.traceFunction; }; - this._setTrace = function(trace) { - if(typeof trace === "function"){ - client.traceFunction = trace; - }else{ - throw new Error(format(ERROR.INVALID_TYPE, [typeof trace, "onTrace"])); - } - }; - - /** - * Connect this Messaging client to its server. - * - * @name Paho.MQTT.Client#connect - * @function - * @param {Object} connectOptions - attributes used with the connection. - * @param {number} connectOptions.timeout - If the connect has not succeeded within this - * number of seconds, it is deemed to have failed. - * The default is 30 seconds. - * @param {string} connectOptions.userName - Authentication username for this connection. - * @param {string} connectOptions.password - Authentication password for this connection. - * @param {Paho.MQTT.Message} connectOptions.willMessage - sent by the server when the client - * disconnects abnormally. - * @param {Number} connectOptions.keepAliveInterval - the server disconnects this client if - * there is no activity for this number of seconds. - * The default value of 60 seconds is assumed if not set. - * @param {boolean} connectOptions.cleanSession - if true(default) the client and server - * persistent state is deleted on successful connect. - * @param {boolean} connectOptions.useSSL - if present and true, use an SSL Websocket connection. - * @param {object} connectOptions.invocationContext - passed to the onSuccess callback or onFailure callback. - * @param {function} connectOptions.onSuccess - called when the connect acknowledgement - * has been received from the server. - * A single response object parameter is passed to the onSuccess callback containing the following fields: - *
    - *
  1. invocationContext as passed in to the onSuccess method in the connectOptions. - *
- * @config {function} [onFailure] called when the connect request has failed or timed out. - * A single response object parameter is passed to the onFailure callback containing the following fields: - *
    - *
  1. invocationContext as passed in to the onFailure method in the connectOptions. - *
  2. errorCode a number indicating the nature of the error. - *
  3. errorMessage text describing the error. - *
- * @config {Array} [hosts] If present this contains either a set of hostnames or fully qualified - * WebSocket URIs (ws://example.com:1883/mqtt), that are tried in order in place - * of the host and port paramater on the construtor. The hosts are tried one at at time in order until - * one of then succeeds. - * @config {Array} [ports] If present the set of ports matching the hosts. If hosts contains URIs, this property - * is not used. - * @throws {InvalidState} if the client is not in disconnected state. The client must have received connectionLost - * or disconnected before calling connect for a second or subsequent time. - */ - this.connect = function (connectOptions) { - connectOptions = connectOptions || {} ; - validate(connectOptions, {timeout:"number", - userName:"string", - password:"string", - willMessage:"object", - keepAliveInterval:"number", - cleanSession:"boolean", - useSSL:"boolean", - invocationContext:"object", - onSuccess:"function", - onFailure:"function", - hosts:"object", - ports:"object", - mqttVersion:"number"}); - - // If no keep alive interval is set, assume 60 seconds. - if (connectOptions.keepAliveInterval === undefined) - connectOptions.keepAliveInterval = 60; - - if (connectOptions.mqttVersion > 4 || connectOptions.mqttVersion < 3) { - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.mqttVersion, "connectOptions.mqttVersion"])); - } - - if (connectOptions.mqttVersion === undefined) { - connectOptions.mqttVersionExplicit = false; - connectOptions.mqttVersion = 4; - } else { - connectOptions.mqttVersionExplicit = true; - } - - //Check that if password is set, so is username - if (connectOptions.password === undefined && connectOptions.userName !== undefined) - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.password, "connectOptions.password"])) - - if (connectOptions.willMessage) { - if (!(connectOptions.willMessage instanceof Message)) - throw new Error(format(ERROR.INVALID_TYPE, [connectOptions.willMessage, "connectOptions.willMessage"])); - // The will message must have a payload that can be represented as a string. - // Cause the willMessage to throw an exception if this is not the case. - connectOptions.willMessage.stringPayload; - - if (typeof connectOptions.willMessage.destinationName === "undefined") - throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.willMessage.destinationName, "connectOptions.willMessage.destinationName"])); - } - if (typeof connectOptions.cleanSession === "undefined") - connectOptions.cleanSession = true; - if (connectOptions.hosts) { - - if (!(connectOptions.hosts instanceof Array) ) - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); - if (connectOptions.hosts.length <1 ) - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); - - var usingURIs = false; - for (var i = 0; i - * @param {object} subscribeOptions - used to control the subscription - * - * @param {number} subscribeOptions.qos - the maiximum qos of any publications sent - * as a result of making this subscription. - * @param {object} subscribeOptions.invocationContext - passed to the onSuccess callback - * or onFailure callback. - * @param {function} subscribeOptions.onSuccess - called when the subscribe acknowledgement - * has been received from the server. - * A single response object parameter is passed to the onSuccess callback containing the following fields: - *
    - *
  1. invocationContext if set in the subscribeOptions. - *
- * @param {function} subscribeOptions.onFailure - called when the subscribe request has failed or timed out. - * A single response object parameter is passed to the onFailure callback containing the following fields: - *
    - *
  1. invocationContext - if set in the subscribeOptions. - *
  2. errorCode - a number indicating the nature of the error. - *
  3. errorMessage - text describing the error. - *
- * @param {number} subscribeOptions.timeout - which, if present, determines the number of - * seconds after which the onFailure calback is called. - * The presence of a timeout does not prevent the onSuccess - * callback from being called when the subscribe completes. - * @throws {InvalidState} if the client is not in connected state. - */ - this.subscribe = function (filter, subscribeOptions) { - if (typeof filter !== "string") - throw new Error("Invalid argument:"+filter); - subscribeOptions = subscribeOptions || {} ; - validate(subscribeOptions, {qos:"number", - invocationContext:"object", - onSuccess:"function", - onFailure:"function", - timeout:"number" - }); - if (subscribeOptions.timeout && !subscribeOptions.onFailure) - throw new Error("subscribeOptions.timeout specified with no onFailure callback."); - if (typeof subscribeOptions.qos !== "undefined" - && !(subscribeOptions.qos === 0 || subscribeOptions.qos === 1 || subscribeOptions.qos === 2 )) - throw new Error(format(ERROR.INVALID_ARGUMENT, [subscribeOptions.qos, "subscribeOptions.qos"])); - client.subscribe(filter, subscribeOptions); - }; - - /** - * Unsubscribe for messages, stop receiving messages sent to destinations described by the filter. - * - * @name Paho.MQTT.Client#unsubscribe - * @function - * @param {string} filter - describing the destinations to receive messages from. - * @param {object} unsubscribeOptions - used to control the subscription - * @param {object} unsubscribeOptions.invocationContext - passed to the onSuccess callback - or onFailure callback. - * @param {function} unsubscribeOptions.onSuccess - called when the unsubscribe acknowledgement has been received from the server. - * A single response object parameter is passed to the - * onSuccess callback containing the following fields: - *
    - *
  1. invocationContext - if set in the unsubscribeOptions. - *
- * @param {function} unsubscribeOptions.onFailure called when the unsubscribe request has failed or timed out. - * A single response object parameter is passed to the onFailure callback containing the following fields: - *
    - *
  1. invocationContext - if set in the unsubscribeOptions. - *
  2. errorCode - a number indicating the nature of the error. - *
  3. errorMessage - text describing the error. - *
- * @param {number} unsubscribeOptions.timeout - which, if present, determines the number of seconds - * after which the onFailure callback is called. The presence of - * a timeout does not prevent the onSuccess callback from being - * called when the unsubscribe completes - * @throws {InvalidState} if the client is not in connected state. - */ - this.unsubscribe = function (filter, unsubscribeOptions) { - if (typeof filter !== "string") - throw new Error("Invalid argument:"+filter); - unsubscribeOptions = unsubscribeOptions || {} ; - validate(unsubscribeOptions, {invocationContext:"object", - onSuccess:"function", - onFailure:"function", - timeout:"number" - }); - if (unsubscribeOptions.timeout && !unsubscribeOptions.onFailure) - throw new Error("unsubscribeOptions.timeout specified with no onFailure callback."); - client.unsubscribe(filter, unsubscribeOptions); - }; - - /** - * Send a message to the consumers of the destination in the Message. - * - * @name Paho.MQTT.Client#send - * @function - * @param {string|Paho.MQTT.Message} topic - mandatory The name of the destination to which the message is to be sent. - * - If it is the only parameter, used as Paho.MQTT.Message object. - * @param {String|ArrayBuffer} payload - The message data to be sent. - * @param {number} qos The Quality of Service used to deliver the message. - *
- *
0 Best effort (default). - *
1 At least once. - *
2 Exactly once. - *
- * @param {Boolean} retained If true, the message is to be retained by the server and delivered - * to both current and future subscriptions. - * If false the server only delivers the message to current subscribers, this is the default for new Messages. - * A received message has the retained boolean set to true if the message was published - * with the retained boolean set to true - * and the subscrption was made after the message has been published. - * @throws {InvalidState} if the client is not connected. - */ - this.send = function (topic,payload,qos,retained) { - var message ; - - if(arguments.length == 0){ - throw new Error("Invalid argument."+"length"); - - }else if(arguments.length == 1) { - - if (!(topic instanceof Message) && (typeof topic !== "string")) - throw new Error("Invalid argument:"+ typeof topic); - - message = topic; - if (typeof message.destinationName === "undefined") - throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"])); - client.send(message); - - }else { - //parameter checking in Message object - message = new Message(payload); - message.destinationName = topic; - if(arguments.length >= 3) - message.qos = qos; - if(arguments.length >= 4) - message.retained = retained; - client.send(message); - } - }; - - /** - * Normal disconnect of this Messaging client from its server. - * - * @name Paho.MQTT.Client#disconnect - * @function - * @throws {InvalidState} if the client is already disconnected. - */ - this.disconnect = function () { - client.disconnect(); - }; - - /** - * Get the contents of the trace log. - * - * @name Paho.MQTT.Client#getTraceLog - * @function - * @return {Object[]} tracebuffer containing the time ordered trace records. - */ - this.getTraceLog = function () { - return client.getTraceLog(); - } - - /** - * Start tracing. - * - * @name Paho.MQTT.Client#startTrace - * @function - */ - this.startTrace = function () { - client.startTrace(); - }; - - /** - * Stop tracing. - * - * @name Paho.MQTT.Client#stopTrace - * @function - */ - this.stopTrace = function () { - client.stopTrace(); - }; - - this.isConnected = function() { - return client.connected; - }; - }; - - Client.prototype = { - get host() { return this._getHost(); }, - set host(newHost) { this._setHost(newHost); }, - - get port() { return this._getPort(); }, - set port(newPort) { this._setPort(newPort); }, - - get path() { return this._getPath(); }, - set path(newPath) { this._setPath(newPath); }, - - get clientId() { return this._getClientId(); }, - set clientId(newClientId) { this._setClientId(newClientId); }, - - get onConnectionLost() { return this._getOnConnectionLost(); }, - set onConnectionLost(newOnConnectionLost) { this._setOnConnectionLost(newOnConnectionLost); }, - - get onMessageDelivered() { return this._getOnMessageDelivered(); }, - set onMessageDelivered(newOnMessageDelivered) { this._setOnMessageDelivered(newOnMessageDelivered); }, - - get onMessageArrived() { return this._getOnMessageArrived(); }, - set onMessageArrived(newOnMessageArrived) { this._setOnMessageArrived(newOnMessageArrived); }, - - get trace() { return this._getTrace(); }, - set trace(newTraceFunction) { this._setTrace(newTraceFunction); } - - }; - - /** - * An application message, sent or received. - *

- * All attributes may be null, which implies the default values. - * - * @name Paho.MQTT.Message - * @constructor - * @param {String|ArrayBuffer} payload The message data to be sent. - *

- * @property {string} payloadString read only The payload as a string if the payload consists of valid UTF-8 characters. - * @property {ArrayBuffer} payloadBytes read only The payload as an ArrayBuffer. - *

- * @property {string} destinationName mandatory The name of the destination to which the message is to be sent - * (for messages about to be sent) or the name of the destination from which the message has been received. - * (for messages received by the onMessage function). - *

- * @property {number} qos The Quality of Service used to deliver the message. - *

- *
0 Best effort (default). - *
1 At least once. - *
2 Exactly once. - *
- *

- * @property {Boolean} retained If true, the message is to be retained by the server and delivered - * to both current and future subscriptions. - * If false the server only delivers the message to current subscribers, this is the default for new Messages. - * A received message has the retained boolean set to true if the message was published - * with the retained boolean set to true - * and the subscrption was made after the message has been published. - *

- * @property {Boolean} duplicate read only If true, this message might be a duplicate of one which has already been received. - * This is only set on messages received from the server. - * - */ - var Message = function (newPayload) { - var payload; - if ( typeof newPayload === "string" - || newPayload instanceof ArrayBuffer - || newPayload instanceof Int8Array - || newPayload instanceof Uint8Array - || newPayload instanceof Int16Array - || newPayload instanceof Uint16Array - || newPayload instanceof Int32Array - || newPayload instanceof Uint32Array - || newPayload instanceof Float32Array - || newPayload instanceof Float64Array - ) { - payload = newPayload; - } else { - throw (format(ERROR.INVALID_ARGUMENT, [newPayload, "newPayload"])); - } - - this._getPayloadString = function () { - if (typeof payload === "string") - return payload; - else - return parseUTF8(payload, 0, payload.length); - }; - - this._getPayloadBytes = function() { - if (typeof payload === "string") { - var buffer = new ArrayBuffer(UTF8Length(payload)); - var byteStream = new Uint8Array(buffer); - stringToUTF8(payload, byteStream, 0); - - return byteStream; - } else { - return payload; - }; - }; - - var destinationName = undefined; - this._getDestinationName = function() { return destinationName; }; - this._setDestinationName = function(newDestinationName) { - if (typeof newDestinationName === "string") - destinationName = newDestinationName; - else - throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, "newDestinationName"])); - }; - - var qos = 0; - this._getQos = function() { return qos; }; - this._setQos = function(newQos) { - if (newQos === 0 || newQos === 1 || newQos === 2 ) - qos = newQos; - else - throw new Error("Invalid argument:"+newQos); - }; - - var retained = false; - this._getRetained = function() { return retained; }; - this._setRetained = function(newRetained) { - if (typeof newRetained === "boolean") - retained = newRetained; - else - throw new Error(format(ERROR.INVALID_ARGUMENT, [newRetained, "newRetained"])); - }; - - var duplicate = false; - this._getDuplicate = function() { return duplicate; }; - this._setDuplicate = function(newDuplicate) { duplicate = newDuplicate; }; - }; - - Message.prototype = { - get payloadString() { return this._getPayloadString(); }, - get payloadBytes() { return this._getPayloadBytes(); }, - - get destinationName() { return this._getDestinationName(); }, - set destinationName(newDestinationName) { this._setDestinationName(newDestinationName); }, - - get qos() { return this._getQos(); }, - set qos(newQos) { this._setQos(newQos); }, - - get retained() { return this._getRetained(); }, - set retained(newRetained) { this._setRetained(newRetained); }, - - get duplicate() { return this._getDuplicate(); }, - set duplicate(newDuplicate) { this._setDuplicate(newDuplicate); } - }; - - // Module contents. - return { - Client: Client, - Message: Message - }; -})(window); diff --git a/apps/emqttd/priv/www/websocket.html b/apps/emqttd/priv/www/websocket.html deleted file mode 100644 index 7ac1fda17..000000000 --- a/apps/emqttd/priv/www/websocket.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - MQTT Over Mochiweb websocket - - -

MQTT Over Mochiweb websocket

- -
- -   State: -
-
Protip: open your javascript error console, just in case..
-
-
-
- - -
-
-
-
- - - - - diff --git a/apps/emqttd/src/emqttd_cluster.erl b/apps/emqttd/src/emqttd_cluster.erl deleted file mode 100644 index 242661119..000000000 --- a/apps/emqttd/src/emqttd_cluster.erl +++ /dev/null @@ -1,39 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% emqttd cluster to monitor clusted nodes. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_cluster). - --author("Feng Lee "). - --export([running_nodes/0]). - -%%------------------------------------------------------------------------------ -%% @doc Get running nodes -%% @end -%%------------------------------------------------------------------------------ -running_nodes() -> - mnesia:system_info(running_db_nodes). - diff --git a/apps/emqttd/src/emqttd_cm.erl b/apps/emqttd/src/emqttd_cm.erl deleted file mode 100644 index 7758f8ec0..000000000 --- a/apps/emqttd/src/emqttd_cm.erl +++ /dev/null @@ -1,158 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% MQTT Client Manager -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_cm). - --author("Feng Lee "). - --behaviour(gen_server). - --define(SERVER, ?MODULE). - -%% API Exports --export([start_link/2, pool/0, table/0]). - --export([lookup/1, register/1, unregister/1]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {id, tab, statsfun}). - --define(CM_POOL, cm_pool). - --define(CLIENT_TAB, mqtt_client). - -%%%============================================================================= -%%% API -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc Start client manager -%% @end -%%------------------------------------------------------------------------------ --spec start_link(Id, StatsFun) -> {ok, pid()} | ignore | {error, any()} when - Id :: pos_integer(), - StatsFun :: fun(). -start_link(Id, StatsFun) -> - gen_server:start_link(?MODULE, [Id, StatsFun], []). - -pool() -> ?CM_POOL. - -table() -> ?CLIENT_TAB. - -%%------------------------------------------------------------------------------ -%% @doc Lookup client pid with clientId -%% @end -%%------------------------------------------------------------------------------ --spec lookup(ClientId :: binary()) -> pid() | undefined. -lookup(ClientId) when is_binary(ClientId) -> - case ets:lookup(?CLIENT_TAB, ClientId) of - [{_, Pid, _}] -> Pid; - [] -> undefined - end. - -%%------------------------------------------------------------------------------ -%% @doc Register clientId with pid. -%% @end -%%------------------------------------------------------------------------------ --spec register(ClientId :: binary()) -> ok. -register(ClientId) when is_binary(ClientId) -> - CmPid = gproc_pool:pick_worker(?CM_POOL, ClientId), - gen_server:call(CmPid, {register, ClientId, self()}, infinity). - -%%------------------------------------------------------------------------------ -%% @doc Unregister clientId with pid. -%% @end -%%------------------------------------------------------------------------------ --spec unregister(ClientId :: binary()) -> ok. -unregister(ClientId) when is_binary(ClientId) -> - CmPid = gproc_pool:pick_worker(?CM_POOL, ClientId), - gen_server:cast(CmPid, {unregister, ClientId, self()}). - -%%%============================================================================= -%%% gen_server callbacks -%%%============================================================================= - -init([Id, StatsFun]) -> - gproc_pool:connect_worker(?CM_POOL, {?MODULE, Id}), - {ok, #state{id = Id, statsfun = StatsFun}}. - -handle_call({register, ClientId, Pid}, _From, State) -> - case ets:lookup(?CLIENT_TAB, ClientId) of - [{_, Pid, _}] -> - lager:error("clientId '~s' has been registered with ~p", [ClientId, Pid]), - ignore; - [{_, OldPid, MRef}] -> - lager:error("clientId '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, Pid, OldPid]), - OldPid ! {stop, duplicate_id, Pid}, - erlang:demonitor(MRef), - ets:insert(?CLIENT_TAB, {ClientId, Pid, erlang:monitor(process, Pid)}); - [] -> - ets:insert(?CLIENT_TAB, {ClientId, Pid, erlang:monitor(process, Pid)}) - end, - {reply, ok, setstats(State)}; - -handle_call(Req, _From, State) -> - lager:error("unexpected request: ~p", [Req]), - {reply, {error, badreq}, State}. - -handle_cast({unregister, ClientId, Pid}, State) -> - case ets:lookup(?CLIENT_TAB, ClientId) of - [{_, Pid, MRef}] -> - erlang:demonitor(MRef, [flush]), - ets:delete(?CLIENT_TAB, ClientId); - [_] -> - ignore; - [] -> - lager:error("cannot find clientId '~s' with ~p", [ClientId, Pid]) - end, - {noreply, setstats(State)}; - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> - ets:match_delete(?CLIENT_TAB, {'_', DownPid, MRef}), - {noreply, setstats(State)}; - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, #state{id = Id}) -> - gproc_pool:disconnect_worker(?CM_POOL, {?MODULE, Id}), ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -setstats(State = #state{statsfun = StatsFun}) -> - StatsFun(ets:info(?CLIENT_TAB, size)), State. - diff --git a/apps/emqttd/src/emqttd_cm_sup.erl b/apps/emqttd/src/emqttd_cm_sup.erl deleted file mode 100644 index 53a338404..000000000 --- a/apps/emqttd/src/emqttd_cm_sup.erl +++ /dev/null @@ -1,59 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% emqttd client manager supervisor. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_cm_sup). - --author("Feng Lee "). - --include("emqttd.hrl"). - --behaviour(supervisor). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -init([]) -> - ets:new(emqttd_cm:table(), [set, named_table, public, - {write_concurrency, true}]), - Schedulers = erlang:system_info(schedulers), - gproc_pool:new(emqttd_cm:pool(), hash, [{size, Schedulers}]), - StatsFun = emqttd_stats:statsfun('clients/count', 'clients/max'), - Children = lists:map( - fun(I) -> - Name = {emqttd_cm, I}, - gproc_pool:add_worker(emqttd_cm:pool(), Name, I), - {Name, {emqttd_cm, start_link, [I, StatsFun]}, - permanent, 10000, worker, [emqttd_cm]} - end, lists:seq(1, Schedulers)), - {ok, {{one_for_all, 10, 100}, Children}}. - - diff --git a/apps/emqttd/src/emqttd_event.erl b/apps/emqttd/src/emqttd_event.erl deleted file mode 100644 index e226de5de..000000000 --- a/apps/emqttd/src/emqttd_event.erl +++ /dev/null @@ -1,122 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% emqttd event manager. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_event). - --author("Feng Lee "). - --include_lib("emqtt/include/emqtt.hrl"). - -%% API Function Exports --export([start_link/0, add_handler/2, notify/1]). - -%% gen_event Function Exports --export([init/1, handle_event/2, handle_call/2, handle_info/2, - terminate/2, code_change/3]). - --record(state, {systop}). - -%%------------------------------------------------------------------------------ -%% @doc Start event manager -%% @end -%%------------------------------------------------------------------------------ --spec start_link() -> {ok, pid()} | {error, any()}. -start_link() -> - case gen_event:start_link({local, ?MODULE}) of - {ok, Pid} -> - add_handler(?MODULE, []), - {ok, Pid}; - {error, Reason} -> - {error, Reason} - end. - -add_handler(Handler, Args) -> - gen_event:add_handler(?MODULE, Handler, Args). - -notify(Event) -> - gen_event:notify(?MODULE, Event). - -%%%============================================================================= -%%% gen_event callbacks -%%%============================================================================= - -init([]) -> - SysTop = list_to_binary(lists:concat(["$SYS/brokers/", node(), "/"])), - {ok, #state{systop = SysTop}}. - -handle_event({connected, ClientId, Params}, State = #state{systop = SysTop}) -> - Topic = <>, - Msg = #mqtt_message{topic = Topic, payload = payload(connected, Params)}, - emqttd_pubsub:publish(event, Msg), - {ok, State}; - -%%TODO: Protect from undefined clientId... -handle_event({disconnected, undefined, Reason}, State = #state{systop = SysTop}) -> - {ok, State}; - -handle_event({disconnected, ClientId, Reason}, State = #state{systop = SysTop}) -> - Topic = <>, - Msg = #mqtt_message{topic = Topic, payload = payload(disconnected, Reason)}, - emqttd_pubsub:publish(event, Msg), - {ok, State}; - -handle_event({subscribed, ClientId, TopicTable}, State) -> - lager:error("TODO: subscribed ~s, ~p", [ClientId, TopicTable]), - {ok, State}; - -handle_event({unsubscribed, ClientId, Topics}, State) -> - lager:error("TODO: unsubscribed ~s, ~p", [ClientId, Topics]), - {ok, State}; - -handle_event(_Event, State) -> - {ok, State}. - -handle_call(_Request, State) -> - Reply = ok, - {ok, Reply, State}. - -handle_info(_Info, State) -> - {ok, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -payload(connected, Params) -> - From = proplists:get_value(from, Params), - Proto = proplists:get_value(protocol, Params), - Sess = proplists:get_value(session, Params), - iolist_to_binary(io_lib:format("from: ~s~nprotocol: ~p~nsession: ~s", [From, Proto, Sess])); - -payload(disconnected, Reason) -> - list_to_binary(io_lib:format("reason: ~p", [Reason])). - diff --git a/apps/emqttd/src/emqttd_queue.erl b/apps/emqttd/src/emqttd_queue.erl deleted file mode 100644 index 34b34a58f..000000000 --- a/apps/emqttd/src/emqttd_queue.erl +++ /dev/null @@ -1,98 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% emqttd simple queue. -%%% -%%% @end -%%%----------------------------------------------------------------------------- - -%% TODO: this module should be removed... - --module(emqttd_queue). - --author("Feng Lee "). - --include_lib("emqtt/include/emqtt.hrl"). - --export([new/1, new/2, in/3, all/1, clear/1]). - --define(DEFAULT_MAX_LEN, 1000). - --record(mqtt_queue_wrapper, {queue = queue:new(), - max_len = ?DEFAULT_MAX_LEN, - store_qos0 = false}). - --type mqtt_queue() :: #mqtt_queue_wrapper{}. - -%%------------------------------------------------------------------------------ -%% @doc -%% New Queue. -%% -%% @end -%%------------------------------------------------------------------------------ --spec new(non_neg_integer()) -> mqtt_queue(). -new(MaxLen) -> #mqtt_queue_wrapper{max_len = MaxLen}. - -new(MaxLen, StoreQos0) -> #mqtt_queue_wrapper{max_len = MaxLen, store_qos0 = StoreQos0}. - -%%------------------------------------------------------------------------------ -%% @doc -%% Queue one message. -%% -%% @end -%%------------------------------------------------------------------------------ --spec in(binary(), mqtt_message(), mqtt_queue()) -> mqtt_queue(). -in(ClientId, Message = #mqtt_message{qos = Qos}, - Wrapper = #mqtt_queue_wrapper{queue = Queue, max_len = MaxLen}) -> - case queue:len(Queue) < MaxLen of - true -> - Wrapper#mqtt_queue_wrapper{queue = queue:in(Message, Queue)}; - false -> % full - if - Qos =:= ?QOS_0 -> - lager:warning("Queue ~s drop qos0 message: ~p", [ClientId, Message]), - Wrapper; - true -> - {{value, Msg}, Queue1} = queue:drop(Queue), - lager:warning("Queue ~s drop message: ~p", [ClientId, Msg]), - Wrapper#mqtt_queue_wrapper{queue = Queue1} - end - end. - -%%------------------------------------------------------------------------------ -%% @doc -%% Get all messages in queue. -%% -%% @end -%%------------------------------------------------------------------------------ --spec all(mqtt_queue()) -> list(). -all(#mqtt_queue_wrapper { queue = Queue }) -> queue:to_list(Queue). - -%%------------------------------------------------------------------------------ -%% @doc -%% Clear queue. -%% -%% @end -%%------------------------------------------------------------------------------ --spec clear(mqtt_queue()) -> mqtt_queue(). -clear(Queue) -> Queue#mqtt_queue_wrapper{queue = queue:new()}. - diff --git a/apps/emqttd/src/emqttd_session.erl b/apps/emqttd/src/emqttd_session.erl deleted file mode 100644 index 454363f11..000000000 --- a/apps/emqttd/src/emqttd_session.erl +++ /dev/null @@ -1,416 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% emqttd session. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_session). - --author("Feng Lee "). - --include("emqttd.hrl"). - --include_lib("emqtt/include/emqtt.hrl"). - --include_lib("emqtt/include/emqtt_packet.hrl"). - -%% API Function Exports --export([start/1, - resume/3, - publish/3, - puback/2, - subscribe/2, - unsubscribe/2, - destroy/2]). - --export([store/2]). - -%% Start gen_server --export([start_link/2]). - -%% gen_server Function Exports --export([init/1, handle_call/3, handle_cast/2, handle_info/2, - terminate/2, code_change/3]). - --record(session_state, { - clientid :: binary(), - client_pid :: pid(), - message_id = 1, - submap :: map(), - msg_queue, %% do not receive rel - awaiting_ack :: map(), - awaiting_rel :: map(), - awaiting_comp :: map(), - expires, - expire_timer}). - --type session() :: #session_state{} | pid(). - -%%%============================================================================= -%%% Session API -%%%============================================================================= - -%%------------------------------------------------------------------------------ -%% @doc Start Session -%% @end -%%------------------------------------------------------------------------------ --spec start({boolean(), binary(), pid()}) -> {ok, session()}. -start({true = _CleanSess, ClientId, _ClientPid}) -> - %%Destroy old session if CleanSess is true before. - ok = emqttd_sm:destroy_session(ClientId), - {ok, initial_state(ClientId)}; - -start({false = _CleanSess, ClientId, ClientPid}) -> - {ok, SessPid} = emqttd_sm:start_session(ClientId, ClientPid), - {ok, SessPid}. - -%%------------------------------------------------------------------------------ -%% @doc Resume Session -%% @end -%%------------------------------------------------------------------------------ --spec resume(session(), binary(), pid()) -> session(). -resume(SessState = #session_state{}, _ClientId, _ClientPid) -> - SessState; -resume(SessPid, ClientId, ClientPid) when is_pid(SessPid) -> - gen_server:cast(SessPid, {resume, ClientId, ClientPid}), - SessPid. - -%%------------------------------------------------------------------------------ -%% @doc Publish message -%% @end -%%------------------------------------------------------------------------------ --spec publish(session(), mqtt_clientid(), {mqtt_qos(), mqtt_message()}) -> session(). -publish(Session, ClientId, {?QOS_0, Message}) -> - emqttd_pubsub:publish(ClientId, Message), Session; - -publish(Session, ClientId, {?QOS_1, Message}) -> - emqttd_pubsub:publish(ClientId, Message), Session; - -publish(SessState = #session_state{awaiting_rel = AwaitingRel}, _ClientId, - {?QOS_2, Message = #mqtt_message{msgid = MsgId}}) -> - %% store in awaiting_rel - SessState#session_state{awaiting_rel = maps:put(MsgId, Message, AwaitingRel)}; - -publish(SessPid, ClientId, {?QOS_2, Message}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {publish, ClientId, {?QOS_2, Message}}), - SessPid. - -%%------------------------------------------------------------------------------ -%% @doc PubAck message -%% @end -%%------------------------------------------------------------------------------ --spec puback(session(), {mqtt_packet_type(), mqtt_packet_id()}) -> session(). -puback(SessState = #session_state{clientid = ClientId, awaiting_ack = Awaiting}, {?PUBACK, PacketId}) -> - case maps:is_key(PacketId, Awaiting) of - true -> ok; - false -> lager:warning("Session ~s: PUBACK PacketId '~p' not found!", [ClientId, PacketId]) - end, - SessState#session_state{awaiting_ack = maps:remove(PacketId, Awaiting)}; -puback(SessPid, {?PUBACK, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {puback, PacketId}), SessPid; - -%% PUBREC -puback(SessState = #session_state{clientid = ClientId, - awaiting_ack = AwaitingAck, - awaiting_comp = AwaitingComp}, {?PUBREC, PacketId}) -> - case maps:is_key(PacketId, AwaitingAck) of - true -> ok; - false -> lager:warning("Session ~s: PUBREC PacketId '~p' not found!", [ClientId, PacketId]) - end, - SessState#session_state{awaiting_ack = maps:remove(PacketId, AwaitingAck), - awaiting_comp = maps:put(PacketId, true, AwaitingComp)}; - -puback(SessPid, {?PUBREC, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {pubrec, PacketId}), SessPid; - -%% PUBREL -puback(SessState = #session_state{clientid = ClientId, - awaiting_rel = Awaiting}, {?PUBREL, PacketId}) -> - case maps:find(PacketId, Awaiting) of - {ok, Msg} -> emqttd_pubsub:publish(ClientId, Msg); - error -> lager:warning("Session ~s: PUBREL PacketId '~p' not found!", [ClientId, PacketId]) - end, - SessState#session_state{awaiting_rel = maps:remove(PacketId, Awaiting)}; - -puback(SessPid, {?PUBREL, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {pubrel, PacketId}), SessPid; - -%% PUBCOMP -puback(SessState = #session_state{clientid = ClientId, - awaiting_comp = AwaitingComp}, {?PUBCOMP, PacketId}) -> - case maps:is_key(PacketId, AwaitingComp) of - true -> ok; - false -> lager:warning("Session ~s: PUBREC PacketId '~p' not exist", [ClientId, PacketId]) - end, - SessState#session_state{awaiting_comp = maps:remove(PacketId, AwaitingComp)}; - -puback(SessPid, {?PUBCOMP, PacketId}) when is_pid(SessPid) -> - gen_server:cast(SessPid, {pubcomp, PacketId}), SessPid. - -%%------------------------------------------------------------------------------ -%% @doc Subscribe Topics -%% @end -%%------------------------------------------------------------------------------ --spec subscribe(session(), [{binary(), mqtt_qos()}]) -> {ok, session(), [mqtt_qos()]}. -subscribe(SessState = #session_state{clientid = ClientId, submap = SubMap}, Topics) -> - Resubs = [Topic || {Name, _Qos} = Topic <- Topics, maps:is_key(Name, SubMap)], - case Resubs of - [] -> ok; - _ -> lager:warning("~s resubscribe ~p", [ClientId, Resubs]) - end, - SubMap1 = lists:foldl(fun({Name, Qos}, Acc) -> maps:put(Name, Qos, Acc) end, SubMap, Topics), - {ok, GrantedQos} = emqttd_pubsub:subscribe(Topics), - lager:info([{client, ClientId}], "Client ~s subscribe ~p. Granted QoS: ~p", - [ClientId, Topics, GrantedQos]), - %%TODO: should be gen_event and notification... - [emqttd_msg_store:redeliver(Name, self()) || {Name, _} <- Topics], - {ok, SessState#session_state{submap = SubMap1}, GrantedQos}; - -subscribe(SessPid, Topics) when is_pid(SessPid) -> - {ok, GrantedQos} = gen_server:call(SessPid, {subscribe, Topics}), - {ok, SessPid, GrantedQos}. - -%%------------------------------------------------------------------------------ -%% @doc Unsubscribe Topics -%% @end -%%------------------------------------------------------------------------------ --spec unsubscribe(session(), [binary()]) -> {ok, session()}. -unsubscribe(SessState = #session_state{clientid = ClientId, submap = SubMap}, Topics) -> - %%TODO: refactor later. - case Topics -- maps:keys(SubMap) of - [] -> ok; - BadUnsubs -> lager:warning("~s should not unsubscribe ~p", [ClientId, BadUnsubs]) - end, - %%unsubscribe from topic tree - ok = emqttd_pubsub:unsubscribe(Topics), - lager:info([{client, ClientId}], "Client ~s unsubscribe ~p.", [ClientId, Topics]), - SubMap1 = lists:foldl(fun(Topic, Acc) -> maps:remove(Topic, Acc) end, SubMap, Topics), - {ok, SessState#session_state{submap = SubMap1}}; - -unsubscribe(SessPid, Topics) when is_pid(SessPid) -> - gen_server:call(SessPid, {unsubscribe, Topics}), - {ok, SessPid}. - -%%------------------------------------------------------------------------------ -%% @doc Destroy Session -%% @end -%%------------------------------------------------------------------------------ --spec destroy(SessPid :: pid(), ClientId :: binary()) -> ok. -destroy(SessPid, ClientId) when is_pid(SessPid) -> - gen_server:cast(SessPid, {destroy, ClientId}). - -%store message(qos1) that sent to client -store(SessState = #session_state{message_id = MsgId, awaiting_ack = Awaiting}, - Message = #mqtt_message{qos = Qos}) when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - %%assign msgid before send - Message1 = Message#mqtt_message{msgid = MsgId}, - Message2 = - if - Qos =:= ?QOS_2 -> Message1#mqtt_message{dup = false}; - true -> Message1 - end, - Awaiting1 = maps:put(MsgId, Message2, Awaiting), - {Message1, next_msg_id(SessState#session_state{awaiting_ack = Awaiting1})}. - -initial_state(ClientId) -> - #session_state{clientid = ClientId, - submap = #{}, - awaiting_ack = #{}, - awaiting_rel = #{}, - awaiting_comp = #{}}. - -initial_state(ClientId, ClientPid) -> - State = initial_state(ClientId), - State#session_state{client_pid = ClientPid}. - -%%------------------------------------------------------------------------------ -%% @doc Start a session process. -%% @end -%%------------------------------------------------------------------------------ -start_link(ClientId, ClientPid) -> - gen_server:start_link(?MODULE, [ClientId, ClientPid], []). - -%%%============================================================================= -%%% gen_server callbacks -%%%============================================================================= - -init([ClientId, ClientPid]) -> - process_flag(trap_exit, true), - %%TODO: Is this OK? or should monitor... - true = link(ClientPid), - SessOpts = emqttd:env(mqtt, session), - State = initial_state(ClientId, ClientPid), - Expires = proplists:get_value(expires, SessOpts, 1) * 3600, - MsgQueue = emqttd_queue:new(proplists:get_value(max_queue, SessOpts, 1000), - proplists:get_value(store_qos0, SessOpts, false)), - {ok, State#session_state{expires = Expires, - msg_queue = MsgQueue}, hibernate}. - -handle_call({subscribe, Topics}, _From, State) -> - {ok, NewState, GrantedQos} = subscribe(State, Topics), - {reply, {ok, GrantedQos}, NewState}; - -handle_call({unsubscribe, Topics}, _From, State) -> - {ok, NewState} = unsubscribe(State, Topics), - {reply, ok, NewState}; - -handle_call(Req, _From, State) -> - lager:error("Unexpected request: ~p", [Req]), - {reply, error, State}. - -handle_cast({resume, ClientId, ClientPid}, State = #session_state{ - clientid = ClientId, - client_pid = OldClientPid, - msg_queue = Queue, - awaiting_ack = AwaitingAck, - awaiting_comp = AwaitingComp, - expire_timer = ETimer}) -> - - lager:info([{client, ClientId}], "Session ~s resumed by ~p",[ClientId, ClientPid]), - - %% kick old client... - if - OldClientPid =:= undefined -> - ok; - OldClientPid =:= ClientPid -> - ok; - true -> - lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]), - unlink(OldClientPid), - OldClientPid ! {stop, duplicate_id, ClientPid} - end, - - %% cancel timeout timer - emqttd_util:cancel_timer(ETimer), - - %% redelivery PUBREL - lists:foreach(fun(PacketId) -> - ClientPid ! {redeliver, {?PUBREL, PacketId}} - end, maps:keys(AwaitingComp)), - - %% redelivery messages that awaiting PUBACK or PUBREC - Dup = fun(Msg) -> Msg#mqtt_message{dup = true} end, - lists:foreach(fun(Msg) -> - ClientPid ! {dispatch, {self(), Dup(Msg)}} - end, maps:values(AwaitingAck)), - - %% send offline messages - lists:foreach(fun(Msg) -> - ClientPid ! {dispatch, {self(), Msg}} - end, emqttd_queue:all(Queue)), - - {noreply, State#session_state{client_pid = ClientPid, - msg_queue = emqttd_queue:clear(Queue), - expire_timer = undefined}, hibernate}; - -handle_cast({publish, ClientId, {?QOS_2, Message}}, State) -> - NewState = publish(State, ClientId, {?QOS_2, Message}), - {noreply, NewState}; - -handle_cast({puback, PacketId}, State) -> - NewState = puback(State, {?PUBACK, PacketId}), - {noreply, NewState}; - -handle_cast({pubrec, PacketId}, State) -> - NewState = puback(State, {?PUBREC, PacketId}), - {noreply, NewState}; - -handle_cast({pubrel, PacketId}, State) -> - NewState = puback(State, {?PUBREL, PacketId}), - {noreply, NewState}; - -handle_cast({pubcomp, PacketId}, State) -> - NewState = puback(State, {?PUBCOMP, PacketId}), - {noreply, NewState}; - -handle_cast({destroy, ClientId}, State = #session_state{clientid = ClientId}) -> - lager:warning("Session ~s destroyed", [ClientId]), - {stop, normal, State}; - -handle_cast(Msg, State) -> - lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), - {noreply, State}. - -handle_info({dispatch, {_From, Messages}}, State) when is_list(Messages) -> - F = fun(Message, S) -> dispatch(Message, S) end, - {noreply, lists:foldl(F, State, Messages)}; - -handle_info({dispatch, {_From, Message}}, State) -> - {noreply, dispatch(Message, State)}; - -handle_info({'EXIT', ClientPid, Reason}, State = #session_state{clientid = ClientId, - client_pid = ClientPid}) -> - lager:error("Session: client ~s@~p exited, caused by ~p", [ClientId, ClientPid, Reason]), - {noreply, start_expire_timer(State#session_state{client_pid = undefined})}; - -handle_info({'EXIT', ClientPid0, _Reason}, State = #session_state{client_pid = ClientPid}) -> - lager:error("Unexpected Client EXIT: pid=~p, pid(state): ~p", [ClientPid0, ClientPid]), - {noreply, State}; - -handle_info(session_expired, State = #session_state{clientid = ClientId}) -> - lager:warning("Session ~s expired!", [ClientId]), - {stop, {shutdown, expired}, State}; - -handle_info(Info, State) -> - lager:critical("Unexpected Info: ~p, State: ~p", [Info, State]), - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -%%%============================================================================= -%%% Internal functions -%%%============================================================================= - -dispatch(Message, State = #session_state{clientid = ClientId, - client_pid = undefined}) -> - queue(ClientId, Message, State); - -dispatch(Message = #mqtt_message{qos = ?QOS_0}, State = #session_state{client_pid = ClientPid}) -> - ClientPid ! {dispatch, {self(), Message}}, - State; - -dispatch(Message = #mqtt_message{qos = Qos}, State = #session_state{client_pid = ClientPid}) - when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - {Message1, NewState} = store(State, Message), - ClientPid ! {dispatch, {self(), Message1}}, - NewState. - -queue(ClientId, Message, State = #session_state{msg_queue = Queue}) -> - State#session_state{msg_queue = emqttd_queue:in(ClientId, Message, Queue)}. - -next_msg_id(State = #session_state{message_id = 16#ffff}) -> - State#session_state{message_id = 1}; - -next_msg_id(State = #session_state{message_id = MsgId}) -> - State#session_state{message_id = MsgId + 1}. - -start_expire_timer(State = #session_state{expires = Expires, - expire_timer = OldTimer}) -> - emqttd_util:cancel_timer(OldTimer), - Timer = erlang:send_after(Expires * 1000, self(), session_expired), - State#session_state{expire_timer = Timer}. - diff --git a/apps/emqttd/test/emqttd_acl_tests.erl b/apps/emqttd/test/emqttd_acl_tests.erl deleted file mode 100644 index ba21ecda4..000000000 --- a/apps/emqttd/test/emqttd_acl_tests.erl +++ /dev/null @@ -1,88 +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 -%%% emqttd_acl tests. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_acl_tests). - --include("emqttd.hrl"). - --ifdef(TEST). - --include_lib("eunit/include/eunit.hrl"). - -all_modules_test() -> - with_acl( - fun() -> - ?assertMatch([{emqttd_acl_internal, _State}], emqttd_acl:all_modules()) - end). - -reload_test() -> - with_acl( - fun() -> - ?assertEqual([ok], emqttd_acl:reload()) - end). - -register_mod_test() -> - with_acl( - fun() -> - emqttd_acl:register_mod(emqttd_acl_test_mod, []), - ?assertMatch([{emqttd_acl_test_mod, _}, {emqttd_acl_internal, _}], - emqttd_acl:all_modules()) - end). - -unregister_mod_test() -> - with_acl( - fun() -> - emqttd_acl:register_mod(emqttd_acl_test_mod, []), - ?assertMatch([{emqttd_acl_test_mod, _}, {emqttd_acl_internal, _}], - emqttd_acl:all_modules()), - emqttd_acl:unregister_mod(emqttd_acl_test_mod), - timer:sleep(5), - ?assertMatch([{emqttd_acl_internal, _}], emqttd_acl:all_modules()) - end). - -check_test() -> - with_acl( - fun() -> - User1 = #mqtt_user{clientid = <<"client1">>, username = <<"testuser">>}, - User2 = #mqtt_user{clientid = <<"client2">>, username = <<"xyz">>}, - ?assertEqual(allow, emqttd_acl:check({User1, subscribe, <<"users/testuser/1">>})), - ?assertEqual(allow, emqttd_acl:check({User1, subscribe, <<"clients/client1">>})), - ?assertEqual(deny, emqttd_acl:check({User1, subscribe, <<"clients/client1/x/y">>})), - ?assertEqual(allow, emqttd_acl:check({User1, publish, <<"users/testuser/1">>})), - ?assertEqual(allow, emqttd_acl:check({User1, subscribe, <<"a/b/c">>})), - ?assertEqual(deny, emqttd_acl:check({User2, subscribe, <<"a/b/c">>})) - end). - -with_acl(Fun) -> - process_flag(trap_exit, true), - AclOpts = [{internal, [{file, "../test/test_acl.config"}, - {nomatch, allow}]}], - {ok, _AclSrv} = emqttd_acl:start_link(AclOpts), - Fun(), - emqttd_acl:stop(). - --endif. - diff --git a/doc/.retain.md.swp b/doc/.retain.md.swp deleted file mode 100644 index 648c9731d..000000000 Binary files a/doc/.retain.md.swp and /dev/null differ diff --git a/doc/design/Seq.graphml b/doc/design/Seq.graphml new file mode 100644 index 000000000..8ddc9353a --- /dev/null +++ b/doc/design/Seq.graphml @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + T + + + + + + + + + + + + + + + + + + C1 + + + + + + + + + + + + + + + + + + S1 + + + + + + + + + + + + + + + + + + C2 + + + + + + + + + + + + + + + + + + S2 + + + + + + + + + + + + + + + + + + Queue + + + + + + + + + + + + + + + + + + Queue + + + + + + + + + + + + + + + + + + Dispatch + + + + + + + + + + + + + Deliver + + + + + + + + + + + + + Publish QoS1/2 + + + + + + + + + + + + + + + + + + + + + + + + + + Publish Qos0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/design/Seq.md b/doc/design/Seq.md new file mode 100644 index 000000000..924de84d3 --- /dev/null +++ b/doc/design/Seq.md @@ -0,0 +1,14 @@ +## QoS0 Publish Sequence + + +``` +title QoS0 Publish Sequence + +C1->PubSub: Publish QoS0 +PubSub-->S2: Dispatch QoS0 +S2-->C2: Deliver QoS0 +``` + +## QoS1 Publish Sequence + + diff --git a/doc/design/Seq.png b/doc/design/Seq.png new file mode 100644 index 000000000..b77f682a1 Binary files /dev/null and b/doc/design/Seq.png differ diff --git a/doc/design/qos0_seq.png b/doc/design/qos0_seq.png new file mode 100644 index 000000000..a1ca32c0d Binary files /dev/null and b/doc/design/qos0_seq.png differ diff --git a/go b/go deleted file mode 100755 index ac0709fd1..000000000 --- a/go +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -# -*- tab-width:4;indent-tabs-mode:nil -*- -# ex: ts=4 sw=4 et - -make && make dist && cd rel/emqttd && ./bin/emqttd console diff --git a/plugins/emqttd_plugin_mysql/include/emqttd.hrl b/include/emqttd.hrl similarity index 69% rename from plugins/emqttd_plugin_mysql/include/emqttd.hrl rename to include/emqttd.hrl index 9c2ab934a..bf76cb389 100644 --- a/plugins/emqttd_plugin_mysql/include/emqttd.hrl +++ b/include/emqttd.hrl @@ -36,6 +36,12 @@ -define(ERTS_MINIMUM, "6.0"). +%% System Topics. +-define(SYSTOP, <<"$SYS">>). + +%% Queue Topics. +-define(QTop, <<"$Q">>). + %%------------------------------------------------------------------------------ %% PubSub %%------------------------------------------------------------------------------ @@ -56,8 +62,8 @@ %%------------------------------------------------------------------------------ -record(mqtt_subscriber, { topic :: binary(), - qos = 0 :: 0 | 1 | 2, - pid :: pid() + subpid :: pid(), + qos = 0 :: 0 | 1 | 2 }). -type mqtt_subscriber() :: #mqtt_subscriber{}. @@ -77,9 +83,13 @@ %% MQTT Client %%------------------------------------------------------------------------------ -record(mqtt_client, { - clientid :: binary(), + clientid :: binary() | undefined, username :: binary() | undefined, - ipaddr :: inet:ip_address() + ipaddress :: inet:ip_address(), + client_pid :: pid(), + client_mon :: reference(), + clean_sess :: boolean(), + proto_ver :: 3 | 4 }). -type mqtt_client() :: #mqtt_client{}. @@ -90,13 +100,30 @@ -record(mqtt_session, { clientid, session_pid, - subscriptions = [], - awaiting_ack, - awaiting_rel + subscriptions = [] }). -type mqtt_session() :: #mqtt_session{}. +%%------------------------------------------------------------------------------ +%% MQTT Message +%%------------------------------------------------------------------------------ +-type mqtt_msgid() :: undefined | 1..16#ffff. + +-record(mqtt_message, { + topic :: binary(), %% Topic that the message is published to + from :: binary() | atom(), %% ClientId of publisher + qos = 0 :: 0 | 1 | 2, %% Message QoS + retain = false :: boolean(), %% Retain flag + dup = false :: boolean(), %% Dup flag + sys = false :: boolean(), %% $SYS flag + msgid :: mqtt_msgid(), %% Message ID + payload :: binary(), %% Payload + timestamp :: erlang:timestamp() %% Timestamp +}). + +-type mqtt_message() :: #mqtt_message{}. + %%------------------------------------------------------------------------------ %% MQTT Plugin %%------------------------------------------------------------------------------ @@ -109,4 +136,16 @@ -type mqtt_plugin() :: #mqtt_plugin{}. +%%------------------------------------------------------------------------------ +%% MQTT Alarm +%%------------------------------------------------------------------------------ +-record(mqtt_alarm, { + id :: binary(), + severity :: warning | error | critical, + title :: binary(), + summary :: binary(), + timestamp :: erlang:timestamp() %% Timestamp +}). + +-type mqtt_alarm() :: #mqtt_alarm{}. diff --git a/apps/emqtt/include/emqtt_packet.hrl b/include/emqttd_protocol.hrl similarity index 88% rename from apps/emqtt/include/emqtt_packet.hrl rename to include/emqttd_protocol.hrl index 105a939cf..35637e3d5 100644 --- a/apps/emqtt/include/emqtt_packet.hrl +++ b/include/emqttd_protocol.hrl @@ -20,11 +20,35 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% MQTT Packet Header. +%%% MQTT Protocol Header. %%% %%% @end %%%----------------------------------------------------------------------------- +%%------------------------------------------------------------------------------ +%% 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. + +%%------------------------------------------------------------------------------ +%% MQTT QoS +%%------------------------------------------------------------------------------ + +-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)). + +-type mqtt_qos() :: ?QOS_0 | ?QOS_1 | ?QOS_2. + %%------------------------------------------------------------------------------ %% Max ClientId Length. Why 1024? NiDongDe! %%------------------------------------------------------------------------------ @@ -162,7 +186,7 @@ #mqtt_packet{header = #mqtt_packet_header{type = ?CONNECT}, variable = Var}). -define(CONNACK_PACKET(ReturnCode), - #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, + #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, variable = #mqtt_packet_connack{return_code = ReturnCode}}). -define(PUBLISH_PACKET(Qos, Topic, PacketId, Payload), @@ -172,6 +196,11 @@ packet_id = PacketId}, payload = Payload}). +-define(PUBLISH(Qos, PacketId), + #mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH, + qos = Qos}, + variable = #mqtt_packet_publish{packet_id = PacketId}}). + -define(PUBACK_PACKET(Type, PacketId), #mqtt_packet{header = #mqtt_packet_header{type = Type}, variable = #mqtt_packet_puback{packet_id = PacketId}}). @@ -199,4 +228,3 @@ -define(PACKET(Type), #mqtt_packet{header = #mqtt_packet_header{type = Type}}). - diff --git a/plugins/emqttd_amqp/src/emqttd_amqp.app.src b/plugins/emqttd_amqp/src/emqttd_amqp.app.src deleted file mode 100644 index 1664bee18..000000000 --- a/plugins/emqttd_amqp/src/emqttd_amqp.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_amqp, - [ - {description, ""}, - {vsn, "1"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, { emqttd_amqp_app, []}}, - {env, []} - ]}. diff --git a/plugins/emqttd_amqp/src/emqttd_amqp_app.erl b/plugins/emqttd_amqp/src/emqttd_amqp_app.erl deleted file mode 100644 index 0087e7a7d..000000000 --- a/plugins/emqttd_amqp/src/emqttd_amqp_app.erl +++ /dev/null @@ -1,16 +0,0 @@ --module(emqttd_amqp_app). - --behaviour(application). - -%% Application callbacks --export([start/2, stop/1]). - -%% =================================================================== -%% Application callbacks -%% =================================================================== - -start(_StartType, _StartArgs) -> - emqttd_amqp_sup:start_link(). - -stop(_State) -> - ok. diff --git a/plugins/emqttd_amqp/src/emqttd_amqp_sup.erl b/plugins/emqttd_amqp/src/emqttd_amqp_sup.erl deleted file mode 100644 index 79ef00dff..000000000 --- a/plugins/emqttd_amqp/src/emqttd_amqp_sup.erl +++ /dev/null @@ -1,27 +0,0 @@ --module(emqttd_amqp_sup). - --behaviour(supervisor). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -%% Helper macro for declaring children of supervisor --define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). - -%% =================================================================== -%% API functions -%% =================================================================== - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% =================================================================== -%% Supervisor callbacks -%% =================================================================== - -init([]) -> - {ok, { {one_for_one, 5, 10}, []} }. - diff --git a/plugins/emqttd_auth_ldap/.placeholder b/plugins/emqttd_auth_ldap/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/emqttd_auth_ldap/README.md b/plugins/emqttd_auth_ldap/README.md deleted file mode 100644 index 083b318ac..000000000 --- a/plugins/emqttd_auth_ldap/README.md +++ /dev/null @@ -1,23 +0,0 @@ -## Overview - -Authentication with LDAP. - -## Plugin Config - -``` - {emqttd_auth_ldap, [ - {servers, ["localhost"]}, - {port, 389}, - {timeout, 30}, - {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, - {ssl, fasle}, - {sslopts, [ - {"certfile", "ssl.crt"}, - {"keyfile", "ssl.key"}]} - ]} -``` - -## Load Plugin - -Merge the'etc/plugin.config' to emqttd/etc/plugins.config, and the plugin will be loaded automatically. - diff --git a/plugins/emqttd_auth_ldap/etc/plugin.config b/plugins/emqttd_auth_ldap/etc/plugin.config deleted file mode 100644 index ac582d7d4..000000000 --- a/plugins/emqttd_auth_ldap/etc/plugin.config +++ /dev/null @@ -1,12 +0,0 @@ -[ - {emqttd_auth_ldap, [ - {servers, ["localhost"]}, - {port, 389}, - {timeout, 30}, - {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, - {ssl, fasle}, - {sslopts, [ - {"certfile", "ssl.crt"}, - {"keyfile", "ssl.key"}]} - ]} -]. diff --git a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.app.src b/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.app.src deleted file mode 100644 index e699fdba3..000000000 --- a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_auth_ldap, - [ - {description, "emqttd LDAP Authentication Plugin"}, - {vsn, "1.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, { emqttd_auth_ldap_app, []}}, - {env, []} - ]}. diff --git a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap_app.erl b/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap_app.erl deleted file mode 100644 index 1cea23075..000000000 --- a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap_app.erl +++ /dev/null @@ -1,58 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% LDAP Authentication Plugin. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_auth_ldap_app). - --behaviour(application). -%% Application callbacks --export([start/2, prep_stop/1, stop/1]). - --behaviour(supervisor). -%% Supervisor callbacks --export([init/1]). - -%%%============================================================================= -%%% Application callbacks -%%%============================================================================= - -start(_StartType, _StartArgs) -> - Env = application:get_all_env(emqttd_auth_ldap), - emqttd_access_control:register_mod(auth, emqttd_auth_ldap, Env), - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -prep_stop(State) -> - emqttd_access_control:unregister_mod(auth, emqttd_auth_ldap), State. - -stop(_State) -> - ok. - -%%%============================================================================= -%%% Supervisor callbacks(Dummy) -%%%============================================================================= - -init([]) -> - {ok, { {one_for_one, 5, 10}, []} }. - diff --git a/plugins/emqttd_auth_mysql/.placehodler b/plugins/emqttd_auth_mysql/.placehodler deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/emqttd_auth_mysql/README.md b/plugins/emqttd_auth_mysql/README.md deleted file mode 100644 index 02db17e10..000000000 --- a/plugins/emqttd_auth_mysql/README.md +++ /dev/null @@ -1,48 +0,0 @@ - -## Overview - -Authentication with user table of MySQL database. - -## etc/plugin.config - -``` - {emysql, [ - {pool, 4}, - {host, "localhost"}, - {port, 3306}, - {username, ""}, - {password, ""}, - {database, "mqtt"}, - {encoding, utf8} - ]}, - {emqttd_auth_mysql, [ - {user_table, mqtt_users}, - %% plain password only - {password_hash, plain}, - {field_mapper, [ - {username, username}, - {password, password} - ]} - ]} -``` - -## Users Table(Demo) - -Notice: This is a demo table. You could authenticate with any user tables. - -``` -CREATE TABLE `mqtt_users` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `username` varchar(60) DEFAULT NULL, - `password` varchar(60) DEFAULT NULL, - `salt` varchar(20) DEFAULT NULL, - `created` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `mqtt_users_username` (`username`) -) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -``` - -## Load Plugin - -Merge the'etc/plugin.config' to emqttd/etc/plugins.config, and the plugin will be loaded by the broker. - diff --git a/plugins/emqttd_auth_mysql/etc/plugin.config b/plugins/emqttd_auth_mysql/etc/plugin.config deleted file mode 100644 index a5ef4bc41..000000000 --- a/plugins/emqttd_auth_mysql/etc/plugin.config +++ /dev/null @@ -1,16 +0,0 @@ -{emysql, [ - {pool, 4}, - {host, "localhost"}, - {port, 3306}, - {username, "root"}, - {password, "public"}, - {database, "mqtt"}, - {encoding, utf8} -]}, -{emqttd_auth_mysql, [ - {users_table, mqtt_users}, - {field_mapper, [ - {username, username}, - {password, password, plain} - ]} -]} diff --git a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.app.src b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.app.src deleted file mode 100644 index 965e1825e..000000000 --- a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_auth_mysql, - [ - {description, "emqttd MySQL Authentication Plugin"}, - {vsn, "1.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, {emqttd_auth_mysql_app, []}}, - {env, []} - ]}. diff --git a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl deleted file mode 100644 index cf2d32cbc..000000000 --- a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql.erl +++ /dev/null @@ -1,75 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% emqttd authentication by mysql 'user' table. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_auth_mysql). - --author("Feng Lee "). - --include_lib("emqttd/include/emqttd.hrl"). - --behaviour(emqttd_auth_mod). - --export([init/1, check/3, description/0]). - --record(state, {user_table, name_field, pass_field, pass_hash}). - -init(Opts) -> - Mapper = proplists:get_value(field_mapper, Opts), - {ok, #state{user_table = proplists:get_value(user_table, Opts, mqtt_users), - name_field = proplists:get_value(username, Mapper), - pass_field = proplists:get_value(password, Mapper), - pass_hash = proplists:get_value(Opts, password_hash)}}. - -check(#mqtt_client{username = undefined}, _Password, _State) -> - {error, "Username undefined"}; -check(_Client, undefined, _State) -> - {error, "Password undefined"}; -check(#mqtt_client{username = Username}, Password, - #state{user_table = UserTab, pass_hash = Type, - name_field = NameField, pass_field = PassField}) -> - Where = {'and', {NameField, Username}, {PassField, hash(Type, Password)}}, - case emysql:select(UserTab, Where) of - {ok, []} -> {error, "Username or Password "}; - {ok, _Record} -> ok - end. - -description() -> "Authentication by MySQL". - -hash(plain, Password) -> - Password; - -hash(md5, Password) -> - hexstring(crypto:hash(md5, Password)); - -hash(sha, Password) -> - hexstring(crypto:hash(sha, Password)). - -hexstring(<>) -> - lists:flatten(io_lib:format("~32.16.0b", [X])); - -hexstring(<>) -> - lists:flatten(io_lib:format("~40.16.0b", [X])). - diff --git a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl b/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl deleted file mode 100644 index 86881a4bb..000000000 --- a/plugins/emqttd_auth_mysql/src/emqttd_auth_mysql_app.erl +++ /dev/null @@ -1,59 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% emqttd mysql authentication app. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_auth_mysql_app). - --behaviour(application). -%% Application callbacks --export([start/2, prep_stop/1, stop/1]). - --behaviour(supervisor). -%% Supervisor callbacks --export([init/1]). - -%%%============================================================================= -%%% Application callbacks -%%%============================================================================= - -start(_StartType, _StartArgs) -> - Env = application:get_all_env(), - emqttd_access_control:register_mod(auth, emqttd_auth_mysql, Env), - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -prep_stop(State) -> - emqttd_access_control:unregister_mod(auth, emqttd_auth_mysql), State. - -stop(_State) -> - ok. - -%%%============================================================================= -%%% Supervisor callbacks(Dummy) -%%%============================================================================= - -init([]) -> - {ok, { {one_for_one, 5, 10}, []} }. - - diff --git a/plugins/emqttd_dashboard/src/emqttd_dashboard.app.src b/plugins/emqttd_dashboard/src/emqttd_dashboard.app.src deleted file mode 100644 index 12342ddc5..000000000 --- a/plugins/emqttd_dashboard/src/emqttd_dashboard.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_dashboard, - [ - {description, "emqttd management dashboard"}, - {vsn, "0.1"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, {emqttd_dashboard_app, []}}, - {env, []} -]}. diff --git a/plugins/emqttd_dashboard/src/emqttd_dashboard_app.erl b/plugins/emqttd_dashboard/src/emqttd_dashboard_app.erl deleted file mode 100644 index 8e0898679..000000000 --- a/plugins/emqttd_dashboard/src/emqttd_dashboard_app.erl +++ /dev/null @@ -1,27 +0,0 @@ --module(emqttd_dashboard_app). - --behaviour(application). - -%% Application callbacks --export([start/2, stop/1]). - -%% =================================================================== -%% Application callbacks -%% =================================================================== - -start(_StartType, _StartArgs) -> - {ok, Sup} = emqttd_dashboard_sup:start_link(), - open_listener(application:get_env(listener)), - {ok, Sup}. - -stop(_State) -> - ok. - -%% open http port -open_listener({_Http, Port, Options}) -> - MFArgs = {emqttd_dashboard, handle_request, []}, - mochiweb:start_http(Port, Options, MFArgs). - -close_listener(Port) -> - mochiweb:stop_http(Port). - diff --git a/plugins/emqttd_dashboard/src/emqttd_dashboard_sup.erl b/plugins/emqttd_dashboard/src/emqttd_dashboard_sup.erl deleted file mode 100644 index 895f00d83..000000000 --- a/plugins/emqttd_dashboard/src/emqttd_dashboard_sup.erl +++ /dev/null @@ -1,27 +0,0 @@ --module(emqttd_dashboard_sup). - --behaviour(supervisor). - -%% API --export([start_link/0]). - -%% Supervisor callbacks --export([init/1]). - -%% Helper macro for declaring children of supervisor --define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). - -%% =================================================================== -%% API functions -%% =================================================================== - -start_link() -> - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -%% =================================================================== -%% Supervisor callbacks -%% =================================================================== - -init([]) -> - {ok, { {one_for_one, 5, 10}, []} }. - diff --git a/plugins/emqttd_plugin_mysql/.placehodler b/plugins/emqttd_plugin_mysql/.placehodler deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/emqttd_plugin_mysql/Makefile b/plugins/emqttd_plugin_mysql/Makefile deleted file mode 100755 index 086a1fa40..000000000 --- a/plugins/emqttd_plugin_mysql/Makefile +++ /dev/null @@ -1,32 +0,0 @@ -REBAR?=./rebar - - -all: build - - -clean: - $(REBAR) clean - rm -rf logs - rm -rf .eunit - rm -f test/*.beam - - -distclean: clean - git clean -fxd - -build: depends - $(REBAR) compile - - -eunit: - $(REBAR) eunit skip_deps=true - - -check: build eunit - - -%.beam: %.erl - erlc -o test/ $< - - -.PHONY: all clean distclean depends build eunit check diff --git a/plugins/emqttd_plugin_mysql/README.md b/plugins/emqttd_plugin_mysql/README.md deleted file mode 100644 index 481a8f3e7..000000000 --- a/plugins/emqttd_plugin_mysql/README.md +++ /dev/null @@ -1,49 +0,0 @@ -## Overview - -Authentication with user table of MySQL database. - -## etc/plugin.config - -```erlang -[ - {emysql, [ - {pool, 4}, - {host, "localhost"}, - {port, 3306}, - {username, ""}, - {password, ""}, - {database, "mqtt"}, - {encoding, utf8} - ]}, - {emqttd_auth_mysql, [ - {user_table, mqtt_users}, - %% plain password only - {password_hash, plain}, - {field_mapper, [ - {username, username}, - {password, password} - ]} - ]} -]. -``` - -## Users Table(Demo) - -Notice: This is a demo table. You could authenticate with any user tables. - -``` -CREATE TABLE `mqtt_users` ( - `id` int(11) unsigned NOT NULL AUTO_INCREMENT, - `username` varchar(60) DEFAULT NULL, - `password` varchar(60) DEFAULT NULL, - `salt` varchar(20) DEFAULT NULL, - `created` datetime DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `mqtt_users_username` (`username`) -) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -``` - -## Load Plugin - -Merge the'etc/plugin.config' to emqttd/etc/plugins.config, and the plugin will be loaded by the broker. - diff --git a/plugins/emqttd_plugin_mysql/c_src/base64.c b/plugins/emqttd_plugin_mysql/c_src/base64.c deleted file mode 100755 index 457c1a138..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/base64.c +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 1995, 1996, 1997 Kungliga Tekniska Hgskolan - * (Royal Institute of Technology, Stockholm, Sweden). - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software - * must display the following acknowledgement: - * This product includes software developed by the Kungliga Tekniska - * Hgskolan and its contributors. - * - * 4. Neither the name of the Institute nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ - -#ifdef HAVE_CONFIG_H -#include -/*RCSID("$Id: base64.c,v 1.1 2005/02/11 07:34:35 jpm Exp jpm $");*/ -#endif -#include -#include -#include "base64.h" - -static char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -static int pos(char c) -{ - char *p; - for(p = base64; *p; p++) - if(*p == c) - return p - base64; - return -1; -} - -int base64_encode(const void *data, int size, char **str) -{ - char *s, *p; - int i; - int c; - unsigned char *q; - - p = s = (char*)malloc(size*4/3+4); - if (p == NULL) - return -1; - q = (unsigned char*)data; - i=0; - for(i = 0; i < size;){ - c=q[i++]; - c*=256; - if(i < size) - c+=q[i]; - i++; - c*=256; - if(i < size) - c+=q[i]; - i++; - p[0]=base64[(c&0x00fc0000) >> 18]; - p[1]=base64[(c&0x0003f000) >> 12]; - p[2]=base64[(c&0x00000fc0) >> 6]; - p[3]=base64[(c&0x0000003f) >> 0]; - if(i > size) - p[3]='='; - if(i > size+1) - p[2]='='; - p+=4; - } - *p=0; - *str = s; - return strlen(s); -} - -int base64_decode(const char *str, void *data) -{ - const char *p; - unsigned char *q; - int c; - int x; - int done = 0; - q=(unsigned char*)data; - for(p=str; *p && !done; p+=4){ - x = pos(p[0]); - if(x >= 0) - c = x; - else{ - done = 3; - break; - } - c*=64; - - x = pos(p[1]); - if(x >= 0) - c += x; - else - return -1; - c*=64; - - if(p[2] == '=') - done++; - else{ - x = pos(p[2]); - if(x >= 0) - c += x; - else - return -1; - } - c*=64; - - if(p[3] == '=') - done++; - else{ - if(done) - return -1; - x = pos(p[3]); - if(x >= 0) - c += x; - else - return -1; - } - if(done < 3) - *q++=(c&0x00ff0000)>>16; - - if(done < 2) - *q++=(c&0x0000ff00)>>8; - if(done < 1) - *q++=(c&0x000000ff)>>0; - } - return q - (unsigned char*)data; -} diff --git a/plugins/emqttd_plugin_mysql/c_src/base64.h b/plugins/emqttd_plugin_mysql/c_src/base64.h deleted file mode 100755 index 380a31d49..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/base64.h +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (c) 1995, 1996, 1997 Kungliga Tekniska Hgskolan - * (Royal Institute of Technology, Stockholm, Sweden). - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. All advertising materials mentioning features or use of this software - * must display the following acknowledgement: - * This product includes software developed by the Kungliga Tekniska - * Hgskolan and its contributors. - * - * 4. Neither the name of the Institute nor the names of its contributors - * may be used to endorse or promote products derived from this software - * without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF - * SUCH DAMAGE. - */ - -/* $Id: base64.h,v 1.1 2005/02/11 07:34:35 jpm Exp jpm $ */ - -#ifndef _BASE64_H_ -#define _BASE64_H_ - -int base64_encode(const void *data, int size, char **str); -int base64_decode(const char *str, void *data); - -#endif diff --git a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c b/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c deleted file mode 100755 index 374de99ac..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.c +++ /dev/null @@ -1,60 +0,0 @@ -// This file is part of Jiffy released under the MIT license. -// See the LICENSE file for more information. - -#include "emqttd_plugin_mysql_app.h" - -static int -load(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) -{ - emqttd_plugin_mysql_app_st* st = enif_alloc(sizeof(emqttd_plugin_mysql_app_st)); - if(st == NULL) { - return 1; - } - - st->atom_ok = make_atom(env, "ok"); - st->atom_error = make_atom(env, "error"); - st->atom_null = make_atom(env, "null"); - st->atom_true = make_atom(env, "true"); - st->atom_false = make_atom(env, "false"); - st->atom_bignum = make_atom(env, "bignum"); - st->atom_bignum_e = make_atom(env, "bignum_e"); - st->atom_bigdbl = make_atom(env, "bigdbl"); - st->atom_partial = make_atom(env, "partial"); - st->atom_uescape = make_atom(env, "uescape"); - st->atom_pretty = make_atom(env, "pretty"); - st->atom_force_utf8 = make_atom(env, "force_utf8"); - - // Markers used in encoding - st->ref_object = make_atom(env, "$object_ref$"); - st->ref_array = make_atom(env, "$array_ref$"); - - *priv = (void*) st; - - return 0; -} - -static int -reload(ErlNifEnv* env, void** priv, ERL_NIF_TERM info) -{ - return 0; -} - -static int -upgrade(ErlNifEnv* env, void** priv, void** old_priv, ERL_NIF_TERM info) -{ - return load(env, priv, info); -} - -static void -unload(ErlNifEnv* env, void* priv) -{ - enif_free(priv); - return; -} - -static ErlNifFunc funcs[] = -{ - {"nif_pbkdf2_check", 2, pbkdf2_check} -}; - -ERL_NIF_INIT(emqttd_plugin_mysql_app, funcs, &load, &reload, &upgrade, &unload); diff --git a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h b/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h deleted file mode 100755 index e77ae3b29..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/emqttd_plugin_mysql_app.h +++ /dev/null @@ -1,44 +0,0 @@ -// This file is part of Jiffy released under the MIT license. -// See the LICENSE file for more information. - -#ifndef EMQTTD_PLUGIN_MYSQL_APP_H -#define EMQTTD_PLUGIN_MYSQL_APP_H - -#include "erl_nif.h" - -typedef struct { - ERL_NIF_TERM atom_ok; - ERL_NIF_TERM atom_error; - ERL_NIF_TERM atom_null; - ERL_NIF_TERM atom_true; - ERL_NIF_TERM atom_false; - ERL_NIF_TERM atom_bignum; - ERL_NIF_TERM atom_bignum_e; - ERL_NIF_TERM atom_bigdbl; - ERL_NIF_TERM atom_partial; - ERL_NIF_TERM atom_uescape; - ERL_NIF_TERM atom_pretty; - ERL_NIF_TERM atom_force_utf8; - - ERL_NIF_TERM ref_object; - ERL_NIF_TERM ref_array; -} emqttd_plugin_mysql_app_st; - -ERL_NIF_TERM make_atom(ErlNifEnv* env, const char* name); -ERL_NIF_TERM make_ok(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, ERL_NIF_TERM data); -ERL_NIF_TERM make_error(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, const char* error); - -ERL_NIF_TERM pbkdf2_check(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]); - -int int_from_hex(const unsigned char* p); -int int_to_hex(int val, char* p); -int utf8_len(int c); -int utf8_esc_len(int c); -int utf8_validate(unsigned char* data, size_t size); -int utf8_to_unicode(unsigned char* buf, size_t size); -int unicode_to_utf8(int c, unsigned char* buf); -int unicode_from_pair(int hi, int lo); -int unicode_uescape(int c, char* buf); -int double_to_shortest(char *buf, size_t size, size_t* len, double val); - -#endif // Included EMQTTD_PLUGIN_MYSQL_APP_H diff --git a/plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c b/plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c deleted file mode 100644 index 0e40b933b..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/pbkdf2_check.c +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (c) 2013 Jan-Piet Mens - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of mosquitto nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include -#include -#include -#include -#include -#include "base64.h" -#include "erl_nif.h" -#include "emqttd_plugin_mysql_app.h" - -#define KEY_LENGTH 24 -#define SEPARATOR "$" -#define SEPARATOR1 "_" -#define TRUE (1) -#define FALSE (0) - -/* - * Split PBKDF2$... string into their components. The caller must free() - * the strings. - */ - -static int detoken(char *pbkstr, char **sha, int *iter, char **salt, char **key) -{ - char *p, *s, *save; - int rc = 1; - - save = s = strdup(pbkstr); - - if ((p = strsep(&s, SEPARATOR1)) == NULL) - goto out; - if (strcmp(p, "pbkdf2") != 0) - goto out; - - if ((p = strsep(&s, SEPARATOR)) == NULL) - goto out; - *sha = strdup(p); - - if ((p = strsep(&s, SEPARATOR)) == NULL) - goto out; - *iter = atoi(p); - - if ((p = strsep(&s, SEPARATOR)) == NULL) - goto out; - *salt = strdup(p); - - if ((p = strsep(&s, SEPARATOR)) == NULL) - goto out; - *key = strdup(p); - - rc = 0; - -out: - free(save); - return rc; -} - - ERL_NIF_TERM -pbkdf2_check(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) -{ - ERL_NIF_TERM ret; - ErlNifBinary binps, binhash; - emqttd_plugin_mysql_app_st* st = enif_alloc(sizeof(emqttd_plugin_mysql_app_st)); - if(st == NULL) { - return make_atom(env, "alloc_error"); - } - - st->atom_ok = make_atom(env, "ok"); - st->atom_error = make_atom(env, "error"); - st->atom_null = make_atom(env, "null"); - st->atom_true = make_atom(env, "true"); - st->atom_false = make_atom(env, "false"); - st->atom_bignum = make_atom(env, "bignum"); - st->atom_bignum_e = make_atom(env, "bignum_e"); - st->atom_bigdbl = make_atom(env, "bigdbl"); - st->atom_partial = make_atom(env, "partial"); - st->atom_uescape = make_atom(env, "uescape"); - st->atom_pretty = make_atom(env, "pretty"); - st->atom_force_utf8 = make_atom(env, "force_utf8"); - - // Markers used in encoding - st->ref_object = make_atom(env, "$object_ref$"); - st->ref_array = make_atom(env, "$array_ref$"); - - if(argc != 2) { - return make_error(st, env, "Bad args"); - } else if(!enif_inspect_binary(env, argv[0], &binps)|!enif_inspect_binary(env, argv[1], &binhash)) { - return make_error(st, env, "Bad args password or username inspect error"); - } - - char* password = (char*)binps.data; - char* hash = (char*)binhash.data; - static char *sha, *salt, *h_pw; - int iterations, saltlen, blen; - char *b64, *keybuf; - unsigned char *out; - int match = FALSE; - const EVP_MD *evpmd; - int keylen, rc; - - if (detoken(hash, &sha, &iterations, &salt, &h_pw) != 0) - return match; - - /* Determine key length by decoding base64 */ - if ((keybuf = malloc(strlen(h_pw) + 1)) == NULL) { - return make_error(st, env, "internal_error: Out Of memory"); - } - keylen = base64_decode(h_pw, keybuf); - if (keylen < 1) { - free(keybuf); - return make_atom(env, "false"); - } - free(keybuf); - - if ((out = malloc(keylen)) == NULL) { - return make_error(st, env, "Cannot allocate out; out of memory\n"); - } - -#ifdef PWDEBUG - fprintf(stderr, "sha =[%s]\n", sha); - fprintf(stderr, "iterations =%d\n", iterations); - fprintf(stderr, "salt =[%s]\n", salt); - fprintf(stderr, "h_pw =[%s]\n", h_pw); - fprintf(stderr, "kenlen =[%d]\n", keylen); -#endif - - saltlen = strlen((char *)salt); - - evpmd = EVP_sha256(); - if (strcmp(sha, "sha1") == 0) { - evpmd = EVP_sha1(); - } else if (strcmp(sha, "sha512") == 0) { - evpmd = EVP_sha512(); - } - - rc = PKCS5_PBKDF2_HMAC(password, strlen(password), - (unsigned char *)salt, saltlen, - iterations, - evpmd, keylen, out); - if (rc != 1) { - goto out; - } - - blen = base64_encode(out, keylen, &b64); - if (blen > 0) { - int i, diff = 0, hlen = strlen(h_pw); -#ifdef PWDEBUG - fprintf(stderr, "HMAC b64 =[%s]\n", b64); -#endif - - /* "manual" strcmp() to ensure constant time */ - for (i = 0; (i < blen) && (i < hlen); i++) { - diff |= h_pw[i] ^ b64[i]; - } - - match = diff == 0; - if (hlen != blen) - match = 0; - - free(b64); - } - -out: - free(sha); - free(salt); - free(h_pw); - free(out); - - if(match == 0){ - ret = make_atom(env, "false"); - }else{ - ret = make_atom(env, "true"); - } - return ret; -} - -int pbkdf2_check_native(char *password, char *hash) -{ - static char *sha, *salt, *h_pw; - int iterations, saltlen, blen; - char *b64; - unsigned char key[128]; - int match = FALSE; - const EVP_MD *evpmd; - - if (detoken(hash, &sha, &iterations, &salt, &h_pw) != 0) - return match; - -#ifdef PWDEBUG - fprintf(stderr, "sha =[%s]\n", sha); - fprintf(stderr, "iterations =%d\n", iterations); - fprintf(stderr, "salt =[%s]\n", salt); - fprintf(stderr, "h_pw =[%s]\n", h_pw); -#endif - - saltlen = strlen((char *)salt); - - evpmd = EVP_sha256(); - if (strcmp(sha, "sha1") == 0) { - evpmd = EVP_sha1(); - } else if (strcmp(sha, "sha512") == 0) { - evpmd = EVP_sha512(); - } - - PKCS5_PBKDF2_HMAC(password, strlen(password), - (unsigned char *)salt, saltlen, - iterations, - evpmd, KEY_LENGTH, key); - - blen = base64_encode(key, KEY_LENGTH, &b64); - if (blen > 0) { - int i, diff = 0, hlen = strlen(h_pw); -#ifdef PWDEBUG - fprintf(stderr, "HMAC b64 =[%s]\n", b64); -#endif - - /* "manual" strcmp() to ensure constant time */ - for (i = 0; (i < blen) && (i < hlen); i++) { - diff |= h_pw[i] ^ b64[i]; - } - - match = diff == 0; - if (hlen != blen) - match = 0; - - free(b64); - } - - free(sha); - free(salt); - free(h_pw); - - return match; -} -int main() -{ - // char password[] = "hello"; - // char PB1[] = "PBKDF2$sha256$10000$eytf9sEo8EprP9P3$2eO6tROHiqI3bm+gg+vpmWooWMpz1zji"; - char password[] = "supersecret"; - //char PB1[] = "PBKDF2$sha256$10000$YEbSTt8FaMRDq/ib$Kt97+sMCYg00mqMOBAYinqZlnxX8HqHk"; - char PB1[] = "pbkdf2_sha256$10000$YEbSTt8FaMRDq/ib$Kt97+sMCYg00mqMOBAYinqZlnxX8HqHk"; - // char PB1[] = "PBKDF2$sha1$10000$XWfyPLeC9gsD6SbI$HOnjU4Ux7RpeBHdqYxpIGH1R5qCCtNA1"; - // char PB1[] = "PBKDF2$sha512$10000$v/aaCgBZ+VZN5L8n$BpgjSTyb4weVxr9cA2mvQ+jaCyaAPeYe"; - int match; - - printf("Checking password [%s] for %s\n", password, PB1); - - match = pbkdf2_check_native(password, PB1); - printf("match == %d\n", match); - return match; -} diff --git a/plugins/emqttd_plugin_mysql/c_src/util.c b/plugins/emqttd_plugin_mysql/c_src/util.c deleted file mode 100755 index 8c0291b22..000000000 --- a/plugins/emqttd_plugin_mysql/c_src/util.c +++ /dev/null @@ -1,26 +0,0 @@ -// This file is part of Jiffy released under the MIT license. -// See the LICENSE file for more information. - -#include "emqttd_plugin_mysql_app.h" - -ERL_NIF_TERM -make_atom(ErlNifEnv* env, const char* name) -{ - ERL_NIF_TERM ret; - if(enif_make_existing_atom(env, name, &ret, ERL_NIF_LATIN1)) { - return ret; - } - return enif_make_atom(env, name); -} - -ERL_NIF_TERM -make_ok(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, ERL_NIF_TERM value) -{ - return enif_make_tuple2(env, st->atom_ok, value); -} - -ERL_NIF_TERM -make_error(emqttd_plugin_mysql_app_st* st, ErlNifEnv* env, const char* error) -{ - return enif_make_tuple2(env, st->atom_error, make_atom(env, error)); -} diff --git a/plugins/emqttd_plugin_mysql/etc/plugin.config b/plugins/emqttd_plugin_mysql/etc/plugin.config deleted file mode 100644 index 4b5044bde..000000000 --- a/plugins/emqttd_plugin_mysql/etc/plugin.config +++ /dev/null @@ -1,23 +0,0 @@ -[ - {emysql, [ - {pool, 4}, - {host, "localhost"}, - {port, 3306}, - {username, "root"}, - {password, "root"}, - {database, "emqtt"}, - {encoding, utf8} - ]}, - {emqttd_plugin_mysql, [ - {users_table, auth_user}, - {acls_table, auth_acl}, - {field_mapper, [ - {username, username}, - {password, password, pbkdf2}, - {user_super, is_super_user}, - {acl_username, username}, - {acl_rw, rw}, - {acl_topic, topic} - ]} - ]} -]. diff --git a/plugins/emqttd_plugin_mysql/etc/plugins.config b/plugins/emqttd_plugin_mysql/etc/plugins.config deleted file mode 100644 index 7b132dd3f..000000000 --- a/plugins/emqttd_plugin_mysql/etc/plugins.config +++ /dev/null @@ -1,23 +0,0 @@ -[ - {emysql, [ - {pool, 4}, - {host, "59.188.253.198"}, - {port, 3306}, - {username, "root"}, - {password, "lhroot."}, - {database, "musicfield"}, - {encoding, utf8} - ]}, - {emqttd_plugin_mysql, [ - {users_table, auth_user}, - {acls_table, auth_acl}, - {field_mapper, [ - {username, username}, - {password, password, pbkdf2}, - {user_super, is_super_user}, - {acl_username, username}, - {acl_rw, rw}, - {acl_topic, topic} - ]} - ]} -]. diff --git a/plugins/emqttd_plugin_mysql/priv/.placeholder b/plugins/emqttd_plugin_mysql/priv/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugins/emqttd_plugin_mysql/rebar b/plugins/emqttd_plugin_mysql/rebar deleted file mode 100755 index 36ef01107..000000000 Binary files a/plugins/emqttd_plugin_mysql/rebar and /dev/null differ diff --git a/plugins/emqttd_plugin_mysql/rebar.config b/plugins/emqttd_plugin_mysql/rebar.config deleted file mode 100755 index df10d9593..000000000 --- a/plugins/emqttd_plugin_mysql/rebar.config +++ /dev/null @@ -1,32 +0,0 @@ -{port_specs, [ - {"priv/emqttd_plugin_mysql_app.so", [ - "c_src/*.c" - ]} -]}. - -{port_env, [ - {".*", "CXXFLAGS", "$CXXFLAGS -g -Wall -Werror -O3"}, - - {"(linux|solaris|freebsd|netbsd|openbsd|dragonfly|darwin)", - "LDFLAGS", "$LDFLAGS -lstdc++ -lcrypto"}, - - %% OS X Leopard flags for 64-bit - {"darwin9.*-64$", "CXXFLAGS", "-m64"}, - {"darwin9.*-64$", "LDFLAGS", "-arch x86_64"}, - - %% OS X Snow Leopard flags for 32-bit - {"darwin10.*-32$", "CXXFLAGS", "-m32"}, - {"darwin10.*-32$", "LDFLAGS", "-arch i386"}, - - %% This will merge into basho/rebar/rebar.config eventually - {"win32", "CFLAGS", "/Wall /DWIN32 /D_WINDOWS /D_WIN32 /DWINDOWS"}, - {"win32", "CXXFLAGS", "-g -Wall -O3"} -]}. - - -{eunit_opts, [ - verbose, - {report, { - eunit_surefire, [{dir,"."}] - }} -]}. diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_acl_mysql.erl b/plugins/emqttd_plugin_mysql/src/emqttd_acl_mysql.erl deleted file mode 100644 index a2c995367..000000000 --- a/plugins/emqttd_plugin_mysql/src/emqttd_acl_mysql.erl +++ /dev/null @@ -1,70 +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 -%%% emqttd demo acl module. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_acl_mysql). - --include("emqttd.hrl"). - --behaviour(emqttd_acl_mod). - -%% ACL callbacks --export([init/1, check_acl/2, reload_acl/1, description/0]). --record(state, {user_table, acl_table, acl_username_field, acl_topic_field, acl_rw_field, user_name_field, user_super_field}). - -init(Opts) -> - Mapper = proplists:get_value(field_mapper, Opts), - State = - #state{ - user_table = proplists:get_value(users_table, Opts, auth_user), - user_super_field = proplists:get_value(is_super, Mapper, is_superuser), - user_name_field = proplists:get_value(username, Mapper, username), - acl_table = proplists:get_value(acls_table, Opts, auth_acl), - acl_username_field = proplists:get_value(acl_username, Mapper, username), - acl_rw_field = proplists:get_value(acl_rw, Mapper, rw), - acl_topic_field = proplists:get_value(acl_topic, Mapper, topic) - }, - {ok, State}. - -check_acl({#mqtt_client{username = Username}, PubSub, Topic}, #state{user_table = UserTab, acl_table = AclTab, user_name_field = UsernameField, user_super_field = SuperField, acl_topic_field = TopicField, acl_username_field = AclUserField, acl_rw_field = AclRwField}) -> - Flag = case PubSub of publish -> 2; subscribe -> 1; pubsub -> 2 end, - Where = {'and', {'>=', AclRwField, Flag}, {TopicField, Topic}}, - Where1 = {'or', {AclUserField, Username}, {AclUserField, "*"}}, - Where2 = {'and', Where, Where1}, - case emysql:select(UserTab, {'and', {UsernameField, Username}, {SuperField, 1}}) of - {ok, []} -> - case emysql:select(UserTab, {UsernameField, Username}) of - {ok, []} -> ignore; - {ok, _} -> case emysql:select(AclTab, Where2) of - {ok, []} -> deny; - {ok, _Record} -> allow - end - end; - {ok, _} -> allow - end. - -reload_acl(_State) -> ok. - -description() -> "ACL Module by Mysql". diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl b/plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl deleted file mode 100644 index a913dd587..000000000 --- a/plugins/emqttd_plugin_mysql/src/emqttd_auth_mysql.erl +++ /dev/null @@ -1,110 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% emqttd authentication by mysql 'user' table. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_auth_mysql). - --author("Feng Lee "). - --include("emqttd.hrl"). - --behaviour(emqttd_auth_mod). - --export([init/1, check/3, description/0]). - --define(NOT_LOADED, not_loaded(?LINE)). - --record(state, {user_table, name_field, pass_field, pass_hash}). - -init(Opts) -> - Mapper = proplists:get_value(field_mapper, Opts), - {ok, #state{user_table = proplists:get_value(user_table, Opts, auth_user), - name_field = proplists:get_value(username, Mapper), - pass_field = proplists:get_value(password, Mapper), - pass_hash = proplists:get_value(Opts, password_hash)}}. - -check(#mqtt_client{username = undefined}, _Password, _State) -> - {error, "Username undefined"}; -check(_Client, undefined, _State) -> - {error, "Password undefined"}; -check(#mqtt_client{username = Username}, Password, - #state{user_table = UserTab, pass_hash = Type, - name_field = NameField, pass_field = PassField}) -> - Where = {'and', {NameField, Username}, {PassField, hash(Type, Password)}}, - if Type =:= pbkdf2 -> - case emysql:select(UserTab, [PassField], {NameField, Username}) of - {ok, []} -> {error, "User not exist"}; - {ok, Records} -> - if length(Records) =:= 1 -> - case pbkdf2_check(Password, lists:nth(Records, 1)) of - true -> - {ok, []}; - false -> - {error, "UserName or Password is invalid"}; - ErrorInfo -> - {error, ErrorInfo} - end; - true -> - {error, "UserName is ambiguous"} - end - end; - true -> - case emysql:select(UserTab, Where) of - {ok, []} -> {error, "Username or Password "}; - {ok, _Record} -> ok - end - end. - -description() -> "Authentication by MySQL". - -hash(plain, Password) -> - Password; - -hash(md5, Password) -> - hexstring(crypto:hash(md5, Password)); - -hash(sha, Password) -> - hexstring(crypto:hash(sha, Password)). - -hexstring(<>) -> - lists:flatten(io_lib:format("~32.16.0b", [X])); - -hexstring(<>) -> - lists:flatten(io_lib:format("~40.16.0b", [X])). - -not_loaded(Line) -> - erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}). - -pbkdf2_check(Password, Pbkstr) -> - case nif_pbkdf2_check(Password, Pbkstr) of - {error, _} = Error -> - throw(Error); - IOData -> - IOData - end. - -nif_pbkdf2_check(Password, Pbkstr) -> - ?NOT_LOADED. - diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src b/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src deleted file mode 100644 index 389bd33cb..000000000 --- a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql.app.src +++ /dev/null @@ -1,12 +0,0 @@ -{application, emqttd_plugin_mysql, - [ - {description, "emqttd MySQL Authentication Plugin"}, - {vsn, "1.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib - ]}, - {mod, {emqttd_plugin_mysql_app, []}}, - {env, []} - ]}. diff --git a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl b/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl deleted file mode 100644 index 712e75ea6..000000000 --- a/plugins/emqttd_plugin_mysql/src/emqttd_plugin_mysql_app.erl +++ /dev/null @@ -1,80 +0,0 @@ -%%%----------------------------------------------------------------------------- -%%% Copyright (c) 2012-2015 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 -%%% emqttd mysql authentication app. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_plugin_mysql_app). --on_load(init/0). --behaviour(application). -%% Application callbacks --export([start/2, prep_stop/1, stop/1, nif_pbkdf2_check/2]). - --behaviour(supervisor). -%% Supervisor callbacks --export([init/1]). --define(NOT_LOADED, not_loaded(?LINE)). - - -%%%============================================================================= -%%% Application callbacks -%%%============================================================================= - -start(_StartType, _StartArgs) -> - Env = application:get_all_env(), - emqttd_access_control:register_mod(auth, emqttd_auth_mysql, Env), - emqttd_access_control:register_mod(acl, emqttd_acl_mysql, Env), - crypto:start(), - supervisor:start_link({local, ?MODULE}, ?MODULE, []). - -prep_stop(State) -> - emqttd_access_control:unregister_mod(auth, emqttd_auth_mysql), State, - emqttd_access_control:unregister_mod(acl, emqttd_acl_mysql), State, - crypto:stop(). - -stop(_State) -> - ok. - -init() -> - PrivDir = case code:priv_dir(?MODULE) of - {error, _} -> - EbinDir = filename:dirname(code:which(?MODULE)), - AppPath = filename:dirname(EbinDir), - filename:join(AppPath, "priv"); - Path -> - Path - end, - erlang:load_nif(filename:join(PrivDir, "emqttd_plugin_mysql_app"), 0). - -%%%============================================================================= -%%% Supervisor callbacks(Dummy) -%%%============================================================================= - -init([]) -> - {ok, {{one_for_one, 5, 10}, []}}. - -not_loaded(Line) -> - erlang:nif_error({not_loaded, [{module, ?MODULE}, {line, Line}]}). - -nif_pbkdf2_check(Password, Hash) -> - ?NOT_LOADED. diff --git a/plugins/emysql/README.md b/plugins/emysql/README.md deleted file mode 100644 index 18b61598c..000000000 --- a/plugins/emysql/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# emysql - -Erlang MySQL client - -## config - -``` - -``` - -## Select API - -* emyssql:select(tab). -* emysql:select({tab, [col1,col2]}). -* emysql:select({tab, [col1, col2], {id,1}}). -* emysql:select(Query, Load). - -## Update API - -* emysql:update(tab, [{Field1, Val}, {Field2, Val2}], {id, 1}). - -## Insert API - -* emysql:insert(tab, [{Field1, Val}, {Field2, Val2}]). - -## Delete API - -* emysql:delete(tab, {name, Name}]). - -## Query API - -* emysql:sqlquery("select * from tab;"). - -## Prepare API - -* emysql:prepare(find_with_id, "select * from tab where id = ?;"). -* emysql:execute(find_with_id, [Id]). -* emysql:unprepare(find_with_id). - -## MySQL Client Protocal - -* http://forge.mysql.com/wiki/MySQL_Internals_ClientServer_Protocol diff --git a/plugins/emysql/include/emysql.hrl b/plugins/emysql/include/emysql.hrl deleted file mode 100755 index fde65fe7a..000000000 --- a/plugins/emysql/include/emysql.hrl +++ /dev/null @@ -1,2 +0,0 @@ -%% MySQL result record: --record(mysql_result, {fieldinfo = [], rows = [], affectedrows = 0, insert_id =0, error = ""}). diff --git a/plugins/emysql/src/emysql.app.src b/plugins/emysql/src/emysql.app.src deleted file mode 100644 index d494fd954..000000000 --- a/plugins/emysql/src/emysql.app.src +++ /dev/null @@ -1,14 +0,0 @@ -{application, emysql, - [{description, "Erlang MySQL Driver"}, - {vsn, "1.0"}, - {modules, [ - emysql, - emysql_app, - emysql_sup, - emysql_auth, - emysql_conn, - emysql_recv]}, - {registered, []}, - {applications, [kernel, stdlib, sasl, crypto]}, - {env, []}, - {mod, {emysql_app, []}}]}. diff --git a/plugins/emysql/src/emysql.erl b/plugins/emysql/src/emysql.erl deleted file mode 100644 index f6a620cfd..000000000 --- a/plugins/emysql/src/emysql.erl +++ /dev/null @@ -1,514 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : emysql.erl -%%% Author : Ery Lee -%%% Purpose : Mysql access api. -%%% Created : 19 May 2009 -%%% License : http://www.opengoss.com -%%% -%%% Copyright (C) 2012, www.opengoss.com -%%%---------------------------------------------------------------------- --module(emysql). - --author('ery.lee@gmail.com'). - --include("emysql.hrl"). - --export([start_link/1]). - --ifdef(use_specs). - --spec(conns/0 :: () -> list()). - --endif. - -%command functions --export([info/0, - pool/1, - conns/0]). - -%sql functions --export([insert/2, - insert/3, - select/1, - select/2, - select/3, - update/2, - update/3, - delete/1, - delete/2, - truncate/1, - prepare/2, - execute/1, - execute/2, - unprepare/1, - sqlquery/1, - sqlquery/2]). - --behavior(gen_server). - --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). - --record(state, {ids}). - -%% External exports --export([encode/1, - encode/2, - escape/1, - escape_like/1]). - -start_link(PoolSize) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [PoolSize], []). - -info() -> - [emysql_conn:info(Pid) || Pid <- - pg2:get_local_members(emysql_conn)]. - -%pool pool -pool(Id) -> - gen_server:cast(?MODULE, {pool, Id}). - -conns() -> - gen_server:call(?MODULE, conns). - -insert(Tab, Record) when is_atom(Tab) -> - sqlquery(encode_insert(Tab, Record)). - -insert(_Tab, _Fields, Values) when length(Values) == 0 -> - {updated, {0, 0}}; - -insert(Tab, Fields, Values) when length(Values) > 0 -> - sqlquery(encode_insert(Tab, Fields, Values)). - -encode_insert(Tab, Record) -> - {Fields, Values} = lists:unzip([{atom_to_list(F), encode(V)} - || {F, V} <- Record]), - ["insert into ", atom_to_list(Tab), "(", - string:join(Fields, ","), ") values(", - string:join(Values, ","), ");"]. - -encode_insert(Tab, Fields, Rows) -> - Encode = fun(Row) -> string:join([encode(V) || V <- Row], ",") end, - Rows1 = [lists:concat(["(", Encode(Row), ")"]) || Row <- Rows], - ["insert into ", atom_to_list(Tab), "(", - string:join([atom_to_list(F) || F <- Fields], ","), - ") values", string:join(Rows1, ","), ";"]. - -select(Tab) when is_atom(Tab) -> - sqlquery(encode_select(Tab)); - -select(Select) when is_tuple(Select) -> - sqlquery(encode_select(Select)). - -select(Tab, Where) when is_atom(Tab) and is_tuple(Where) -> - sqlquery(encode_select({Tab, Where})); - -select(Tab, Fields) when is_atom(Tab) and is_list(Fields) -> - sqlquery(encode_select({Tab, Fields})); - -select(Select, Load) when is_tuple(Select) and is_integer(Load) -> - sqlquery(encode_select(Select), Load). - -select(Tab, Fields, Where) when is_atom(Tab) - and is_list(Fields) and is_tuple(Where) -> - sqlquery(encode_select({Tab, Fields, Where})). - -encode_select(Tab) when is_atom(Tab) -> - encode_select({Tab, ['*'], undefined}); - -encode_select({Tab, Fields}) when is_atom(Tab) - and is_list(Fields) -> - encode_select({Tab, Fields, undefined}); - -encode_select({Tab, Where}) when is_atom(Tab) - and is_tuple(Where) -> - encode_select({Tab, ['*'], Where}); - -encode_select({Tab, Fields, undefined}) when is_atom(Tab) - and is_list(Fields) -> - ["select ", encode_fields(Fields), " from ", atom_to_list(Tab), ";"]; - -encode_select({Tab, Fields, Where}) when is_atom(Tab) - and is_list(Fields) and is_tuple(Where) -> - ["select ", encode_fields(Fields), " from ", - atom_to_list(Tab), " where ", encode_where(Where), ";"]. - -encode_fields(Fields) -> - string:join([atom_to_list(F) || F <- Fields], " ,"). - -update(Tab, Record) when is_atom(Tab) - and is_list(Record) -> - case proplists:get_value(id, Record) of - undefined -> - Updates = string:join([encode_column(Col) || Col <- Record], ","), - Query = ["update ", atom_to_list(Tab), " set ", Updates, ";"], - sqlquery(Query); - Id -> - update(Tab, lists:keydelete(id, 1, Record), {id, Id}) - end. - -update(Tab, Record, Where) -> - Update = string:join([encode_column(Col) || Col <- Record], ","), - Query = ["update ", atom_to_list(Tab), " set ", Update, - " where ", encode_where(Where), ";"], - sqlquery(Query). - -encode_column({F, V}) when is_atom(F) -> - lists:concat([atom_to_list(F), "=", encode(V)]). - -delete(Tab) when is_atom(Tab) -> - sqlquery(["delete from ", atom_to_list(Tab), ";"]). - -delete(Tab, Id) when is_atom(Tab) - and is_integer(Id) -> - Query = ["delete from ", atom_to_list(Tab), - " where ", encode_where({id, Id})], - sqlquery(Query); - -delete(Tab, Where) when is_atom(Tab) - and is_tuple(Where) -> - Query = ["delete from ", atom_to_list(Tab), - " where ", encode_where(Where)], - sqlquery(Query). - -truncate(Tab) when is_atom(Tab) -> - sqlquery(["truncate table ", atom_to_list(Tab), ";"]). - -sqlquery(Query) -> - sqlquery(Query, 1). - -sqlquery(Query, Load) -> - with_next_conn(fun(Conn) -> - case catch mysql_to_odbc(emysql_conn:sqlquery(Conn, iolist_to_binary(Query))) of - {selected, NewFields, Records} -> - {ok, to_tuple_records(NewFields, Records)}; - {error, Reason} -> - {error, Reason}; - Res -> - Res - end - end, Load). - -prepare(Name, Stmt) when is_list(Stmt) -> - prepare(Name, list_to_binary(Stmt)); - -prepare(Name, Stmt) when is_binary(Stmt) -> - with_all_conns(fun(Conn) -> - emysql_conn:prepare(Conn, Name, Stmt) - end). - -execute(Name) -> - execute(Name, []). - -execute(Name, Params) -> - with_next_conn(fun(Conn) -> - case catch mysql_to_odbc(emysql_conn:execute(Conn, Name, Params)) of - {selected, NewFields, Records} -> - {ok, to_tuple_records(NewFields, Records)}; - {error, Reason} -> - {error, Reason}; - Res -> - Res - end - end, 1). - -unprepare(Name) -> - with_all_conns(fun(Conn) -> - emysql_conn:unprepare(Conn, Name) - end). - -with_next_conn(Fun, _Load) -> - Fun(pg2:get_closest_pid(emysql_conn)). - -with_all_conns(Fun) -> - [Fun(Pid) || Pid <- pg2:get_local_members(emysql_conn)]. - -%%-------------------------------------------------------------------- -%% Function: init(Args) -> {ok, State} | -%% {ok, State, Timeout} | -%% ignore | -%% {stop, Reason} -%% Description: Initiates the server -%%-------------------------------------------------------------------- -init([PoolSize]) -> - Ids = lists:seq(1, PoolSize), - [put(Id, 0) || Id <- Ids], - [put({count, Id}, 0) || Id <- Ids], - {ok, #state{ids = Ids}}. - -%%-------------------------------------------------------------------- -%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} | -%% {reply, Reply, State, Timeout} | -%% {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, Reply, State} | -%% {stop, Reason, State} -%% Description: Handling call messages -%%-------------------------------------------------------------------- - -handle_call(info, _From, State) -> - Reply = [{conn, Id, Pid, get(Id), get({total, Id})} - || {Id, Pid} <- get_all_conns()], - {reply, Reply, State}; - -handle_call({next_conn, Load}, _From, #state{ids = Ids} = State) -> - {ConnId, ConnLoad} = - lists:foldl(fun(Id, {MinId, MinLoad}) -> - ThisLoad = get(Id), - if - ThisLoad =< MinLoad -> {Id, ThisLoad}; - true -> {MinId, MinLoad} - end - end, {undefined, 16#ffffffff}, Ids), - Reply = - case ConnId of - undefined -> - undefined; - _ -> - ConnPid = get_conn_pid(ConnId), - put(ConnId, ConnLoad+Load), - Count = get({total, ConnId}), - put({total, ConnId}, Count+1), - {ConnId, ConnPid} - end, - {reply, Reply, State}; - -handle_call(conns, _From, State) -> - Conns = get_all_conns(), - {reply, Conns, State}; - -handle_call(Req, From, State) -> - gen_server:reply(From, {badcall, Req}), - {stop, {badcall, Req}, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_cast(Msg, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling cast messages -%%-------------------------------------------------------------------- -handle_cast({pool, Id}, State) -> - put(Id, 0), - put({total, Id}, 0), - {noreply, State}; - -handle_cast({done, ConnId, Load}, State) -> - put(ConnId, get(ConnId) - Load), - {noreply, State}; - -handle_cast(Msg, State) -> - {stop, {badcast, Msg}, State}. - -%%-------------------------------------------------------------------- -%% Function: handle_info(Info, State) -> {noreply, State} | -%% {noreply, State, Timeout} | -%% {stop, Reason, State} -%% Description: Handling all non call/cast messages -%%-------------------------------------------------------------------- -handle_info(Info, State) -> - {stop, {badinfo, Info}, State}. - -%%-------------------------------------------------------------------- -%% Function: terminate(Reason, State) -> void() -%% Description: This function is called by a gen_server 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_server terminates with Reason. -%% The return value is ignored. -%%-------------------------------------------------------------------- -terminate(_Reason, _State) -> - ok. -%%-------------------------------------------------------------------- -%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState} -%% Description: Convert process state when code is changed -%%-------------------------------------------------------------------- -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -get_conn_pid(CId) -> - [{CId, Pid, _Type, _Modules} | _] = - lists:dropwhile(fun ({Id, _Pid, _Type, _Modules}) - when Id =:= CId -> false; - (_) -> true - end, - supervisor:which_children(emysql_sup)), - Pid. - -get_all_conns() -> - [{Id, Pid} || {Id, Pid, _Type, _Modules} <- - supervisor:which_children(emysql_sup), is_integer(Id)]. - -%% Convert MySQL query result to Erlang ODBC result formalism -mysql_to_odbc({updated, #mysql_result{affectedrows=AffectedRows, insert_id = InsertId} = _MySQLRes}) -> - {updated, {AffectedRows, InsertId}}; - -mysql_to_odbc({data, #mysql_result{fieldinfo = FieldInfo, rows=AllRows} = _MySQLRes}) -> - mysql_item_to_odbc(FieldInfo, AllRows); - -mysql_to_odbc({error, MySQLRes}) when is_list(MySQLRes) -> - {error, MySQLRes}; - -mysql_to_odbc({error, #mysql_result{error=Reason} = _MySQLRes}) -> - {error, Reason}; - -mysql_to_odbc({error, Reason}) -> - {error, Reason}. - -%% When tabular data is returned, convert it to the ODBC formalism -mysql_item_to_odbc(Columns, Recs) -> - %% For now, there is a bug and we do not get the correct value from MySQL - %% module: - {selected, - [element(2, Column) || Column <- Columns], - [list_to_tuple(Rec) || Rec <- Recs]}. - -%%internal functions -encode_where({'and', L, R}) -> - encode_where(L) ++ " and " ++ encode_where(R); - -encode_where({'and', List}) when is_list(List) -> - string:join([encode_where(E) || E <- List], " and "); - -encode_where({'or', L, R}) -> - encode_where(L) ++ " or " ++ encode_where(R); - -encode_where({'or', List}) when is_list(List) -> - string:join([encode_where(E) || E <- List], " or "); - -encode_where({like, Field, Value}) -> - atom_to_list(Field) ++ " like " ++ encode(Value); - -encode_where({'<', Field, Value}) -> - atom_to_list(Field) ++ " < " ++ encode(Value); - -encode_where({'<=', Field, Value}) -> - atom_to_list(Field) ++ " <= " ++ encode(Value); - -encode_where({'>', Field, Value}) -> - atom_to_list(Field) ++ " > " ++ encode(Value); - -encode_where({'>=', Field, Value}) -> - atom_to_list(Field) ++ " >= " ++ encode(Value); - -encode_where({'in', Field, Values}) -> - InStr = string:join([encode(Value) || Value <- Values], ","), - atom_to_list(Field) ++ " in (" ++ InStr ++ ")"; - -encode_where({Field, Value}) -> - atom_to_list(Field) ++ " = " ++ encode(Value). - -to_tuple_records(_Fields, []) -> - []; - -to_tuple_records(Fields, Records) -> - [to_tuple_record(Fields, tuple_to_list(Record)) || Record <- Records]. - -to_tuple_record(Fields, Record) when length(Fields) == length(Record) -> - to_tuple_record(Fields, Record, []). - -to_tuple_record([], [], Acc) -> - Acc; - -to_tuple_record([_F|FT], [undefined|VT], Acc) -> - to_tuple_record(FT, VT, Acc); - -to_tuple_record([F|FT], [V|VT], Acc) -> - to_tuple_record(FT, VT, [{list_to_atom(binary_to_list(F)), V} | Acc]). - -%% Escape character that will confuse an SQL engine -%% Percent and underscore only need to be escaped for pattern matching like -%% statement -escape_like(S) when is_list(S) -> - [escape_like(C) || C <- S]; -escape_like($%) -> "\\%"; -escape_like($_) -> "\\_"; -escape_like(C) -> escape(C). - -%% Escape character that will confuse an SQL engine -escape(S) when is_list(S) -> - [escape(C) || C <- S]; -%% Characters to escape -escape($\0) -> "\\0"; -escape($\n) -> "\\n"; -escape($\t) -> "\\t"; -escape($\b) -> "\\b"; -escape($\r) -> "\\r"; -escape($') -> "\\'"; -escape($") -> "\\\""; -escape($\\) -> "\\\\"; -escape(C) -> C. - -encode(Val) -> - encode(Val, false). -encode(Val, false) when Val == undefined; Val == null -> - "NULL"; -encode(Val, true) when Val == undefined; Val == null -> - <<"NULL">>; -encode(Val, false) when is_binary(Val) -> - binary_to_list(quote(Val)); -encode(Val, true) when is_binary(Val) -> - quote(Val); -encode(Val, true) -> - list_to_binary(encode(Val,false)); -encode(Val, false) when is_atom(Val) -> - quote(atom_to_list(Val)); -encode(Val, false) when is_list(Val) -> - quote(Val); -encode(Val, false) when is_integer(Val) -> - integer_to_list(Val); -encode(Val, false) when is_float(Val) -> - [Res] = io_lib:format("~w", [Val]), - Res; -encode({datetime, Val}, AsBinary) -> - encode(Val, AsBinary); -encode({{Year, Month, Day}, {Hour, Minute, Second}}, false) -> - Res = two_digits([Year, Month, Day, Hour, Minute, Second]), - lists:flatten(Res); -encode({TimeType, Val}, AsBinary) - when TimeType == 'date'; - TimeType == 'time' -> - encode(Val, AsBinary); -encode({Time1, Time2, Time3}, false) -> - Res = two_digits([Time1, Time2, Time3]), - lists:flatten(Res); -encode(Val, _AsBinary) -> - {error, {unrecognized_value, Val}}. - -two_digits(Nums) when is_list(Nums) -> - [two_digits(Num) || Num <- Nums]; -two_digits(Num) -> - [Str] = io_lib:format("~b", [Num]), - case length(Str) of - 1 -> [$0 | Str]; - _ -> Str - end. - -%% Quote a string or binary value so that it can be included safely in a -%% MySQL query. -quote(String) when is_list(String) -> - [39 | lists:reverse([39 | quote(String, [])])]; %% 39 is $' -quote(Bin) when is_binary(Bin) -> - list_to_binary(quote(binary_to_list(Bin))). - -quote([], Acc) -> - Acc; -quote([0 | Rest], Acc) -> - quote(Rest, [$0, $\\ | Acc]); -quote([10 | Rest], Acc) -> - quote(Rest, [$n, $\\ | Acc]); -quote([13 | Rest], Acc) -> - quote(Rest, [$r, $\\ | Acc]); -quote([$\\ | Rest], Acc) -> - quote(Rest, [$\\ , $\\ | Acc]); -quote([39 | Rest], Acc) -> %% 39 is $' - quote(Rest, [39, $\\ | Acc]); %% 39 is $' -quote([34 | Rest], Acc) -> %% 34 is $" - quote(Rest, [34, $\\ | Acc]); %% 34 is $" -quote([26 | Rest], Acc) -> - quote(Rest, [$Z, $\\ | Acc]); -quote([C | Rest], Acc) -> - quote(Rest, [C | Acc]). - diff --git a/plugins/emysql/src/emysql_app.erl b/plugins/emysql/src/emysql_app.erl deleted file mode 100644 index 4be2e70db..000000000 --- a/plugins/emysql/src/emysql_app.erl +++ /dev/null @@ -1,27 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : emysql_app.erl -%%% Author : Ery Lee -%%% Purpose : mysql driver application -%%% Created : 21 May 2009 -%%% Updated : 11 Jan 2010 -%%% License : http://www.opengoss.com -%%% -%%% Copyright (C) 2007-2010, www.opengoss.com -%%%---------------------------------------------------------------------- --module(emysql_app). - --author('ery.lee@gmail.com'). - --behavior(application). - --export([start/0, start/2, stop/1]). - -start() -> - application:start(emysql). - -start(normal, _Args) -> - emysql_sup:start_link(application:get_all_env()). - -stop(_) -> - ok. - diff --git a/plugins/emysql/src/emysql_auth.erl b/plugins/emysql/src/emysql_auth.erl deleted file mode 100644 index 72aebfd3b..000000000 --- a/plugins/emysql/src/emysql_auth.erl +++ /dev/null @@ -1,102 +0,0 @@ --module(emysql_auth). - --export([make_auth/2, make_new_auth/3, password_old/2, password_new/2]). - -%%-------------------------------------------------------------------- -%% Macros -%%-------------------------------------------------------------------- --define(LONG_PASSWORD, 1). --define(LONG_FLAG, 4). --define(PROTOCOL_41, 512). --define(TRANSACTIONS, 8192). --define(SECURE_CONNECTION, 32768). --define(CONNECT_WITH_DB, 8). --define(MAX_PACKET_SIZE, 1000000). - -password_old(Password, Salt) -> - {P1, P2} = hash(Password), - {S1, S2} = hash(Salt), - Seed1 = P1 bxor S1, - Seed2 = P2 bxor S2, - List = rnd(9, Seed1, Seed2), - {L, [Extra]} = lists:split(8, List), - list_to_binary(lists:map(fun (E) -> E bxor (Extra - 64) end, L)). - -%% part of do_old_auth/4, which is part of mysql_init/4 -make_auth(User, Password) -> - Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?TRANSACTIONS, - Maxsize = 0, - UserB = list_to_binary(User), - PasswordB = Password, - <>. - -%% part of do_new_auth/4, which is part of mysql_init/4 -make_new_auth(User, Password, Database) -> - DBCaps = case Database of - none -> - 0; - _ -> - ?CONNECT_WITH_DB - end, - Caps = ?LONG_PASSWORD bor ?LONG_FLAG bor ?TRANSACTIONS bor - ?PROTOCOL_41 bor ?SECURE_CONNECTION bor DBCaps, - Maxsize = ?MAX_PACKET_SIZE, - UserB = list_to_binary(User), - PasswordL = size(Password), - DatabaseB = case Database of - none -> - <<>>; - _ -> - list_to_binary(Database) - end, - <>. - -hash(S) -> - hash(S, 1345345333, 305419889, 7). - -hash([C | S], N1, N2, Add) -> - N1_1 = N1 bxor (((N1 band 63) + Add) * C + N1 * 256), - N2_1 = N2 + ((N2 * 256) bxor N1_1), - Add_1 = Add + C, - hash(S, N1_1, N2_1, Add_1); -hash([], N1, N2, _Add) -> - Mask = (1 bsl 31) - 1, - {N1 band Mask , N2 band Mask}. - -rnd(N, Seed1, Seed2) -> - Mod = (1 bsl 30) - 1, - rnd(N, [], Seed1 rem Mod, Seed2 rem Mod). - -rnd(0, List, _, _) -> - lists:reverse(List); -rnd(N, List, Seed1, Seed2) -> - Mod = (1 bsl 30) - 1, - NSeed1 = (Seed1 * 3 + Seed2) rem Mod, - NSeed2 = (NSeed1 + Seed2 + 33) rem Mod, - Float = (float(NSeed1) / float(Mod))*31, - Val = trunc(Float)+64, - rnd(N - 1, [Val | List], NSeed1, NSeed2). - - -dualmap(_F, [], []) -> - []; -dualmap(F, [E1 | R1], [E2 | R2]) -> - [F(E1, E2) | dualmap(F, R1, R2)]. - -bxor_binary(B1, B2) -> - list_to_binary(dualmap(fun (E1, E2) -> - E1 bxor E2 - end, binary_to_list(B1), binary_to_list(B2))). - -password_new(Password, Salt) -> - Stage1 = crypto:sha(Password), - Stage2 = crypto:sha(Stage1), - Res = crypto:sha_final( - crypto:sha_update( - crypto:sha_update(crypto:sha_init(), Salt), - Stage2) - ), - bxor_binary(Res, Stage1). - diff --git a/plugins/emysql/src/emysql_conn.erl b/plugins/emysql/src/emysql_conn.erl deleted file mode 100644 index 124e23a9d..000000000 --- a/plugins/emysql/src/emysql_conn.erl +++ /dev/null @@ -1,739 +0,0 @@ -%%% File : emysql_conn.erl -%%% Author : Ery Lee -%%% Purpose : connection of mysql driver -%%% Created : 11 Jan 2010 -%%% License : http://www.opengoss.com -%%% -%%% Copyright (C) 2012, www.opengoss.com -%%%---------------------------------------------------------------------- --module(emysql_conn). - --include("emysql.hrl"). - --import(proplists, [get_value/2, get_value/3]). - --behaviour(gen_server). - -%% External exports --export([start_link/2, - info/1, - sqlquery/2, - sqlquery/3, - prepare/3, - execute/3, - execute/4, - unprepare/2]). - -%% Callback --export([init/1, - handle_call/3, - handle_cast/2, - handle_info/2, - terminate/2, - code_change/3]). - --record(state, { - id, - host, - port, - user, - password, - database, - encoding, - mysql_version, - recv_pid, - socket, - data}). - -%%-define(KEEPALIVE_QUERY, <<"SELECT 1;">>). - --define(SECURE_CONNECTION, 32768). - --define(MYSQL_QUERY_OP, 3). - -%CALL > CONNECT --define(CALL_TIMEOUT, 301000). - --define(CONNECT_TIMEOUT, 300000). - --define(MYSQL_4_0, 40). %% Support for MySQL 4.0.x - --define(MYSQL_4_1, 41). %% Support for MySQL 4.1.x et 5.0.x - -%%-------------------------------------------------------------------- -%% Function: start(Opts) -%% Descrip.: Starts a mysql_conn process that connects to a MySQL -%% server, logs in and chooses a database. -%% Returns : {ok, Pid} | {error, Reason} -%% Pid = pid() -%% Reason = string() -%%-------------------------------------------------------------------- -start_link(Id, Opts) -> - gen_server:start_link(?MODULE, [Id, Opts], []). - -info(Conn) -> - gen_server:call(Conn, info). - -%%-------------------------------------------------------------------- -%% Function: sqlquery(Query) -%% Queries = A single binary() query or a list of binary() queries. -%% If a list is provided, the return value is the return -%% of the last query, or the first query that has -%% returned an error. If an error occurs, execution of -%% the following queries is aborted. -%% From = pid() or term(), use a From of self() when -%% using this module for a single connection, -%% or pass the gen_server:call/3 From argument if -%% using a gen_server to do the querys (e.g. the -%% mysql_dispatcher) -%% Timeout = integer() | infinity, gen_server timeout value -%% Descrip.: Send a query or a list of queries and wait for the result -%% if running stand-alone (From = self()), but don't block -%% the caller if we are not running stand-alone -%% (From = gen_server From). -%% Returns : ok | (non-stand-alone mode) -%% {data, #mysql_result} | (stand-alone mode) -%% {updated, #mysql_result} | (stand-alone mode) -%% {error, #mysql_result} (stand-alone mode) -%% FieldInfo = term() -%% Rows = list() of [string()] -%% Reason = term() -%%-------------------------------------------------------------------- -sqlquery(Conn, Query) -> - sqlquery(Conn, Query, ?CALL_TIMEOUT). - -sqlquery(Conn, Query, Timeout) -> - call(Conn, {sqlquery, Query}, Timeout). - -prepare(Conn, Name, Stmt) -> - call(Conn, {prepare, Name, Stmt}). - -execute(Conn, Name, Params) -> - execute(Conn, Name, Params, ?CALL_TIMEOUT). - -execute(Conn, Name, Params, Timeout) -> - call(Conn, {execute, Name, Params}, Timeout). - -unprepare(Conn, Name) -> - call(Conn, {unprepare, Name}). - -%%-------------------------------------------------------------------- -%% Function: init(Host, Port, User, Password, Database, Parent) -%% Host = string() -%% Port = integer() -%% User = string() -%% Password = string() -%% Database = string() -%% Parent = pid() of process starting this mysql_conn -%% Descrip.: Connect to a MySQL server, log in and chooses a database. -%% Report result of this to Parent, and then enter loop() if -%% we were successfull. -%% Returns : void() | does not return -%%-------------------------------------------------------------------- -init([Id, Opts]) -> - put(queries, 0), - Host = get_value(host, Opts, "localhost"), - Port = get_value(port, Opts, 3306), - UserName = get_value(username, Opts, "root"), - Password = get_value(password, Opts, "public"), - Database = get_value(database, Opts), - Encoding = get_value(encoding, Opts, utf8), - case emysql_recv:start_link(Host, Port) of - {ok, RecvPid, Sock} -> - case mysql_init(Sock, RecvPid, UserName, Password) of - {ok, Version} -> - Db = iolist_to_binary(Database), - case do_query(Sock, RecvPid, <<"use ", Db/binary>>, Version) of - {error, #mysql_result{error = Error} = _MySQLRes} -> - error_logger:error_msg("emysql_conn: use '~p' error: ~p", [Database, Error]), - {stop, using_db_error}; - {_ResultType, _MySQLRes} -> - emysql:pool(Id), %pool it - pg2:create(emysql_conn), - pg2:join(emysql_conn, self()), - EncodingBinary = list_to_binary(atom_to_list(Encoding)), - do_query(Sock, RecvPid, <<"set names '", EncodingBinary/binary, "'">>, Version), - State = #state{ - id = Id, - host = Host, - port = Port, - user = UserName, - password = Password, - database = Database, - encoding = Encoding, - mysql_version = Version, - recv_pid = RecvPid, - socket = Sock, - data = <<>>}, - {ok, State} - end; - {error, Reason} -> - {stop, {login_failed, Reason}} - end; - {error, Reason} -> - {stop, Reason} - end. - -handle_call(info, _From, #state{id = Id} = State) -> - Reply = {Id, self(), get(queries)}, - {reply, Reply, State}; - -handle_call({sqlquery, Query}, _From, #state{socket = Socket, - recv_pid = RecvPid, mysql_version = Ver} = State) -> - put(queries, get(queries) + 1), - case do_query(Socket, RecvPid, Query, Ver) of - {error, mysql_timeout} = Err -> - {stop, mysql_timeout, Err, State}; - Res -> - {reply, Res, State} - end; - -handle_call({prepare, Name, Stmt}, _From, #state{socket = Socket, - recv_pid = RecvPid, mysql_version = Ver} = State) -> - - case do_prepare(Socket, RecvPid, Name, Stmt, Ver) of - {error, mysql_timeout} -> - {stop, mysql_timeout, State}; - _ -> - {reply, ok, State} - end; - -handle_call({unprepare, Name}, _From, #state{socket = Socket, - recv_pid = RecvPid, mysql_version = Ver} = State) -> - case do_unprepare(Socket, RecvPid, Name, Ver) of - {error, mysql_timeout} -> - {stop, mysql_timeout, State}; - _ -> - {reply, ok, State} - end; - -handle_call({execute, Name, Params}, _From, #state{socket = Socket, - recv_pid = RecvPid, mysql_version = Ver} = State) -> - case do_execute(Socket, RecvPid, Name, Params, Ver) of - {error, mysql_timeout} = Err -> - {stop, mysql_timeout, Err, State}; - Res -> - {reply, Res, State} - end; - -handle_call(Req, _From, State) -> - error_logger:error_msg("badreq to emysql_conn: ~p", [Req]), - {reply, {error, badreq}, State}. - -handle_cast(_Msg, State) -> - {noreply, State}. - -handle_info({mysql_recv, _RecvPid, data, _Packet, SeqNum}, State) -> - error_logger:error_msg("unexpected mysql_recv: seq_num = ~p", [SeqNum]), - {noreply, State}; - -handle_info({mysql_recv, _RecvPid, closed, E}, State) -> - error_logger:error_msg("mysql socket closed: ~p", [E]), - {stop, socket_closed, State}; - -handle_info(_Info, State) -> - {noreply, State}. - -terminate(_Reason, _State) -> - ok. - -code_change(_OldVsn, State, _Extra) -> - {ok, State}. - -do_queries(Sock, RecvPid, Queries, Version) -> - catch - lists:foldl( - fun(Query, _LastResponse) -> - case do_query(Sock, RecvPid, Query, Version) of - {error, _} = Err -> throw(Err); - Res -> Res - end - end, ok, Queries). - -do_query(Sock, RecvPid, Query, Version) -> - Query1 = iolist_to_binary(Query), - %?DEBUG("sqlquery ~p (id ~p)", [Query1, RecvPid]), - Packet = <>, - case do_send(Sock, Packet, 0) of - ok -> - get_query_response(RecvPid, Version); - {error, Reason} -> - {error, Reason} - end. - -do_prepare(Socket, RecvPid, Name, Stmt, Ver) -> - NameBin = atom_to_binary(Name), - StmtBin = <<"PREPARE ", NameBin/binary, " FROM '", Stmt/binary, "'">>, - do_query(Socket, RecvPid, StmtBin, Ver). - -do_execute(Socket, RecvPid, Name, Params, Ver) -> - Stmts = make_statements(Name, Params), - do_queries(Socket, RecvPid, Stmts, Ver). - -do_unprepare(Socket, RecvPid, Name, Ver) -> - NameBin = atom_to_binary(Name), - StmtBin = <<"UNPREPARE ", NameBin/binary>>, - do_query(Socket, RecvPid, StmtBin, Ver). - -make_statements(Name, []) -> - NameBin = atom_to_binary(Name), - [<<"EXECUTE ", NameBin/binary>>]; - -make_statements(Name, Params) -> - NumParams = length(Params), - ParamNums = lists:seq(1, NumParams), - NameBin = atom_to_binary(Name), - ParamNames = - lists:foldl( - fun(Num, Acc) -> - ParamName = [$@ | integer_to_list(Num)], - if Num == 1 -> - ParamName ++ Acc; - true -> - [$, | ParamName] ++ Acc - end - end, [], lists:reverse(ParamNums)), - ParamNamesBin = list_to_binary(ParamNames), - ExecStmt = <<"EXECUTE ", NameBin/binary, " USING ", - ParamNamesBin/binary>>, - - ParamVals = lists:zip(ParamNums, Params), - Stmts = lists:foldl( - fun({Num, Val}, Acc) -> - NumBin = emysql:encode(Num, true), - ValBin = emysql:encode(Val, true), - [<<"SET @", NumBin/binary, "=", ValBin/binary>> | Acc] - end, [ExecStmt], lists:reverse(ParamVals)), - Stmts. - -atom_to_binary(Val) -> - <<_:4/binary, Bin/binary>> = term_to_binary(Val), - Bin. - -%%-------------------------------------------------------------------- -%% authentication -%%-------------------------------------------------------------------- -do_old_auth(Sock, RecvPid, SeqNum, User, Password, Salt1) -> - Auth = emysql_auth:password_old(Password, Salt1), - Packet = emysql_auth:make_auth(User, Auth), - do_send(Sock, Packet, SeqNum), - do_recv(RecvPid, SeqNum). - -do_new_auth(Sock, RecvPid, SeqNum, User, Password, Salt1, Salt2) -> - Auth = emysql_auth:password_new(Password, Salt1 ++ Salt2), - Packet2 = emysql_auth:make_new_auth(User, Auth, none), - do_send(Sock, Packet2, SeqNum), - case do_recv(RecvPid, SeqNum) of - {ok, Packet3, SeqNum2} -> - case Packet3 of - <<254:8>> -> - AuthOld = emysql_auth:password_old(Password, Salt1), - do_send(Sock, <>, SeqNum2 + 1), - do_recv(RecvPid, SeqNum2 + 1); - _ -> - {ok, Packet3, SeqNum2} - end; - {error, Reason} -> - {error, Reason} - end. - -%%-------------------------------------------------------------------- -%% Function: mysql_init(Sock, RecvPid, User, Password) -%% Sock = term(), gen_tcp socket -%% RecvPid = pid(), mysql_recv process -%% User = string() -%% Password = string() -%% LogFun = undefined | function() with arity 3 -%% Descrip.: Try to authenticate on our new socket. -%% Returns : ok | {error, Reason} -%% Reason = string() -%%-------------------------------------------------------------------- -mysql_init(Sock, RecvPid, User, Password) -> - case do_recv(RecvPid, undefined) of - {ok, Packet, InitSeqNum} -> - {Version, Salt1, Salt2, Caps} = greeting(Packet), - %?DEBUG("version: ~p, ~p, ~p, ~p", [Version, Salt1, Salt2, Caps]), - AuthRes = - case Caps band ?SECURE_CONNECTION of - ?SECURE_CONNECTION -> - do_new_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, Salt1, Salt2); - _ -> - do_old_auth(Sock, RecvPid, InitSeqNum + 1, User, Password, Salt1) - end, - case AuthRes of - {ok, <<0:8, _Rest/binary>>, _RecvNum} -> - {ok,Version}; - {ok, <<255:8, _Code:16/little, Message/binary>>, _RecvNum} -> - {error, binary_to_list(Message)}; - {ok, RecvPacket, _RecvNum} -> - {error, binary_to_list(RecvPacket)}; - {error, Reason} -> - %?ERROR("init failed receiving data : ~p", [Reason]), - {error, Reason} - end; - {error, Reason} -> - {error, Reason} - end. - -greeting(Packet) -> - <<_Protocol:8, Rest/binary>> = Packet, - {Version, Rest2} = asciz(Rest), - <<_TreadID:32/little, Rest3/binary>> = Rest2, - {Salt, Rest4} = asciz(Rest3), - <> = Rest4, - <<_ServerChar:16/binary-unit:8, Rest6/binary>> = Rest5, - {Salt2, _Rest7} = asciz(Rest6), - %?DEBUG("greeting version ~p (protocol ~p) salt ~p caps ~p serverchar ~p" - %"salt2 ~p", - %[Version, Protocol, Salt, Caps, ServerChar, Salt2]), - {normalize_version(Version), Salt, Salt2, Caps}. - -%% part of greeting/2 -asciz(Data) when is_binary(Data) -> - asciz_binary(Data, []); -asciz(Data) when is_list(Data) -> - {String, [0 | Rest]} = lists:splitwith(fun (C) -> - C /= 0 - end, Data), - {String, Rest}. - -%% @doc Find the first zero-byte in Data and add everything before it -%% to Acc, as a string. -%% -%% @spec asciz_binary(Data::binary(), Acc::list()) -> -%% {NewList::list(), Rest::binary()} -asciz_binary(<<>>, Acc) -> - {lists:reverse(Acc), <<>>}; -asciz_binary(<<0:8, Rest/binary>>, Acc) -> - {lists:reverse(Acc), Rest}; -asciz_binary(<>, Acc) -> - asciz_binary(Rest, [C | Acc]). - -%%-------------------------------------------------------------------- -%% Function: get_query_response(RecvPid) -%% RecvPid = pid(), mysql_recv process -%% Version = integer(), Representing MySQL version used -%% Descrip.: Wait for frames until we have a complete query response. -%% Returns : {data, #mysql_result} -%% {updated, #mysql_result} -%% {error, #mysql_result} -%% FieldInfo = list() of term() -%% Rows = list() of [string()] -%% AffectedRows = int() -%% Reason = term() -%%-------------------------------------------------------------------- -get_query_response(RecvPid, Version) -> - case do_recv(RecvPid, undefined) of - {ok, <>, _} -> - case Fieldcount of - 0 -> - %% No Tabular data - {AffectedRows, Rest1} = decode_length_binary(Rest), - {InsertId, _} = decode_length_binary(Rest1), - {updated, #mysql_result{insert_id = InsertId, affectedrows=AffectedRows}}; - 255 -> - <<_Code:16/little, Message/binary>> = Rest, - {error, #mysql_result{error=Message}}; - _ -> - %% Tabular data received - case get_fields(RecvPid, [], Version) of - {ok, Fields} -> - case get_rows(Fields, RecvPid, []) of - {ok, Rows} -> - {data, #mysql_result{fieldinfo=Fields, - rows=Rows}}; - {error, Reason} -> - {error, Reason} - end; - {error, Reason} -> - {error, Reason} - end - end; - {error, Reason} -> - {error, Reason} - end. - -decode_length_binary(<>) -> - if - Len =< 251 -> - {Len, Rest}; - Len == 252 -> %two bytes - <> = Rest, - {Val, Rest1}; - Len == 253 -> %three - <> = Rest, - {Val, Rest1}; - Len == 254 -> %eight - <> = Rest, - {Val, Rest1}; - true -> - %?ERROR("affectedrows: ~p", [Len]), - {0, Rest} - end. - -%%-------------------------------------------------------------------- -%% Function: do_recv(RecvPid, SeqNum) -%% RecvPid = pid(), mysql_recv process -%% SeqNum = undefined | integer() -%% Descrip.: Wait for a frame decoded and sent to us by RecvPid. -%% Either wait for a specific frame if SeqNum is an integer, -%% or just any frame if SeqNum is undefined. -%% Returns : {ok, Packet, Num} | -%% {error, Reason} -%% Reason = term() -%% -%% Note : Only to be used externally by the 'mysql_auth' module. -%%-------------------------------------------------------------------- -do_recv(RecvPid, SeqNum) when SeqNum == undefined -> - receive - {mysql_recv, RecvPid, data, Packet, Num} -> - {ok, Packet, Num}; - {mysql_recv, RecvPid, closed, _E} -> - {error, socket_closed} - after ?CONNECT_TIMEOUT -> - {error, mysql_timeout} - end; - -do_recv(RecvPid, SeqNum) when is_integer(SeqNum) -> - ResponseNum = SeqNum + 1, - receive - {mysql_recv, RecvPid, data, Packet, ResponseNum} -> - {ok, Packet, ResponseNum}; - {mysql_recv, RecvPid, closed, _E} -> - {error, socket_closed} - after ?CONNECT_TIMEOUT -> - {error, mysql_timeout} - end. - -call(Conn, Req) -> - gen_server:call(Conn, Req). - -call(Conn, Req, Timeout) -> - gen_server:call(Conn, Req, Timeout). - -%%-------------------------------------------------------------------- -%% Function: get_fields(RecvPid, [], Version) -%% RecvPid = pid(), mysql_recv process -%% Version = integer(), Representing MySQL version used -%% Descrip.: Received and decode field information. -%% Returns : {ok, FieldInfo} | -%% {error, Reason} -%% FieldInfo = list() of term() -%% Reason = term() -%%-------------------------------------------------------------------- -%% Support for MySQL 4.0.x: -get_fields(RecvPid, Res, ?MYSQL_4_0) -> - case do_recv(RecvPid, undefined) of - {ok, Packet, _Num} -> - case Packet of - <<254:8>> -> - {ok, lists:reverse(Res)}; - <<254:8, Rest/binary>> when size(Rest) < 8 -> - {ok, lists:reverse(Res)}; - _ -> - {Table, Rest} = get_with_length(Packet), - {Field, Rest2} = get_with_length(Rest), - {LengthB, Rest3} = get_with_length(Rest2), - LengthL = size(LengthB) * 8, - <> = LengthB, - {Type, Rest4} = get_with_length(Rest3), - {_Flags, _Rest5} = get_with_length(Rest4), - This = {Table, - Field, - Length, - %% TODO: Check on MySQL 4.0 if types are specified - %% using the same 4.1 formalism and could - %% be expanded to atoms: - Type}, - get_fields(RecvPid, [This | Res], ?MYSQL_4_0) - end; - {error, Reason} -> - {error, Reason} - end; -%% Support for MySQL 4.1.x and 5.x: -get_fields(RecvPid, Res, ?MYSQL_4_1) -> - case do_recv(RecvPid, undefined) of - {ok, Packet, _Num} -> - case Packet of - <<254:8>> -> - {ok, lists:reverse(Res)}; - <<254:8, Rest/binary>> when size(Rest) < 8 -> - {ok, lists:reverse(Res)}; - _ -> - {_Catalog, Rest} = get_with_length(Packet), - {_Database, Rest2} = get_with_length(Rest), - {Table, Rest3} = get_with_length(Rest2), - %% OrgTable is the real table name if Table is an alias - {_OrgTable, Rest4} = get_with_length(Rest3), - {Field, Rest5} = get_with_length(Rest4), - %% OrgField is the real field name if Field is an alias - {_OrgField, Rest6} = get_with_length(Rest5), - - <<_Metadata:8/little, _Charset:16/little, - Length:32/little, Type:8/little, - _Flags:16/little, _Decimals:8/little, - _Rest7/binary>> = Rest6, - - This = {Table, - Field, - Length, - get_field_datatype(Type)}, - get_fields(RecvPid, [This | Res], ?MYSQL_4_1) - end; - {error, Reason} -> - {error, Reason} - end. - -%%-------------------------------------------------------------------- -%% Function: get_rows(N, RecvPid, []) -%% N = integer(), number of rows to get -%% RecvPid = pid(), mysql_recv process -%% Descrip.: Receive and decode a number of rows. -%% Returns : {ok, Rows} | -%% {error, Reason} -%% Rows = list() of [string()] -%%-------------------------------------------------------------------- -get_rows(Fields, RecvPid, Res) -> - case do_recv(RecvPid, undefined) of - {ok, Packet, _Num} -> - case Packet of - <<254:8, Rest/binary>> when size(Rest) < 8 -> - {ok, lists:reverse(Res)}; - _ -> - {ok, This} = get_row(Fields, Packet, []), - get_rows(Fields, RecvPid, [This | Res]) - end; - {error, Reason} -> - {error, Reason} - end. - -%% part of get_rows/4 -get_row([], _Data, Res) -> - {ok, lists:reverse(Res)}; -get_row([Field | OtherFields], Data, Res) -> - {Col, Rest} = get_with_length(Data), - This = case Col of - null -> - undefined; - _ -> - convert_type(Col, element(4, Field)) - end, - get_row(OtherFields, Rest, [This | Res]). - -get_with_length(<<251:8, Rest/binary>>) -> - {null, Rest}; -get_with_length(<<252:8, Length:16/little, Rest/binary>>) -> - split_binary(Rest, Length); -get_with_length(<<253:8, Length:24/little, Rest/binary>>) -> - split_binary(Rest, Length); -get_with_length(<<254:8, Length:64/little, Rest/binary>>) -> - split_binary(Rest, Length); -get_with_length(<>) when Length < 251 -> - split_binary(Rest, Length). - - -%%-------------------------------------------------------------------- -%% Function: do_send(Sock, Packet, SeqNum) -%% Sock = term(), gen_tcp socket -%% Packet = binary() -%% SeqNum = integer(), packet sequence number -%% Descrip.: Send a packet to the MySQL server. -%% Returns : result of gen_tcp:send/2 -%%-------------------------------------------------------------------- -do_send(Sock, Packet, SeqNum) when is_binary(Packet), is_integer(SeqNum) -> - Data = <<(size(Packet)):24/little, SeqNum:8, Packet/binary>>, - gen_tcp:send(Sock, Data). - -%%-------------------------------------------------------------------- -%% Function: normalize_version(Version) -%% Version = string() -%% Descrip.: Return a flag corresponding to the MySQL version used. -%% The protocol used depends on this flag. -%% Returns : Version = string() -%%-------------------------------------------------------------------- -normalize_version([$4,$.,$0|_T]) -> - %?DEBUG("switching to MySQL 4.0.x protocol.", []), - ?MYSQL_4_0; -normalize_version([$4,$.,$1|_T]) -> - ?MYSQL_4_1; -normalize_version([$5|_T]) -> - %% MySQL version 5.x protocol is compliant with MySQL 4.1.x: - ?MYSQL_4_1; -normalize_version([$6|_T]) -> - %% MySQL version 6.x protocol is compliant with MySQL 4.1.x: - ?MYSQL_4_1; -normalize_version(_Other) -> - %?ERROR("MySQL version '~p' not supported: MySQL Erlang module " - % "might not work correctly.", [Other]), - %% Error, but trying the oldest protocol anyway: - ?MYSQL_4_0. - -%%-------------------------------------------------------------------- -%% Function: get_field_datatype(DataType) -%% DataType = integer(), MySQL datatype -%% Descrip.: Return MySQL field datatype as description string -%% Returns : String, MySQL datatype -%%-------------------------------------------------------------------- -get_field_datatype(0) -> 'DECIMAL'; -get_field_datatype(1) -> 'TINY'; -get_field_datatype(2) -> 'SHORT'; -get_field_datatype(3) -> 'LONG'; -get_field_datatype(4) -> 'FLOAT'; -get_field_datatype(5) -> 'DOUBLE'; -get_field_datatype(6) -> 'NULL'; -get_field_datatype(7) -> 'TIMESTAMP'; -get_field_datatype(8) -> 'LONGLONG'; -get_field_datatype(9) -> 'INT24'; -get_field_datatype(10) -> 'DATE'; -get_field_datatype(11) -> 'TIME'; -get_field_datatype(12) -> 'DATETIME'; -get_field_datatype(13) -> 'YEAR'; -get_field_datatype(14) -> 'NEWDATE'; -get_field_datatype(246) -> 'NEWDECIMAL'; -get_field_datatype(247) -> 'ENUM'; -get_field_datatype(248) -> 'SET'; -get_field_datatype(249) -> 'TINYBLOB'; -get_field_datatype(250) -> 'MEDIUM_BLOG'; -get_field_datatype(251) -> 'LONG_BLOG'; -get_field_datatype(252) -> 'BLOB'; -get_field_datatype(253) -> 'VAR_STRING'; -get_field_datatype(254) -> 'STRING'; -get_field_datatype(255) -> 'GEOMETRY'. - -convert_type(Val, ColType) -> - case ColType of - T when T == 'TINY'; - T == 'SHORT'; - T == 'LONG'; - T == 'LONGLONG'; - T == 'INT24'; - T == 'YEAR' -> - list_to_integer(binary_to_list(Val)); - T when T == 'TIMESTAMP'; - T == 'DATETIME' -> - {ok, [Year, Month, Day, Hour, Minute, Second], _Leftovers} = - io_lib:fread("~d-~d-~d ~d:~d:~d", binary_to_list(Val)), - {datetime, {{Year, Month, Day}, {Hour, Minute, Second}}}; - 'TIME' -> - {ok, [Hour, Minute, Second], _Leftovers} = - io_lib:fread("~d:~d:~d", binary_to_list(Val)), - {time, {Hour, Minute, Second}}; - 'DATE' -> - {ok, [Year, Month, Day], _Leftovers} = - io_lib:fread("~d-~d-~d", binary_to_list(Val)), - {date, {Year, Month, Day}}; - T when T == 'DECIMAL'; - T == 'NEWDECIMAL'; - T == 'FLOAT'; - T == 'DOUBLE' -> - {ok, [Num], _Leftovers} = - case io_lib:fread("~f", binary_to_list(Val)) of - {error, _} -> - io_lib:fread("~d", binary_to_list(Val)); - Res -> - Res - end, - Num; - _Other -> - Val - end. diff --git a/plugins/emysql/src/emysql_recv.erl b/plugins/emysql/src/emysql_recv.erl deleted file mode 100644 index 5577c2e9a..000000000 --- a/plugins/emysql/src/emysql_recv.erl +++ /dev/null @@ -1,130 +0,0 @@ -%%%------------------------------------------------------------------- -%%% File : emysql_recv.erl -%%% Author : Fredrik Thulin -%%% Descrip.: Handles data being received on a MySQL socket. Decodes -%%% per-row framing and sends each row to parent. -%%% -%%% Created : 4 Aug 2005 by Fredrik Thulin -%%% -%%% Note : All MySQL code was written by Magnus Ahltorp, originally -%%% in the file mysql.erl - I just moved it here. -%%% -%%% Copyright (c) 2001-2004 Kungliga Tekniska -%%% See the file COPYING -%%% -%%% Signals this receiver process can send to it's parent -%%% (the parent is a mysql_conn connection handler) : -%%% -%%% {mysql_recv, self(), data, Packet, Num} -%%% {mysql_recv, self(), closed, {error, Reason}} -%%% {mysql_recv, self(), closed, normal} -%%% -%%% Internally (from inside init/4 to start_link/4) the -%%% following signals may be sent to the parent process : -%%% -%%% {mysql_recv, self(), init, {ok, Sock}} -%%% {mysql_recv, self(), init, {error, E}} -%%% -%%%------------------------------------------------------------------- --module(emysql_recv). - -%%-------------------------------------------------------------------- -%% External exports (should only be used by the 'mysql_conn' module) -%%-------------------------------------------------------------------- --export([start_link/2]). - -%callback --export([init/3]). - --record(state, { - socket, - parent, - log_fun, - data}). - --define(SECURE_CONNECTION, 32768). - --define(CONNECT_TIMEOUT, 10000). - -%%-------------------------------------------------------------------- -%% Function: start_link(Host, Port, Parent) -%% Host = string() -%% Port = integer() -%% Parent = pid(), process that should get received frames -%% Descrip.: Start a process that connects to Host:Port and waits for -%% data. When it has received a MySQL frame, it sends it to -%% Parent and waits for the next frame. -%% Returns : {ok, RecvPid, Socket} | -%% {error, Reason} -%% RecvPid = pid(), receiver process pid -%% Socket = term(), gen_tcp socket -%% Reason = atom() | string() -%%-------------------------------------------------------------------- -start_link(Host, Port) -> - proc_lib:start_link(?MODULE, init, [self(), Host, Port]). - -%%-------------------------------------------------------------------- -%% Function: init((Host, Port, Parent) -%% Host = string() -%% Port = integer() -%% Parent = pid(), process that should get received frames -%% Descrip.: Connect to Host:Port and then enter receive-loop. -%% Returns : error | never returns -%%-------------------------------------------------------------------- -init(Parent, Host, Port) -> - case gen_tcp:connect(Host, Port, [binary, {packet, 0}]) of - {ok, Sock} -> - proc_lib:init_ack(Parent, {ok, self(), Sock}), - loop(#state{socket = Sock, parent = Parent, data = <<>>}); - {error, Reason} -> - proc_lib:init_ack(Parent, {error, Reason}) - end. - -%%-------------------------------------------------------------------- -%% Function: loop(State) -%% State = state record() -%% Descrip.: The main loop. Wait for data from our TCP socket and act -%% on received data or signals that our socket was closed. -%% Returns : error | never returns -%%-------------------------------------------------------------------- -loop(State) -> - Sock = State#state.socket, - receive - {tcp, Sock, InData} -> - NewData = list_to_binary([State#state.data, InData]), - %% send data to parent if we have enough data - Rest = sendpacket(State#state.parent, NewData), - loop(State#state{data = Rest}); - {tcp_error, Sock, Reason} -> - State#state.parent ! {mysql_recv, self(), closed, {error, Reason}}, - error; - {tcp_closed, Sock} -> - State#state.parent ! {mysql_recv, self(), closed, normal}, - error; - _Other -> %maybe system message - loop(State) - end. - -%%-------------------------------------------------------------------- -%% Function: sendpacket(Parent, Data) -%% Parent = pid() -%% Data = binary() -%% Descrip.: Check if we have received one or more complete frames by -%% now, and if so - send them to Parent. -%% Returns : Rest = binary() -%%-------------------------------------------------------------------- -%% send data to parent if we have enough data -sendpacket(Parent, Data) -> - case Data of - <> -> - if - Length =< size(D) -> - {Packet, Rest} = split_binary(D, Length), - Parent ! {mysql_recv, self(), data, Packet, Num}, - sendpacket(Parent, Rest); - true -> - Data - end; - _ -> - Data - end. diff --git a/plugins/emysql/src/emysql_sup.erl b/plugins/emysql/src/emysql_sup.erl deleted file mode 100644 index b915f3593..000000000 --- a/plugins/emysql/src/emysql_sup.erl +++ /dev/null @@ -1,34 +0,0 @@ -%%%---------------------------------------------------------------------- -%%% File : emysql_sup.erl -%%% Author : Ery Lee -%%% Purpose : Mysql driver supervisor -%%% Created : 21 May 2009 -%%% Updated : 11 Jan 2010 -%%% License : http://www.opengoss.com -%%% -%%% Copyright (C) 2012, www.opengoss.com -%%%---------------------------------------------------------------------- --module(emysql_sup). - --author('ery.lee@gmail.com'). - --behavior(supervisor). - -%% API --export([start_link/1, init/1]). - -start_link(Opts) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, Opts). - -init(Opts) -> - PoolSize = proplists:get_value(pool, Opts, - erlang:system_info(schedulers)), - {ok, {{one_for_one, 10, 10}, - [{emysql, {emysql, start_link, [PoolSize]}, transient, - 16#ffffffff, worker, [emysql]} | - [{I, {emysql_conn, start_link, [I, Opts]}, transient, 16#ffffffff, - worker, [emysql_conn, emysql_recv]} || I <- lists:seq(1, PoolSize)]] - } - }. - - diff --git a/rebar.config b/rebar.config index 7755b06d7..55093b0f0 100644 --- a/rebar.config +++ b/rebar.config @@ -20,7 +20,6 @@ {sub_dirs, [ "rel", - "apps/*/", "plugins/*/"]}. {deps, [ diff --git a/rel/files/emqttd.config b/rel/files/emqttd.config index 6a723470c..eabd4e119 100644 --- a/rel/files/emqttd.config +++ b/rel/files/emqttd.config @@ -1,4 +1,4 @@ -%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- +% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et [{kernel, [{start_timer, true}, @@ -45,8 +45,22 @@ {auth, [ %% Authentication with username, password %{username, []}, + %% Authentication with clientid %{clientid, [{password, no}, {file, "etc/clients.config"}]}, + + %% Authentication with LDAP + % {ldap, [ + % {servers, ["localhost"]}, + % {port, 389}, + % {timeout, 30}, + % {user_dn, "uid=$u,ou=People,dc=example,dc=com"}, + % {ssl, fasle}, + % {sslopts, [ + % {"certfile", "ssl.crt"}, + % {"keyfile", "ssl.key"}]} + % ]}, + %% Allow all {anonymous, []} ]}, @@ -73,12 +87,40 @@ ]}, %% Session {session, [ - %% Expired after 24 hours - {expires, 24}, - %% Max offline message queue - {max_queue, 100}, - %% Store Qos0? - {store_qos0, false} + %% Expired after 2 days + {expired_after, 48}, + + %% Max number of QoS 1 and 2 messages that can be “in flight” at one time. + %% 0 means no limit + {max_inflight, 100}, + + %% Max retries for unack Qos1/2 messages + {unack_retries, 3}, + + %% Retry after 4, 8, 16 seconds + {unack_timeout, 4}, + + %% Awaiting PUBREL Timeout + {await_rel_timeout, 8}, + + %% Max Packets that Awaiting PUBREL, 0 means no limit + {max_awaiting_rel, 0} + + ]}, + %% Session + {queue, [ + %% Max queue length. enqueued messages when persistent client disconnected, + %% or inflight window is full. + {max_length, 1000}, + + %% Low-water mark of queued messsages + {low_watermark, 0.2}, + + %% High-water mark of queued messsages + {high_watermark, 0.6}, + + %% Queue Qos0 messages? + {queue_qos0, true} ]} ]}, %% Broker Options @@ -101,15 +143,21 @@ %% Bridge {bridge, [ %%TODO: bridge queue size - {max_queue_len, 1000}, + {max_queue_len, 10000}, + %% Ping Interval of bridge node {ping_down_interval, 1} %seconds ]} ]}, %% Modules {modules, [ + %% Client presence management module. + %% Publish messages when client connected or disconnected + {presence, [{qos, 0}]}, + %% Subscribe topics automatically when client connected {autosub, [{"$Q/client/$c", 0}]} + %% Rewrite rules %% {rewrite, [{file, "etc/rewrite.config"}]} @@ -155,10 +203,7 @@ %% Maximum number of concurrent clients {max_clients, 512}, %% Socket Access Control - {access, [ - {allow, "127.0.0.1"}, - {deny, all} - ]}, + {access, [{allow, all}]}, %% Socket Options {sockopts, [ {backlog, 1024} diff --git a/rel/files/plugins.config b/rel/files/plugins.config index 57dcb8abf..ce65e0625 100644 --- a/rel/files/plugins.config +++ b/rel/files/plugins.config @@ -9,9 +9,13 @@ % {encoding, utf8} % ]}, % {emqttd_auth_mysql, [ -% {user_table, mqtt_users} -% ]} -% +% {user_table, mqtt_users}, +% {password_hash, plain}, +% {field_mapper, [ +% {username, username}, +% {password, password} +% ]} +% ]}, % {emqttd_dashboard, [ % {listener, % {http, 18083, [ diff --git a/rel/reltool.config b/rel/reltool.config index ac5d09e83..42e4755ef 100644 --- a/rel/reltool.config +++ b/rel/reltool.config @@ -1,26 +1,30 @@ %% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ft=erlang ts=4 sw=4 et {sys, [ - {lib_dirs, ["../apps", "../deps", "../plugins"]}, + {lib_dirs, ["../deps"]}, {erts, [{mod_cond, derived}, {app_file, strip}]}, {app_file, strip}, - {rel, "emqttd", "0.7.0", + {rel, "emqttd", git, [ kernel, stdlib, sasl, + asn1, syntax_tools, ssl, crypto, %mnesia, + eldap, + xmerl, os_mon, inets, goldrush, + compiler, lager, + {gen_logger, load}, gproc, esockd, mochiweb, - {emqtt, load}, emqttd ]}, {rel, "start_clean", "", @@ -30,7 +34,7 @@ ]}, {boot_rel, "emqttd"}, {profile, embedded}, - {incl_cond, derived}, + {incl_cond, exclude}, %{mod_cond, derived}, {excl_archive_filters, [".*"]}, %% Do not archive built libs {excl_sys_filters, ["^bin/(?!start_clean.boot)", @@ -40,20 +44,24 @@ {app, kernel, [{incl_cond, include}]}, {app, stdlib, [{incl_cond, include}]}, {app, sasl, [{incl_cond, include}]}, - {app, crypto, [{mod_cond, app}, {incl_cond, include}]}, - {app, ssl, [{mod_cond, app}, {incl_cond, include}]}, - {app, os_mon, [{mod_cond, app}, {incl_cond, include}]}, - {app, syntax_tools, [{mod_cond, app}, {incl_cond, include}]}, - {app, public_key, [{mod_cond, app}, {incl_cond, include}]}, - {app, mnesia, [{mod_cond, app}, {incl_cond, include}]}, - {app, inets, [{mod_cond, app},{incl_cond, include}]}, - {app, goldrush, [{mod_cond, app}, {incl_cond, include}]}, - {app, lager, [{mod_cond, app}, {incl_cond, include}]}, - {app, gproc, [{mod_cond, app}, {incl_cond, include}]}, + {app, asn1, [{incl_cond, include}]}, + {app, crypto, [{incl_cond, include}]}, + {app, ssl, [{incl_cond, include}]}, + {app, xmerl, [{incl_cond, include}]}, + {app, os_mon, [{incl_cond, include}]}, + {app, syntax_tools, [{incl_cond, include}]}, + {app, public_key, [{incl_cond, include}]}, + {app, mnesia, [{incl_cond, include}]}, + {app, eldap, [{incl_cond, include}]}, + {app, inets, [{incl_cond, include}]}, + {app, compiler, [{incl_cond, include}]}, + {app, goldrush, [{incl_cond, include}]}, + {app, gen_logger, [{incl_cond, include}]}, + {app, lager, [{incl_cond, include}]}, + {app, gproc, [{incl_cond, include}]}, {app, esockd, [{mod_cond, app}, {incl_cond, include}]}, {app, mochiweb, [{mod_cond, app}, {incl_cond, include}]}, - {app, emqtt, [{mod_cond, app}, {incl_cond, include}]}, - {app, emqttd, [{mod_cond, app}, {incl_cond, include}]} + {app, emqttd, [{mod_cond, app}, {incl_cond, include}, {lib_dir, ".."}]} ]}. {target_dir, "emqttd"}. diff --git a/apps/emqttd/src/emqttd.app.src b/src/emqttd.app.src similarity index 91% rename from apps/emqttd/src/emqttd.app.src rename to src/emqttd.app.src index f598c13ba..024fcdac9 100644 --- a/apps/emqttd/src/emqttd.app.src +++ b/src/emqttd.app.src @@ -1,7 +1,7 @@ {application, emqttd, [ {description, "Erlang MQTT Broker"}, - {vsn, "0.8.0"}, + {vsn, git}, {modules, []}, {registered, []}, {applications, [kernel, diff --git a/apps/emqttd/src/emqttd.erl b/src/emqttd.erl similarity index 95% rename from apps/emqttd/src/emqttd.erl rename to src/emqttd.erl index 30faa3e2d..ce5f1faa5 100644 --- a/apps/emqttd/src/emqttd.erl +++ b/src/emqttd.erl @@ -114,9 +114,15 @@ close_listener({Protocol, Port, _Options}) -> -spec load_all_plugins() -> [{App :: atom(), ok | {error, any()}}]. load_all_plugins() -> %% save first - {ok, [PluginApps]} = file:consult("etc/plugins.config"), - application:set_env(emqttd, plugins, [App || {App, _Env} <- PluginApps]), - [{App, load_plugin(App)} || {App, _Env} <- PluginApps]. + case file:consult("etc/plugins.config") of + {ok, [PluginApps]} -> + application:set_env(emqttd, plugins, [App || {App, _Env} <- PluginApps]), + [{App, load_plugin(App)} || {App, _Env} <- PluginApps]; + {error, enoent} -> + lager:error("etc/plugins.config not found!"); + {error, Error} -> + lager:error("Load etc/plugins.config error: ~p", [Error]) + end. %%------------------------------------------------------------------------------ %% @doc Load plugin @@ -169,7 +175,6 @@ unload_all_plugins() -> PluginApps = application:get_env(emqttd, plugins, []), [{App, unload_plugin(App)} || App <- PluginApps]. - %%------------------------------------------------------------------------------ %% @doc Unload plugin %% @end diff --git a/apps/emqttd/src/emqttd_access_control.erl b/src/emqttd_access_control.erl similarity index 96% rename from apps/emqttd/src/emqttd_access_control.erl rename to src/emqttd_access_control.erl index 3f1ca1b36..82ac2552d 100644 --- a/apps/emqttd/src/emqttd_access_control.erl +++ b/src/emqttd_access_control.erl @@ -36,6 +36,7 @@ %% API Function Exports -export([start_link/0, + start_link/1, auth/2, % authentication check_acl/3, % acl check reload_acl/0, % reload acl @@ -60,7 +61,12 @@ %%------------------------------------------------------------------------------ -spec start_link() -> {ok, pid()} | ignore | {error, any()}. start_link() -> - gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). + {ok, AcOpts} = application:get_env(emqttd, access), + start_link(AcOpts). + +-spec start_link(AcOpts :: list()) -> {ok, pid()} | ignore | {error, any()}. +start_link(AcOpts) -> + gen_server:start_link({local, ?SERVER}, ?MODULE, [AcOpts], []). %%------------------------------------------------------------------------------ %% @doc Authenticate MQTT Client @@ -151,8 +157,7 @@ stop() -> %%% gen_server callbacks %%%============================================================================= -init([]) -> - {ok, AcOpts} = application:get_env(emqttd, access), +init([AcOpts]) -> ets:new(?ACCESS_CONTROL_TAB, [set, named_table, protected, {read_concurrency, true}]), ets:insert(?ACCESS_CONTROL_TAB, {auth_modules, init_mods(auth, proplists:get_value(auth, AcOpts))}), ets:insert(?ACCESS_CONTROL_TAB, {acl_modules, init_mods(acl, proplists:get_value(acl, AcOpts))}), @@ -179,6 +184,7 @@ handle_call({register_mod, Type, Mod, Opts}, _From, State) -> ets:insert(?ACCESS_CONTROL_TAB, {tab_key(Type), [{Mod, ModState}|Mods]}), ok; {'EXIT', Error} -> + lager:error("Access Control: register ~s error - ~p", [Mod, Error]), {error, Error} end; _ -> diff --git a/apps/emqttd/src/emqttd_access_rule.erl b/src/emqttd_access_rule.erl similarity index 93% rename from apps/emqttd/src/emqttd_access_rule.erl rename to src/emqttd_access_rule.erl index 8f7761822..0b0fdc6ba 100644 --- a/apps/emqttd/src/emqttd_access_rule.erl +++ b/src/emqttd_access_rule.erl @@ -73,9 +73,9 @@ compile(who, {user, Username}) -> {user, bin(Username)}; compile(topic, {eq, Topic}) -> - {eq, emqtt_topic:words(bin(Topic))}; + {eq, emqttd_topic:words(bin(Topic))}; compile(topic, Topic) -> - Words = emqtt_topic:words(bin(Topic)), + Words = emqttd_topic:words(bin(Topic)), case 'pattern?'(Words) of true -> {pattern, Words}; false -> Words @@ -114,9 +114,9 @@ match_who(#mqtt_client{clientid = ClientId}, {client, ClientId}) -> true; match_who(#mqtt_client{username = Username}, {user, Username}) -> true; -match_who(#mqtt_client{ipaddr = undefined}, {ipaddr, _Tup}) -> +match_who(#mqtt_client{ipaddress = undefined}, {ipaddr, _Tup}) -> false; -match_who(#mqtt_client{ipaddr = IP}, {ipaddr, {_CDIR, Start, End}}) -> +match_who(#mqtt_client{ipaddress = IP}, {ipaddr, {_CDIR, Start, End}}) -> I = esockd_access:atoi(IP), I >= Start andalso I =< End; match_who(_Client, _Who) -> @@ -126,12 +126,12 @@ match_topics(_Client, _Topic, []) -> false; match_topics(Client, Topic, [{pattern, PatternFilter}|Filters]) -> TopicFilter = feed_var(Client, PatternFilter), - case match_topic(emqtt_topic:words(Topic), TopicFilter) of + case match_topic(emqttd_topic:words(Topic), TopicFilter) of true -> true; false -> match_topics(Client, Topic, Filters) end; match_topics(Client, Topic, [TopicFilter|Filters]) -> - case match_topic(emqtt_topic:words(Topic), TopicFilter) of + case match_topic(emqttd_topic:words(Topic), TopicFilter) of true -> true; false -> match_topics(Client, Topic, Filters) end. @@ -139,7 +139,7 @@ match_topics(Client, Topic, [TopicFilter|Filters]) -> match_topic(Topic, {eq, TopicFilter}) -> Topic =:= TopicFilter; match_topic(Topic, TopicFilter) -> - emqtt_topic:match(Topic, TopicFilter). + emqttd_topic:match(Topic, TopicFilter). feed_var(Client, Pattern) -> feed_var(Client, Pattern, []). diff --git a/apps/emqttd/src/emqttd_acl_internal.erl b/src/emqttd_acl_internal.erl similarity index 100% rename from apps/emqttd/src/emqttd_acl_internal.erl rename to src/emqttd_acl_internal.erl diff --git a/apps/emqttd/src/emqttd_acl_mod.erl b/src/emqttd_acl_mod.erl similarity index 100% rename from apps/emqttd/src/emqttd_acl_mod.erl rename to src/emqttd_acl_mod.erl diff --git a/src/emqttd_alarm.erl b/src/emqttd_alarm.erl new file mode 100644 index 000000000..0c7061490 --- /dev/null +++ b/src/emqttd_alarm.erl @@ -0,0 +1,137 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 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 +%%% copy alarm_handler. +%%% +%%% @end +%%%----------------------------------------------------------------------------- + +-module(emqttd_alarm). + +-include("emqttd.hrl"). + +-export([start_link/0, alarm_fun/0, get_alarms/0, + set_alarm/1, clear_alarm/1, + add_alarm_handler/1, add_alarm_handler/2, + delete_alarm_handler/1]). + +-export([init/1, handle_event/2, handle_call/2, handle_info/2, + terminate/2]). + +-define(SERVER, ?MODULE). + +start_link() -> + case gen_event:start_link({local, ?SERVER}) of + {ok, Pid} -> + gen_event:add_handler(?SERVER, ?MODULE, []), + {ok, Pid}; + Error -> + Error + end. + +alarm_fun() -> + alarm_fun(false). + +alarm_fun(Bool) -> + fun(alert, _Alarm) when Bool =:= true -> alarm_fun(true); + (alert, Alarm) when Bool =:= false -> set_alarm(Alarm), alarm_fun(true); + (clear, AlarmId) when Bool =:= true -> clear_alarm(AlarmId), alarm_fun(false); + (clear, _AlarmId) when Bool =:= false -> alarm_fun(false) + end. + +-spec set_alarm(mqtt_alarm()) -> ok. +set_alarm(Alarm) when is_record(Alarm, mqtt_alarm) -> + gen_event:notify(?SERVER, {set_alarm, Alarm}). + +-spec clear_alarm(any()) -> ok. +clear_alarm(AlarmId) when is_binary(AlarmId) -> + gen_event:notify(?SERVER, {clear_alarm, AlarmId}). + +get_alarms() -> + gen_event:call(?SERVER, ?MODULE, get_alarms). + +add_alarm_handler(Module) when is_atom(Module) -> + gen_event:add_handler(?SERVER, Module, []). + +add_alarm_handler(Module, Args) when is_atom(Module) -> + gen_event:add_handler(?SERVER, Module, Args). + +delete_alarm_handler(Module) when is_atom(Module) -> + gen_event:delete_handler(?SERVER, Module, []). + +%%----------------------------------------------------------------- +%% Default Alarm handler +%%----------------------------------------------------------------- +init(_) -> + {ok, []}. + +handle_event({set_alarm, Alarm = #mqtt_alarm{id = AlarmId, + severity = Severity, + title = Title, + summary = Summary}}, Alarms)-> + Timestamp = os:timestamp(), + Json = mochijson2:encode([{id, AlarmId}, + {severity, Severity}, + {title, iolist_to_binary(Title)}, + {summary, iolist_to_binary(Summary)}, + {ts, emqttd_util:now_to_secs(Timestamp)}]), + emqttd_pubsub:publish(alarm_msg(alert, AlarmId, Json)), + {ok, [Alarm#mqtt_alarm{timestamp = Timestamp} | Alarms]}; + +handle_event({clear_alarm, AlarmId}, Alarms)-> + Json = mochijson2:encode([{id, AlarmId}, {ts, emqttd_util:now_to_secs()}]), + emqttd_pubsub:publish(alarm_msg(clear, AlarmId, Json)), + {ok, lists:keydelete(AlarmId, 2, Alarms)}; + +handle_event(_, Alarms)-> + {ok, Alarms}. + +handle_info(_, Alarms) -> + {ok, Alarms}. + +handle_call(get_alarms, Alarms) -> + {ok, Alarms, Alarms}; + +handle_call(_Query, Alarms) -> + {ok, {error, bad_query}, Alarms}. + +terminate(swap, Alarms) -> + {?MODULE, Alarms}; + +terminate(_, _) -> + ok. + +alarm_msg(Type, AlarmId, Json) -> + #mqtt_message{from = alarm, + qos = 1, + sys = true, + topic = topic(Type, AlarmId), + payload = iolist_to_binary(Json), + timestamp = os:timestamp()}. + +topic(alert, AlarmId) -> + emqttd_topic:systop(<<"alarms/", AlarmId/binary, "/alert">>); + +topic(clear, AlarmId) -> + emqttd_topic:systop(<<"alarms/", AlarmId/binary, "/clear">>). + + diff --git a/apps/emqttd/src/emqttd_app.erl b/src/emqttd_app.erl similarity index 95% rename from apps/emqttd/src/emqttd_app.erl rename to src/emqttd_app.erl index 5a8daf60f..c1ffd10e6 100644 --- a/apps/emqttd/src/emqttd_app.erl +++ b/src/emqttd_app.erl @@ -68,17 +68,15 @@ print_vsn() -> ?PRINT("~s ~s is running now~n", [Desc, Vsn]). start_servers(Sup) -> - Servers = [{"emqttd event", emqttd_event}, - {"emqttd trace", emqttd_trace}, + Servers = [{"emqttd trace", emqttd_trace}, {"emqttd pooler", {supervisor, emqttd_pooler_sup}}, - {"emqttd client manager", {supervisor, emqttd_cm_sup}}, {"emqttd session manager", {supervisor, emqttd_sm_sup}}, {"emqttd session supervisor", {supervisor, emqttd_session_sup}}, {"emqttd pubsub", {supervisor, emqttd_pubsub_sup}}, {"emqttd stats", emqttd_stats}, {"emqttd metrics", emqttd_metrics}, - %{"emqttd router", emqttd_router}, {"emqttd broker", emqttd_broker}, + {"emqttd alarm", emqttd_alarm}, {"emqttd mode supervisor", emqttd_mod_sup}, {"emqttd bridge supervisor", {supervisor, emqttd_bridge_sup}}, {"emqttd access control", emqttd_access_control}, diff --git a/apps/emqttd/src/emqttd_auth_anonymous.erl b/src/emqttd_auth_anonymous.erl similarity index 100% rename from apps/emqttd/src/emqttd_auth_anonymous.erl rename to src/emqttd_auth_anonymous.erl diff --git a/apps/emqttd/src/emqttd_auth_clientid.erl b/src/emqttd_auth_clientid.erl similarity index 95% rename from apps/emqttd/src/emqttd_auth_clientid.erl rename to src/emqttd_auth_clientid.erl index 2f3d51617..e5171a239 100644 --- a/apps/emqttd/src/emqttd_auth_clientid.erl +++ b/src/emqttd_auth_clientid.erl @@ -101,10 +101,10 @@ init(Opts) -> check(#mqtt_client{clientid = undefined}, _Password, []) -> {error, "ClientId undefined"}; -check(#mqtt_client{clientid = ClientId, ipaddr = IpAddr}, _Password, []) -> - check_clientid_only(ClientId, IpAddr); -check(#mqtt_client{clientid = ClientId, ipaddr = IpAddr}, _Password, [{password, no}|_]) -> - check_clientid_only(ClientId, IpAddr); +check(#mqtt_client{clientid = ClientId, ipaddress = IpAddress}, _Password, []) -> + check_clientid_only(ClientId, IpAddress); +check(#mqtt_client{clientid = ClientId, ipaddress = IpAddress}, _Password, [{password, no}|_]) -> + check_clientid_only(ClientId, IpAddress); check(_Client, undefined, [{password, yes}|_]) -> {error, "Password undefined"}; check(#mqtt_client{clientid = ClientId}, Password, [{password, yes}|_]) -> diff --git a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl b/src/emqttd_auth_ldap.erl similarity index 96% rename from plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl rename to src/emqttd_auth_ldap.erl index ba1bdca4d..965aaeefe 100644 --- a/plugins/emqttd_auth_ldap/src/emqttd_auth_ldap.erl +++ b/src/emqttd_auth_ldap.erl @@ -82,10 +82,7 @@ ldap_bind(LDAP, UserDn, Password) -> end. fill(Username, UserDn) -> - lists:append(lists:map( - fun("$u") -> Username; - (S) -> S - end, string:tokens(UserDn, ",="))). + re:replace(UserDn, "\\$u", Username, [global, {return, list}]). description() -> "LDAP Authentication Module". diff --git a/apps/emqttd/src/emqttd_auth_mod.erl b/src/emqttd_auth_mod.erl similarity index 100% rename from apps/emqttd/src/emqttd_auth_mod.erl rename to src/emqttd_auth_mod.erl diff --git a/apps/emqttd/src/emqttd_auth_username.erl b/src/emqttd_auth_username.erl similarity index 100% rename from apps/emqttd/src/emqttd_auth_username.erl rename to src/emqttd_auth_username.erl diff --git a/apps/emqttd/src/emqttd_bridge.erl b/src/emqttd_bridge.erl similarity index 96% rename from apps/emqttd/src/emqttd_bridge.erl rename to src/emqttd_bridge.erl index 26919b47f..2129a6bdb 100644 --- a/apps/emqttd/src/emqttd_bridge.erl +++ b/src/emqttd_bridge.erl @@ -30,13 +30,13 @@ -include("emqttd.hrl"). --include_lib("emqtt/include/emqtt.hrl"). - --behaviour(gen_server). +-include("emqttd_protocol.hrl"). %% API Function Exports -export([start_link/3]). +-behaviour(gen_server). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -106,11 +106,11 @@ handle_call(_Request, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info({dispatch, {_From, Msg}}, State = #state{node = Node, status = down}) -> +handle_info({dispatch, Msg}, State = #state{node = Node, status = down}) -> lager:warning("Bridge Dropped Msg for ~p Down:~n~p", [Node, Msg]), {noreply, State}; -handle_info({dispatch, {_From, Msg}}, State = #state{node = Node, status = up}) -> +handle_info({dispatch, Msg}, State = #state{node = Node, status = up}) -> rpc:cast(Node, emqttd_pubsub, publish, [transform(Msg, State)]), {noreply, State}; diff --git a/apps/emqttd/src/emqttd_bridge_sup.erl b/src/emqttd_bridge_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_bridge_sup.erl rename to src/emqttd_bridge_sup.erl diff --git a/apps/emqttd/src/emqttd_broker.erl b/src/emqttd_broker.erl similarity index 89% rename from apps/emqttd/src/emqttd_broker.erl rename to src/emqttd_broker.erl index bff331d9c..e4deb6414 100644 --- a/apps/emqttd/src/emqttd_broker.erl +++ b/src/emqttd_broker.erl @@ -28,17 +28,14 @@ -author("Feng Lee "). --include("emqttd_systop.hrl"). - --include_lib("emqtt/include/emqtt.hrl"). - --behaviour(gen_server). - --define(SERVER, ?MODULE). +-include_lib("emqttd.hrl"). %% API Function Exports -export([start_link/0]). +%% Running nodes +-export([running_nodes/0]). + %% Event API -export([subscribe/1, notify/2]). @@ -51,6 +48,10 @@ %% Tick API -export([start_tick/1, stop_tick/1]). +-behaviour(gen_server). + +-define(SERVER, ?MODULE). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -59,6 +60,14 @@ -record(state, {started_at, sys_interval, tick_tref}). +%% $SYS Topics of Broker +-define(SYSTOP_BROKERS, [ + version, % Broker version + uptime, % Broker uptime + datetime, % Broker local datetime + sysdescr % Broker description +]). + %%%============================================================================= %%% API %%%============================================================================= @@ -71,6 +80,13 @@ start_link() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). +%%------------------------------------------------------------------------------ +%% @doc Get running nodes +%% @end +%%------------------------------------------------------------------------------ +running_nodes() -> + mnesia:system_info(running_db_nodes). + %%------------------------------------------------------------------------------ %% @doc Subscribe broker event %% @end @@ -205,6 +221,7 @@ init([]) -> random:seed(now()), ets:new(?BROKER_TAB, [set, public, named_table]), % Create $SYS Topics + emqttd_pubsub:create(<<"$SYS/brokers">>), [ok = create_topic(Topic) || Topic <- ?SYSTOP_BROKERS], % Tick {ok, #state{started_at = os:timestamp(), tick_tref = start_tick(tick)}, hibernate}. @@ -244,6 +261,7 @@ handle_cast(_Msg, State) -> {noreply, State}. handle_info(tick, State) -> + retain(brokers), retain(version, list_to_binary(version())), retain(sysdescr, list_to_binary(sysdescr())), publish(uptime, list_to_binary(uptime(State))), @@ -264,20 +282,28 @@ code_change(_OldVsn, State, _Extra) -> %%%============================================================================= create_topic(Topic) -> - emqttd_pubsub:create(emqtt_topic:systop(Topic)). + emqttd_pubsub:create(emqttd_topic:systop(Topic)). + +retain(brokers) -> + Payload = list_to_binary(string:join([atom_to_list(N) || N <- running_nodes()], ",")), + publish(#mqtt_message{from = broker, + retain = true, + topic = <<"$SYS/brokers">>, + payload = Payload}). retain(Topic, Payload) when is_binary(Payload) -> - publish(#mqtt_message{retain = true, - topic = emqtt_topic:systop(Topic), + publish(#mqtt_message{from = broker, + retain = true, + topic = emqttd_topic:systop(Topic), payload = Payload}). publish(Topic, Payload) when is_binary(Payload) -> - publish( #mqtt_message{topic = emqtt_topic:systop(Topic), + publish( #mqtt_message{from = broker, + topic = emqttd_topic:systop(Topic), payload = Payload}). publish(Msg) -> - emqttd_pubsub:publish(broker, Msg). - + emqttd_pubsub:publish(Msg). uptime(#state{started_at = Ts}) -> Secs = timer:now_diff(os:timestamp(), Ts) div 1000000, diff --git a/apps/emqttd/src/emqttd_client.erl b/src/emqttd_client.erl similarity index 84% rename from apps/emqttd/src/emqttd_client.erl rename to src/emqttd_client.erl index 0c9e2413a..bd9f16414 100644 --- a/apps/emqttd/src/emqttd_client.erl +++ b/src/emqttd_client.erl @@ -28,9 +28,9 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). --include_lib("emqtt/include/emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% API Function Exports -export([start_link/2, info/1]). @@ -68,7 +68,7 @@ init([SockArgs = {Transport, Sock, _SockFun}, PacketOpts]) -> {ok, ConnStr} = emqttd_net:connection_string(Sock, inbound), lager:info("Connect from ~s", [ConnStr]), SendFun = fun(Data) -> Transport:send(NewSock, Data) end, - ParserState = emqtt_parser:init(PacketOpts), + ParserState = emqttd_parser:init(PacketOpts), ProtoState = emqttd_protocol:init(Peername, SendFun, PacketOpts), State = control_throttle(#state{transport = Transport, socket = NewSock, @@ -87,42 +87,35 @@ handle_call(info, _From, State = #state{conn_name=ConnName, proto_state = ProtoState}) -> {reply, [{conn_name, ConnName} | emqttd_protocol:info(ProtoState)], State}; -handle_call(Req, _From, State) -> - {stop, {badreq, Req}, State}. +handle_call(Req, _From, State = #state{peername = Peername}) -> + lager:critical("Client ~s: unexpected request - ~p",[emqttd_net:format(Peername), Req]), + {reply, {error, unsupported_request}, State}. -handle_cast(Msg, State) -> - {stop, {badmsg, Msg}, State}. +handle_cast(Msg, State = #state{peername = Peername}) -> + lager:critical("Client ~s: unexpected msg - ~p",[emqttd_net:format(Peername), Msg]), + {noreply, State}. handle_info(timeout, State) -> stop({shutdown, timeout}, State); handle_info({stop, duplicate_id, _NewPid}, State=#state{proto_state = ProtoState, conn_name=ConnName}) -> - %% TODO: to... %% need transfer data??? %% emqttd_client:transfer(NewPid, Data), lager:error("Shutdown for duplicate clientid: ~s, conn:~s", - [emqttd_protocol:clientid(ProtoState), ConnName]), + [emqttd_protocol:clientid(ProtoState), ConnName]), stop({shutdown, duplicate_id}, State); -%%TODO: ok?? -handle_info({dispatch, {From, Messages}}, #state{proto_state = ProtoState} = State) when is_list(Messages) -> - ProtoState1 = - lists:foldl(fun(Message, PState) -> - {ok, PState1} = emqttd_protocol:send({From, Message}, PState), PState1 - end, ProtoState, Messages), - {noreply, State#state{proto_state = ProtoState1}}; - -handle_info({dispatch, {From, Message}}, #state{proto_state = ProtoState} = State) -> - {ok, ProtoState1} = emqttd_protocol:send({From, Message}, ProtoState), +handle_info({deliver, Message}, #state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqttd_protocol:send(Message, ProtoState), {noreply, State#state{proto_state = ProtoState1}}; handle_info({redeliver, {?PUBREL, PacketId}}, #state{proto_state = ProtoState} = State) -> {ok, ProtoState1} = emqttd_protocol:redeliver({?PUBREL, PacketId}, ProtoState), {noreply, State#state{proto_state = ProtoState1}}; -handle_info({subscribe, Topic, Qos}, #state{proto_state = ProtoState} = State) -> - {ok, ProtoState1} = emqttd_protocol:handle({subscribe, Topic, Qos}, ProtoState), +handle_info({subscribe, TopicTable}, #state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqttd_protocol:handle({subscribe, TopicTable}, ProtoState), {noreply, State#state{proto_state = ProtoState1}}; handle_info({inet_reply, _Ref, ok}, State) -> @@ -158,17 +151,16 @@ handle_info({keepalive, timeout}, State = #state{peername = Peername, keepalive handle_info(Info, State = #state{peername = Peername}) -> lager:critical("Client ~s: unexpected info ~p",[emqttd_net:format(Peername), Info]), - {stop, {badinfo, Info}, State}. + {noreply, State}. terminate(Reason, #state{peername = Peername, keepalive = KeepAlive, proto_state = ProtoState}) -> - lager:info("Client ~s: ~p terminated, reason: ~p~n", [emqttd_net:format(Peername), self(), Reason]), - notify(disconnected, Reason, ProtoState), + lager:info("Client ~s terminated, reason: ~p", [emqttd_net:format(Peername), Reason]), emqttd_keepalive:cancel(KeepAlive), case {ProtoState, Reason} of {undefined, _} -> ok; {_, {shutdown, Error}} -> emqttd_protocol:shutdown(Error, ProtoState); - {_, Reason} -> + {_, Reason} -> emqttd_protocol:shutdown(Reason, ProtoState) end. @@ -185,7 +177,7 @@ process_received_bytes(Bytes, State = #state{packet_opts = PacketOpts, parse_state = ParseState, proto_state = ProtoState, conn_name = ConnStr}) -> - case emqtt_parser:parse(Bytes, ParseState) of + case emqttd_parser:parse(Bytes, ParseState) of {more, ParseState1} -> {noreply, control_throttle(State #state{parse_state = ParseState1}), @@ -194,7 +186,7 @@ process_received_bytes(Bytes, State = #state{packet_opts = PacketOpts, received_stats(Packet), case emqttd_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - process_received_bytes(Rest, State#state{parse_state = emqtt_parser:init(PacketOpts), + process_received_bytes(Rest, State#state{parse_state = emqttd_parser:init(PacketOpts), proto_state = ProtoState1}); {error, Error} -> lager:error("MQTT protocol error ~p for connection ~p~n", [Error, ConnStr]), @@ -231,7 +223,7 @@ control_throttle(State = #state{conn_state = Flow, {_, _} -> run_socket(State) end. -stop(Reason, State ) -> +stop(Reason, State) -> {stop, Reason, State}. received_stats(?PACKET(Type)) -> @@ -253,12 +245,3 @@ inc(?DISCONNECT) -> inc(_) -> ignore. -%%TODO: should be moved to emqttd_protocol... for event emitted when protocol shutdown... -notify(disconnected, _Reason, undefined) -> ingore; - -notify(disconnected, {shutdown, Reason}, ProtoState) -> - emqttd_event:notify({disconnected, emqttd_protocol:clientid(ProtoState), Reason}); - -notify(disconnected, Reason, ProtoState) -> - emqttd_event:notify({disconnected, emqttd_protocol:clientid(ProtoState), Reason}). - diff --git a/apps/emqttd/src/emqttd_ctl.erl b/src/emqttd_ctl.erl similarity index 100% rename from apps/emqttd/src/emqttd_ctl.erl rename to src/emqttd_ctl.erl diff --git a/apps/emqttd/src/emqttd_gen_mod.erl b/src/emqttd_gen_mod.erl similarity index 100% rename from apps/emqttd/src/emqttd_gen_mod.erl rename to src/emqttd_gen_mod.erl diff --git a/apps/emqttd/src/emqttd_http.erl b/src/emqttd_http.erl similarity index 92% rename from apps/emqttd/src/emqttd_http.erl rename to src/emqttd_http.erl index 71c844ead..8b7afe54e 100644 --- a/apps/emqttd/src/emqttd_http.erl +++ b/src/emqttd_http.erl @@ -29,8 +29,7 @@ -author("Feng Lee "). -include("emqttd.hrl"). - --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd_protocol.hrl"). -import(proplists, [get_value/2, get_value/3]). @@ -53,10 +52,11 @@ handle_request('POST', "/mqtt/publish", Req) -> Message = list_to_binary(get_value("message", Params)), case {validate(qos, Qos), validate(topic, Topic)} of {true, true} -> - emqttd_pubsub:publish(http, #mqtt_message{qos = Qos, - retain = Retain, - topic = Topic, - payload = Message}), + emqttd_pubsub:publish(#mqtt_message{from = http, + qos = Qos, + retain = Retain, + topic = Topic, + payload = Message}), Req:ok({"text/plan", <<"ok\n">>}); {false, _} -> Req:respond({400, [], <<"Bad QoS">>}); @@ -121,7 +121,7 @@ validate(qos, Qos) -> (Qos >= ?QOS_0) and (Qos =< ?QOS_2); validate(topic, Topic) -> - emqtt_topic:validate({name, Topic}). + emqttd_topic:validate({name, Topic}). int(S) -> list_to_integer(S). diff --git a/apps/emqttd/src/emqttd_keepalive.erl b/src/emqttd_keepalive.erl similarity index 100% rename from apps/emqttd/src/emqttd_keepalive.erl rename to src/emqttd_keepalive.erl diff --git a/apps/emqtt/src/emqtt_message.erl b/src/emqttd_message.erl similarity index 96% rename from apps/emqtt/src/emqtt_message.erl rename to src/emqttd_message.erl index 54a46526c..d96e5d922 100644 --- a/apps/emqtt/src/emqtt_message.erl +++ b/src/emqttd_message.erl @@ -24,15 +24,15 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_message). +-module(emqttd_message). -author("Feng Lee "). --include("emqtt.hrl"). +-include("emqttd.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). --export([from_packet/1, to_packet/1]). +-export([from_packet/1, from_packet/2, to_packet/1]). -export([set_flag/1, set_flag/2, unset_flag/1, unset_flag/2]). @@ -70,6 +70,9 @@ from_packet(#mqtt_packet_connect{will_retain = Retain, dup = false, payload = Msg}. +from_packet(ClientId, Packet) -> + Msg = from_packet(Packet), Msg#mqtt_message{from = ClientId}. + %%------------------------------------------------------------------------------ %% @doc Message to packet %% @end diff --git a/apps/emqttd/src/emqttd_metrics.erl b/src/emqttd_metrics.erl similarity index 79% rename from apps/emqttd/src/emqttd_metrics.erl rename to src/emqttd_metrics.erl index e4a7fea15..f0d45ec02 100644 --- a/apps/emqttd/src/emqttd_metrics.erl +++ b/src/emqttd_metrics.erl @@ -28,9 +28,7 @@ -author("Feng Lee "). --include("emqttd_systop.hrl"). - --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). -behaviour(gen_server). @@ -48,9 +46,41 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-record(state, {tick_tref}). + -define(METRIC_TAB, mqtt_metric). --record(state, {tick_tref}). +%% Bytes sent and received of Broker +-define(SYSTOP_BYTES, [ + {counter, 'bytes/received'}, % Total bytes received + {counter, 'bytes/sent'} % Total bytes sent +]). + +%% Packets sent and received of Broker +-define(SYSTOP_PACKETS, [ + {counter, 'packets/received'}, % All Packets received + {counter, 'packets/sent'}, % All Packets sent + {counter, 'packets/connect'}, % CONNECT Packets received + {counter, 'packets/connack'}, % CONNACK Packets sent + {counter, 'packets/publish/received'}, % PUBLISH packets received + {counter, 'packets/publish/sent'}, % PUBLISH packets sent + {counter, 'packets/subscribe'}, % SUBSCRIBE Packets received + {counter, 'packets/suback'}, % SUBACK packets sent + {counter, 'packets/unsubscribe'}, % UNSUBSCRIBE Packets received + {counter, 'packets/unsuback'}, % UNSUBACK Packets sent + {counter, 'packets/pingreq'}, % PINGREQ packets received + {counter, 'packets/pingresp'}, % PINGRESP Packets sent + {counter, 'packets/disconnect'} % DISCONNECT Packets received +]). + +%% Messages sent and received of broker +-define(SYSTOP_MESSAGES, [ + {counter, 'messages/received'}, % Messages received + {counter, 'messages/sent'}, % Messages sent + {gauge, 'messages/retained/count'},% Messagea retained + {gauge, 'messages/stored/count'}, % Messages stored + {counter, 'messages/dropped'} % Messages dropped +]). %%%============================================================================= %%% API @@ -163,7 +193,7 @@ init([]) -> % Init metrics [create_metric(Metric) || Metric <- Metrics], % $SYS Topics for metrics - [ok = create_topic(Topic) || {_, Topic} <- Metrics], + [ok = emqttd_pubsub:create(metric_topic(Topic)) || {_, Topic} <- Metrics], % Tick to publish metrics {ok, #state{tick_tref = emqttd_broker:start_tick(tick)}, hibernate}. @@ -192,8 +222,9 @@ code_change(_OldVsn, State, _Extra) -> %%%============================================================================= publish(Metric, Val) -> - emqttd_pubsub:publish(metrics, #mqtt_message{topic = emqtt_topic:systop(Metric), - payload = emqttd_util:integer_to_binary(Val)}). + emqttd_pubsub:publish(#mqtt_message{topic = metric_topic(Metric), + from = metrics, + payload = emqttd_util:integer_to_binary(Val)}). create_metric({gauge, Name}) -> ets:insert(?METRIC_TAB, {{Name, 0}, 0}); @@ -202,7 +233,7 @@ create_metric({counter, Name}) -> Schedulers = lists:seq(1, erlang:system_info(schedulers)), [ets:insert(?METRIC_TAB, {{Name, I}, 0}) || I <- Schedulers]. -create_topic(Topic) -> - emqttd_pubsub:create(emqtt_topic:systop(Topic)). +metric_topic(Metric) -> + emqttd_topic:systop(list_to_binary(lists:concat(['metrics/', Metric]))). diff --git a/apps/emqttd/src/emqttd_mnesia.erl b/src/emqttd_mnesia.erl similarity index 100% rename from apps/emqttd/src/emqttd_mnesia.erl rename to src/emqttd_mnesia.erl diff --git a/apps/emqttd/src/emqttd_mod_autosub.erl b/src/emqttd_mod_autosub.erl similarity index 73% rename from apps/emqttd/src/emqttd_mod_autosub.erl rename to src/emqttd_mod_autosub.erl index 0ed1be5e1..c5a1e136e 100644 --- a/apps/emqttd/src/emqttd_mod_autosub.erl +++ b/src/emqttd_mod_autosub.erl @@ -29,22 +29,30 @@ -author("Feng Lee "). +-include("emqttd.hrl"). + +-include("emqttd_protocol.hrl"). + -behaviour(emqttd_gen_mod). --export([load/1, subscribe/2, unload/1]). +-export([load/1, client_connected/3, unload/1]). -record(state, {topics}). load(Opts) -> Topics = [{list_to_binary(Topic), Qos} || {Topic, Qos} <- Opts, 0 =< Qos, Qos =< 2], - emqttd_broker:hook(client_connected, {?MODULE, subscribe}, - {?MODULE, subscribe, [Topics]}), + emqttd_broker:hook(client_connected, {?MODULE, client_connected}, + {?MODULE, client_connected, [Topics]}), {ok, #state{topics = Topics}}. -subscribe({Client, ClientId}, Topics) -> - F = fun(Topic) -> emqtt_topic:feed_var(<<"$c">>, ClientId, Topic) end, - [Client ! {subscribe, F(Topic), Qos} || {Topic, Qos} <- Topics]. +client_connected(?CONNACK_ACCEPT, #mqtt_client{clientid = ClientId, client_pid = ClientPid}, Topics) -> + F = fun(Topic) -> emqttd_topic:feed_var(<<"$c">>, ClientId, Topic) end, + ClientPid ! {subscribe, [{F(Topic), Qos} || {Topic, Qos} <- Topics]}; + +client_connected(_ConnAck, _Client, _Topics) -> + ignore. unload(_Opts) -> - emqttd_broker:unhook(client_connected, {?MODULE, subscribe}). + emqttd_broker:unhook(client_connected, {?MODULE, client_connected}). + diff --git a/src/emqttd_mod_presence.erl b/src/emqttd_mod_presence.erl new file mode 100644 index 000000000..248eb6bcf --- /dev/null +++ b/src/emqttd_mod_presence.erl @@ -0,0 +1,85 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 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 +%%% emqttd presence management module. +%%% +%%% @end +%%%----------------------------------------------------------------------------- +-module(emqttd_mod_presence). + +-author("Feng Lee "). + +-include("emqttd.hrl"). + +-export([load/1, unload/1]). + +-export([client_connected/3, client_disconnected/3]). + +load(Opts) -> + emqttd_broker:hook(client_connected, {?MODULE, client_connected}, {?MODULE, client_connected, [Opts]}), + emqttd_broker:hook(client_disconnected, {?MODULE, client_disconnected}, {?MODULE, client_disconnected, [Opts]}), + {ok, Opts}. + +client_connected(ConnAck, #mqtt_client{clientid = ClientId, + username = Username, + ipaddress = IpAddress, + clean_sess = CleanSess, + proto_ver = ProtoVer}, Opts) -> + Sess = case CleanSess of + true -> false; + false -> true + end, + Json = mochijson2:encode([{clientid, ClientId}, + {username, Username}, + {ipaddress, list_to_binary(emqttd_net:ntoa(IpAddress))}, + {session, Sess}, + {protocol, ProtoVer}, + {connack, ConnAck}, + {ts, emqttd_util:now_to_secs()}]), + Message = #mqtt_message{from = presence, + qos = proplists:get_value(qos, Opts, 0), + topic = topic(connected, ClientId), + payload = iolist_to_binary(Json)}, + emqttd_pubsub:publish(Message). + +client_disconnected(Reason, ClientId, Opts) -> + Json = mochijson2:encode([{clientid, ClientId}, + {reason, reason(Reason)}, + {ts, emqttd_util:now_to_secs()}]), + emqttd_pubsub:publish(#mqtt_message{from = presence, + qos = proplists:get_value(qos, Opts, 0), + topic = topic(disconnected, ClientId), + payload = iolist_to_binary(Json)}). + +unload(_Opts) -> + emqttd_broker:unhook(client_connected, {?MODULE, client_connected}), + emqttd_broker:unhook(client_disconnected, {?MODULE, client_disconnected}). + +topic(connected, ClientId) -> + emqttd_topic:systop(list_to_binary(["clients/", ClientId, "/connected"])); +topic(disconnected, ClientId) -> + emqttd_topic:systop(list_to_binary(["clients/", ClientId, "/disconnected"])). + +reason(Reason) when is_atom(Reason) -> Reason; +reason({Error, _}) when is_atom(Error) -> Error; +reason(_) -> internal_error. + diff --git a/apps/emqttd/src/emqttd_mod_rewrite.erl b/src/emqttd_mod_rewrite.erl similarity index 98% rename from apps/emqttd/src/emqttd_mod_rewrite.erl rename to src/emqttd_mod_rewrite.erl index fc038d95f..eaaf287ae 100644 --- a/apps/emqttd/src/emqttd_mod_rewrite.erl +++ b/src/emqttd_mod_rewrite.erl @@ -29,7 +29,7 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). -behaviour(emqttd_gen_mod). @@ -104,7 +104,7 @@ compile(Sections) -> match_topic(Topic, []) -> Topic; match_topic(Topic, [{topic, Filter, Rules} | Sections]) -> - case emqtt_topic:match(Topic, Filter) of + case emqttd_topic:match(Topic, Filter) of true -> match_rule(Topic, Rules); false -> diff --git a/apps/emqttd/src/emqttd_mod_sup.erl b/src/emqttd_mod_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_mod_sup.erl rename to src/emqttd_mod_sup.erl diff --git a/src/emqttd_mqueue.erl b/src/emqttd_mqueue.erl new file mode 100644 index 000000000..d9f3c3c58 --- /dev/null +++ b/src/emqttd_mqueue.erl @@ -0,0 +1,155 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 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 +%%% +%%% A Simple in-memory message queue. +%%% +%%% Notice that MQTT is not an enterprise messaging queue. MQTT assume that client +%%% should be online in most of the time. +%%% +%%% This module implements a simple in-memory queue for MQTT persistent session. +%%% +%%% If the broker restarted or crashed, all the messages queued will be gone. +%%% +%%% Desgin of The Queue: +%%% |<----------------- Max Len ----------------->| +%%% ----------------------------------------------- +%%% IN -> | Pending Messages | Inflight Window | -> Out +%%% ----------------------------------------------- +%%% |<--- Win Size --->| +%%% +%%% +%%% 1. Inflight Window to store the messages awaiting for ack. +%%% +%%% 2. Suspend IN messages when the queue is deactive, or inflight windows is full. +%%% +%%% 3. If the queue is full, dropped qos0 messages if store_qos0 is true, +%%% otherwise dropped the oldest pending one. +%%% +%%% @end +%%%----------------------------------------------------------------------------- + +-module(emqttd_mqueue). + +-author("Feng Lee "). + +-include("emqttd.hrl"). + +-include("emqttd_protocol.hrl"). + +-export([new/3, name/1, + is_empty/1, is_full/1, + len/1, in/2, out/1]). + +-define(LOW_WM, 0.2). + +-define(HIGH_WM, 0.6). + +-record(mqueue, {name, + q = queue:new(), %% pending queue + len = 0, %% current queue len + low_wm = ?LOW_WM, + high_wm = ?HIGH_WM, + max_len = ?MAX_LEN, + qos0 = false, + alarm_fun}). + +-type mqueue() :: #mqueue{}. + +-type mqueue_option() :: {max_length, pos_integer()} %% Max queue length + | {low_watermark, float()} %% Low watermark + | {high_watermark, float()} %% High watermark + | {queue_qos0, boolean()}. %% Queue Qos0 + +-export_type([mqueue/0]). + +%%------------------------------------------------------------------------------ +%% @doc New Queue. +%% @end +%%------------------------------------------------------------------------------ +-spec new(binary(), list(mqueue_option()), fun()) -> mqueue(). +new(Name, Opts, AlarmFun) -> + MaxLen = emqttd_opts:g(max_length, Opts, 1000), + #mqueue{name = Name, + max_len = MaxLen, + low_wm = round(MaxLen * emqttd_opts:g(low_watermark, Opts, ?LOW_WM)), + high_wm = round(MaxLen * emqttd_opts:g(high_watermark, Opts, ?HIGH_WM)), + qos0 = emqttd_opts:g(queue_qos0, Opts, true), + alarm_fun = AlarmFun}. + +name(#mqueue{name = Name}) -> + Name. + +is_empty(#mqueue{len = 0}) -> true; +is_empty(_MQ) -> false. + +is_full(#mqueue{len = Len, max_len = MaxLen}) + when Len =:= MaxLen -> true; +is_full(_MQ) -> false. + +len(#mqueue{len = Len}) -> Len. + +%%------------------------------------------------------------------------------ +%% @doc Queue one message. +%% @end +%%------------------------------------------------------------------------------ + +-spec in(mqtt_message(), mqueue()) -> mqueue(). +%% drop qos0 +in(#mqtt_message{qos = ?QOS_0}, MQ = #mqueue{qos0 = false}) -> + MQ; + +%% simply drop the oldest one if queue is full, improve later +in(Msg, MQ = #mqueue{name = Name, q = Q, len = Len, max_len = MaxLen}) + when Len =:= MaxLen -> + {{value, OldMsg}, Q2} = queue:out(Q), + lager:error("MQueue(~s) drop ~s", [Name, emqttd_message:format(OldMsg)]), + MQ#mqueue{q = queue:in(Msg, Q2)}; + +in(Msg, MQ = #mqueue{q = Q, len = Len}) -> + maybe_set_alarm(MQ#mqueue{q = queue:in(Msg, Q), len = Len + 1}). + +out(MQ = #mqueue{len = 0}) -> + {empty, MQ}; + +out(MQ = #mqueue{q = Q, len = Len}) -> + {Result, Q2} = queue:out(Q), + {Result, maybe_clear_alarm(MQ#mqueue{q = Q2, len = Len - 1})}. + +maybe_set_alarm(MQ = #mqueue{name = Name, len = Len, high_wm = HighWM, alarm_fun = AlarmFun}) + when Len > HighWM -> + Alarm = #mqtt_alarm{id = list_to_binary(["queue_high_watermark.", Name]), + severity = warning, + title = io_lib:format("Queue ~s high-water mark", [Name]), + summary = io_lib:format("queue len ~p > high_watermark ~p", [Len, HighWM])}, + MQ#mqueue{alarm_fun = AlarmFun(alert, Alarm)}; + +maybe_set_alarm(MQ) -> + MQ. + +maybe_clear_alarm(MQ = #mqueue{name = Name, len = Len, low_wm = LowWM, alarm_fun = AlarmFun}) + when Len < LowWM -> + MQ#mqueue{alarm_fun = AlarmFun(clear, list_to_binary(["queue_high_watermark.", Name]))}; + +maybe_clear_alarm(MQ) -> + MQ. + diff --git a/apps/emqttd/src/emqttd_msg_store.erl b/src/emqttd_msg_store.erl similarity index 94% rename from apps/emqttd/src/emqttd_msg_store.erl rename to src/emqttd_msg_store.erl index 511b7cb22..fb2df9171 100644 --- a/apps/emqttd/src/emqttd_msg_store.erl +++ b/src/emqttd_msg_store.erl @@ -28,7 +28,7 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). %% Mnesia callbacks -export([mnesia/1]). @@ -74,7 +74,7 @@ retain(Msg = #mqtt_message{topic = Topic, TabSize = mnesia:table_info(message, size), case {TabSize < limit(table), size(Payload) < limit(payload)} of {true, true} -> - lager:debug("Retained ~s", [emqtt_message:format(Msg)]), + lager:debug("Retained ~s", [emqttd_message:format(Msg)]), mnesia:async_dirty(fun mnesia:write/3, [message, Msg, write]), emqttd_metrics:set('messages/retained/count', mnesia:table_info(message, size)); @@ -106,12 +106,12 @@ env() -> Topic :: binary(), CPid :: pid(). redeliver(Topic, CPid) when is_binary(Topic) andalso is_pid(CPid) -> - case emqtt_topic:wildcard(Topic) of + case emqttd_topic:wildcard(Topic) of false -> dispatch(CPid, mnesia:dirty_read(message, Topic)); true -> Fun = fun(Msg = #mqtt_message{topic = Name}, Acc) -> - case emqtt_topic:match(Name, Topic) of + case emqttd_topic:match(Name, Topic) of true -> [Msg|Acc]; false -> Acc end @@ -123,7 +123,7 @@ redeliver(Topic, CPid) when is_binary(Topic) andalso is_pid(CPid) -> dispatch(_CPid, []) -> ignore; dispatch(CPid, Msgs) when is_list(Msgs) -> - CPid ! {dispatch, {self(), [Msg || Msg <- Msgs]}}; + [CPid ! {dispatch, Msg} || Msg <- Msgs]; dispatch(CPid, Msg) when is_record(Msg, mqtt_message) -> - CPid ! {dispatch, {self(), Msg}}. + CPid ! {dispatch, Msg}. diff --git a/apps/emqttd/src/emqttd_net.erl b/src/emqttd_net.erl similarity index 99% rename from apps/emqttd/src/emqttd_net.erl rename to src/emqttd_net.erl index 0b0517f6b..8488fe76e 100644 --- a/apps/emqttd/src/emqttd_net.erl +++ b/src/emqttd_net.erl @@ -32,7 +32,7 @@ -export([tcp_name/3, tcp_host/1, getopts/2, setopts/2, getaddr/2, port_to_listeners/1]). --export([peername/1, sockname/1, format/2, format/1, connection_string/2]). +-export([peername/1, sockname/1, format/2, format/1, connection_string/2, ntoa/1]). -define(FIRST_TEST_BIND_PORT, 10000). diff --git a/apps/emqttd/src/emqttd_opts.erl b/src/emqttd_opts.erl similarity index 86% rename from apps/emqttd/src/emqttd_opts.erl rename to src/emqttd_opts.erl index a83466a73..dfd5b74b6 100644 --- a/apps/emqttd/src/emqttd_opts.erl +++ b/src/emqttd_opts.erl @@ -28,7 +28,7 @@ -author("Feng Lee "). --export([merge/2]). +-export([merge/2, g/2, g/3]). %%------------------------------------------------------------------------------ %% @doc Merge Options @@ -50,3 +50,13 @@ merge(Defaults, Options) -> end end, Defaults, Options). +%%------------------------------------------------------------------------------ +%% @doc Get option +%% @end +%%------------------------------------------------------------------------------ +g(Key, Options) -> + proplists:get_value(Key, Options). + +g(Key, Options, Default) -> + proplists:get_value(Key, Options, Default). + diff --git a/apps/emqtt/src/emqtt_packet.erl b/src/emqttd_packet.erl similarity index 98% rename from apps/emqtt/src/emqtt_packet.erl rename to src/emqttd_packet.erl index f2b001236..6cb6b415d 100644 --- a/apps/emqtt/src/emqtt_packet.erl +++ b/src/emqttd_packet.erl @@ -24,13 +24,13 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_packet). +-module(emqttd_packet). -author("Feng Lee "). --include("emqtt.hrl"). +-include("emqttd.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% API -export([protocol_name/1, type_name/1, connack_name/1]). diff --git a/apps/emqtt/src/emqtt_parser.erl b/src/emqttd_parser.erl similarity index 98% rename from apps/emqtt/src/emqtt_parser.erl rename to src/emqttd_parser.erl index d2a70993e..bcc9fa1b4 100644 --- a/apps/emqtt/src/emqtt_parser.erl +++ b/src/emqttd_parser.erl @@ -24,13 +24,13 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_parser). +-module(emqttd_parser). -author("Feng Lee "). --include("emqtt.hrl"). +-include("emqttd.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% API -export([init/1, parse/2]). @@ -196,7 +196,7 @@ wrap(Header, Rest) -> % parse_qos(Rest, [QoS | Acc]). parse_topics(_, <<>>, Topics) -> - Topics; + lists:reverse(Topics); parse_topics(?SUBSCRIBE = Sub, Bin, Topics) -> {Name, <<_:6, QoS:2, Rest/binary>>} = parse_utf(Bin), parse_topics(Sub, Rest, [{Name, QoS}| Topics]); diff --git a/apps/emqttd/src/emqttd_pooler.erl b/src/emqttd_pooler.erl similarity index 100% rename from apps/emqttd/src/emqttd_pooler.erl rename to src/emqttd_pooler.erl diff --git a/apps/emqttd/src/emqttd_pooler_sup.erl b/src/emqttd_pooler_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_pooler_sup.erl rename to src/emqttd_pooler_sup.erl diff --git a/apps/emqttd/src/emqttd_protocol.erl b/src/emqttd_protocol.erl similarity index 61% rename from apps/emqttd/src/emqttd_protocol.erl rename to src/emqttd_protocol.erl index 08b24674f..9be6b41fc 100644 --- a/apps/emqttd/src/emqttd_protocol.erl +++ b/src/emqttd_protocol.erl @@ -28,20 +28,17 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). --include_lib("emqtt/include/emqtt_packet.hrl"). - -include("emqttd.hrl"). +-include("emqttd_protocol.hrl"). + %% API --export([init/3, clientid/1]). +-export([init/3, info/1, clientid/1, client/1]). -export([received/2, send/2, redeliver/2, shutdown/2]). -export([handle/2]). --export([info/1]). - %% Protocol State -record(proto_state, { peername, @@ -49,30 +46,28 @@ connected = false, %received CONNECT action? proto_ver, proto_name, - %packet_id, username, clientid, clean_sess, - session, %% session state or session pid + session, will_msg, - max_clientid_len = ?MAX_CLIENTID_LEN + max_clientid_len = ?MAX_CLIENTID_LEN, + client_pid }). -type proto_state() :: #proto_state{}. +%%------------------------------------------------------------------------------ +%% @doc Init protocol +%% @end +%%------------------------------------------------------------------------------ init(Peername, SendFun, Opts) -> MaxLen = proplists:get_value(max_clientid_len, Opts, ?MAX_CLIENTID_LEN), - #proto_state{ - peername = Peername, - sendfun = SendFun, - max_clientid_len = MaxLen}. + #proto_state{peername = Peername, + sendfun = SendFun, + max_clientid_len = MaxLen, + client_pid = self()}. -clientid(#proto_state{clientid = ClientId}) -> ClientId. - -client(#proto_state{peername = {Addr, _Port}, clientid = ClientId, username = Username}) -> - #mqtt_client{clientid = ClientId, username = Username, ipaddr = Addr}. - -%%SHOULD be registered in emqttd_cm info(#proto_state{proto_ver = ProtoVer, proto_name = ProtoName, clientid = ClientId, @@ -80,11 +75,27 @@ info(#proto_state{proto_ver = ProtoVer, will_msg = WillMsg}) -> [{proto_ver, ProtoVer}, {proto_name, ProtoName}, - {clientid, ClientId}, + {clientid, ClientId}, {clean_sess, CleanSess}, {will_msg, WillMsg}]. -%%CONNECT – Client requests a connection to a Server +clientid(#proto_state{clientid = ClientId}) -> + ClientId. + +client(#proto_state{peername = {Addr, _Port}, + clientid = ClientId, + username = Username, + clean_sess = CleanSess, + proto_ver = ProtoVer, + client_pid = Pid}) -> + #mqtt_client{clientid = ClientId, + username = Username, + ipaddress = Addr, + clean_sess = CleanSess, + proto_ver = ProtoVer, + client_pid = Pid}. + +%% CONNECT – Client requests a connection to a Server %%A Client can only send the CONNECT Packet once over a Network Connection. -spec received(mqtt_packet(), proto_state()) -> {ok, proto_state()} | {error, any()}. @@ -107,42 +118,42 @@ received(Packet = ?PACKET(_Type), State) -> {error, Reason, State} end. -handle(Packet = ?CONNECT_PACKET(Var), State = #proto_state{peername = Peername = {Addr, _}}) -> +handle(Packet = ?CONNECT_PACKET(Var), State0 = #proto_state{peername = Peername}) -> #mqtt_packet_connect{proto_ver = ProtoVer, + proto_name = ProtoName, username = Username, password = Password, clean_sess = CleanSess, keep_alive = KeepAlive, - clientid = ClientId} = Var, + clientid = ClientId} = Var, - trace(recv, Packet, State#proto_state{clientid = ClientId}), %%TODO: fix later... + State1 = State0#proto_state{proto_ver = ProtoVer, + proto_name = ProtoName, + username = Username, + clientid = ClientId, + clean_sess = CleanSess}, - State1 = State#proto_state{proto_ver = ProtoVer, - username = Username, - clientid = ClientId, - clean_sess = CleanSess}, - {ReturnCode1, State2} = - case validate_connect(Var, State) of + trace(recv, Packet, State1), + + {ReturnCode1, State3} = + case validate_connect(Var, State1) of ?CONNACK_ACCEPT -> - Client = #mqtt_client{clientid = ClientId, username = Username, ipaddr = Addr}, - case emqttd_access_control:auth(Client, Password) of + case emqttd_access_control:auth(client(State1), Password) of ok -> - %% Generate one if null - ClientId1 = clientid(ClientId, State), - %% Register clientId - emqttd_cm:register(ClientId1), + %% Generate clientId if null + State2 = State1#proto_state{clientid = clientid(ClientId, State1)}, + %%Starting session - {ok, Session} = emqttd_session:start({CleanSess, ClientId1, self()}), + {ok, Session} = emqttd_sm:start_session(CleanSess, clientid(State2)), + %% Start keepalive start_keepalive(KeepAlive), - %% Run hooks - emqttd_broker:foreach_hooks(client_connected, [{self(), ClientId1}]), - {?CONNACK_ACCEPT, State1#proto_state{clientid = ClientId1, - session = Session, - will_msg = willmsg(Var)}}; + + %% ACCEPT + {?CONNACK_ACCEPT, State2#proto_state{session = Session, will_msg = willmsg(Var)}}; {error, Reason}-> - lager:error("~s@~s: username '~s' login failed - ~s", + lager:error("~s@~s: username '~s', login failed - ~s", [ClientId, emqttd_net:format(Peername), Username, Reason]), {?CONNACK_CREDENTIALS, State1} @@ -150,55 +161,39 @@ handle(Packet = ?CONNECT_PACKET(Var), State = #proto_state{peername = Peername = ReturnCode -> {ReturnCode, State1} end, - %%TODO: this is not right... - notify(connected, ReturnCode1, State2), - send(?CONNACK_PACKET(ReturnCode1), State2); + %% Run hooks + emqttd_broker:foreach_hooks('client.connected', [ReturnCode1, client(State3)]), + %% Send connack + send(?CONNACK_PACKET(ReturnCode1), State3); + +handle(Packet = ?PUBLISH_PACKET(_Qos, Topic, _PacketId, _Payload), + State = #proto_state{clientid = ClientId}) -> -handle(Packet = ?PUBLISH_PACKET(?QOS_0, Topic, _PacketId, _Payload), - State = #proto_state{clientid = ClientId, session = Session}) -> case check_acl(publish, Topic, State) of - allow -> - do_publish(Session, ClientId, ?QOS_0, Packet); + allow -> + publish(Packet, State); deny -> lager:error("ACL Deny: ~s cannot publish to ~s", [ClientId, Topic]) end, {ok, State}; -handle(Packet = ?PUBLISH_PACKET(?QOS_1, Topic, PacketId, _Payload), - State = #proto_state{clientid = ClientId, session = Session}) -> - case check_acl(publish, Topic, State) of - allow -> - do_publish(Session, ClientId, ?QOS_1, Packet), - send(?PUBACK_PACKET(?PUBACK, PacketId), State); - deny -> - lager:error("ACL Deny: ~s cannot publish to ~s", [ClientId, Topic]), - {ok, State} - end; -handle(Packet = ?PUBLISH_PACKET(?QOS_2, Topic, PacketId, _Payload), - State = #proto_state{clientid = ClientId, session = Session}) -> - case check_acl(publish, Topic, State) of - allow -> - NewSession = do_publish(Session, ClientId, ?QOS_2, Packet), - send(?PUBACK_PACKET(?PUBREC, PacketId), State#proto_state{session = NewSession}); - deny -> - lager:error("ACL Deny: ~s cannot publish to ~s", [ClientId, Topic]), - {ok, State} - end; -handle(?PUBACK_PACKET(Type, PacketId), State = #proto_state{session = Session}) - when Type >= ?PUBACK andalso Type =< ?PUBCOMP -> - NewSession = emqttd_session:puback(Session, {Type, PacketId}), - NewState = State#proto_state{session = NewSession}, - if - Type =:= ?PUBREC -> - send(?PUBREL_PACKET(PacketId), NewState); - Type =:= ?PUBREL -> - send(?PUBACK_PACKET(?PUBCOMP, PacketId), NewState); - true -> - ok - end, - {ok, NewState}; +handle(?PUBACK_PACKET(?PUBACK, PacketId), State = #proto_state{session = Session}) -> + emqttd_session:puback(Session, PacketId), + {ok, State}; + +handle(?PUBACK_PACKET(?PUBREC, PacketId), State = #proto_state{session = Session}) -> + emqttd_session:pubrec(Session, PacketId), + send(?PUBREL_PACKET(PacketId), State); + +handle(?PUBACK_PACKET(?PUBREL, PacketId), State = #proto_state{session = Session}) -> + emqttd_session:pubrel(Session, PacketId), + send(?PUBACK_PACKET(?PUBCOMP, PacketId), State); + +handle(?PUBACK_PACKET(?PUBCOMP, PacketId), State = #proto_state{session = Session}) -> + emqttd_session:pubcomp(Session, PacketId), + {ok, State}; %% protect from empty topic list handle(?SUBSCRIBE_PACKET(PacketId, []), State) -> @@ -214,13 +209,13 @@ handle(?SUBSCRIBE_PACKET(PacketId, TopicTable), State = #proto_state{clientid = false -> TopicTable1 = emqttd_broker:foldl_hooks(client_subscribe, [], TopicTable), %%TODO: GrantedQos should be renamed. - {ok, NewSession, GrantedQos} = emqttd_session:subscribe(Session, TopicTable1), - send(?SUBACK_PACKET(PacketId, GrantedQos), State#proto_state{session = NewSession}) + {ok, GrantedQos} = emqttd_session:subscribe(Session, TopicTable1), + send(?SUBACK_PACKET(PacketId, GrantedQos), State) end; -handle({subscribe, Topic, Qos}, State = #proto_state{session = Session}) -> - {ok, NewSession, _GrantedQos} = emqttd_session:subscribe(Session, [{Topic, Qos}]), - {ok, State#proto_state{session = NewSession}}; +handle({subscribe, TopicTable}, State = #proto_state{session = Session}) -> + {ok, _GrantedQos} = emqttd_session:subscribe(Session, TopicTable), + {ok, State}; %% protect from empty topic list handle(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> @@ -228,40 +223,45 @@ handle(?UNSUBSCRIBE_PACKET(PacketId, []), State) -> handle(?UNSUBSCRIBE_PACKET(PacketId, Topics), State = #proto_state{session = Session}) -> Topics1 = emqttd_broker:foldl_hooks(client_unsubscribe, [], Topics), - {ok, NewSession} = emqttd_session:unsubscribe(Session, Topics1), - send(?UNSUBACK_PACKET(PacketId), State#proto_state{session = NewSession}); + ok = emqttd_session:unsubscribe(Session, Topics1), + send(?UNSUBACK_PACKET(PacketId), State); handle(?PACKET(?PINGREQ), State) -> send(?PACKET(?PINGRESP), State); handle(?PACKET(?DISCONNECT), State) -> - %%TODO: how to handle session? % clean willmsg {stop, normal, State#proto_state{will_msg = undefined}}. -do_publish(Session, ClientId, Qos, Packet) -> - Message = emqttd_broker:foldl_hooks(client_publish, [], emqtt_message:from_packet(Packet)), - emqttd_session:publish(Session, ClientId, {Qos, Message}). +publish(Packet = ?PUBLISH(?QOS_0, _PacketId), #proto_state{clientid = ClientId, session = Session}) -> + emqttd_session:publish(Session, emqttd_message:from_packet(ClientId, Packet)); --spec send({pid() | tuple(), mqtt_message()} | mqtt_packet(), proto_state()) -> {ok, proto_state()}. -%% qos0 message -send({_From, Message = #mqtt_message{qos = ?QOS_0}}, State) -> - send(emqtt_message:to_packet(Message), State); +publish(Packet = ?PUBLISH(?QOS_1, PacketId), State = #proto_state{clientid = ClientId, session = Session}) -> + case emqttd_session:publish(Session, emqttd_message:from_packet(ClientId, Packet)) of + ok -> + send(?PUBACK_PACKET(?PUBACK, PacketId), State); + {error, Error} -> + %%TODO: log format... + lager:error("Client ~s: publish qos1 error ~p", [ClientId, Error]) + end; -%% message from session -send({_From = SessPid, Message}, State = #proto_state{session = SessPid}) when is_pid(SessPid) -> - send(emqtt_message:to_packet(Message), State); +publish(Packet = ?PUBLISH(?QOS_2, PacketId), State = #proto_state{clientid = ClientId, session = Session}) -> + case emqttd_session:publish(Session, emqttd_message:from_packet(ClientId, Packet)) of + ok -> + send(?PUBACK_PACKET(?PUBREC, PacketId), State); + {error, Error} -> + %%TODO: log format... + lager:error("Client ~s: publish qos2 error ~p", [ClientId, Error]) + end. -%% message(qos1, qos2) not from session -send({_From, Message = #mqtt_message{qos = Qos}}, State = #proto_state{session = Session}) - when (Qos =:= ?QOS_1) orelse (Qos =:= ?QOS_2) -> - {Message1, NewSession} = emqttd_session:store(Session, Message), - send(emqtt_message:to_packet(Message1), State#proto_state{session = NewSession}); +-spec send(mqtt_message() | mqtt_packet(), proto_state()) -> {ok, proto_state()}. +send(Msg, State) when is_record(Msg, mqtt_message) -> + send(emqttd_message:to_packet(Msg), State); send(Packet, State = #proto_state{sendfun = SendFun, peername = Peername}) when is_record(Packet, mqtt_packet) -> trace(send, Packet, State), sent_stats(Packet), - Data = emqtt_serialiser:serialise(Packet), + Data = emqttd_serialiser:serialise(Packet), lager:debug("SENT to ~s: ~p", [emqttd_net:format(Peername), Data]), emqttd_metrics:inc('bytes/sent', size(Data)), SendFun(Data), @@ -269,25 +269,30 @@ send(Packet, State = #proto_state{sendfun = SendFun, peername = Peername}) when trace(recv, Packet, #proto_state{peername = Peername, clientid = ClientId}) -> lager:info([{client, ClientId}], "RECV from ~s@~s: ~s", - [ClientId, emqttd_net:format(Peername), emqtt_packet:format(Packet)]); + [ClientId, emqttd_net:format(Peername), emqttd_packet:format(Packet)]); trace(send, Packet, #proto_state{peername = Peername, clientid = ClientId}) -> lager:info([{client, ClientId}], "SEND to ~s@~s: ~s", - [ClientId, emqttd_net:format(Peername), emqtt_packet:format(Packet)]). + [ClientId, emqttd_net:format(Peername), emqttd_packet:format(Packet)]). %% @doc redeliver PUBREL PacketId redeliver({?PUBREL, PacketId}, State) -> send(?PUBREL_PACKET(PacketId), State). +shutdown(duplicate_id, _State) -> + quiet; %% + +shutdown(_, #proto_state{clientid = undefined}) -> + ignore; + shutdown(Error, #proto_state{peername = Peername, clientid = ClientId, will_msg = WillMsg}) -> - send_willmsg(ClientId, WillMsg), - try_unregister(ClientId, self()), - lager:info([{client, ClientId}], "Protocol ~s@~s Shutdown: ~p", + lager:info([{client, ClientId}], "Client ~s@~s: shutdown ~p", [ClientId, emqttd_net:format(Peername), Error]), - ok. + send_willmsg(ClientId, WillMsg), + emqttd_broker:foreach_hooks('client.disconnected', [Error, ClientId]). willmsg(Packet) when is_record(Packet, mqtt_packet_connect) -> - emqtt_message:from_packet(Packet). + emqttd_message:from_packet(Packet). %% generate a clientId clientid(undefined, State) -> @@ -300,9 +305,9 @@ clientid(ClientId, _State) -> ClientId. send_willmsg(_ClientId, undefined) -> ignore; -%%TODO:should call session... send_willmsg(ClientId, WillMsg) -> - emqttd_pubsub:publish(ClientId, WillMsg). + lager:info("Client ~s send willmsg: ~p", [ClientId, WillMsg]), + emqttd_pubsub:publish(WillMsg#mqtt_message{from = ClientId}). start_keepalive(0) -> ignore; @@ -343,7 +348,7 @@ validate_clientid(#mqtt_packet_connect {proto_ver = Ver, clean_sess = CleanSess, validate_packet(#mqtt_packet{header = #mqtt_packet_header{type = ?PUBLISH}, variable = #mqtt_packet_publish{topic_name = Topic}}) -> - case emqtt_topic:validate({name, Topic}) of + case emqttd_topic:validate({name, Topic}) of true -> ok; false -> lager:warning("Error publish topic: ~p", [Topic]), {error, badtopic} end; @@ -367,7 +372,7 @@ validate_topics(Type, []) when Type =:= name orelse Type =:= filter -> validate_topics(Type, Topics) when Type =:= name orelse Type =:= filter -> ErrTopics = [Topic || {Topic, Qos} <- Topics, - not (emqtt_topic:validate({Type, Topic}) and validate_qos(Qos))], + not (emqttd_topic:validate({Type, Topic}) and validate_qos(Qos))], case ErrTopics of [] -> ok; _ -> lager:error("Error Topics: ~p", [ErrTopics]), {error, badtopic} @@ -377,9 +382,6 @@ validate_qos(undefined) -> true; validate_qos(Qos) when Qos =< ?QOS_2 -> true; validate_qos(_) -> false. -try_unregister(undefined, _) -> ok; -try_unregister(ClientId, _) -> emqttd_cm:unregister(ClientId). - %% publish ACL is cached in process dictionary. check_acl(publish, Topic, State) -> case get({acl, publish, Topic}) of @@ -411,18 +413,3 @@ inc(?PINGRESP) -> inc(_) -> ingore. -notify(connected, ReturnCode, #proto_state{peername = Peername, - proto_ver = ProtoVer, - clientid = ClientId, - clean_sess = CleanSess}) -> - Sess = case CleanSess of - true -> false; - false -> true - end, - Params = [{from, emqttd_net:format(Peername)}, - {protocol, ProtoVer}, - {session, Sess}, - {connack, ReturnCode}], - emqttd_event:notify({connected, ClientId, Params}). - - diff --git a/apps/emqttd/src/emqttd_pubsub.erl b/src/emqttd_pubsub.erl similarity index 90% rename from apps/emqttd/src/emqttd_pubsub.erl rename to src/emqttd_pubsub.erl index b52037d29..3699bfc9f 100644 --- a/apps/emqttd/src/emqttd_pubsub.erl +++ b/src/emqttd_pubsub.erl @@ -24,15 +24,14 @@ %%% %%% @end %%%----------------------------------------------------------------------------- + -module(emqttd_pubsub). -author("Feng Lee "). -include("emqttd.hrl"). --include_lib("emqtt/include/emqtt.hrl"). - --include_lib("emqtt/include/emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% Mnesia Callbacks -export([mnesia/1]). @@ -48,7 +47,7 @@ -export([create/1, subscribe/1, unsubscribe/1, - publish/2, + publish/1, %local node dispatch/2, match/1]). @@ -82,7 +81,7 @@ mnesia(boot) -> {ram_copies, [node()]}, {record_name, mqtt_subscriber}, {attributes, record_info(fields, mqtt_subscriber)}, - {index, [pid]}, + {index, [subpid]}, {local_content, true}]); mnesia(copy) -> @@ -157,29 +156,33 @@ cast(Msg) -> %% @doc Publish to cluster nodes %% @end %%------------------------------------------------------------------------------ --spec publish(From :: mqtt_clientid() | atom(), Msg :: mqtt_message()) -> ok. -publish(From, #mqtt_message{topic=Topic} = Msg) -> +-spec publish(Msg :: mqtt_message()) -> ok. +publish(#mqtt_message{topic=Topic, from = From} = Msg) -> trace(publish, From, Msg), + + %%TODO:call hooks here... + %%Msg1 = emqttd_broker:foldl_hooks(client_publish, [], Msg), + %% Retain message first. Don't create retained topic. case emqttd_msg_store:retain(Msg) of ok -> %TODO: why unset 'retain' flag? - publish(From, Topic, emqtt_message:unset_flag(Msg)); + publish(Topic, emqttd_message:unset_flag(Msg)); ignore -> - publish(From, Topic, Msg) + publish(Topic, Msg) end. -publish(From, <<"$Q/", _/binary>> = Queue, #mqtt_message{qos = Qos} = Msg) -> +publish(<<"$Q/", _/binary>> = Queue, #mqtt_message{qos = Qos} = Msg) -> lists:foreach( fun(#mqtt_queue{subpid = SubPid, qos = SubQos}) -> Msg1 = if Qos > SubQos -> Msg#mqtt_message{qos = SubQos}; true -> Msg end, - SubPid ! {dispatch, {self(), Msg1}} + SubPid ! {dispatch, Msg1} end, mnesia:dirty_read(queue, Queue)); -publish(_From, Topic, Msg) when is_binary(Topic) -> +publish(Topic, Msg) when is_binary(Topic) -> lists:foreach(fun(#mqtt_topic{topic=Name, node=Node}) -> case Node =:= node() of true -> dispatch(Name, Msg); @@ -196,12 +199,12 @@ dispatch(Topic, #mqtt_message{qos = Qos} = Msg ) when is_binary(Topic) -> Subscribers = mnesia:dirty_read(subscriber, Topic), setstats(dropped, Subscribers =:= []), %%TODO:... lists:foreach( - fun(#mqtt_subscriber{qos = SubQos, pid=SubPid}) -> + fun(#mqtt_subscriber{subpid=SubPid, qos = SubQos}) -> Msg1 = if Qos > SubQos -> Msg#mqtt_message{qos = SubQos}; true -> Msg end, - SubPid ! {dispatch, {self(), Msg1}} + SubPid ! {dispatch, Msg1} end, Subscribers), length(Subscribers). @@ -217,6 +220,7 @@ match(Topic) when is_binary(Topic) -> init([Id, _Opts]) -> process_flag(min_heap_size, 1024*1024), gproc_pool:connect_worker(pubsub, {?MODULE, Id}), + %%TODO: gb_trees to replace maps? {ok, #state{id = Id, submap = maps:new()}}. handle_call({subscribe, SubPid, Topics}, _From, State) -> @@ -224,7 +228,7 @@ handle_call({subscribe, SubPid, Topics}, _From, State) -> #mqtt_queue{name = Queue, subpid = SubPid, qos = Qos}; ({Topic, Qos}) -> {#mqtt_topic{topic = Topic, node = node()}, - #mqtt_subscriber{topic = Topic, qos = Qos, pid = SubPid}} + #mqtt_subscriber{topic = Topic, subpid = SubPid, qos = Qos}} end, Topics), F = fun() -> lists:map(fun(QueueR) when is_record(QueueR, mqtt_queue) -> @@ -259,7 +263,7 @@ handle_call({subscribe, SubPid, <<"$Q/", _/binary>> = Queue, Qos}, _From, State) handle_call({subscribe, SubPid, Topic, Qos}, _From, State) -> TopicR = #mqtt_topic{topic = Topic, node = node()}, - Subscriber = #mqtt_subscriber{topic = Topic, qos = Qos, pid = SubPid}, + Subscriber = #mqtt_subscriber{topic = Topic, subpid = SubPid, qos = Qos}, case mnesia:transaction(fun add_subscriber/1, [{TopicR, Subscriber}]) of {atomic, ok} -> setstats(all), @@ -278,7 +282,7 @@ handle_cast({unsubscribe, SubPid, Topics}, State) when is_list(Topics) -> #mqtt_queue{name = Queue, subpid = SubPid}; (Topic) -> {#mqtt_topic{topic = Topic, node = node()}, - #mqtt_subscriber{topic = Topic, _ = '_', pid = SubPid}} + #mqtt_subscriber{topic = Topic, subpid = SubPid, _ = '_'}} end, Topics), F = fun() -> lists:foreach( @@ -307,7 +311,7 @@ handle_cast({unsubscribe, SubPid, <<"$Q/", _/binary>> = Queue}, State) -> handle_cast({unsubscribe, SubPid, Topic}, State) -> TopicR = #mqtt_topic{topic = Topic, node = node()}, - Subscriber = #mqtt_subscriber{topic = Topic, _ = '_', pid = SubPid}, + Subscriber = #mqtt_subscriber{topic = Topic, subpid = SubPid, _ = '_'}, case mnesia:transaction(fun remove_subscriber/1, [{TopicR, Subscriber}]) of {atomic, _} -> ok; {aborted, Error} -> lager:error("unsubscribe ~s error: ~p", [Topic, Error]) @@ -331,7 +335,7 @@ handle_info({'DOWN', _Mon, _Type, DownPid, _Info}, State = #state{submap = SubMa end, Queues), %% remove subscribers... - Subscribers = mnesia:index_read(subscriber, DownPid, #mqtt_subscriber.pid), + Subscribers = mnesia:index_read(subscriber, DownPid, #mqtt_subscriber.subpid), lists:foreach(fun(Sub = #mqtt_subscriber{topic = Topic}) -> mnesia:delete_object(subscriber, Sub, write), try_remove_topic(#mqtt_topic{topic = Topic, node = Node}) @@ -384,9 +388,24 @@ add_topic(TopicR = #mqtt_topic{topic = Topic}) -> end end. -add_subscriber({TopicR, Subscriber}) when is_record(TopicR, mqtt_topic) -> +%% Fix issue #53 - Remove Overlapping Subscriptions +add_subscriber({TopicR, Subscriber = #mqtt_subscriber{topic = Topic, subpid = SubPid, qos = Qos}}) + when is_record(TopicR, mqtt_topic) -> case add_topic(TopicR) of ok -> + OverlapSubs = [Sub || Sub = #mqtt_subscriber{topic = SubTopic, qos = SubQos} + <- mnesia:index_read(subscriber, SubPid, #mqtt_subscriber.subpid), + SubTopic =:= Topic, SubQos =/= Qos], + + %% remove overlapping subscribers + if + length(OverlapSubs) =:= 0 -> ok; + true -> + lager:warning("Remove overlapping subscribers: ~p", [OverlapSubs]), + [mnesia:delete_object(subscriber, OverlapSub, write) || OverlapSub <- OverlapSubs] + end, + + %% insert subscriber mnesia:write(subscriber, Subscriber, write); Error -> Error diff --git a/apps/emqttd/src/emqttd_pubsub_sup.erl b/src/emqttd_pubsub_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_pubsub_sup.erl rename to src/emqttd_pubsub_sup.erl diff --git a/apps/emqtt/src/emqtt_serialiser.erl b/src/emqttd_serialiser.erl similarity index 98% rename from apps/emqtt/src/emqtt_serialiser.erl rename to src/emqttd_serialiser.erl index 60d1f6dfa..e109c19a3 100644 --- a/apps/emqtt/src/emqtt_serialiser.erl +++ b/src/emqttd_serialiser.erl @@ -24,13 +24,13 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_serialiser). +-module(emqttd_serialiser). -author("Feng Lee "). --include("emqtt.hrl"). +-include("emqttd.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). %% API -export([serialise/1]). diff --git a/src/emqttd_session.erl b/src/emqttd_session.erl new file mode 100644 index 000000000..93c9fd54b --- /dev/null +++ b/src/emqttd_session.erl @@ -0,0 +1,613 @@ +%%%----------------------------------------------------------------------------- +%%% Copyright (c) 2012-2015 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 +%%% +%%% Session for persistent MQTT client. +%%% +%%% Session State in the broker consists of: +%%% +%%% 1. The Client’s subscriptions. +%%% +%%% 2. inflight qos1/2 messages sent to the client but unacked, QoS 1 and QoS 2 +%%% messages which have been sent to the Client, but have not been completely +%%% acknowledged. +%%% +%%% 3. inflight qos2 messages received from client and waiting for pubrel. QoS 2 +%%% messages which have been received from the Client, but have not been +%%% completely acknowledged. +%%% +%%% 4. all qos1, qos2 messages published to when client is disconnected. +%%% QoS 1 and QoS 2 messages pending transmission to the Client. +%%% +%%% 5. Optionally, QoS 0 messages pending transmission to the Client. +%%% +%%% State of Message: newcome, inflight, pending +%%% +%%% @end +%%%----------------------------------------------------------------------------- + +-module(emqttd_session). + +-author("Feng Lee "). + +-include("emqttd.hrl"). + +-include("emqttd_protocol.hrl"). + +%% Session API +-export([start_link/3, resume/3, destroy/2]). + +%% PubSub APIs +-export([publish/2, + puback/2, pubrec/2, pubrel/2, pubcomp/2, + subscribe/2, unsubscribe/2]). + +-behaviour(gen_server). + +%% gen_server Function Exports +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + terminate/2, code_change/3]). + +-record(session, { + + %% Clean Session Flag + clean_sess = true, + + %% ClientId: Identifier of Session + clientid :: binary(), + + %% Client Pid linked with session + client_pid :: pid(), + + %% Last message id of the session + message_id = 1, + + %% Client’s subscriptions. + subscriptions :: list(), + + %% Inflight qos1, qos2 messages sent to the client but unacked, + %% QoS 1 and QoS 2 messages which have been sent to the Client, + %% but have not been completely acknowledged. + %% Client <- Broker + inflight_queue :: list(), + + max_inflight = 0, + + %% All qos1, qos2 messages published to when client is disconnected. + %% QoS 1 and QoS 2 messages pending transmission to the Client. + %% + %% Optionally, QoS 0 messages pending transmission to the Client. + message_queue :: emqttd_mqueue:mqueue(), + + %% Inflight qos2 messages received from client and waiting for pubrel. + %% QoS 2 messages which have been received from the Client, + %% but have not been completely acknowledged. + %% Client -> Broker + awaiting_rel :: map(), + + %% Awaiting PUBREL timeout + await_rel_timeout = 8, + + %% Max Packets that Awaiting PUBREL + max_awaiting_rel = 100, + + %% Awaiting timers for ack, rel and comp. + awaiting_ack :: map(), + + %% Retries to resend the unacked messages + unack_retries = 3, + + %% 4, 8, 16 seconds if 3 retries:) + unack_timeout = 4, + + %% Awaiting for PUBCOMP + awaiting_comp :: map(), + + %% session expired after 48 hours + expired_after = 172800, + + expired_timer, + + timestamp}). + +%%------------------------------------------------------------------------------ +%% @doc Start a session. +%% @end +%%------------------------------------------------------------------------------ +-spec start_link(boolean(), mqtt_clientid(), pid()) -> {ok, pid()} | {error, any()}. +start_link(CleanSess, ClientId, ClientPid) -> + gen_server:start_link(?MODULE, [CleanSess, ClientId, ClientPid], []). + +%%------------------------------------------------------------------------------ +%% @doc Resume a session. +%% @end +%%------------------------------------------------------------------------------ +-spec resume(pid(), mqtt_clientid(), pid()) -> ok. +resume(Session, ClientId, ClientPid) -> + gen_server:cast(Session, {resume, ClientId, ClientPid}). + +%%------------------------------------------------------------------------------ +%% @doc Destroy a session. +%% @end +%%------------------------------------------------------------------------------ +-spec destroy(pid(), mqtt_clientid()) -> ok. +destroy(Session, ClientId) -> + gen_server:call(Session, {destroy, ClientId}). + +%%------------------------------------------------------------------------------ +%% @doc Subscribe Topics +%% @end +%%------------------------------------------------------------------------------ +-spec subscribe(pid(), [{binary(), mqtt_qos()}]) -> {ok, [mqtt_qos()]}. +subscribe(Session, TopicTable) -> + gen_server:call(Session, {subscribe, TopicTable}). + +%%------------------------------------------------------------------------------ +%% @doc Publish message +%% @end +%%------------------------------------------------------------------------------ +-spec publish(pid(), mqtt_message()) -> ok. +publish(_Session, Msg = #mqtt_message{qos = ?QOS_0}) -> + %% publish qos0 directly + emqttd_pubsub:publish(Msg); + +publish(_Session, Msg = #mqtt_message{qos = ?QOS_1}) -> + %% publish qos1 directly, and client will puback automatically + emqttd_pubsub:publish(Msg); + +publish(Session, Msg = #mqtt_message{qos = ?QOS_2}) -> + %% publish qos2 by session + gen_server:call(Session, {publish, Msg}). + +%%------------------------------------------------------------------------------ +%% @doc PubAck message +%% @end +%%------------------------------------------------------------------------------ +-spec puback(pid(), mqtt_msgid()) -> ok. +puback(Session, MsgId) -> + gen_server:cast(Session, {puback, MsgId}). + +-spec pubrec(pid(), mqtt_msgid()) -> ok. +pubrec(Session, MsgId) -> + gen_server:cast(Session, {pubrec, MsgId}). + +-spec pubrel(pid(), mqtt_msgid()) -> ok. +pubrel(Session, MsgId) -> + gen_server:cast(Session, {pubrel, MsgId}). + +-spec pubcomp(pid(), mqtt_msgid()) -> ok. +pubcomp(Session, MsgId) -> + gen_server:cast(Session, {pubcomp, MsgId}). + +%%------------------------------------------------------------------------------ +%% @doc Unsubscribe Topics +%% @end +%%------------------------------------------------------------------------------ +-spec unsubscribe(pid(), [binary()]) -> ok. +unsubscribe(Session, Topics) -> + gen_server:call(Session, {unsubscribe, Topics}). + +%%%============================================================================= +%%% gen_server callbacks +%%%============================================================================= + +init([CleanSess, ClientId, ClientPid]) -> + process_flag(trap_exit, true), + true = link(ClientPid), + QEnv = emqttd:env(mqtt, queue), + SessEnv = emqttd:env(mqtt, session), + Session = #session{ + clean_sess = CleanSess, + clientid = ClientId, + client_pid = ClientPid, + subscriptions = [], + inflight_queue = [], + max_inflight = emqttd_opts:g(max_inflight, SessEnv, 0), + message_queue = emqttd_mqueue:new(ClientId, QEnv, emqttd_alarm:alarm_fun()), + awaiting_rel = #{}, + awaiting_ack = #{}, + awaiting_comp = #{}, + unack_retries = emqttd_opts:g(unack_retries, SessEnv), + unack_timeout = emqttd_opts:g(unack_timeout, SessEnv), + await_rel_timeout = emqttd_opts:g(await_rel_timeout, SessEnv), + max_awaiting_rel = emqttd_opts:g(max_awaiting_rel, SessEnv), + expired_after = emqttd_opts:g(expired_after, SessEnv) * 3600, + timestamp = os:timestamp()}, + {ok, Session, hibernate}. + +handle_call({subscribe, Topics}, _From, Session = #session{clientid = ClientId, + subscriptions = Subscriptions}) -> + + %% subscribe first and don't care if the subscriptions have been existed + {ok, GrantedQos} = emqttd_pubsub:subscribe(Topics), + + lager:info([{client, ClientId}], "Session ~s subscribe ~p, Granted QoS: ~p", + [ClientId, Topics, GrantedQos]), + + Subscriptions1 = + lists:foldl(fun({Topic, Qos}, Acc) -> + case lists:keyfind(Topic, 1, Acc) of + {Topic, Qos} -> + lager:warning([{client, ClientId}], "Session ~s " + "resubscribe ~p: qos = ~p", [ClientId, Topic, Qos]), Acc; + {Topic, OldQos} -> + lager:warning([{client, ClientId}], "Session ~s " + "resubscribe ~p: old qos=~p, new qos=~p", [ClientId, Topic, OldQos, Qos]), + lists:keyreplace(Topic, 1, Acc, {Topic, Qos}); + false -> + %%TODO: the design is ugly, rewrite later...:( + %% : 3.8.4 + %% Where the Topic Filter is not identical to any existing Subscription’s filter, + %% a new Subscription is created and all matching retained messages are sent. + emqttd_msg_store:redeliver(Topic, self()), + [{Topic, Qos} | Acc] + end + end, Subscriptions, Topics), + {reply, {ok, GrantedQos}, Session#session{subscriptions = Subscriptions1}}; + +handle_call({unsubscribe, Topics}, _From, Session = #session{clientid = ClientId, + subscriptions = Subscriptions}) -> + + %% unsubscribe from topic tree + ok = emqttd_pubsub:unsubscribe(Topics), + + lager:info([{client, ClientId}], "Session ~s unsubscribe ~p", [ClientId, Topics]), + + Subscriptions1 = + lists:foldl(fun(Topic, Acc) -> + case lists:keyfind(Topic, 1, Acc) of + {Topic, _Qos} -> + lists:keydelete(Topic, 1, Acc); + false -> + lager:warning([{client, ClientId}], "Session ~s not subscribe ~s", [ClientId, Topic]), Acc + end + end, Subscriptions, Topics), + + {reply, ok, Session#session{subscriptions = Subscriptions1}}; + +handle_call({publish, Msg = #mqtt_message{qos = ?QOS_2, msgid = MsgId}}, _From, + Session = #session{clientid = ClientId, + awaiting_rel = AwaitingRel, + await_rel_timeout = Timeout}) -> + case check_awaiting_rel(Session) of + true -> + TRef = timer(Timeout, {timeout, awaiting_rel, MsgId}), + AwaitingRel1 = maps:put(MsgId, {Msg, TRef}, AwaitingRel), + {reply, ok, Session#session{awaiting_rel = AwaitingRel1}}; + false -> + lager:critical([{clientid, ClientId}], "Session ~s dropped Qos2 message " + "for too many awaiting_rel: ~p", [ClientId, Msg]), + {reply, {error, dropped}, Session} + end; + +handle_call({destroy, ClientId}, _From, Session = #session{clientid = ClientId}) -> + lager:warning("Session ~s destroyed", [ClientId]), + {stop, {shutdown, destroy}, ok, Session}; + +handle_call(Req, _From, State) -> + lager:critical("Unexpected Request: ~p", [Req]), + {reply, {error, badreq}, State}. + +handle_cast({resume, ClientId, ClientPid}, Session) -> + + #session{clientid = ClientId, + client_pid = OldClientPid, + inflight_queue = InflightQ, + awaiting_ack = AwaitingAck, + awaiting_comp = AwaitingComp, + expired_timer = ETimer} = Session, + + lager:info([{client, ClientId}], "Session ~s resumed by ~p",[ClientId, ClientPid]), + + %% cancel expired timer + cancel_timer(ETimer), + + kick(ClientId, ClientPid, OldClientPid), + + true = link(ClientPid), + + %% Redeliver PUBREL + [ClientPid ! {redeliver, {?PUBREL, MsgId}} || MsgId <- maps:keys(AwaitingComp)], + + %% Clear awaiting_ack timers + [cancel_timer(TRef) || {_, TRef} <- maps:values(AwaitingAck)], + + %% Clear awaiting_comp timers + [cancel_timer(TRef) || TRef <- maps:values(AwaitingComp)], + + Session1 = Session#session{client_pid = ClientPid, + awaiting_ack = #{}, + awaiting_comp = #{}, + expired_timer = undefined}, + + %% Redeliver inflight messages + Session2 = + lists:foldl(fun({_Id, Msg}, Sess) -> + redeliver(Msg#mqtt_message{dup = true}, Sess) + end, Session1, lists:reverse(InflightQ)), + + %% Dequeue pending messages + {noreply, dequeue(Session2), hibernate}; + +%% PUBRAC +handle_cast({puback, MsgId}, Session = #session{clientid = ClientId, awaiting_ack = Awaiting}) -> + case maps:find(MsgId, Awaiting) of + {ok, {_, TRef}} -> + cancel_timer(TRef), + Session1 = acked(MsgId, Session), + {noreply, dequeue(Session1)}; + error -> + lager:error("Session ~s cannot find PUBACK '~p'!", [ClientId, MsgId]), + {noreply, Session} + end; + +%% PUBREC +handle_cast({pubrec, MsgId}, Session = #session{clientid = ClientId, + awaiting_ack = AwaitingAck, + awaiting_comp = AwaitingComp, + await_rel_timeout = Timeout}) -> + case maps:find(MsgId, AwaitingAck) of + {ok, {_, TRef}} -> + cancel_timer(TRef), + TRef1 = timer(Timeout, {timeout, awaiting_comp, MsgId}), + Session1 = acked(MsgId, Session#session{awaiting_comp = maps:put(MsgId, TRef1, AwaitingComp)}), + {noreply, dequeue(Session1)}; + error -> + lager:error("Session ~s cannot find PUBREC '~p'!", [ClientId, MsgId]), + {noreply, Session} + end; + +%% PUBREL +handle_cast({pubrel, MsgId}, Session = #session{clientid = ClientId, + awaiting_rel = AwaitingRel}) -> + case maps:find(MsgId, AwaitingRel) of + {ok, {Msg, TRef}} -> + cancel_timer(TRef), + emqttd_pubsub:publish(Msg), + {noreply, Session#session{awaiting_rel = maps:remove(MsgId, AwaitingRel)}}; + error -> + lager:error("Session ~s cannot find PUBREL: msgid=~p!", [ClientId, MsgId]), + {noreply, Session} + end; + +%% PUBCOMP +handle_cast({pubcomp, MsgId}, Session = #session{clientid = ClientId, awaiting_comp = AwaitingComp}) -> + case maps:find(MsgId, AwaitingComp) of + {ok, TRef} -> + cancel_timer(TRef), + {noreply, Session#session{awaiting_comp = maps:remove(MsgId, AwaitingComp)}}; + error -> + lager:error("Session ~s cannot find PUBCOMP: MsgId=~p", [ClientId, MsgId]), + {noreply, Session} + end; + +handle_cast(Msg, State) -> + lager:critical("Unexpected Msg: ~p, State: ~p", [Msg, State]), + {noreply, State}. + +%% Queue messages when client is offline +handle_info({dispatch, Msg}, Session = #session{client_pid = undefined, + message_queue = Q}) + when is_record(Msg, mqtt_message) -> + {noreply, Session#session{message_queue = emqttd_mqueue:in(Msg, Q)}}; + +%% Dispatch qos0 message directly to client +handle_info({dispatch, Msg = #mqtt_message{qos = ?QOS_0}}, + Session = #session{client_pid = ClientPid}) -> + ClientPid ! {deliver, Msg}, + {noreply, Session}; + +handle_info({dispatch, Msg = #mqtt_message{qos = QoS}}, + Session = #session{clientid = ClientId, message_queue = MsgQ}) + when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> + + case check_inflight(Session) of + true -> + {noreply, deliver(Msg, Session)}; + false -> + lager:warning([{client, ClientId}], "Session ~s inflight queue is full!", [ClientId]), + {noreply, Session#session{message_queue = emqttd_mqueue:in(Msg, MsgQ)}} + end; + +handle_info({timeout, awaiting_ack, MsgId}, Session = #session{client_pid = undefined, + awaiting_ack = AwaitingAck}) -> + %% just remove awaiting + {noreply, Session#session{awaiting_ack = maps:remove(MsgId, AwaitingAck)}}; + +handle_info({timeout, awaiting_ack, MsgId}, Session = #session{clientid = ClientId, + inflight_queue = InflightQ, + awaiting_ack = AwaitingAck}) -> + case maps:find(MsgId, AwaitingAck) of + {ok, {{0, _Timeout}, _TRef}} -> + Session1 = Session#session{inflight_queue = lists:keydelete(MsgId, 1, InflightQ), + awaiting_ack = maps:remove(MsgId, AwaitingAck)}, + {noreply, dequeue(Session1)}; + {ok, {{Retries, Timeout}, _TRef}} -> + TRef = timer(Timeout, {timeout, awaiting_ack, MsgId}), + AwaitingAck1 = maps:put(MsgId, {{Retries-1, Timeout*2}, TRef}, AwaitingAck), + {noreply, Session#session{awaiting_ack = AwaitingAck1}}; + error -> + lager:error([{client, ClientId}], "Session ~s " + "cannot find Awaiting Ack:~p", [ClientId, MsgId]), + {noreply, Session} + end; + +handle_info({timeout, awaiting_rel, MsgId}, Session = #session{clientid = ClientId, + awaiting_rel = AwaitingRel}) -> + case maps:find(MsgId, AwaitingRel) of + {ok, {Msg, _TRef}} -> + lager:error([{client, ClientId}], "Session ~s AwaitingRel Timout!~n" + "Drop Message:~p", [ClientId, Msg]), + {noreply, Session#session{awaiting_rel = maps:remove(MsgId, AwaitingRel)}}; + error -> + lager:error([{client, ClientId}], "Session ~s Cannot find AwaitingRel: MsgId=~p", [ClientId, MsgId]), + {noreply, Session} + end; + +handle_info({timeout, awaiting_comp, MsgId}, Session = #session{clientid = ClientId, + awaiting_comp = Awaiting}) -> + case maps:find(MsgId, Awaiting) of + {ok, _TRef} -> + lager:error([{client, ClientId}], "Session ~s " + "Awaiting PUBCOMP Timout: MsgId=~p!", [ClientId, MsgId]), + {noreply, Session#session{awaiting_comp = maps:remove(MsgId, Awaiting)}}; + error -> + lager:error([{client, ClientId}], "Session ~s " + "Cannot find Awaiting PUBCOMP: MsgId=~p", [ClientId, MsgId]), + {noreply, Session} + end; + +handle_info({'EXIT', ClientPid, _Reason}, Session = #session{clean_sess = true, + client_pid = ClientPid}) -> + {stop, normal, Session}; + +handle_info({'EXIT', ClientPid, Reason}, Session = #session{clean_sess = false, + clientid = ClientId, + client_pid = ClientPid, + expired_after = Expires}) -> + lager:info("Session ~s unlink with client ~p: reason=~p", [ClientId, ClientPid, Reason]), + TRef = timer(Expires, session_expired), + {noreply, Session#session{client_pid = undefined, expired_timer = TRef}, hibernate}; + +handle_info({'EXIT', Pid, _Reason}, Session = #session{clientid = ClientId, + client_pid = ClientPid}) -> + + lager:error("Session ~s received unexpected EXIT:" + " client_pid=~p, exit_pid=~p", [ClientId, ClientPid, Pid]), + {noreply, Session}; + +handle_info(session_expired, Session = #session{clientid = ClientId}) -> + lager:error("Session ~s expired, shutdown now!", [ClientId]), + {stop, {shutdown, expired}, Session}; + +handle_info(Info, Session = #session{clientid = ClientId}) -> + lager:critical("Session ~s received unexpected info: ~p", [ClientId, Info]), + {noreply, Session}. + +terminate(_Reason, _Session) -> + ok. + +code_change(_OldVsn, Session, _Extra) -> + {ok, Session}. + +%%%============================================================================= +%%% Internal functions +%%%============================================================================= + +%%------------------------------------------------------------------------------ +%% Kick duplicated client +%%------------------------------------------------------------------------------ + +kick(_ClientId, _ClientPid, undefined) -> + ok; +kick(_ClientId, ClientPid, ClientPid) -> + ok; +kick(ClientId, ClientPid, OldClientPid) -> + lager:error("Session '~s' is duplicated: pid=~p, oldpid=~p", [ClientId, ClientPid, OldClientPid]), + unlink(OldClientPid), + OldClientPid ! {stop, duplicate_id, ClientPid}. + +%%------------------------------------------------------------------------------ +%% Check inflight and awaiting_rel +%%------------------------------------------------------------------------------ + +check_inflight(#session{max_inflight = 0}) -> + true; +check_inflight(#session{max_inflight = Max, inflight_queue = Q}) -> + Max > length(Q). + +check_awaiting_rel(#session{max_awaiting_rel = 0}) -> + true; +check_awaiting_rel(#session{awaiting_rel = AwaitingRel, + max_awaiting_rel = MaxLen}) -> + maps:size(AwaitingRel) < MaxLen. + +%%------------------------------------------------------------------------------ +%% Dequeue and Deliver +%%------------------------------------------------------------------------------ + +dequeue(Session = #session{client_pid = undefined}) -> + %% do nothing if client is disconnected + Session; + +dequeue(Session) -> + case check_inflight(Session) of + true -> dequeue2(Session); + false -> Session + end. + +dequeue2(Session = #session{message_queue = Q}) -> + case emqttd_mqueue:out(Q) of + {empty, _Q} -> Session; + {{value, Msg}, Q1} -> + Session1 = deliver(Msg, Session#session{message_queue = Q1}), + dequeue(Session1) %% dequeue more + end. + +deliver(Msg = #mqtt_message{qos = ?QOS_0}, Session = #session{client_pid = ClientPid}) -> + ClientPid ! {deliver, Msg}, Session; + +deliver(Msg = #mqtt_message{qos = QoS}, Session = #session{message_id = MsgId, + client_pid = ClientPid, + inflight_queue = InflightQ}) + when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> + Msg1 = Msg#mqtt_message{msgid = MsgId, dup = false}, + ClientPid ! {deliver, Msg1}, + await(Msg1, next_msgid(Session#session{inflight_queue = [{MsgId, Msg1}|InflightQ]})). + +redeliver(Msg = #mqtt_message{qos = ?QOS_0}, Session) -> + deliver(Msg, Session); + +redeliver(Msg = #mqtt_message{qos = QoS}, Session = #session{client_pid = ClientPid}) + when QoS =:= ?QOS_1 orelse QoS =:= ?QOS_2 -> + ClientPid ! {deliver, Msg}, + await(Msg, Session). + +%%------------------------------------------------------------------------------ +%% Awaiting ack for qos1, qos2 message +%%------------------------------------------------------------------------------ +await(#mqtt_message{msgid = MsgId}, Session = #session{awaiting_ack = Awaiting, + unack_retries = Retries, + unack_timeout = Timeout}) -> + TRef = timer(Timeout, {timeout, awaiting_ack, MsgId}), + Awaiting1 = maps:put(MsgId, {{Retries, Timeout}, TRef}, Awaiting), + Session#session{awaiting_ack = Awaiting1}. + +acked(MsgId, Session = #session{inflight_queue = InflightQ, + awaiting_ack = Awaiting}) -> + Session#session{inflight_queue = lists:keydelete(MsgId, 1, InflightQ), + awaiting_ack = maps:remove(MsgId, Awaiting)}. + +next_msgid(Session = #session{message_id = 16#ffff}) -> + Session#session{message_id = 1}; + +next_msgid(Session = #session{message_id = MsgId}) -> + Session#session{message_id = MsgId + 1}. + +timer(Timeout, TimeoutMsg) -> + erlang:send_after(Timeout * 1000, self(), TimeoutMsg). + +cancel_timer(undefined) -> + undefined; +cancel_timer(Ref) -> + catch erlang:cancel_timer(Ref). + diff --git a/apps/emqttd/src/emqttd_session_sup.erl b/src/emqttd_session_sup.erl similarity index 91% rename from apps/emqttd/src/emqttd_session_sup.erl rename to src/emqttd_session_sup.erl index 7ab0cc18a..ff12b5055 100644 --- a/apps/emqttd/src/emqttd_session_sup.erl +++ b/src/emqttd_session_sup.erl @@ -30,7 +30,7 @@ -behavior(supervisor). --export([start_link/0, start_session/2]). +-export([start_link/0, start_session/3]). -export([init/1]). @@ -46,9 +46,9 @@ start_link() -> %% @doc Start a session %% @end %%------------------------------------------------------------------------------ --spec start_session(binary(), pid()) -> {ok, pid()}. -start_session(ClientId, ClientPid) -> - supervisor:start_child(?MODULE, [ClientId, ClientPid]). +-spec start_session(boolean(), binary(), pid()) -> {ok, pid()}. +start_session(CleanSess, ClientId, ClientPid) -> + supervisor:start_child(?MODULE, [CleanSess, ClientId, ClientPid]). %%%============================================================================= %%% Supervisor callbacks @@ -59,3 +59,4 @@ init([]) -> [{session, {emqttd_session, start_link, []}, transient, 10000, worker, [emqttd_session]}]}}. + diff --git a/apps/emqttd/src/emqttd_sm.erl b/src/emqttd_sm.erl similarity index 76% rename from apps/emqttd/src/emqttd_sm.erl rename to src/emqttd_sm.erl index 05d9e62b0..59e32f2a1 100644 --- a/apps/emqttd/src/emqttd_sm.erl +++ b/src/emqttd_sm.erl @@ -38,8 +38,6 @@ -author("Feng Lee "). -%%cleanSess: true | false - -include("emqttd.hrl"). -behaviour(gen_server). @@ -47,13 +45,15 @@ %% API Function Exports -export([start_link/2, pool/0, table/0]). --export([lookup_session/1, start_session/2, destroy_session/1]). +-export([lookup_session/1, + start_session/2, + destroy_session/1]). %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). --record(state, {id, tabid, statsfun}). +-record(state, {id, statsfun}). -define(SM_POOL, sm_pool). @@ -67,11 +67,12 @@ %% @doc Start a session manager %% @end %%------------------------------------------------------------------------------ --spec start_link(Id, StatsFun) -> {ok, pid()} | ignore | {error, any()} when +-spec start_link(Id, SessStatsFun) -> {ok, pid()} | ignore | {error, any()} when Id :: pos_integer(), - StatsFun :: fun(). -start_link(Id, StatsFun) -> - gen_server:start_link(?MODULE, [Id, StatsFun], []). + %ClientStatsFun :: fun(), + SessStatsFun :: fun(). +start_link(Id, SessStatsFun) -> + gen_server:start_link(?MODULE, [Id, SessStatsFun], []). %%------------------------------------------------------------------------------ %% @doc Pool name. @@ -85,34 +86,37 @@ pool() -> ?SM_POOL. %%------------------------------------------------------------------------------ table() -> ?SESSION_TAB. +%%------------------------------------------------------------------------------ +%% @doc Start a session +%% @end +%%------------------------------------------------------------------------------ + +-spec start_session(CleanSess :: boolean(), binary()) -> {ok, pid()} | {error, any()}. +start_session(CleanSess, ClientId) -> + SM = gproc_pool:pick_worker(?SM_POOL, ClientId), + call(SM, {start_session, {CleanSess, ClientId, self()}}). + %%------------------------------------------------------------------------------ %% @doc Lookup Session Pid %% @end %%------------------------------------------------------------------------------ -spec lookup_session(binary()) -> pid() | undefined. lookup_session(ClientId) -> - case ets:lookup(emqttd_sm_sup:table(), ClientId) of + case ets:lookup(?SESSION_TAB, ClientId) of [{_, SessPid, _}] -> SessPid; [] -> undefined end. -%%------------------------------------------------------------------------------ -%% @doc Start a session -%% @end -%%------------------------------------------------------------------------------ --spec start_session(binary(), pid()) -> {ok, pid()} | {error, any()}. -start_session(ClientId, ClientPid) -> - SmPid = gproc_pool:pick_worker(?SM_POOL, ClientId), - gen_server:call(SmPid, {start_session, ClientId, ClientPid}). - %%------------------------------------------------------------------------------ %% @doc Destroy a session %% @end %%------------------------------------------------------------------------------ -spec destroy_session(binary()) -> ok. destroy_session(ClientId) -> - SmPid = gproc_pool:pick_worker(?SM_POOL, ClientId), - gen_server:call(SmPid, {destroy_session, ClientId}). + SM = gproc_pool:pick_worker(?SM_POOL, ClientId), + call(SM, {destroy_session, ClientId}). + +call(SM, Req) -> gen_server:call(SM, Req). %%%============================================================================= %%% gen_server callbacks @@ -122,23 +126,27 @@ init([Id, StatsFun]) -> gproc_pool:connect_worker(?SM_POOL, {?MODULE, Id}), {ok, #state{id = Id, statsfun = StatsFun}}. -handle_call({start_session, ClientId, ClientPid}, _From, State) -> +handle_call({start_session, {false, ClientId, ClientPid}}, _From, State) -> Reply = case ets:lookup(?SESSION_TAB, ClientId) of [{_, SessPid, _MRef}] -> - emqttd_session:resume(SessPid, ClientId, ClientPid), + emqttd_session:resume(SessPid, ClientId, ClientPid), {ok, SessPid}; [] -> - case emqttd_session_sup:start_session(ClientId, ClientPid) of - {ok, SessPid} -> - ets:insert(?SESSION_TAB, {ClientId, SessPid, erlang:monitor(process, SessPid)}), - {ok, SessPid}; - {error, Error} -> - {error, Error} - end + new_session(false, ClientId, ClientPid) end, {reply, Reply, setstats(State)}; +handle_call({start_session, {true, ClientId, ClientPid}}, _From, State) -> + case ets:lookup(?SESSION_TAB, ClientId) of + [{_, SessPid, MRef}] -> + erlang:demonitor(MRef, [flush]), + emqttd_session:destroy(SessPid, ClientId); + [] -> + ok + end, + {reply, new_session(true, ClientId, ClientPid), setstats(State)}; + handle_call({destroy_session, ClientId}, _From, State) -> case ets:lookup(?SESSION_TAB, ClientId) of [{_, SessPid, MRef}] -> @@ -156,8 +164,8 @@ handle_call(_Request, _From, State) -> handle_cast(_Msg, State) -> {noreply, State}. -handle_info({'DOWN', MRef, process, DownPid, _Reason}, State = #state{tabid = Tab}) -> - ets:match_delete(Tab, {'_', DownPid, MRef}), +handle_info({'DOWN', MRef, process, DownPid, _Reason}, State) -> + ets:match_delete(?SESSION_TAB, {'_', DownPid, MRef}), {noreply, setstats(State)}; handle_info(_Info, State) -> @@ -173,6 +181,16 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%============================================================================= +new_session(CleanSess, ClientId, ClientPid) -> + case emqttd_session_sup:start_session(CleanSess, ClientId, ClientPid) of + {ok, SessPid} -> + ets:insert(?SESSION_TAB, {ClientId, SessPid, erlang:monitor(process, SessPid)}), + {ok, SessPid}; + {error, Error} -> + {error, Error} + end. + setstats(State = #state{statsfun = StatsFun}) -> StatsFun(ets:info(?SESSION_TAB, size)), State. + diff --git a/apps/emqttd/src/emqttd_sm_sup.erl b/src/emqttd_sm_sup.erl similarity index 89% rename from apps/emqttd/src/emqttd_sm_sup.erl rename to src/emqttd_sm_sup.erl index ece44dd38..03ee63a47 100644 --- a/apps/emqttd/src/emqttd_sm_sup.erl +++ b/src/emqttd_sm_sup.erl @@ -20,7 +20,7 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% emqttd client manager supervisor. +%%% emqttd session manager supervisor. %%% %%% @end %%%----------------------------------------------------------------------------- @@ -46,12 +46,13 @@ init([]) -> {write_concurrency, true}]), Schedulers = erlang:system_info(schedulers), gproc_pool:new(emqttd_sm:pool(), hash, [{size, Schedulers}]), - StatsFun = emqttd_stats:statsfun('sessions/count', 'sessions/max'), + %%ClientStatsFun = emqttd_stats:statsfun('clients/count', 'clients/max'), + SessStatsFun = emqttd_stats:statsfun('sessions/count', 'sessions/max'), Children = lists:map( fun(I) -> Name = {emqttd_sm, I}, gproc_pool:add_worker(emqttd_sm:pool(), Name, I), - {Name, {emqttd_sm, start_link, [I, StatsFun]}, + {Name, {emqttd_sm, start_link, [I, SessStatsFun]}, permanent, 10000, worker, [emqttd_sm]} end, lists:seq(1, Schedulers)), {ok, {{one_for_all, 10, 100}, Children}}. diff --git a/apps/emqttd/src/emqttd_stats.erl b/src/emqttd_stats.erl similarity index 85% rename from apps/emqttd/src/emqttd_stats.erl rename to src/emqttd_stats.erl index d55a5942c..1f005369e 100644 --- a/apps/emqttd/src/emqttd_stats.erl +++ b/src/emqttd_stats.erl @@ -28,16 +28,14 @@ -author("Feng Lee "). --include("emqttd_systop.hrl"). +-include("emqttd.hrl"). --include_lib("emqtt/include/emqtt.hrl"). +-export([start_link/0]). -behaviour(gen_server). -define(SERVER, ?MODULE). --export([start_link/0]). - %% statistics API. -export([statsfun/1, statsfun/2, getstats/0, getstat/1, @@ -47,9 +45,31 @@ -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). +-record(state, {tick_tref}). + -define(STATS_TAB, mqtt_stats). --record(state, {tick_tref}). +%% $SYS Topics for Clients +-define(SYSTOP_CLIENTS, [ + 'clients/count', % clients connected current + 'clients/max' % max clients connected +]). + +%% $SYS Topics for Sessions +-define(SYSTOP_SESSIONS, [ + 'sessions/count', + 'sessions/max' +]). + +%% $SYS Topics for Subscribers +-define(SYSTOP_PUBSUB, [ + 'topics/count', % ... + 'topics/max', % ... + 'subscribers/count', % ... + 'subscribers/max', % ... + 'queues/count', % ... + 'queues/max' % ... +]). %%%============================================================================= %%% API @@ -126,7 +146,7 @@ init([]) -> Topics = ?SYSTOP_CLIENTS ++ ?SYSTOP_SESSIONS ++ ?SYSTOP_PUBSUB, [ets:insert(?STATS_TAB, {Topic, 0}) || Topic <- Topics], % Create $SYS Topics - [ok = emqttd_pubsub:create(emqtt_topic:systop(Topic)) || Topic <- Topics], + [ok = emqttd_pubsub:create(stats_topic(Topic)) || Topic <- Topics], % Tick to publish stats {ok, #state{tick_tref = emqttd_broker:start_tick(tick)}, hibernate}. @@ -154,6 +174,10 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%============================================================================= publish(Stat, Val) -> - emqttd_pubsub:publish(stats, #mqtt_message{topic = emqtt_topic:systop(Stat), - payload = emqttd_util:integer_to_binary(Val)}). + emqttd_pubsub:publish(#mqtt_message{from = stats, + topic = stats_topic(Stat), + payload = emqttd_util:integer_to_binary(Val)}). + +stats_topic(Stat) -> + emqttd_topic:systop(list_to_binary(lists:concat(['stats/', Stat]))). diff --git a/apps/emqttd/src/emqttd_sup.erl b/src/emqttd_sup.erl similarity index 100% rename from apps/emqttd/src/emqttd_sup.erl rename to src/emqttd_sup.erl diff --git a/apps/emqttd/src/emqttd_sysmon.erl b/src/emqttd_sysmon.erl similarity index 100% rename from apps/emqttd/src/emqttd_sysmon.erl rename to src/emqttd_sysmon.erl diff --git a/apps/emqttd/src/emqttd_throttle.erl b/src/emqttd_throttle.erl similarity index 100% rename from apps/emqttd/src/emqttd_throttle.erl rename to src/emqttd_throttle.erl diff --git a/apps/emqtt/src/emqtt_topic.erl b/src/emqttd_topic.erl similarity index 99% rename from apps/emqtt/src/emqtt_topic.erl rename to src/emqttd_topic.erl index 0fa183614..13e9d55a3 100644 --- a/apps/emqtt/src/emqtt_topic.erl +++ b/src/emqttd_topic.erl @@ -24,7 +24,7 @@ %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_topic). +-module(emqttd_topic). -author("Feng Lee "). diff --git a/apps/emqttd/src/emqttd_trace.erl b/src/emqttd_trace.erl similarity index 100% rename from apps/emqttd/src/emqttd_trace.erl rename to src/emqttd_trace.erl diff --git a/apps/emqttd/src/emqttd_trie.erl b/src/emqttd_trie.erl similarity index 97% rename from apps/emqttd/src/emqttd_trie.erl rename to src/emqttd_trie.erl index 9beaa339a..30e6d69d2 100644 --- a/apps/emqttd/src/emqttd_trie.erl +++ b/src/emqttd_trie.erl @@ -102,7 +102,7 @@ insert(Topic) when is_binary(Topic) -> mnesia:write(TrieNode#trie_node{topic=Topic}); [] -> %add trie path - [add_path(Triple) || Triple <- emqtt_topic:triples(Topic)], + [add_path(Triple) || Triple <- emqttd_topic:triples(Topic)], %add last node mnesia:write(#trie_node{node_id=Topic, topic=Topic}) end. @@ -113,7 +113,7 @@ insert(Topic) when is_binary(Topic) -> %%------------------------------------------------------------------------------ -spec find(Topic :: binary()) -> list(MatchedTopic :: binary()). find(Topic) when is_binary(Topic) -> - TrieNodes = match_node(root, emqtt_topic:words(Topic), []), + TrieNodes = match_node(root, emqttd_topic:words(Topic), []), [Name || #trie_node{topic=Name} <- TrieNodes, Name=/= undefined]. %%------------------------------------------------------------------------------ @@ -125,7 +125,7 @@ delete(Topic) when is_binary(Topic) -> case mnesia:read(trie_node, Topic) of [#trie_node{edge_count=0}] -> mnesia:delete({trie_node, Topic}), - delete_path(lists:reverse(emqtt_topic:triples(Topic))); + delete_path(lists:reverse(emqttd_topic:triples(Topic))); [TrieNode] -> mnesia:write(TrieNode#trie_node{topic=Topic}); [] -> diff --git a/apps/emqttd/src/emqttd_util.erl b/src/emqttd_util.erl similarity index 94% rename from apps/emqttd/src/emqttd_util.erl rename to src/emqttd_util.erl index eca61b4b0..81b1bd54d 100644 --- a/apps/emqttd/src/emqttd_util.erl +++ b/src/emqttd_util.erl @@ -31,7 +31,7 @@ -export([apply_module_attributes/1, all_module_attributes/1, cancel_timer/1, - timestamp/0, microsecs/0]). + now_to_secs/0, now_to_secs/1]). -export([integer_to_binary/1]). @@ -91,11 +91,9 @@ cancel_timer(Ref) -> integer_to_binary(I) when is_integer(I) -> list_to_binary(integer_to_list(I)). -timestamp() -> - {MegaSecs, Secs, _MicroSecs} = os:timestamp(), +now_to_secs() -> + now_to_secs(os:timestamp()). + +now_to_secs({MegaSecs, Secs, _MicroSecs}) -> MegaSecs * 1000000 + Secs. -microsecs() -> - {Mega, Sec, Micro} = erlang:now(), - (Mega * 1000000 + Sec) * 1000000 + Micro. - diff --git a/apps/emqttd/src/emqttd_vm.erl b/src/emqttd_vm.erl similarity index 86% rename from apps/emqttd/src/emqttd_vm.erl rename to src/emqttd_vm.erl index d76942619..2c1cf5f7e 100644 --- a/apps/emqttd/src/emqttd_vm.erl +++ b/src/emqttd_vm.erl @@ -24,6 +24,7 @@ %%% %%% @end %%%----------------------------------------------------------------------------- + -module(emqttd_vm). -define(UTIL_ALLOCATORS, [temp_alloc, @@ -67,6 +68,61 @@ min_heap_size]).%, %fullsweep_after]). +-export([timestamp/0, microsecs/0]). + +-export([loads/0]). + +-define(SYSTEM_INFO, [ + allocated_areas, + allocator, + alloc_util_allocators, + build_type, + check_io, + compat_rel, + creation, + debug_compiled, + dist, + dist_ctrl, + driver_version, + elib_malloc, + dist_buf_busy_limit, + %fullsweep_after, % included in garbage_collection + garbage_collection, + %global_heaps_size, % deprecated + heap_sizes, + heap_type, + info, + kernel_poll, + loaded, + logical_processors, + logical_processors_available, + logical_processors_online, + machine, + %min_heap_size, % included in garbage_collection + %min_bin_vheap_size, % included in garbage_collection + modified_timing_level, + multi_scheduling, + multi_scheduling_blockers, + otp_release, + port_count, + process_count, + process_limit, + scheduler_bind_type, + scheduler_bindings, + scheduler_id, + schedulers, + schedulers_online, + smp_support, + system_version, + system_architecture, + threads, + thread_pool_size, + trace_control_word, + update_cpu_info, + version, + wordsize + ]). + -define(SOCKET_OPTS, [ active, broadcast, diff --git a/apps/emqttd/src/emqttd_ws_client.erl b/src/emqttd_ws_client.erl similarity index 69% rename from apps/emqttd/src/emqttd_ws_client.erl rename to src/emqttd_ws_client.erl index 23ac6aff0..c72f46511 100644 --- a/apps/emqttd/src/emqttd_ws_client.erl +++ b/src/emqttd_ws_client.erl @@ -29,27 +29,24 @@ -author("Feng Lee "). --include_lib("emqtt/include/emqtt.hrl"). +-include("emqttd.hrl"). --include_lib("emqtt/include/emqtt_packet.hrl"). - --behaviour(gen_server). +-include("emqttd_protocol.hrl"). %% API Exports -export([start_link/1, ws_loop/3]). +-behaviour(gen_server). + %% gen_server Function Exports -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -%% WebSocket loop state --record(wsocket_state, {request, - client_pid, - packet_opts, - parser_state}). +%% WebSocket Loop State +-record(wsocket_state, {request, client_pid, packet_opts, parser_state}). -%% Client state --record(state, {ws_pid, request, proto_state, keepalive}). +%% Client State +-record(client_state, {ws_pid, request, proto_state, keepalive}). %%------------------------------------------------------------------------------ %% @doc Start WebSocket client. @@ -62,7 +59,7 @@ start_link(Req) -> ReentryWs(#wsocket_state{request = Req, client_pid = ClientPid, packet_opts = PktOpts, - parser_state = emqtt_parser:init(PktOpts)}). + parser_state = emqttd_parser:init(PktOpts)}). %%------------------------------------------------------------------------------ %% @private @@ -85,7 +82,7 @@ ws_loop(Data, State = #wsocket_state{request = Req, parser_state = ParserState}, ReplyChannel) -> Peer = Req:get(peer), lager:debug("RECV from ~s(WebSocket): ~p", [Peer, Data]), - case emqtt_parser:parse(iolist_to_binary(Data), ParserState) of + case emqttd_parser:parse(iolist_to_binary(Data), ParserState) of {more, ParserState1} -> State#wsocket_state{parser_state = ParserState1}; {ok, Packet, Rest} -> @@ -97,7 +94,7 @@ ws_loop(Data, State = #wsocket_state{request = Req, end. reset_parser(State = #wsocket_state{packet_opts = PktOpts}) -> - State#wsocket_state{parser_state = emqtt_parser:init(PktOpts)}. + State#wsocket_state{parser_state = emqttd_parser:init(PktOpts)}. %%%============================================================================= %%% gen_fsm callbacks @@ -109,75 +106,76 @@ init([WsPid, Req, ReplyChannel, PktOpts]) -> {ok, Peername} = emqttd_net:peername(Socket), SendFun = fun(Payload) -> ReplyChannel({binary, Payload}) end, ProtoState = emqttd_protocol:init(Peername, SendFun, PktOpts), - {ok, #state{ws_pid = WsPid, request = Req, proto_state = ProtoState}}. + {ok, #client_state{ws_pid = WsPid, request = Req, proto_state = ProtoState}}. handle_call(_Req, _From, State) -> {reply, error, State}. -handle_cast({received, Packet}, State = #state{proto_state = ProtoState}) -> +handle_cast({received, Packet}, State = #client_state{proto_state = ProtoState}) -> case emqttd_protocol:received(Packet, ProtoState) of {ok, ProtoState1} -> - {noreply, State#state{proto_state = ProtoState1}}; + {noreply, State#client_state{proto_state = ProtoState1}}; {error, Error} -> lager:error("MQTT protocol error ~p", [Error]), stop({shutdown, Error}, State); {error, Error, ProtoState1} -> - stop({shutdown, Error}, State#state{proto_state = ProtoState1}); + stop({shutdown, Error}, State#client_state{proto_state = ProtoState1}); {stop, Reason, ProtoState1} -> - stop(Reason, State#state{proto_state = ProtoState1}) + stop(Reason, State#client_state{proto_state = ProtoState1}) end; handle_cast(_Msg, State) -> {noreply, State}. -handle_info({dispatch, {From, Messages}}, #state{proto_state = ProtoState} = State) when is_list(Messages) -> - ProtoState1 = - lists:foldl(fun(Message, PState) -> - {ok, PState1} = emqttd_protocol:send({From, Message}, PState), PState1 - end, ProtoState, Messages), - {noreply, State#state{proto_state = ProtoState1}}; +handle_info({deliver, Message}, #client_state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqttd_protocol:send(Message, ProtoState), + {noreply, State#client_state{proto_state = ProtoState1}}; -handle_info({dispatch, {From, Message}}, #state{proto_state = ProtoState} = State) -> - {ok, ProtoState1} = emqttd_protocol:send({From, Message}, ProtoState), - {noreply, State#state{proto_state = ProtoState1}}; - -handle_info({redeliver, {?PUBREL, PacketId}}, #state{proto_state = ProtoState} = State) -> +handle_info({redeliver, {?PUBREL, PacketId}}, #client_state{proto_state = ProtoState} = State) -> {ok, ProtoState1} = emqttd_protocol:redeliver({?PUBREL, PacketId}, ProtoState), - {noreply, State#state{proto_state = ProtoState1}}; + {noreply, State#client_state{proto_state = ProtoState1}}; -handle_info({stop, duplicate_id, _NewPid}, State=#state{proto_state = ProtoState}) -> +handle_info({subscribe, TopicTable}, #client_state{proto_state = ProtoState} = State) -> + {ok, ProtoState1} = emqttd_protocol:handle({subscribe, TopicTable}, ProtoState), + {noreply, State#client_state{proto_state = ProtoState1}}; + +handle_info({stop, duplicate_id, _NewPid}, State=#client_state{proto_state = ProtoState}) -> lager:error("Shutdown for duplicate clientid: ~s", [emqttd_protocol:clientid(ProtoState)]), stop({shutdown, duplicate_id}, State); -handle_info({keepalive, start, TimeoutSec}, State = #state{request = Req}) -> +handle_info({keepalive, start, TimeoutSec}, State = #client_state{request = Req}) -> lager:debug("Client(WebSocket) ~s: Start KeepAlive with ~p seconds", [Req:get(peer), TimeoutSec]), - %%TODO: fix esockd_transport... KeepAlive = emqttd_keepalive:new({esockd_transport, Req:get(socket)}, TimeoutSec, {keepalive, timeout}), - {noreply, State#state{keepalive = KeepAlive}}; + {noreply, State#client_state{keepalive = KeepAlive}}; -handle_info({keepalive, timeout}, State = #state{request = Req, keepalive = KeepAlive}) -> +handle_info({keepalive, timeout}, State = #client_state{request = Req, keepalive = KeepAlive}) -> case emqttd_keepalive:resume(KeepAlive) of timeout -> lager:debug("Client(WebSocket) ~s: Keepalive Timeout!", [Req:get(peer)]), - stop({shutdown, keepalive_timeout}, State#state{keepalive = undefined}); + stop({shutdown, keepalive_timeout}, State#client_state{keepalive = undefined}); {resumed, KeepAlive1} -> lager:debug("Client(WebSocket) ~s: Keepalive Resumed", [Req:get(peer)]), - {noreply, State#state{keepalive = KeepAlive1}} + {noreply, State#client_state{keepalive = KeepAlive1}} end; -handle_info({'EXIT', WsPid, Reason}, State = #state{ws_pid = WsPid}) -> - stop(Reason, State); +handle_info({'EXIT', WsPid, Reason}, State = #client_state{ws_pid = WsPid, proto_state = ProtoState}) -> + ClientId = emqttd_protocol:clientid(ProtoState), + lager:warning("Websocket client ~s exit: reason=~p", [ClientId, Reason]), + stop({shutdown, websocket_closed}, State); -handle_info(_Info, State) -> +handle_info(Info, State = #client_state{request = Req}) -> + lager:critical("Client(WebSocket) ~s: Unexpected Info - ~p", [Req:get(peer), Info]), {noreply, State}. -terminate(Reason, #state{proto_state = ProtoState, keepalive = KeepAlive}) -> +terminate(Reason, #client_state{proto_state = ProtoState, keepalive = KeepAlive}) -> + lager:info("WebSocket client terminated: ~p", [Reason]), emqttd_keepalive:cancel(KeepAlive), case Reason of {shutdown, Error} -> emqttd_protocol:shutdown(Error, ProtoState); - _ -> ok + _ -> + emqttd_protocol:shutdown(Reason, ProtoState) end. code_change(_OldVsn, State, _Extra) -> diff --git a/apps/emqttd/test/emqttd_access_control_tests.erl b/test/emqttd_access_control_tests.erl similarity index 97% rename from apps/emqttd/test/emqttd_access_control_tests.erl rename to test/emqttd_access_control_tests.erl index 5c906804e..6b55a0fae 100644 --- a/apps/emqttd/test/emqttd_access_control_tests.erl +++ b/test/emqttd_access_control_tests.erl @@ -98,8 +98,8 @@ with_acl(Fun) -> {internal, [{file, "../test/test_acl.config"}, {nomatch, allow}]} ]} ], - application:set_env(emqttd, access, AclOpts), - emqttd_access_control:start_link(), + %application:set_env(emqttd, access, AclOpts), + emqttd_access_control:start_link(AclOpts), Fun(), emqttd_access_control:stop(). diff --git a/apps/emqttd/test/emqttd_access_rule_tests.erl b/test/emqttd_access_rule_tests.erl similarity index 95% rename from apps/emqttd/test/emqttd_access_rule_tests.erl rename to test/emqttd_access_rule_tests.erl index e0a5427df..8f25a787f 100644 --- a/apps/emqttd/test/emqttd_access_rule_tests.erl +++ b/test/emqttd_access_rule_tests.erl @@ -53,8 +53,8 @@ compile_test() -> ?assertEqual({deny, all}, compile({deny, all})). match_test() -> - User = #mqtt_client{ipaddr = {127,0,0,1}, clientid = <<"testClient">>, username = <<"TestUser">>}, - User2 = #mqtt_client{ipaddr = {192,168,0,10}, clientid = <<"testClient">>, username = <<"TestUser">>}, + User = #mqtt_client{ipaddress = {127,0,0,1}, clientid = <<"testClient">>, username = <<"TestUser">>}, + User2 = #mqtt_client{ipaddress = {192,168,0,10}, clientid = <<"testClient">>, username = <<"TestUser">>}, ?assertEqual({matched, allow}, match(User, <<"Test/Topic">>, {allow, all})), ?assertEqual({matched, deny}, match(User, <<"Test/Topic">>, {deny, all})), diff --git a/apps/emqttd/test/emqttd_acl_test_mod.erl b/test/emqttd_acl_test_mod.erl similarity index 100% rename from apps/emqttd/test/emqttd_acl_test_mod.erl rename to test/emqttd_acl_test_mod.erl diff --git a/apps/emqttd/test/emqttd_auth_anonymous_test_mod.erl b/test/emqttd_auth_anonymous_test_mod.erl similarity index 100% rename from apps/emqttd/test/emqttd_auth_anonymous_test_mod.erl rename to test/emqttd_auth_anonymous_test_mod.erl diff --git a/plugins/emqttd_dashboard/src/emqttd_dashboard.erl b/test/emqttd_mqueue_tests.erl similarity index 50% rename from plugins/emqttd_dashboard/src/emqttd_dashboard.erl rename to test/emqttd_mqueue_tests.erl index fae297b16..8e6c4fb16 100644 --- a/plugins/emqttd_dashboard/src/emqttd_dashboard.erl +++ b/test/emqttd_mqueue_tests.erl @@ -19,20 +19,53 @@ %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE %%% SOFTWARE. %%%----------------------------------------------------------------------------- -%%% @doc -%%% emqttd management dashboard. -%%% -%%% @end -%%%----------------------------------------------------------------------------- --module(emqttd_dashboard). --author("Feng Lee "). +-module(emqttd_mqueue_tests). --export([handle_request/1]). +-include("emqttd.hrl"). -%%TODO... +-define(QM, emqttd_mqueue). -handle_request(Req) -> - Req:ok("hello!"). +-ifdef(TEST). + +-include_lib("eunit/include/eunit.hrl"). + +in_test() -> + Opts = [{max_length, 5}, + {queue_qos0, true}], + Q = ?QM:new(<<"testQ">>, Opts, alarm_fun()), + ?assertEqual(true, ?QM:is_empty(Q)), + Q1 = ?QM:in(#mqtt_message{}, Q), + ?assertEqual(1, ?QM:len(Q1)), + Q2 = ?QM:in(#mqtt_message{qos = 1}, Q1), + ?assertEqual(2, ?QM:len(Q2)), + Q3 = ?QM:in(#mqtt_message{qos = 2}, Q2), + Q4 = ?QM:in(#mqtt_message{}, Q3), + Q5 = ?QM:in(#mqtt_message{}, Q4), + ?assertEqual(true, ?QM:is_full(Q5)). + +in_qos0_test() -> + Opts = [{max_length, 5}, + {queue_qos0, false}], + Q = ?QM:new(<<"testQ">>, Opts, alarm_fun()), + Q1 = ?QM:in(#mqtt_message{}, Q), + ?assertEqual(true, ?QM:is_empty(Q1)), + Q2 = ?QM:in(#mqtt_message{qos = 0}, Q1), + ?assertEqual(true, ?QM:is_empty(Q2)). + +out_test() -> + Opts = [{max_length, 5}, + {queue_qos0, true}], + Q = ?QM:new(<<"testQ">>, Opts, alarm_fun()), + ?assertMatch({empty, Q}, ?QM:out(Q)), + Q1 = ?QM:in(#mqtt_message{}, Q), + {Value, Q2} = ?QM:out(Q1), + ?assertEqual(0, ?QM:len(Q2)), + ?assertMatch({value, #mqtt_message{}}, Value). + +alarm_fun() -> + fun(_, _) -> alarm_fun() end. + +-endif. diff --git a/apps/emqttd/test/emqttd_opts_tests.erl b/test/emqttd_opts_tests.erl similarity index 100% rename from apps/emqttd/test/emqttd_opts_tests.erl rename to test/emqttd_opts_tests.erl diff --git a/apps/emqtt/test/emqtt_parser_tests.erl b/test/emqttd_parser_tests.erl similarity index 92% rename from apps/emqtt/test/emqtt_parser_tests.erl rename to test/emqttd_parser_tests.erl index 6cb3c9286..fb15019eb 100644 --- a/apps/emqtt/test/emqtt_parser_tests.erl +++ b/test/emqttd_parser_tests.erl @@ -20,21 +20,20 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% emqtt_parser tests. +%%% emqttd_parser tests. %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_parser_tests). +-module(emqttd_parser_tests). --include("emqtt.hrl"). --include("emqtt_packet.hrl"). +-include("emqttd_protocol.hrl"). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). parse_connect_test() -> - State = emqtt_parser:init([]), + State = emqttd_parser:init([]), %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) V31ConnBin = <<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>>, ?assertMatch({ok, #mqtt_packet{ @@ -46,7 +45,7 @@ parse_connect_test() -> proto_name = <<"MQIsdp">>, clientid = <<"mosqpub/10451-iMac.loca">>, clean_sess = true, - keep_alive = 60}}, <<>>}, emqtt_parser:parse(V31ConnBin, State)), + keep_alive = 60}}, <<>>}, emqttd_parser:parse(V31ConnBin, State)), %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId=mosqpub/10451-iMac.loca, ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60, Username=undefined, Password=undefined) V311ConnBin = <<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>>, ?assertMatch({ok, #mqtt_packet{ @@ -58,7 +57,7 @@ parse_connect_test() -> proto_name = <<"MQTT">>, clientid = <<"mosqpub/10451-iMac.loca">>, clean_sess = true, - keep_alive = 60 } }, <<>>}, emqtt_parser:parse(V311ConnBin, State)), + keep_alive = 60 } }, <<>>}, emqttd_parser:parse(V311ConnBin, State)), %% CONNECT(Qos=0, Retain=false, Dup=false, ClientId="", ProtoName=MQTT, ProtoVsn=4, CleanSess=true, KeepAlive=60) V311ConnWithoutClientId = <<16,12,0,4,77,81,84,84,4,2,0,60,0,0>>, @@ -71,7 +70,7 @@ parse_connect_test() -> proto_name = <<"MQTT">>, clientid = <<>>, clean_sess = true, - keep_alive = 60 } }, <<>>}, emqtt_parser:parse(V311ConnWithoutClientId, State)), + keep_alive = 60 } }, <<>>}, emqttd_parser:parse(V311ConnWithoutClientId, State)), %%CONNECT(Qos=0, Retain=false, Dup=false, ClientId=mosqpub/10452-iMac.loca, ProtoName=MQIsdp, ProtoVsn=3, CleanSess=true, KeepAlive=60, Username=test, Password=******, Will(Qos=1, Retain=false, Topic=/will, Msg=willmsg)) ConnBinWithWill = <<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>>, ?assertMatch({ok, #mqtt_packet{ @@ -91,11 +90,11 @@ parse_connect_test() -> will_msg = <<"willmsg">> , username = <<"test">>, password = <<"public">>}}, - <<>> }, emqtt_parser:parse(ConnBinWithWill, State)), + <<>> }, emqttd_parser:parse(ConnBinWithWill, State)), ok. parse_publish_test() -> - State = emqtt_parser:init([]), + State = emqttd_parser:init([]), %%PUBLISH(Qos=1, Retain=false, Dup=false, TopicName=a/b/c, PacketId=1, Payload=<<"hahah">>) PubBin = <<50,14,0,5,97,47,98,47,99,0,1,104,97,104,97,104>>, ?assertMatch({ok, #mqtt_packet{ @@ -105,7 +104,7 @@ parse_publish_test() -> retain = false}, variable = #mqtt_packet_publish{topic_name = <<"a/b/c">>, packet_id = 1}, - payload = <<"hahah">> }, <<>>}, emqtt_parser:parse(PubBin, State)), + payload = <<"hahah">> }, <<>>}, emqttd_parser:parse(PubBin, State)), %PUBLISH(Qos=0, Retain=false, Dup=false, TopicName=xxx/yyy, PacketId=undefined, Payload=<<"hello">>) %DISCONNECT(Qos=0, Retain=false, Dup=false) @@ -117,13 +116,13 @@ parse_publish_test() -> retain = false}, variable = #mqtt_packet_publish{topic_name = <<"xxx/yyy">>, packet_id = undefined}, - payload = <<"hello">> }, <<224,0>>}, emqtt_parser:parse(PubBin1, State)), + payload = <<"hello">> }, <<224,0>>}, emqttd_parser:parse(PubBin1, State)), ?assertMatch({ok, #mqtt_packet{ header = #mqtt_packet_header{type = ?DISCONNECT, dup = false, qos = 0, retain = false} - }, <<>>}, emqtt_parser:parse(<<224, 0>>, State)). + }, <<>>}, emqttd_parser:parse(<<224, 0>>, State)). parse_puback_test() -> %%PUBACK(Qos=0, Retain=false, Dup=false, PacketId=1) @@ -133,7 +132,7 @@ parse_puback_test() -> dup = false, qos = 0, retain = false } - }, <<>>}, emqtt_parser:parse(PubAckBin, emqtt_parser:init([]))), + }, <<>>}, emqttd_parser:parse(PubAckBin, emqttd_parser:init([]))), ok. parse_subscribe_test() -> @@ -150,7 +149,7 @@ parse_disconnect_test() -> dup = false, qos = 0, retain = false} - }, <<>>}, emqtt_parser:parse(Bin, emqtt_parser:init([]))). + }, <<>>}, emqttd_parser:parse(Bin, emqttd_parser:init([]))). -endif. diff --git a/apps/emqtt/test/emqtt_serialiser_tests.erl b/test/emqttd_serialiser_tests.erl similarity index 67% rename from apps/emqtt/test/emqtt_serialiser_tests.erl rename to test/emqttd_serialiser_tests.erl index 5cec36a18..1f3a2b4c0 100644 --- a/apps/emqtt/test/emqtt_serialiser_tests.erl +++ b/test/emqttd_serialiser_tests.erl @@ -20,58 +20,59 @@ %%% SOFTWARE. %%%----------------------------------------------------------------------------- %%% @doc -%%% emqtt_serialiser tests. +%%% emqttd_serialiser tests. %%% %%% @end %%%----------------------------------------------------------------------------- --module(emqtt_serialiser_tests). - --include("emqtt.hrl"). --include("emqtt_packet.hrl"). +-module(emqttd_serialiser_tests). -ifdef(TEST). +-include("emqttd_protocol.hrl"). + -include_lib("eunit/include/eunit.hrl"). +-import(emqttd_serialiser, [serialise/1]). + serialise_connect_test() -> - emqtt_serialiser:serialise(?CONNECT_PACKET(#mqtt_packet_connect{})). + serialise(?CONNECT_PACKET(#mqtt_packet_connect{})). serialise_connack_test() -> ConnAck = #mqtt_packet{header = #mqtt_packet_header{type = ?CONNACK}, variable = #mqtt_packet_connack{ack_flags = 0, return_code = 0}}, - ?assertEqual(<<32,2,0,0>>, emqtt_serialiser:serialise(ConnAck)). + ?assertEqual(<<32,2,0,0>>, serialise(ConnAck)). serialise_publish_test() -> - emqtt_serialiser:serialise(?PUBLISH_PACKET(?QOS_0, <<"Topic">>, undefined, <<"Payload">>)), - emqtt_serialiser:serialise(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 938, <<"Payload">>)). + serialise(?PUBLISH_PACKET(?QOS_0, <<"Topic">>, undefined, <<"Payload">>)), + serialise(?PUBLISH_PACKET(?QOS_1, <<"Topic">>, 938, <<"Payload">>)). serialise_puback_test() -> - emqtt_serialiser:serialise(?PUBACK_PACKET(?PUBACK, 10384)). + serialise(?PUBACK_PACKET(?PUBACK, 10384)). serialise_pubrel_test() -> - emqtt_serialiser:serialise(?PUBREL_PACKET(10384)). + serialise(?PUBREL_PACKET(10384)). serialise_subscribe_test() -> TopicTable = [{<<"TopicQos0">>, ?QOS_0}, {<<"TopicQos1">>, ?QOS_1}, {<<"TopicQos2">>, ?QOS_2}], - emqtt_serialiser:serialise(?SUBSCRIBE_PACKET(10, TopicTable)). + serialise(?SUBSCRIBE_PACKET(10, TopicTable)). serialise_suback_test() -> - emqtt_serialiser:serialise(?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128])). + serialise(?SUBACK_PACKET(10, [?QOS_0, ?QOS_1, 128])). serialise_unsubscribe_test() -> - emqtt_serialiser:serialise(?UNSUBSCRIBE_PACKET(10, [<<"Topic1">>, <<"Topic2">>])). + serialise(?UNSUBSCRIBE_PACKET(10, [<<"Topic1">>, <<"Topic2">>])). serialise_unsuback_test() -> - emqtt_serialiser:serialise(?UNSUBACK_PACKET(10)). + serialise(?UNSUBACK_PACKET(10)). serialise_pingreq_test() -> - emqtt_serialiser:serialise(?PACKET(?PINGREQ)). + serialise(?PACKET(?PINGREQ)). serialise_pingresp_test() -> - emqtt_serialiser:serialise(?PACKET(?PINGRESP)). + serialise(?PACKET(?PINGRESP)). serialise_disconnect_test() -> - emqtt_serialiser:serialise(?PACKET(?DISCONNECT)). + serialise(?PACKET(?DISCONNECT)). -endif. diff --git a/apps/emqtt/test/emqtt_topic_tests.erl b/test/emqttd_topic_tests.erl similarity index 92% rename from apps/emqtt/test/emqtt_topic_tests.erl rename to test/emqttd_topic_tests.erl index c4fb75377..a21b6c520 100644 --- a/apps/emqtt/test/emqtt_topic_tests.erl +++ b/test/emqttd_topic_tests.erl @@ -19,14 +19,14 @@ %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE %% SOFTWARE. %%------------------------------------------------------------------------------ --module(emqtt_topic_tests). - --import(emqtt_topic, [validate/1, wildcard/1, match/2, triples/1, words/1]). +-module(emqttd_topic_tests). -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). +-import(emqttd_topic, [validate/1, wildcard/1, match/2, triples/1, words/1]). + -define(N, 100000). validate_test() -> @@ -116,11 +116,11 @@ words_test() -> ok. feed_var_test() -> - ?assertEqual(<<"$Q/client/clientId">>, emqtt_topic:feed_var(<<"$c">>, <<"clientId">>, <<"$Q/client/$c">>)). + ?assertEqual(<<"$Q/client/clientId">>, emqttd_topic:feed_var(<<"$c">>, <<"clientId">>, <<"$Q/client/$c">>)). join_test() -> - ?assertEqual(<<"/ab/cd/ef/">>, emqtt_topic:join(words(<<"/ab/cd/ef/">>))), - ?assertEqual(<<"ab/+/#">>, emqtt_topic:join(words(<<"ab/+/#">>))). + ?assertEqual(<<"/ab/cd/ef/">>, emqttd_topic:join(words(<<"/ab/cd/ef/">>))), + ?assertEqual(<<"ab/+/#">>, emqttd_topic:join(words(<<"ab/+/#">>))). -endif. diff --git a/apps/emqttd/test/esockd_access.erl b/test/esockd_access.erl similarity index 100% rename from apps/emqttd/test/esockd_access.erl rename to test/esockd_access.erl diff --git a/apps/emqttd/test/test_acl.config b/test/test_acl.config similarity index 100% rename from apps/emqttd/test/test_acl.config rename to test/test_acl.config diff --git a/tests/benchmarks/.placeholder b/tests/benchmarks/.placeholder deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/org.eclipse.paho.mqtt.testing b/tests/org.eclipse.paho.mqtt.testing deleted file mode 160000 index 17afdf7fb..000000000 --- a/tests/org.eclipse.paho.mqtt.testing +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 17afdf7fb8e148f376d63592bbf3dd4ccdf19e84