├── .gitignore ├── LICENSE ├── README.md └── src ├── Makefile ├── cpp-test ├── client_broker_test.cpp ├── message_parse_test.cpp └── message_publish_test.cpp ├── cpp ├── abstract_request.h ├── bytebauble.cpp ├── bytebauble.h ├── chronotrigger.cpp ├── chronotrigger.h ├── client.cpp ├── client.h ├── client_listener.cpp ├── client_listener.h ├── client_listener_manager.cpp ├── client_listener_manager.h ├── connections.cpp ├── connections.h ├── dispatcher.cpp ├── dispatcher.h ├── message.cpp ├── message.h ├── nymph_logger.cpp ├── nymph_logger.h ├── request.cpp ├── request.h ├── server.cpp ├── server.h ├── server_connections.cpp ├── server_connections.h ├── server_request.cpp ├── server_request.h ├── session.cpp ├── session.h ├── timer.h ├── worker.cpp └── worker.h └── server └── nmqtt_server.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.exe 2 | *.txt 3 | *_test 4 | *.log 5 | obj/ 6 | bin/ 7 | lib/ 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, Maya Posch 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NymphMQTT # 2 | 3 | This project aims to support both MQTT 3.x and 5, targeting C++ and Ada. It uses components from the [NymphRPC](https://github.com/MayaPosch/NymphRPC "NymphRPC") Remote Procedure Call library for the networking side. 4 | 5 | The C++ version implements the MQTT 3.x client features in 1,412 lines of code: 6 | 7 | ------------------------------------------------------------------------------- 8 | Language files blank comment code 9 | ------------------------------------------------------------------------------- 10 | C++ 10 412 449 964 11 | C/C++ Header 12 209 141 448 12 | ------------------------------------------------------------------------------- 13 | SUM: 22 621 590 1412 14 | ------------------------------------------------------------------------------- 15 | 16 | ## Goals ## 17 | 18 | * MQTT 3.1.1 & MQTT 5 support. 19 | * Easy integration with C++ and Ada client applications. 20 | * Integrated MQTT broker. 21 | * Light-weight and versatile. 22 | * Minimal dependencies. 23 | * Cross-platform (Windows, Linux/BSD, MacOS, etc.). 24 | * Multi-broker (multiple active brokers per client). 25 | 26 | ## Building ## 27 | 28 | **C++:** 29 | 30 | Dependencies are: 31 | 32 | * LibPOCO 33 | * [ByteBauble](https://github.com/MayaPosch/ByteBauble) 34 | 35 | Navigate to the `src/` folder and run the `make` command there. Alternatively use either of these options: 36 | 37 | $ make test 38 | 39 | This will build the library and the test applications, equivalent to running make without options. 40 | 41 | $ make lib 42 | 43 | This will only build the library. It can be found in the 'src/lib' folder afterwards. 44 | 45 | ## Status ## 46 | 47 | The project status, for each port. 48 | 49 | ### C++ ### 50 | 51 | * All MQTT 3.1.1 (v4) client features have been implemented. 52 | * Connecting to multiple brokers should work. 53 | * MQTT 3.1.1 server (broker) features are being implemented. 54 | * MQTT 5 support is being integrated. 55 | 56 | 57 | ### Ada ### 58 | 59 | The Ada port at this point is being planned. Development will likely commence after the C++ port has stabilised sufficiently. 60 | 61 | ## Tests ## 62 | 63 | A number of unit/integration tests can be found in each port's folder, compilable using the provided Makefile. -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for the NymphMQTT MQTT library. 2 | 3 | export TOP := $(CURDIR) 4 | 5 | ifndef ANDROID_ABI_LEVEL 6 | ANDROID_ABI_LEVEL := 24 7 | endif 8 | 9 | ifdef ANDROID 10 | TOOLCHAIN_PREFIX := arm-linux-androideabi- 11 | ARCH := android-armv7/ 12 | ifdef OS 13 | TOOLCHAIN_POSTFIX := .cmd 14 | endif 15 | else ifdef ANDROID64 16 | TOOLCHAIN_PREFIX := aarch64-linux-android- 17 | ARCH := android-aarch64/ 18 | ifdef OS 19 | TOOLCHAIN_POSTFIX := .cmd 20 | endif 21 | else ifdef ANDROIDX86 22 | TOOLCHAIN_PREFIX := i686-linux-android- 23 | ARCH := android-i686/ 24 | ifdef OS 25 | TOOLCHAIN_POSTFIX := .cmd 26 | endif 27 | else ifdef ANDROIDX64 28 | TOOLCHAIN_PREFIX := x86_64-linux-android- 29 | ARCH := android-x86_64/ 30 | ifdef OS 31 | TOOLCHAIN_POSTFIX := .cmd 32 | endif 33 | endif 34 | 35 | ifndef ARCH 36 | ARCH := $(shell g++ -dumpmachine)/ 37 | endif 38 | 39 | USYS := $(shell uname -s) 40 | UMCH := $(shell uname -m) 41 | 42 | ifdef ANDROID 43 | #GCC := $(TOOLCHAIN_PREFIX)g++$(TOOLCHAIN_POSTFIX) 44 | GCC := armv7a-linux-androideabi$(ANDROID_ABI_LEVEL)-clang++$(TOOLCHAIN_POSTFIX) 45 | MAKEDIR = mkdir -p 46 | RM = rm 47 | AR = $(TOOLCHAIN_PREFIX)ar 48 | else ifdef ANDROID64 49 | GCC := aarch64-linux-android$(ANDROID_ABI_LEVEL)-clang++$(TOOLCHAIN_POSTFIX) 50 | MAKEDIR = mkdir -p 51 | RM = rm 52 | AR = $(TOOLCHAIN_PREFIX)ar 53 | else ifdef ANDROIDX86 54 | GCC := i686-linux-android$(ANDROID_ABI_LEVEL)-clang++$(TOOLCHAIN_POSTFIX) 55 | MAKEDIR = mkdir -p 56 | RM = rm 57 | AR = $(TOOLCHAIN_PREFIX)ar 58 | else ifdef ANDROIDX64 59 | GCC := x86_64-linux-android$(ANDROID_ABI_LEVEL)-clang++$(TOOLCHAIN_POSTFIX) 60 | MAKEDIR = mkdir -p 61 | RM = rm 62 | AR = $(TOOLCHAIN_PREFIX)ar 63 | else ifdef WASM 64 | GCC = emc++ 65 | MAKEDIR = mkdir -p 66 | RM = rm 67 | AR = ar 68 | else 69 | GCC = g++ 70 | MAKEDIR = mkdir -p 71 | RM = rm 72 | AR = ar 73 | endif 74 | 75 | 76 | OUTPUT = libnymphmqtt 77 | VERSION = 0.1 78 | 79 | # Use -soname on Linux/BSD, -install_name on Darwin (MacOS). 80 | SONAME = -soname 81 | LIBNAME = $(OUTPUT).so.$(VERSION) 82 | ifeq ($(shell uname -s),Darwin) 83 | SONAME = -install_name 84 | LIBNAME = $(OUTPUT).0.dylib 85 | endif 86 | 87 | #OUTPUT := libnymphmqtt.a 88 | SERVER := nmqtt_server 89 | 90 | SOURCES := $(wildcard cpp/*.cpp) 91 | OBJECTS := $(addprefix obj/static/$(ARCH),$(notdir) $(SOURCES:.cpp=.o)) 92 | SHARED_OBJECTS := $(addprefix obj/shared/$(ARCH),$(notdir) $(SOURCES:.cpp=.o)) 93 | 94 | SERVER_SOURCES := $(wildcard server/*.cpp) 95 | SERVER_OBJECTS := $(addprefix obj/,$(notdir) $(SERVER_SOURCES:.cpp=.o)) 96 | 97 | LIBS := -lPocoNet -lPocoNetSSL -lPocoUtil -lPocoFoundation -lPocoJSON 98 | #-lbytebauble 99 | INCLUDES := -I cpp/ 100 | CFLAGS := -std=c++11 -g3 -O0 101 | SHARED_FLAGS := -fPIC -shared -Wl,$(SONAME),$(LIBNAME) 102 | 103 | ifdef ANDROID 104 | CFLAGS += -fPIC 105 | else ifdef ANDROID64 106 | #CFLAGS += -fPIC 107 | else ifdef ANDROIDX86 108 | CFLAGS += -fPIC 109 | else ifdef ANDROIDX64 110 | CFLAGS += -fPIC 111 | endif 112 | 113 | # Check for MinGW and patch up POCO 114 | # The OS variable is only set on Windows. 115 | ifdef OS 116 | ifndef ANDROID 117 | ifndef ANDROID64 118 | ifndef ANDROIDX86 119 | ifndef ANDROIDX64 120 | CFLAGS := $(CFLAGS) -U__STRICT_ANSI__ -DPOCO_WIN32_UTF8 121 | LIBS += -lws2_32 122 | endif 123 | endif 124 | endif 125 | endif 126 | else 127 | LIBS += -pthread 128 | UNAME_S := $(shell uname -s) 129 | ifeq ($(UNAME_S),Linux) 130 | MAKE := gmake 131 | endif 132 | endif 133 | 134 | all: makedir lib 135 | 136 | lib: $(OBJECTS) lib/$(ARCH)$(OUTPUT).a lib/$(ARCH)$(LIBNAME) 137 | 138 | test: lib build_tests 139 | 140 | makedir: 141 | $(MAKEDIR) bin 142 | $(MAKEDIR) lib/$(ARCH) 143 | $(MAKEDIR) obj/static/$(ARCH)cpp 144 | $(MAKEDIR) obj/shared/$(ARCH)cpp 145 | $(MAKEDIR) obj/server 146 | 147 | obj/static/$(ARCH)%.o: %.cpp 148 | $(GCC) -c -o $@ $< $(CFLAGS) 149 | 150 | obj/shared/$(ARCH)%.o: %.cpp 151 | $(GCC) -c -o $@ $< $(SHARED_FLAGS) $(CFLAGS) $(LIBS) 152 | 153 | lib/$(ARCH)$(OUTPUT).a: $(OBJECTS) 154 | -rm -f $@ 155 | $(AR) rcs $@ $^ 156 | 157 | lib/$(ARCH)$(LIBNAME): $(SHARED_OBJECTS) 158 | $(GCC) -o $@ $(CFLAGS) $(SHARED_FLAGS) $(SHARED_OBJECTS) $(LIBS) 159 | 160 | server: lib $(SERVER_OBJECTS) 161 | $(GCC) -o bin/$(SERVER) $(OBJECTS) $(SERVER_OBJECTS) $(CFLAGS) $(LIBS) $(INCLUDES) 162 | 163 | build_tests: message_parse publish_message subscribe_broker 164 | 165 | message_parse: 166 | g++ -o bin/message_parse_test cpp-test/message_parse_test.cpp $(OBJECTS) $(INCLUDES) $(CFLAGS) $(LIBS) 167 | 168 | publish_message: 169 | g++ -o bin/message_publish_test cpp-test/message_publish_test.cpp $(OBJECTS) $(INCLUDES) -$(CFLAGS) $(LIBS) 170 | 171 | subscribe_broker: 172 | g++ -o bin/client_broker_test cpp-test/client_broker_test.cpp $(OBJECTS) $(INCLUDES) $(CFLAGS) $(LIBS) 173 | 174 | clean: 175 | rm $(OBJECTS) 176 | 177 | PREFIX ?= /usr 178 | ifdef OS 179 | # Assume 64-bit MSYS2 180 | PREFIX = /mingw64 181 | endif 182 | 183 | .PHONY: install lib 184 | install: 185 | install -d $(DESTDIR)$(PREFIX)/lib/ 186 | install -m 644 lib/$(ARCH)$(OUTPUT).a $(DESTDIR)$(PREFIX)/lib/ 187 | ifndef OS 188 | install -m 644 lib/$(ARCH)$(OUTPUT).so.$(VERSION) $(DESTDIR)$(PREFIX)/lib/ 189 | endif 190 | install -d $(DESTDIR)$(PREFIX)/include/nymphmqtt 191 | install -m 644 cpp/*.h $(DESTDIR)$(PREFIX)/include/nymphmqtt/ 192 | ifndef OS 193 | cd $(DESTDIR)$(PREFIX)/lib && \ 194 | if [ -f $(OUTPUT).so ]; then \ 195 | rm $(OUTPUT).so; \ 196 | fi && \ 197 | ln -s $(OUTPUT).so.$(VERSION) $(OUTPUT).so 198 | endif 199 | -------------------------------------------------------------------------------- /src/cpp-test/client_broker_test.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | client_broker_test.cpp - Broker test for the NymphMQTT Client class. 3 | 4 | Revision 0. 5 | 6 | 2019/05/09, Maya Posch 7 | */ 8 | 9 | 10 | #include "../cpp/client.h" 11 | 12 | #include 13 | #include 14 | 15 | #include 16 | 17 | #include 18 | #include 19 | 20 | using namespace Poco; 21 | 22 | 23 | Condition gCon; 24 | Mutex gMutex; 25 | 26 | 27 | void signal_handler(int signal) { 28 | gCon.signal(); 29 | } 30 | 31 | 32 | void logFunction(int level, std::string text) { 33 | std::cout << level << " - " << text << std::endl; 34 | } 35 | 36 | 37 | void messageHandler(int handle, std::string topic, std::string payload) { 38 | std::cout << "New message:" << std::endl; 39 | std::cout << "\tHandle: " << handle << std::endl; 40 | std::cout << "\tTopic: " << topic << std::endl; 41 | std::cout << "\tPayload: " << payload << std::endl; 42 | } 43 | 44 | 45 | int main() { 46 | // Do locale initialisation here to appease Valgrind (prevent data-race reporting). 47 | std::ostringstream dummy; 48 | dummy << 0; 49 | 50 | // Create client instance, connect to remote broker. 51 | NmqttClient client; 52 | client.init(logFunction, NYMPH_LOG_LEVEL_TRACE); 53 | client.setMessageHandler(messageHandler); 54 | 55 | Poco::Thread::sleep(500); 56 | 57 | std::cout << "TEST: Initialised client." << std::endl; 58 | 59 | 60 | std::string result; 61 | int handle; 62 | NmqttBrokerConnection conn; 63 | if (!client.connect("localhost", 1883, handle, 0, conn, result)) { 64 | std::cout << "Failed to connect to broker: " << result << std::endl; 65 | std::cout << "Got reason code: 0x" << (int) conn.responseCode << std::endl; 66 | client.shutdown(); 67 | return 1; 68 | } 69 | 70 | Poco::Thread::sleep(500); 71 | 72 | std::cout << "TEST: Connected to broker." << std::endl; 73 | std::cout << "Got reason code: 0x" << (int) conn.responseCode << std::endl; 74 | 75 | // Subscribe to test topic. 76 | std::string topic = "a/hello"; 77 | if (!client.subscribe(handle, topic, result)) { 78 | std::cout << "Failed to subscribe to topic:" << result << std::endl; 79 | client.shutdown(); 80 | return 1; 81 | } 82 | 83 | Poco::Thread::sleep(500); 84 | 85 | std::cout << "TEST: Subscribed to topic." << std::endl; 86 | 87 | // Publish to test topic. 88 | std::string payload = "Hello World!"; 89 | if (!client.publish(handle, topic, payload, result)) { 90 | std::cout << "Failed to publish to topic: " << result << std::endl; 91 | client.shutdown(); 92 | return 1; 93 | } 94 | 95 | Poco::Thread::sleep(500); 96 | 97 | std::cout << "TEST: Published to topic." << std::endl; 98 | 99 | // Wait until the SIGINT signal has been received. 100 | gMutex.lock(); 101 | gCon.wait(gMutex); 102 | 103 | Poco::Thread::sleep(500); 104 | 105 | std::cout << "TEST: Disconnecting from broker..." << std::endl; 106 | 107 | // Clean-up. 108 | if (!client.disconnect(handle, result)) { 109 | std::cerr << "Failed to disconnect from broker: " << result << std::endl; 110 | client.shutdown(); 111 | return 1; 112 | } 113 | 114 | Poco::Thread::sleep(500); 115 | 116 | std::cout << "TEST: Shutting down client..." << std::endl; 117 | 118 | Poco::Thread::sleep(500); 119 | 120 | client.shutdown(); 121 | 122 | return 0; 123 | } -------------------------------------------------------------------------------- /src/cpp-test/message_parse_test.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | message_parse_test.cpp - Parse test for the NymphMQTT Message class. 3 | 4 | Revision 0. 5 | 6 | 2019/05/09, Maya Posch 7 | */ 8 | 9 | 10 | #include "../cpp/message.h" 11 | 12 | #include 13 | #include 14 | 15 | 16 | int main() { 17 | // Create MQTT binary message, create NmqttMessage instance. 18 | // Test message is a Publish command, with the string payload 'Hello World!'. 19 | NmqttMessage msg; 20 | std::string mqtt_msg({0x30, 0x13, 0x00, 0x04, 'a', '/', 'h', 'i', 0x00}); 21 | mqtt_msg.append("Hello World!"); 22 | 23 | // Parse the binary message with the instance. 24 | msg.parseMessage(mqtt_msg); 25 | 26 | // Validate the results. 27 | if (!msg.valid()) { 28 | std::cerr << "Failed to parse message." << std::endl; 29 | return 1; 30 | } 31 | 32 | std::cout << "Successfully parsed message." << std::endl; 33 | 34 | std::cout << "Found topic: " << msg.getTopic() << std::endl; 35 | std::cout << "Found payload: " << msg.getPayload() << std::endl; 36 | 37 | return 0; 38 | } 39 | -------------------------------------------------------------------------------- /src/cpp-test/message_publish_test.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | message_publish_test.cpp - Publish message test for the NymphMQTT Message class. 3 | 4 | Revision 0. 5 | 6 | 2019/05/09, Maya Posch 7 | */ 8 | 9 | 10 | #include "../cpp/message.h" 11 | 12 | #include 13 | #include 14 | 15 | 16 | int main() { 17 | // 18 | 19 | std::string topic = "a/hello"; 20 | std::string payload = "Hello World!"; 21 | 22 | NmqttMessage msg(MQTT_PUBLISH); 23 | msg.setQoS(MQTT_QOS_AT_MOST_ONCE); 24 | msg.setRetain(false); 25 | msg.setTopic(topic); 26 | msg.setPayload(payload); 27 | 28 | std::string binMsg = msg.serialize(); 29 | 30 | std::cout << "Bin msg: " << std::hex << binMsg << std::endl; 31 | std::cout << "Bin msg length: " << std::hex << binMsg.length() << std::endl; 32 | 33 | NmqttMessage msg2; 34 | msg2.parseMessage(binMsg); 35 | 36 | // Validate the results. 37 | if (!msg2.valid()) { 38 | std::cerr << "Failed to parse message." << std::endl; 39 | return 1; 40 | } 41 | 42 | std::cout << "Successfully parsed message." << std::endl; 43 | 44 | std::cout << "Found topic: " << msg2.getTopic() << std::endl; 45 | std::cout << "Found payload: " << msg2.getPayload() << std::endl; 46 | 47 | return 0; 48 | } 49 | -------------------------------------------------------------------------------- /src/cpp/abstract_request.h: -------------------------------------------------------------------------------- 1 | /* 2 | abstractrequest.h - header file for the AbstractRequest class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - 8 | 9 | 2016/11/19, Maya Posch 10 | (c) Nyanko.ws 11 | */ 12 | 13 | 14 | #pragma once 15 | #ifndef ABSTRACT_REQUEST_H 16 | #define ABSTRACT_REQUEST_H 17 | 18 | 19 | #include 20 | 21 | 22 | class AbstractRequest { 23 | // 24 | 25 | public: 26 | virtual void setValue(std::string value) = 0; 27 | virtual void process() = 0; 28 | virtual void finish() = 0; 29 | }; 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/cpp/bytebauble.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | bytebauble.cpp - Implementation of the ByteBauble library. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/31, Maya Posch 13 | */ 14 | 15 | 16 | #include "bytebauble.h" 17 | 18 | #include 19 | 20 | 21 | // --- CONSTRUCTOR --- 22 | ByteBauble::ByteBauble() { 23 | // Perform host endianness detection. 24 | detectHostEndian(); 25 | } 26 | 27 | 28 | // --- DETECT HOST ENDIAN --- 29 | // Perform a check for the endianness of the host system. 30 | void ByteBauble::detectHostEndian() { 31 | // This check uses a single 16-bit integer, using it as a single byte after assignment. 32 | // Depending on which byte the (<255) value ended up on, we can determine the host endianness. 33 | uint16_t bytes = 1; 34 | if (*((uint8_t*) &bytes) == 1) { 35 | // Detected little endian. 36 | //std::cout << "Detected Host Little Endian." << std::endl; 37 | hostEndian = BB_LE; 38 | } 39 | else { 40 | // Detected big endian. 41 | //std::cout << "Detected Host Big Endian." << std::endl; 42 | hostEndian = BB_BE; 43 | } 44 | } 45 | 46 | 47 | // --- READ PACKED INT --- 48 | // Packed integer format that uses the MSB of each byte to indicate that another byte follows. 49 | // This method supports the input of a single unsigned 32-bit integer. 50 | // Second parameter and output is a 32-bit integer, of which at most 28 bits can contain a value from the 51 | // packed integer. 52 | // Return value is the number of bytes in the packed integer. 53 | uint32_t ByteBauble::readPackedInt(uint32_t packed, uint32_t &output) { 54 | // Check the special bits while copying the data bits. 55 | output = 0; 56 | int idx = 0; // Index into output integer. 57 | int src = 0; // Index into input packed integer. 58 | int i; 59 | for (i = 0; i < 4; ++i) { 60 | // Copy data bits of current byte. 61 | for (int j = 0; j < 7; ++j) { 62 | if ((packed >> src++) & 1UL) { 63 | output |= (1UL << idx++); 64 | } 65 | else { 66 | //output &= ~(1UL << idx++); 67 | idx++; 68 | } 69 | } 70 | 71 | // Check for presence of follow-up byte. 72 | if (!((packed >> src++) & 1UL)) { 73 | // Not found, we're done. 74 | break; 75 | } 76 | } 77 | 78 | return i + 1; 79 | } 80 | 81 | 82 | // --- WRITE PACKED INT --- 83 | uint32_t ByteBauble::writePackedInt(uint32_t integer, uint32_t &output) { 84 | // First determine how many bytes we will be needing. 85 | uint32_t totalBytes = 0; 86 | if (integer <= 0x80) { totalBytes = 1; } 87 | else if (integer <= 0x4000) { totalBytes = 2; } 88 | else if (integer <= 0x200000) { totalBytes = 3; } 89 | else if (integer <= 0x10000000) { totalBytes = 4; } 90 | else { 91 | // Out of range, return error. 92 | return 0; 93 | } 94 | 95 | // Copy the source data bits to as many packed bytes as needed. 96 | output = 0; 97 | int idx = 0; // Index into packed (output) integer. 98 | int src = 0; // Index into source integer. 99 | for (int i = 0; i < totalBytes; ++i) { 100 | // 101 | for (int j = 0; j < 7; ++j) { 102 | if ((integer >> src++) & 1UL) { 103 | output |= (1UL << idx++); 104 | } 105 | else { 106 | //output &= ~(1UL << idx++); 107 | idx++; 108 | } 109 | } 110 | 111 | // Check for follow-up byte and set if necessary. 112 | if ((i + 1) < totalBytes) { 113 | output |= (1UL << idx++); 114 | } 115 | } 116 | 117 | return totalBytes; 118 | } 119 | -------------------------------------------------------------------------------- /src/cpp/bytebauble.h: -------------------------------------------------------------------------------- 1 | /* 2 | bytebauble.h - Header for the ByteBauble library. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/31, Maya Posch 13 | */ 14 | 15 | 16 | #ifndef BYTEBAUBLE_H 17 | #define BYTEBAUBLE_H 18 | 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #ifdef _MSC_VER 25 | #include 26 | #endif 27 | 28 | 29 | enum BBEndianness { 30 | BB_BE, // Big Endian 31 | BB_LE // Little Endian 32 | }; 33 | 34 | 35 | class ByteBauble { 36 | BBEndianness globalEndian = BB_LE; 37 | BBEndianness hostEndian = BB_LE; 38 | 39 | public: 40 | ByteBauble(); 41 | 42 | void detectHostEndian(); 43 | BBEndianness getHostEndian() { return hostEndian; } 44 | 45 | static uint32_t readPackedInt(uint32_t packed, uint32_t &output); 46 | static uint32_t writePackedInt(uint32_t integer, uint32_t &output); 47 | 48 | void setGlobalEndianness(BBEndianness end) { globalEndian = end; } 49 | 50 | // --- TO GLOBAL --- 51 | // 52 | template 53 | T toGlobal(T in, BBEndianness end) { 54 | // Convert to requested format, if different from global. 55 | if (end == globalEndian) { 56 | // Endianness matches, return input. 57 | return in; 58 | } 59 | 60 | // Perform the conversion. 61 | // Flip the bytes, so that the MSB and LSB are switched. 62 | // Compiler intrinsics in GCC/MinGW exist since ~4.3, for MSVC 63 | std::size_t bytesize = sizeof(in); 64 | #if defined(__GNUC__) || defined(__MINGW32__) || defined(__MINGW64__) 65 | if (bytesize == 2) { 66 | return __builtin_bswap16(in); 67 | } 68 | else if (bytesize == 4) { 69 | return __builtin_bswap32(in); 70 | } 71 | else if (bytesize == 8) { 72 | return __builtin_bswap64(in); 73 | } 74 | #elif defined(_MSC_VER) 75 | if (bytesize == 2) { 76 | return _byteswap_ushort(in); 77 | } 78 | else if (bytesize == 4) { 79 | return _byteswap_ulong(in); 80 | } 81 | else if (bytesize == 8) { 82 | return _byteswap_uint64(in); 83 | } 84 | #endif 85 | 86 | // Fallback for other compilers. 87 | // TODO: implement. 88 | return 0; 89 | } 90 | 91 | // --- TO HOST --- 92 | // 93 | template 94 | T toHost(T in, BBEndianness end) { 95 | // 96 | 97 | // Convert to requested format, if different from host. 98 | if (end == hostEndian) { 99 | // Endianness matches, return input. 100 | return in; 101 | } 102 | 103 | // Perform the conversion. 104 | // Flip the bytes, so that the MSB and LSB are switched. 105 | // Compiler intrinsics in GCC/MinGW exist since ~4.3, for MSVC 106 | std::size_t bytesize = sizeof(in); 107 | #if defined(__GNUC__) || defined(__MINGW32__) || defined(__MINGW64__) 108 | if (bytesize == 2) { 109 | return __builtin_bswap16(in); 110 | } 111 | else if (bytesize == 4) { 112 | return __builtin_bswap32(in); 113 | } 114 | else if (bytesize == 8) { 115 | return __builtin_bswap64(in); 116 | } 117 | #elif defined(_MSC_VER) 118 | if (bytesize == 2) { 119 | return _byteswap_ushort(in); 120 | } 121 | else if (bytesize == 4) { 122 | return _byteswap_ulong(in); 123 | } 124 | else if (bytesize == 8) { 125 | return _byteswap_uint64(in); 126 | } 127 | #endif 128 | 129 | // Fallback for other compilers. 130 | // TODO: implement. 131 | return 0; 132 | } 133 | }; 134 | 135 | 136 | #endif 137 | -------------------------------------------------------------------------------- /src/cpp/chronotrigger.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | chronotrigger.cpp - Source for the ChronoTrigger class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - Simple class that implements a periodic, resettable timer. 8 | 9 | Notes: 10 | - 11 | 12 | 2019/09/19 - Maya Posch 13 | */ 14 | 15 | 16 | #include "chronotrigger.h" 17 | 18 | 19 | void ChronoTrigger::setCallback(std::function cb, uint32_t data) { 20 | this->cb = cb; 21 | this->data = data; 22 | } 23 | 24 | 25 | // --- START --- 26 | // Start the processing thread. 27 | bool ChronoTrigger::start(uint32_t interval, bool single) { 28 | thread = std::thread(&ChronoTrigger::run, this, interval, single); 29 | 30 | return true; 31 | } 32 | 33 | 34 | // --- RUN --- 35 | // If single-shot is set, wait for the 'interval' time period, then stop. 36 | // Else, loop until finish() or stop() is called. 37 | // Interval is in milliseconds. 38 | void ChronoTrigger::run(uint32_t interval, bool single) { 39 | running = true; 40 | signaled = false; 41 | restarting = false; 42 | stopping = false; 43 | while (true) { 44 | // Wait in the condition variable until the wait ends, or the condition is signalled. 45 | startTime = std::chrono::steady_clock::now(); 46 | endTime = startTime + std::chrono::milliseconds(interval); 47 | while (std::chrono::steady_clock::now() < endTime) { 48 | // Loop to deal with spurious wake-ups. 49 | std::unique_lock lk(mutex); 50 | if (cv.wait_until(lk, endTime) == std::cv_status::timeout) { break; } 51 | if (signaled) { signaled = false; break; } 52 | } 53 | 54 | // Check flags. 55 | if (restarting) { 56 | // Reset, return to beginning of loop. 57 | running = true; 58 | signaled = false; 59 | restarting = false; 60 | stopping = false; 61 | continue; 62 | } 63 | 64 | if (stopping) { // Stop was called. 65 | break; 66 | } 67 | 68 | // Call the callback with the user data. 69 | cb(data); 70 | 71 | // Exit if single-shot is true. 72 | if (single) { break; } 73 | 74 | // Exit if finish() has been called. 75 | if (!running) { break; } 76 | } 77 | } 78 | 79 | 80 | // --- RESTART --- 81 | void ChronoTrigger::restart() { 82 | restarting = true; 83 | signaled = true; 84 | cv.notify_all(); 85 | } 86 | 87 | 88 | // --- FINISH --- 89 | // Allow the timer to finish and trigger the callback one last time before stopping. 90 | void ChronoTrigger::finish() { 91 | running = false; 92 | } 93 | 94 | 95 | // --- STOP --- 96 | // Signal the condition variable, end the timer. 97 | void ChronoTrigger::stop() { 98 | signaled = true; 99 | stopping = true; 100 | cv.notify_all(); 101 | 102 | thread.join(); 103 | } 104 | -------------------------------------------------------------------------------- /src/cpp/chronotrigger.h: -------------------------------------------------------------------------------- 1 | /* 2 | chronotrigger.h - Header for the ChronoTrigger class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - Simple class that implements a periodic, resettable timer. 8 | 9 | Notes: 10 | - 11 | 12 | 2019/09/19 - Maya Posch 13 | */ 14 | 15 | 16 | #ifndef CHRONO_TRIGGER_H 17 | #define CHRONO_TRIGGER_H 18 | 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | 28 | class ChronoTrigger { 29 | std::thread thread; 30 | std::mutex mutex; 31 | std::condition_variable cv; 32 | std::atomic_bool running; 33 | std::atomic_bool restarting; 34 | std::atomic_bool signaled; 35 | std::atomic_bool stopping; 36 | std::function cb; 37 | uint32_t data; 38 | std::chrono::time_point startTime; 39 | std::chrono::time_point endTime; 40 | 41 | void run(uint32_t interval, bool single); 42 | 43 | public: 44 | void setCallback(std::function cb, uint32_t data); 45 | bool start(uint32_t interval, bool single = false); 46 | void restart(); 47 | void finish(); 48 | void stop(); 49 | }; 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /src/cpp/client.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | client.cpp - Implementation for the NymphMQTT Client class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/08 - Maya Posch 13 | */ 14 | 15 | 16 | #include "client.h" 17 | #include "client_listener_manager.h" 18 | #include "connections.h" 19 | #include "dispatcher.h" 20 | 21 | #include 22 | #include 23 | 24 | using namespace Poco; 25 | 26 | using namespace std; 27 | 28 | 29 | // --- CONSTRUCTOR --- 30 | NmqttClient::NmqttClient() { 31 | // 32 | } 33 | 34 | 35 | // --- INIT --- 36 | // Initialise the runtime and sets the logger function to be used by the Nymph 37 | // Logger class, along with the desired maximum log level: 38 | // NYMPH_LOG_LEVEL_FATAL = 1, 39 | // NYMPH_LOG_LEVEL_CRITICAL, 40 | // NYMPH_LOG_LEVEL_ERROR, 41 | // NYMPH_LOG_LEVEL_WARNING, 42 | // NYMPH_LOG_LEVEL_NOTICE, 43 | // NYMPH_LOG_LEVEL_INFO, 44 | // NYMPH_LOG_LEVEL_DEBUG, 45 | // NYMPH_LOG_LEVEL_TRACE 46 | bool NmqttClient::init(std::function logger, int level, long timeout) { 47 | //NymphRemoteServer::timeout = timeout; // FIXME 48 | setLogger(logger, level); 49 | 50 | // Get the number of concurrent threads supported by the system we are running on. 51 | int numThreads = std::thread::hardware_concurrency(); 52 | 53 | // Initialise the Dispatcher with the maximum number of threads as worker count. 54 | Dispatcher::init(numThreads); 55 | 56 | return true; 57 | } 58 | 59 | 60 | // --- SET LOGGER --- 61 | // Sets the logger function to be used by the Nymph Logger class, along with the 62 | // desired maximum log level: 63 | // NYMPH_LOG_LEVEL_FATAL = 1, 64 | // NYMPH_LOG_LEVEL_CRITICAL, 65 | // NYMPH_LOG_LEVEL_ERROR, 66 | // NYMPH_LOG_LEVEL_WARNING, 67 | // NYMPH_LOG_LEVEL_NOTICE, 68 | // NYMPH_LOG_LEVEL_INFO, 69 | // NYMPH_LOG_LEVEL_DEBUG, 70 | // NYMPH_LOG_LEVEL_TRACE 71 | void NmqttClient::setLogger(std::function logger, int level) { 72 | NymphLogger::setLoggerFunction(logger); 73 | NymphLogger::setLogLevel((Poco::Message::Priority) level); 74 | } 75 | 76 | 77 | // --- SET MESSAGE HANDLER --- 78 | // Set the callback function that will be called every time a message is received from the broker. 79 | void NmqttClient::setMessageHandler(std::function handler) { 80 | messageHandler = handler; 81 | } 82 | 83 | 84 | // --- SHUTDOWN --- 85 | // Shutdown the runtime. Close any open connections and clean up resources. 86 | bool NmqttClient::shutdown() { 87 | socketsMutex.lock(); 88 | map::iterator it; 89 | for (it = sockets.begin(); it != sockets.end(); ++it) { 90 | // Remove socket from listener. 91 | NmqttClientListenerManager::removeConnection(it->first); 92 | 93 | // TODO: try/catch. 94 | it->second->shutdown(); 95 | } 96 | 97 | sockets.clear(); 98 | socketsMutex.unlock(); 99 | 100 | NmqttClientListenerManager::stop(); 101 | 102 | return true; 103 | } 104 | 105 | 106 | // --- CONNECT --- 107 | // Create a new connection with the remote MQTT server and return a handle for 108 | // the connection. 109 | bool NmqttClient::connect(string host, int port, int &handle, void* data, 110 | NmqttBrokerConnection &conn, string &result) { 111 | Poco::Net::SocketAddress sa(host, port); 112 | return connect(sa, handle, data, conn, result); 113 | } 114 | 115 | 116 | bool NmqttClient::connect(string url, int &handle, void* data, 117 | NmqttBrokerConnection &conn, string &result) { 118 | Poco::Net::SocketAddress sa(url); 119 | return connect(sa, handle, data, conn, result); 120 | } 121 | 122 | 123 | bool NmqttClient::connect(Poco::Net::SocketAddress sa, int &handle, void* data, 124 | NmqttBrokerConnection &conn, string &result) { 125 | using namespace std::placeholders; 126 | NymphSocket ns; 127 | try { 128 | if (secureConnection) { 129 | Poco::Net::initializeSSL(); 130 | ns.secure = true; 131 | ns.context = new Poco::Net::Context(Poco::Net::Context::CLIENT_USE, 132 | key, 133 | cert, 134 | ca); 135 | ns.socket = new Poco::Net::SecureStreamSocket(sa, ns.context); 136 | } 137 | else { 138 | ns.secure = false; 139 | ns.socket = new Poco::Net::StreamSocket(sa); 140 | } 141 | } 142 | catch (Poco::Net::ConnectionRefusedException &ex) { 143 | // Handle connection error. 144 | result = "Unable to connect: " + ex.displayText(); 145 | return false; 146 | } 147 | catch (Poco::InvalidArgumentException &ex) { 148 | result = "Invalid argument: " + ex.displayText(); 149 | return false; 150 | } 151 | catch (Poco::Net::InvalidSocketException &ex) { 152 | result = "Invalid socket exception: " + ex.displayText(); 153 | return false; 154 | } 155 | catch (Poco::Net::NetException &ex) { 156 | result = "Net exception: " + ex.displayText(); 157 | return false; 158 | } 159 | 160 | socketsMutex.lock(); 161 | sockets.insert(pair(lastHandle, ns.socket)); 162 | ns.semaphore = new Semaphore(0, 1); 163 | socketSemaphores.insert(pair(lastHandle, ns.semaphore)); 164 | ns.data = data; 165 | ns.handle = lastHandle; 166 | ns.handler = messageHandler; 167 | ns.connackHandler = std::bind(&NmqttClient::connackHandler, this, _1, _2, _3); 168 | ns.pingrespHandler = std::bind(&NmqttClient::pingrespHandler, this, _1); 169 | NmqttConnections::addSocket(ns); 170 | if (!NmqttClientListenerManager::addConnection(lastHandle)) { return false; } 171 | handle = lastHandle++; 172 | socketsMutex.unlock(); 173 | 174 | NYMPH_LOG_DEBUG("Added new connection with handle: " + NumberFormatter::format(handle)); 175 | 176 | // Send Connect message using the previously set data. 177 | brokerConn = 0; 178 | NmqttMessage msg(MQTT_CONNECT); 179 | if (willFlag) { msg.setWill(willTopic, will, willQoS, willRetainFlag); } 180 | if (usernameFlag) { msg.setCredentials(username, password); } 181 | msg.setClientId(clientId); 182 | 183 | NYMPH_LOG_INFORMATION("Sending CONNECT message."); 184 | 185 | if (!sendMessage(handle, msg.serialize())) { 186 | return false; 187 | } 188 | 189 | // Wait for condition. 190 | connectMtx.lock(); 191 | brokerConn = &conn; 192 | if (!connectCnd.tryWait(connectMtx, timeout)) { 193 | result = "Timeout while trying to connect to broker."; 194 | NYMPH_LOG_ERROR("Timeout while trying to connect to broker."); 195 | brokerConn = 0; 196 | connectMtx.unlock(); 197 | return false; 198 | } 199 | 200 | // Start ping timer. Use the Keep Alive value used during the Connect minus one second as 201 | // duration. 202 | // FIXME: check that the Keep Alive value isn't less than one second. Subtract milliseconds if 203 | // less than 10 seconds or so. 204 | int keepAlive = 60; // In seconds. TODO: use connection Keep Alive value. 205 | pingTimer.setCallback(std::bind(&NmqttClient::pingreqHandler, 206 | this, 207 | std::placeholders::_1), 208 | handle); 209 | pingTimer.start((keepAlive - 2) * 400); 210 | 211 | 212 | return true; 213 | } 214 | 215 | 216 | // --- DISCONNECT --- 217 | bool NmqttClient::disconnect(int handle, string &result) { 218 | // Stop the Pingreq timer and delete it. 219 | pingTimer.stop(); 220 | 221 | // Create a Disconnect message, send it to the indicated remote. 222 | NYMPH_LOG_INFORMATION("Sending DISCONNECT message."); 223 | NmqttMessage msg(MQTT_DISCONNECT); 224 | //msg.setWill(will); 225 | 226 | sendMessage(handle, msg.serialize()); 227 | 228 | // FIXME: wait here? 229 | 230 | map::iterator it; 231 | map::iterator sit; 232 | socketsMutex.lock(); 233 | it = sockets.find(handle); 234 | if (it == sockets.end()) { 235 | result = "Provided handle " + NumberFormatter::format(handle) + " was not found."; 236 | socketsMutex.unlock(); 237 | return false; 238 | } 239 | 240 | sit = socketSemaphores.find(handle); 241 | if (sit == socketSemaphores.end()) { 242 | result = "No semaphore found for socket handle."; 243 | socketsMutex.unlock(); 244 | return false; 245 | } 246 | 247 | // TODO: try/catch. 248 | // Shutdown socket. Set the semaphore once done to signal that the socket's 249 | // listener thread that it's safe to delete the socket. 250 | it->second->shutdown(); 251 | it->second->close(); 252 | if (sit->second) { sit->second->set(); } 253 | 254 | // Remove socket from listener. 255 | NmqttClientListenerManager::removeConnection(it->first); 256 | 257 | // Remove socket references from both maps. 258 | sockets.erase(it); 259 | socketSemaphores.erase(sit); 260 | 261 | socketsMutex.unlock(); 262 | 263 | NYMPH_LOG_DEBUG("Removed connection with handle: " + NumberFormatter::format(handle)); 264 | 265 | return true; 266 | } 267 | 268 | 269 | // --- SET CREDENTIALS --- 270 | void NmqttClient::setCredentials(std::string &user, std::string &pass) { 271 | username = user; 272 | password = pass; 273 | usernameFlag = true; 274 | passwordFlag = true; 275 | } 276 | 277 | 278 | // --- SET WILL --- 279 | void NmqttClient::setWill(std::string topic, std::string will, uint8_t qos, bool retain) { 280 | this->will = will; 281 | this->willTopic = topic; 282 | this->willRetainFlag = retain; 283 | this->willQoS = qos; 284 | this->willFlag = true; 285 | } 286 | 287 | 288 | // --- SET TLS --- 289 | void NmqttClient::setTLS(std::string &ca, std::string &cert, std::string &key) { 290 | this->ca = ca; 291 | this->cert = cert; 292 | this->key = key; 293 | secureConnection = true; 294 | } 295 | 296 | 297 | // --- SEND MESSAGE --- 298 | // Private method for sending data to a remote broker. 299 | bool NmqttClient::sendMessage(int handle, std::string binMsg) { 300 | map::iterator it; 301 | socketsMutex.lock(); 302 | it = sockets.find(handle); 303 | if (it == sockets.end()) { 304 | NYMPH_LOG_ERROR("Provided handle " + NumberFormatter::format(handle) + " was not found."); 305 | socketsMutex.unlock(); 306 | return false; 307 | } 308 | 309 | try { 310 | int ret = it->second->sendBytes(((const void*) binMsg.c_str()), binMsg.length()); 311 | if (ret != binMsg.length()) { 312 | // Handle error. 313 | NYMPH_LOG_ERROR("Failed to send message. Not all bytes sent."); 314 | return false; 315 | } 316 | 317 | NYMPH_LOG_DEBUG("Sent " + NumberFormatter::format(ret) + " bytes."); 318 | } 319 | catch (Poco::Exception &e) { 320 | NYMPH_LOG_ERROR("Failed to send message: " + e.message()); 321 | return false; 322 | } 323 | 324 | socketsMutex.unlock(); 325 | 326 | // Reset Ping timer. 327 | pingTimer.restart(); 328 | 329 | NYMPH_LOG_DEBUG("Successfully restarted the timer."); 330 | 331 | return true; 332 | } 333 | 334 | 335 | // --- CONNACK HANDLER --- 336 | // Callback for incoming CONNACK packets. 337 | void NmqttClient::connackHandler(int handle, bool sessionPresent, MqttReasonCodes code) { 338 | // Set the data 339 | if (brokerConn) { 340 | brokerConn->handle = handle; 341 | brokerConn->sessionPresent = sessionPresent; 342 | brokerConn->responseCode = code; 343 | } 344 | 345 | // Signal the condition variable. 346 | connectCnd.signal(); 347 | } 348 | 349 | 350 | // --- PINGREQ HANDLER --- 351 | // Callback for the internal timer to send a ping request to the broker to keep the connection 352 | // alive. 353 | void NmqttClient::pingreqHandler(uint32_t t) { 354 | NmqttMessage msg(MQTT_PINGREQ); 355 | 356 | NYMPH_LOG_INFORMATION("Sending PINGREQ message for handle: " + 357 | Poco::NumberFormatter::format(t)); 358 | 359 | if (!sendMessage(t, msg.serialize())) { 360 | NYMPH_LOG_ERROR("Failed to send PINGREQ message."); 361 | } 362 | } 363 | 364 | 365 | // --- PINGRESP HANDLER --- 366 | // Called when a PINGACK message arrives. Reset the ping timer for this handle. 367 | // TODO: implement per handle timer. 368 | void NmqttClient::pingrespHandler(int handle) { 369 | NYMPH_LOG_DEBUG("PINGRESP handler got called."); 370 | } 371 | 372 | 373 | // --- PUBLISH --- 374 | bool NmqttClient::publish(int handle, std::string topic, std::string payload, std::string &result, 375 | MqttQoS qos, bool retain) { 376 | NmqttMessage msg(MQTT_PUBLISH); 377 | msg.setQoS(qos); 378 | msg.setRetain(retain); 379 | msg.setTopic(topic); 380 | msg.setPayload(payload); 381 | 382 | NYMPH_LOG_INFORMATION("Sending PUBLISH message."); 383 | 384 | return sendMessage(handle, msg.serialize()); 385 | } 386 | 387 | 388 | // --- SUBSCRIBE --- 389 | bool NmqttClient::subscribe(int handle, std::string topic, std::string result) { 390 | // 391 | NmqttMessage msg(MQTT_SUBSCRIBE); 392 | msg.setTopic(topic); 393 | 394 | NYMPH_LOG_INFORMATION("Sending SUBSCRIBE message."); 395 | 396 | return sendMessage(handle, msg.serialize()); 397 | } 398 | 399 | 400 | // --- UNSUBSCRIBE --- 401 | bool NmqttClient::unsubscribe(int handle, std::string topic, std::string result) { 402 | NmqttMessage msg(MQTT_UNSUBSCRIBE); 403 | msg.setTopic(topic); 404 | 405 | NYMPH_LOG_INFORMATION("Sending UNSUBSCRIBE message."); 406 | 407 | return sendMessage(handle, msg.serialize()); 408 | } 409 | 410 | 411 | // --- GET LOCAL ADDRESS 412 | // Returns the local IPv4 address as a string, or an empty string if handle not found. 413 | std::string NmqttClient::getLocalAddress(int handle) { 414 | // 415 | std::map::iterator it; 416 | it = sockets.find(handle); 417 | if (it == sockets.end()) { return std::string(); } 418 | 419 | return it->second->address().toString(); 420 | } 421 | 422 | -------------------------------------------------------------------------------- /src/cpp/client.h: -------------------------------------------------------------------------------- 1 | /* 2 | client.h - Header for the NymphMQTT Client class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/08 - Maya Posch 13 | */ 14 | 15 | 16 | #ifndef NMQTT_CLIENT_H 17 | #define NMQTT_CLIENT_H 18 | 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include "nymph_logger.h" 31 | #include "message.h" 32 | #include "chronotrigger.h" 33 | 34 | 35 | struct NmqttBrokerConnection { 36 | int handle; 37 | std::string host; 38 | int port; 39 | bool sessionPresent; 40 | MqttReasonCodes responseCode; 41 | }; 42 | 43 | 44 | class NmqttClient { 45 | std::map sockets; 46 | std::map socketSemaphores; 47 | Poco::Mutex socketsMutex; 48 | int lastHandle = 0; 49 | long timeout = 3000; 50 | std::string loggerName = "NmqttClient"; 51 | std::function messageHandler; 52 | Poco::Condition connectCnd; 53 | Poco::Mutex connectMtx; 54 | ChronoTrigger pingTimer; 55 | NmqttBrokerConnection* brokerConn = 0; 56 | bool secureConnection = false; 57 | 58 | uint8_t connectFlags; 59 | bool cleanSessionFlag = true; 60 | bool willFlag = false; 61 | bool willRetainFlag = false; 62 | bool usernameFlag = false; 63 | bool passwordFlag = false; 64 | uint8_t willQoS = 0; 65 | std::string will; 66 | std::string willTopic; 67 | std::string clientId = "NymphMQTT-client"; 68 | std::string username; 69 | std::string password; 70 | std::string ca, cert, key; 71 | 72 | bool sendMessage(int handle, std::string binMsg); 73 | void connackHandler(int handle, bool sessionPresent, MqttReasonCodes code); 74 | void pingreqHandler(uint32_t t); 75 | void pingrespHandler(int handle); 76 | 77 | public: 78 | NmqttClient(); 79 | 80 | bool init(std::function logger, int level = NYMPH_LOG_LEVEL_TRACE, long timeout = 3000); 81 | void setLogger(std::function logger, int level); 82 | void setMessageHandler(std::function handler); 83 | bool shutdown(); 84 | bool connect(std::string host, int port, int &handle, void* data, 85 | NmqttBrokerConnection &conn, std::string &result); 86 | bool connect(std::string url, int &handle, void* data, 87 | NmqttBrokerConnection &conn, std::string &result); 88 | bool connect(Poco::Net::SocketAddress sa, int &handle, void* data, 89 | NmqttBrokerConnection &conn, std::string &result); 90 | bool disconnect(int handle, std::string &result); 91 | 92 | void setCredentials(std::string &user, std::string &pass); 93 | void setWill(std::string topic, std::string will, uint8_t qos = 0, bool retain = false); 94 | void setTLS(std::string &ca, std::string &cert, std::string &key); 95 | void setClientId(std::string id) { clientId = id; } 96 | bool publish(int handle, std::string topic, std::string payload, std::string &result, 97 | MqttQoS qos = MQTT_QOS_AT_MOST_ONCE, bool retain = false); 98 | bool subscribe(int handle, std::string topic, std::string result); 99 | bool unsubscribe(int handle, std::string topic, std::string result); 100 | 101 | std::string getLocalAddress(int handle); 102 | }; 103 | 104 | 105 | #endif 106 | -------------------------------------------------------------------------------- /src/cpp/client_listener.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | client_listener.h - Header for the NymphMQTT Client socket listening thread class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - Socket listener for MQTT connections. 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/08 - Maya Posch 13 | */ 14 | 15 | 16 | #include "client_listener.h" 17 | #include "nymph_logger.h" 18 | #include "dispatcher.h" 19 | #include "request.h" 20 | 21 | using namespace std; 22 | 23 | #include 24 | 25 | using namespace Poco; 26 | 27 | 28 | // --- CONSTRUCTOR --- 29 | NmqttClientListener::NmqttClientListener(int handle, Condition* cnd, Mutex* mtx) { 30 | loggerName = "NmqttClientListener"; 31 | listen = true; 32 | init = true; 33 | this->nymphSocket = NmqttConnections::getSocket(handle); 34 | this->socket = nymphSocket->socket; 35 | this->readyCond = cnd; 36 | this->readyMutex = mtx; 37 | } 38 | 39 | 40 | // --- DECONSTRUCTOR --- 41 | NmqttClientListener::~NmqttClientListener() { 42 | // 43 | } 44 | 45 | 46 | // --- RUN --- 47 | void NmqttClientListener::run() { 48 | Poco::Timespan timeout(0, 100); // 100 microsecond timeout 49 | 50 | NYMPH_LOG_INFORMATION("Start listening..."); 51 | 52 | char headerBuff[5]; 53 | while (listen) { 54 | if (socket->poll(timeout, Net::Socket::SELECT_READ)) { 55 | // Attempt to receive the entire message. 56 | // First validate the first two bytes. If it's an MQTT message this will contain the 57 | // command and the first byte of the message length. 58 | // 59 | // Unfortunately, MQTT's message length is a variable length integer, spanning 1-4 bytes. 60 | // Because of this, we have to read in the first byte, see whether a second one follows 61 | // by looking at the 8th bit of the byte, read that in, and so on. 62 | // 63 | // The smallest message we can receive is the Disconnect type, with just two bytes. 64 | int received = socket->receiveBytes((void*) &headerBuff, 2); 65 | if (received == 0) { 66 | // Remote disconnnected. Socket should be discarded. 67 | NYMPH_LOG_INFORMATION("Received remote disconnected notice. Terminating listener thread."); 68 | break; 69 | } 70 | else if (received < 2) { 71 | // TODO: try to wait for more bytes. 72 | NYMPH_LOG_WARNING("Received <2 bytes: " + NumberFormatter::format(received)); 73 | 74 | continue; 75 | } 76 | 77 | // Use the NmqttMessage class's validation feature to extract the message length from 78 | // the fixed header. 79 | NmqttMessage msg; 80 | uint32_t msglen = 0; 81 | int idx = 0; // Will be set to the index after the fixed header by the parse method. 82 | while (!msg.parseHeader((char*) &headerBuff, 5, msglen, idx)) { 83 | // Attempt to read more data. The index parameter is placed at the last valid 84 | // byte in the headerBuff array. Append new data after this. 85 | 86 | NYMPH_LOG_WARNING("Too few bytes to parse header. Aborting..."); 87 | 88 | // TODO: abort reading for now. 89 | continue; 90 | } 91 | 92 | NYMPH_LOG_DEBUG("Received message length: " + NumberFormatter::format(msglen)); 93 | 94 | string binMsg; 95 | if (msglen > 0) { 96 | // Create new buffer for the rest of the message. 97 | char* buff = new char[msglen]; 98 | 99 | // Read the entire message into a string which is then used to 100 | // construct an NmqttMessage instance. 101 | received = socket->receiveBytes((void*) buff, msglen); 102 | binMsg.append(headerBuff, idx); 103 | binMsg.append(buff, received); 104 | if (received != msglen) { 105 | // Handle incomplete message. 106 | NYMPH_LOG_WARNING("Incomplete message: " + NumberFormatter::format(received) + " of " + NumberFormatter::format(msglen)); 107 | 108 | // Loop until the rest of the message has been received. 109 | // TODO: Set a maximum number of loops/timeout? Reset when 110 | // receiving data, timeout when poll times out N times? 111 | //binMsg->reserve(msglen); 112 | int unread = msglen - received; 113 | while (1) { 114 | if (socket->poll(timeout, Net::Socket::SELECT_READ)) { 115 | char* buff1 = new char[unread]; 116 | received = socket->receiveBytes((void*) buff1, unread); 117 | if (received == 0) { 118 | // Remote disconnnected. Socket should be discarded. 119 | NYMPH_LOG_INFORMATION("Received remote disconnected notice. Terminating listener thread."); 120 | delete[] buff1; 121 | break; 122 | } 123 | else if (received != unread) { 124 | binMsg.append((const char*) buff1, received); 125 | delete[] buff1; 126 | unread -= received; 127 | NYMPH_LOG_WARNING("Incomplete message: " + NumberFormatter::format(unread) + "/" + NumberFormatter::format(msglen) + " unread."); 128 | continue; 129 | } 130 | 131 | // Full message was read. Continue with processing. 132 | binMsg.append((const char*) buff1, received); 133 | delete[] buff1; 134 | break; 135 | } // if 136 | } //while 137 | } 138 | else { 139 | NYMPH_LOG_DEBUG("Read 0x" + NumberFormatter::formatHex(received) + " bytes."); 140 | } 141 | 142 | delete[] buff; 143 | } 144 | else { 145 | // 146 | binMsg.append(headerBuff, idx); 147 | } 148 | 149 | // Parse the string into an NmqttMessage instance. 150 | msg.parseMessage(binMsg); 151 | 152 | NYMPH_LOG_DEBUG("Got command: 0x" + Poco::NumberFormatter::formatHex(msg.getCommand())); 153 | 154 | // Call the message handler callback when one exists for this type of message. 155 | // TODO: refactor for Dispatcher. 156 | Request* req = new Request; 157 | req->setMessage(nymphSocket->handle, msg); 158 | Dispatcher::addRequest(req); 159 | } 160 | 161 | // Check whether we're still initialising. 162 | if (init) { 163 | // Signal that this listener thread is ready. 164 | readyMutex->lock(); 165 | readyCond->signal(); 166 | readyMutex->unlock(); 167 | 168 | timeout.assign(1, 0); // Change timeout to 1 second. 169 | init = false; 170 | } 171 | } 172 | 173 | NYMPH_LOG_INFORMATION("Stopping thread..."); 174 | 175 | // Clean-up. 176 | delete readyCond; 177 | delete readyMutex; 178 | nymphSocket->semaphore->wait(); // Wait for the connection to be closed. 179 | delete socket; 180 | delete nymphSocket->semaphore; 181 | nymphSocket->semaphore = 0; 182 | delete this; // Call the destructor ourselves. 183 | } 184 | 185 | 186 | // --- STOP --- 187 | void NmqttClientListener::stop() { 188 | listen = false; 189 | } 190 | 191 | -------------------------------------------------------------------------------- /src/cpp/client_listener.h: -------------------------------------------------------------------------------- 1 | /* 2 | client_listener.h - Header for the NymphMQTT Client socket listening thread class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - Socket listener for MQTT connections. 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/08 - Maya Posch 13 | */ 14 | 15 | 16 | #ifndef NMQTT_CLIENT_LISTENER_H 17 | #define NMQTT_CLIENT_LISTENER_H 18 | 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include "client.h" 26 | #include "message.h" 27 | #include "connections.h" 28 | 29 | #include 30 | #include 31 | 32 | 33 | class NmqttClientListener : public Poco::Runnable { 34 | std::string loggerName; 35 | bool listen; 36 | NymphSocket* nymphSocket; 37 | Poco::Net::StreamSocket* socket; 38 | bool init; 39 | Poco::Condition* readyCond; 40 | Poco::Mutex* readyMutex; 41 | 42 | public: 43 | NmqttClientListener(int handle , Poco::Condition* cond, Poco::Mutex* mtx); 44 | ~NmqttClientListener(); 45 | void run(); 46 | void stop(); 47 | }; 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /src/cpp/client_listener_manager.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | client_listener_manager.cpp - Declares the NymphMQTT Listener Manager class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - 8 | 9 | History: 10 | 2019/06/24, Maya Posch : Initial version. 11 | 12 | (c) Nyanko.ws 13 | */ 14 | 15 | 16 | #include "client_listener_manager.h" 17 | #include "client_listener.h" 18 | #include "nymph_logger.h" 19 | 20 | #include 21 | #include 22 | 23 | using namespace std; 24 | 25 | #include 26 | #include 27 | #include 28 | 29 | using namespace Poco; 30 | 31 | 32 | // Static initialisations. 33 | map NmqttClientListenerManager::listeners; 34 | Mutex NmqttClientListenerManager::listenersMutex; 35 | string NmqttClientListenerManager::loggerName = "NmqttClientListenerManager"; 36 | 37 | 38 | // --- STOP --- 39 | void NmqttClientListenerManager::stop() { 40 | // Shut down all listening threads. 41 | listenersMutex.lock(); 42 | for (int i = 0; i < listeners.size(); ++i) { 43 | listeners[i]->stop(); 44 | } 45 | 46 | listenersMutex.unlock(); 47 | } 48 | 49 | 50 | // --- ADD CONNECTION --- 51 | bool NmqttClientListenerManager::addConnection(int handle) { 52 | NYMPH_LOG_INFORMATION("Adding connection. Handle: " + NumberFormatter::format(handle) + "."); 53 | 54 | // Create new thread for NmqttListener instance which handles 55 | // the new socket. Save reference to this listener. 56 | Poco::Condition* cnd = new Poco::Condition; 57 | Poco::Mutex* mtx = new Poco::Mutex; 58 | long timeout = 1000; // 1 second 59 | mtx->lock(); 60 | NmqttClientListener* esl = new NmqttClientListener(handle, cnd, mtx); 61 | Poco::Thread* thread = new Poco::Thread; 62 | thread->start(*esl); 63 | if (!cnd->tryWait(*mtx, timeout)) { 64 | // Handle listener timeout. 65 | NYMPH_LOG_ERROR("Creating of new listener thread timed out."); 66 | mtx->unlock(); 67 | return false; 68 | } 69 | 70 | mtx->unlock(); 71 | 72 | listenersMutex.lock(); 73 | listeners.insert(std::pair(handle, esl)); 74 | listenersMutex.unlock(); 75 | 76 | NYMPH_LOG_INFORMATION("Listening socket has been added."); 77 | 78 | return true; 79 | } 80 | 81 | 82 | // --- REMOVE CONNECTION --- 83 | // Removes a connection using the Nymph connection handle. 84 | bool NmqttClientListenerManager::removeConnection(int handle) { 85 | map::iterator it; 86 | listenersMutex.lock(); 87 | 88 | NYMPH_LOG_INFORMATION("Removing connection for handle: " + NumberFormatter::format(handle) + "."); 89 | 90 | it = listeners.find(handle); 91 | if (it == listeners.end()) { listenersMutex.unlock(); return true; } 92 | 93 | it->second->stop(); // Tell the listening thread to terminate. 94 | listeners.erase(it); 95 | 96 | NYMPH_LOG_INFORMATION("Listening socket has been removed."); 97 | 98 | listenersMutex.unlock(); 99 | return true; 100 | } 101 | 102 | -------------------------------------------------------------------------------- /src/cpp/client_listener_manager.h: -------------------------------------------------------------------------------- 1 | /* 2 | client_listener_manager.h - Declares the NymphMQTT Listener Manager class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - 8 | 9 | History: 10 | 2019/06/24, Maya Posch : Initial version. 11 | 12 | (c) Nyanko.ws 13 | */ 14 | 15 | 16 | #ifndef NMQTT_CLIENT_LISTENER_MANAGER_H 17 | #define NMQTT_CLIENT_LISTENER_MANAGER_H 18 | 19 | 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | #include "client_listener.h" 26 | 27 | 28 | class NmqttClientListenerManager { 29 | static std::map listeners; 30 | static Poco::Mutex listenersMutex; 31 | static std::string loggerName; 32 | 33 | public: 34 | static void stop(); 35 | 36 | static bool addConnection(int handle); 37 | static bool removeConnection(int handle); 38 | }; 39 | 40 | 41 | #endif 42 | -------------------------------------------------------------------------------- /src/cpp/connections.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | connections.cpp - Impplementation of the NymphMQTT Connections class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - Static class to enable the global management of connections. 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/08 - Maya Posch 13 | */ 14 | 15 | 16 | #include "connections.h" 17 | 18 | 19 | // Static declarations. 20 | std::map NmqttConnections::sockets; 21 | 22 | 23 | void NmqttConnections::addSocket(NymphSocket &ns) { 24 | sockets.insert(std::pair(ns.handle, ns)); 25 | } 26 | 27 | 28 | // --- GET SOCKET --- 29 | NymphSocket* NmqttConnections::getSocket(int handle) { 30 | std::map::iterator it; 31 | it = sockets.find(handle); 32 | if (it == sockets.end()) { 33 | return 0; 34 | } 35 | 36 | return &it->second; 37 | } 38 | -------------------------------------------------------------------------------- /src/cpp/connections.h: -------------------------------------------------------------------------------- 1 | /* 2 | connections.h - Header for the NymphMQTT Connections class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - Static class to enable the global management of connections. 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/08 - Maya Posch 13 | */ 14 | 15 | 16 | #ifndef NMQTT_CONNECTIONS_H 17 | #define NMQTT_CONNECTIONS_H 18 | 19 | 20 | #include 21 | #include 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | #include "client.h" 28 | 29 | 30 | // TYPES 31 | struct NymphSocket { 32 | bool secure; // Are using an SSL/TLS connection or not? 33 | //Poco::Net::SecureStreamSocket* ssocket; // Pointer to a secure socket instance. 34 | Poco::Net::Context::Ptr context; // The security context for TLS connections. 35 | Poco::Net::StreamSocket* socket; // Pointer to a non-secure socket instance. 36 | Poco::Semaphore* semaphore; // Signals when it's safe to delete the socket. 37 | std::function handler; // Publish message handler. 38 | std::function connackHandler; // CONNACK handler. 39 | std::function pingrespHandler; // PINGRESP handler. 40 | void* data; // User data. 41 | int handle; // The Nymph internal socket handle. 42 | }; 43 | 44 | 45 | class NmqttConnections { 46 | static std::map sockets; 47 | 48 | public: 49 | static void addSocket(NymphSocket &ns); 50 | static NymphSocket* getSocket(int handle); 51 | }; 52 | 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /src/cpp/dispatcher.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | dispatcher.cpp - Implementation of the Dispatcher class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - 8 | 9 | Notes: 10 | - 11 | 12 | 2016/11/19, Maya Posch 13 | (c) Nyanko.ws 14 | */ 15 | 16 | 17 | #include "dispatcher.h" 18 | 19 | #include 20 | using namespace std; 21 | 22 | 23 | // Static initialisations. 24 | queue Dispatcher::requests; 25 | queue Dispatcher::workers; 26 | mutex Dispatcher::requestsMutex; 27 | mutex Dispatcher::workersMutex; 28 | vector Dispatcher::allWorkers; 29 | vector Dispatcher::threads; 30 | 31 | 32 | // --- INIT --- 33 | // Start the number of requested worker threads. 34 | bool Dispatcher::init(int workers) { 35 | thread* t = 0; 36 | Worker* w = 0; 37 | for (int i = 0; i < workers; ++i) { 38 | w = new Worker; 39 | allWorkers.push_back(w); 40 | t = new thread(&Worker::run, w); 41 | threads.push_back(t); 42 | } 43 | 44 | return true; 45 | } 46 | 47 | 48 | // --- STOP --- 49 | // Terminate the worker threads and clean up. 50 | bool Dispatcher::stop() { 51 | for (int i = 0; i < allWorkers.size(); ++i) { 52 | allWorkers[i]->stop(); 53 | } 54 | 55 | cout << "Stopped workers.\n"; 56 | 57 | for (int j = 0; j < threads.size(); ++j) { 58 | threads[j]->join(); 59 | delete threads[j]; 60 | 61 | cout << "Joined threads.\n"; 62 | } 63 | 64 | return true; 65 | } 66 | 67 | 68 | // --- ADD REQUEST --- 69 | void Dispatcher::addRequest(AbstractRequest* request) { 70 | // Check whether there's a worker available in the workers queue, else add 71 | // the request to the requests queue. 72 | workersMutex.lock(); 73 | if (!workers.empty()) { 74 | Worker* worker = workers.front(); 75 | worker->setRequest(request); 76 | condition_variable* cv; 77 | mutex* mtx; 78 | worker->getCondition(cv); 79 | worker->getMutex(mtx); 80 | unique_lock lock(*mtx); 81 | cv->notify_one(); 82 | workers.pop(); 83 | workersMutex.unlock(); 84 | } 85 | else { 86 | workersMutex.unlock(); 87 | requestsMutex.lock(); 88 | requests.push(request); 89 | requestsMutex.unlock(); 90 | } 91 | 92 | 93 | } 94 | 95 | 96 | // --- ADD WORKER --- 97 | bool Dispatcher::addWorker(Worker* worker) { 98 | // If a request is waiting in the requests queue, assign it to the worker. 99 | // Else add the worker to the workers queue. 100 | // Returns true if the worker was added to the queue and has to wait for 101 | // its condition variable. 102 | bool wait = true; 103 | requestsMutex.lock(); 104 | if (!requests.empty()) { 105 | AbstractRequest* request = requests.front(); 106 | worker->setRequest(request); 107 | requests.pop(); 108 | wait = false; 109 | requestsMutex.unlock(); 110 | } 111 | else { 112 | requestsMutex.unlock(); 113 | workersMutex.lock(); 114 | workers.push(worker); 115 | workersMutex.unlock(); 116 | } 117 | 118 | return wait; 119 | } 120 | -------------------------------------------------------------------------------- /src/cpp/dispatcher.h: -------------------------------------------------------------------------------- 1 | /* 2 | dispatcher.h - header file for the Dispatcher class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - 8 | 9 | 2016/11/19, Maya Posch 10 | (c) Nyanko.ws. 11 | */ 12 | 13 | 14 | #pragma once 15 | #ifndef DISPATCHER_H 16 | #define DISPATCHER_H 17 | 18 | #include "abstract_request.h" 19 | #include "worker.h" 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | using namespace std; 27 | 28 | 29 | class Dispatcher { 30 | static queue requests; 31 | static queue workers; 32 | static mutex requestsMutex; 33 | static mutex workersMutex; 34 | static vector allWorkers; 35 | static vector threads; 36 | 37 | public: 38 | static bool init(int workers); 39 | static bool stop(); 40 | static void addRequest(AbstractRequest* request); 41 | static bool addWorker(Worker* worker); 42 | }; 43 | 44 | #endif 45 | -------------------------------------------------------------------------------- /src/cpp/message.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | message.cpp - Implementation for the NymphMQTT message class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - 8 | 9 | Notes: 10 | - An MQTT message always consists out of three sections: 11 | - Fixed header 12 | - Variable header (some messages) 13 | - Payload 14 | 15 | - Here the payload is just a binary string, only defined by a length parameter. 16 | - The fixed header defines the message type and flags specific to the type, along with 17 | the length of the message. 18 | - The variable header 19 | 20 | 2019/05/08 - Maya Posch 21 | */ 22 | 23 | 24 | #include "message.h" 25 | #include "nymph_logger.h" 26 | 27 | #include 28 | 29 | #include 30 | 31 | // debug 32 | //#define DEBUG 1 33 | #ifdef DEBUG 34 | #endif 35 | 36 | 37 | // --- CONSTRUCTOR --- 38 | // Default. Create empty message. 39 | NmqttMessage::NmqttMessage() { 40 | // 41 | } 42 | 43 | 44 | // Create new message with command. 45 | // Sets the invalid state if setting the command failed. 46 | NmqttMessage::NmqttMessage(MqttPacketType type) { 47 | if (!createMessage(type)) { parseGood = false; } 48 | } 49 | 50 | 51 | // Parses the provided binary message, setting the appropriate internal variables and status flags 52 | // for reading out with other API functions. 53 | NmqttMessage::NmqttMessage(std::string msg) { 54 | parseMessage(msg); 55 | } 56 | 57 | 58 | // --- DECONSTRUCTOR --- 59 | NmqttMessage::~NmqttMessage() { 60 | // 61 | } 62 | 63 | 64 | // --- SET CREDENTIALS --- 65 | void NmqttMessage::setCredentials(std::string &user, std::string &pass) { 66 | username = user; 67 | password = pass; 68 | usernameFlag = true; 69 | passwordFlag = true; 70 | } 71 | 72 | 73 | // --- SET WILL --- 74 | void NmqttMessage::setWill(std::string topic, std::string will, uint8_t qos, bool retain) { 75 | this->will = will; 76 | this->willTopic = topic; 77 | this->willRetainFlag = retain; 78 | this->willQoS = qos; 79 | this->willFlag = true; 80 | if (qos == 1) { 81 | willQoS1 = true; 82 | } 83 | else if (qos == 2) { 84 | willQoS2 = true; 85 | } 86 | } 87 | 88 | 89 | // --- CREATE MESSAGE --- 90 | // Set the command type. Returns false if the command type is invalid, for example when the current 91 | // protocol version does not support it. 92 | bool NmqttMessage::createMessage(MqttPacketType type) { 93 | if (type == MQTT_AUTH && mqttVersion == MQTT_PROTOCOL_VERSION_4) { return false; } 94 | 95 | if (type == MQTT_CONNECT) { 96 | // Set default connect options. 97 | cleanSessionFlag = true; 98 | willFlag = false; 99 | willRetainFlag = false; 100 | usernameFlag = false; 101 | passwordFlag = false; 102 | willQoS1 = false; 103 | willQoS2 = false; 104 | } 105 | 106 | command = type; 107 | 108 | return true; 109 | } 110 | 111 | 112 | // --- PARSE MESSAGE --- 113 | int NmqttMessage::parseMessage(std::string msg) { 114 | // Set initial flags. 115 | parseGood = false; 116 | 117 | int idx = 0; 118 | 119 | // Start by reading the fixed header, determining which command we're dealing with and the 120 | // remaining message length. 121 | // The message should be at least two bytes long (fixed header). 122 | if (msg.size() < 2) { 123 | // TODO: Return error. 124 | return -1; 125 | } 126 | 127 | // Debug 128 | #ifdef DEBUG 129 | std::cout << "Message(0): " << std::hex << (uint32_t) msg[0] << std::endl; 130 | std::cout << "Message(1): " << std::hex << (uint32_t) msg[1] << std::endl; 131 | std::cout << "Length: " << msg.length() << std::endl; 132 | #endif 133 | 134 | // Read out the first byte. Mask bits 0-3 as they're not used here. 135 | // Also read out the DUP, QoS and Retain flags. 136 | uint8_t byte0 = static_cast(msg[0]); 137 | command = (MqttPacketType) (byte0 & 0xF0); // TODO: validate range. 138 | duplicateMessage = (byte0 >> 3) & 1U; 139 | uint8_t qosCnt = 0; 140 | if ((byte0 >> 2) & 1U) { QoS = MQTT_QOS_AT_LEAST_ONCE; qosCnt++; } 141 | if ((byte0 >> 1) & 1U) { QoS = MQTT_QOS_EXACTLY_ONCE; qosCnt++; } 142 | if (qosCnt > 1) { QoS = MQTT_QOS_AT_MOST_ONCE; } 143 | retainMessage = byte0 & 1U; 144 | idx++; 145 | 146 | // Debug 147 | #ifdef DEBUG 148 | std::cout << "Found command: 0x" << std::hex << command << std::endl; 149 | #endif 150 | 151 | // Get the message length decoded using ByteBauble's method. 152 | uint32_t pInt = (uint32_t) msg[1]; 153 | int pblen = bytebauble.readPackedInt(pInt, messageLength); 154 | idx += pblen; 155 | 156 | // debug 157 | #ifdef DEBUG 158 | std::cout << "Variable integer: " << std::hex << pInt << std::endl; 159 | std::cout << "Packed integer length: " << pblen << " bytes." << std::endl; 160 | std::cout << "Message length: " << messageLength << std::endl; 161 | #endif 162 | 163 | if (pblen + 1 + messageLength != msg.length()) { 164 | // Return error. 165 | std::cerr << "Message length was wrong: expected " << pblen + 1 + messageLength 166 | << ", got: " << msg.length() << std::endl; 167 | return -1; 168 | } 169 | 170 | // Read the variable header (if present). 171 | switch (command) { 172 | case MQTT_CONNECT: { 173 | // Server. 174 | // Decode variable header. 175 | // First field: protocol name '0x00 0x04 M Q T T'. 176 | std::string protName = msg.substr(idx, 6); 177 | std::string protMatch = { 0x00, 0x04, 'M', 'Q', 'T', 'T' }; 178 | if (protName != protMatch) { 179 | std::cerr << "CONNECT protocol name incorrect, got: " << protName << std::endl; 180 | return -1; 181 | } 182 | 183 | idx += 6; 184 | 185 | // Protocol level: one byte. 186 | uint8_t protver = (uint8_t) msg[idx++]; 187 | if (protver == 4) { mqttVersion = MQTT_PROTOCOL_VERSION_4; } 188 | else if (protver == 5) { mqttVersion == MQTT_PROTOCOL_VERSION_5; } 189 | else { 190 | std::cerr << "Invalid MQTT version: " << (uint16_t) protver << std::endl; 191 | // FIXME: Server must return CONNACK with code 0x01 in this case. 192 | return -1; 193 | } 194 | 195 | // Connect flags: one byte. 196 | uint8_t connflags = (uint8_t) msg[idx++]; 197 | usernameFlag = (connflags >> 7) & 1U; 198 | passwordFlag = (connflags >> 6) & 1U; 199 | willRetainFlag = (connflags >> 5) & 1U; 200 | willQoS = (((connflags >> 4) & 1U) << 1) + (connflags >> 3) & 1U; 201 | willFlag = (connflags >> 2) & 1U; 202 | cleanSessionFlag = (connflags >> 1) & 1U; 203 | if (connflags & 1U) { 204 | std::cerr << "Reserved flag in connect flags set. Aborting parse." << std::endl; 205 | return -1; 206 | } 207 | 208 | // Keep alive value: two bytes. 209 | keepAlive = (uint16_t) msg[idx]; 210 | idx += 2; 211 | 212 | // Payload section. 213 | // Client ID. UTF-8 string, preceded by two bytes (MSB, LSB) with the length. 214 | // TODO: convert BE length int to host int. 215 | uint16_t len = (uint16_t) msg[idx]; 216 | idx += 2; 217 | clientId = msg.substr(idx, len); 218 | idx += len; 219 | 220 | if (willFlag) { 221 | uint16_t len = (uint16_t) msg[idx]; 222 | idx += 2; 223 | willTopic = msg.substr(idx, len); 224 | idx += len; 225 | 226 | len = (uint16_t) msg[idx]; 227 | idx += 2; 228 | will = msg.substr(idx, len); 229 | idx += len; 230 | } 231 | 232 | if (usernameFlag) { 233 | uint16_t len = (uint16_t) msg[idx]; 234 | idx += 2; 235 | username = msg.substr(idx, len); 236 | idx += len; 237 | } 238 | 239 | if (passwordFlag) { 240 | uint16_t len = (uint16_t) msg[idx]; 241 | idx += 2; 242 | password = msg.substr(idx, len); 243 | idx += len; 244 | } 245 | } 246 | 247 | break; 248 | case MQTT_CONNACK: { 249 | NYMPH_LOG_INFORMATION("Received CONNACK message."); 250 | 251 | // Basic parse: just get the variable header up till the reason code. 252 | // For MQTT 5 we also need to read out the properties that follow after the reason code. 253 | sessionPresent = msg[idx++]; 254 | reasonCode = (MqttReasonCodes) msg[idx++]; 255 | } 256 | 257 | break; 258 | case MQTT_PUBLISH: { 259 | NYMPH_LOG_INFORMATION("Received PUBLISH message."); 260 | 261 | // Expect just the topic length (two bytes) and the topic string. 262 | // UTF-8 strings in MQTT have a big-endian, two-byte length header. 263 | uint16_t lenBE = *((uint16_t*) &msg[idx]); 264 | 265 | // Debug 266 | #ifdef DEBUG 267 | std::cout << "String length (BE): 0x" << std::hex << lenBE << std::endl; 268 | #endif 269 | 270 | uint16_t strlen = bytebauble.toHost(lenBE, BB_BE); 271 | idx += 2; 272 | topic = msg.substr(idx, strlen); 273 | 274 | // Debug 275 | #ifdef DEBUG 276 | std::cout << "Strlen: " << strlen << ", topic: " << topic << std::endl; 277 | #endif 278 | 279 | idx += strlen; 280 | 281 | // Handle QoS 1+ here. 282 | // Parse out the two bytes containing the packet identifier. This is in BE format 283 | // (MSB/LSB). 284 | if (qosCnt > 0) { 285 | uint16_t pIDBE = *((uint16_t*) &msg[idx]); 286 | packetID = bytebauble.toHost(pIDBE, BB_BE); 287 | idx += 2; 288 | } 289 | 290 | if (mqttVersion == MQTT_PROTOCOL_VERSION_5) { 291 | // MQTT 5: Expect no properties here (0x00). 292 | 293 | // Debug 294 | #ifdef DEBUG 295 | std::cout << "Index for properties: " << idx << std::endl; 296 | #endif 297 | 298 | uint8_t properties = msg[idx++]; 299 | if (properties != 0x00) { 300 | std::cerr << "Expected no properties. Got: " << (int) properties << std::endl; 301 | } 302 | } 303 | 304 | // Read the payload. This is the remaining section of the message (if any). 305 | if (idx + 1 != msg.length()) { 306 | payload = msg.substr(idx); 307 | } 308 | } 309 | 310 | break; 311 | case MQTT_PUBACK: { 312 | NYMPH_LOG_INFORMATION("Received PUBACK message."); 313 | 314 | // 315 | } 316 | 317 | break; 318 | case MQTT_PUBREC: { 319 | // 320 | } 321 | 322 | break; 323 | case MQTT_PUBREL: { 324 | // 325 | } 326 | 327 | break; 328 | case MQTT_PUBCOMP: { 329 | // 330 | } 331 | 332 | break; 333 | case MQTT_SUBSCRIBE: { 334 | // Server. 335 | // TODO: implement. 336 | // 337 | } 338 | 339 | break; 340 | case MQTT_SUBACK: { 341 | NYMPH_LOG_INFORMATION("Received SUBACK message."); 342 | 343 | // 344 | } 345 | 346 | break; 347 | case MQTT_UNSUBSCRIBE: { 348 | // Server. 349 | // TODO: implement. 350 | // 351 | } 352 | 353 | break; 354 | case MQTT_UNSUBACK: { 355 | // Client. 356 | // 357 | NYMPH_LOG_INFORMATION("Received UNSUBACK message."); 358 | } 359 | 360 | break; 361 | case MQTT_PINGREQ: { 362 | // Server. 363 | // No variable header or payload. 364 | } 365 | 366 | break; 367 | case MQTT_PINGRESP: { 368 | NYMPH_LOG_INFORMATION("Received PINGRESP message."); 369 | 370 | // A Ping response has no variable header and no payload. 371 | } 372 | 373 | break; 374 | case MQTT_DISCONNECT: { 375 | NYMPH_LOG_INFORMATION("Received DISCONNECT message."); 376 | 377 | // A disconnect message has no variable header and no payload. 378 | } 379 | 380 | break; 381 | case MQTT_AUTH: { 382 | // MQTT 5. 383 | } 384 | 385 | break; 386 | }; 387 | 388 | // Set new flags. 389 | parseGood = true; 390 | return 1; 391 | } 392 | 393 | 394 | // --- PARSE HEADER --- 395 | // Returns a code to indicate whether the provided buffer contains a full MQTT fixed header section. 396 | // Provides the parsed remaining message length and current index into the buffer where the section 397 | // after the fixed header starts. 398 | // 399 | // Return codes: 400 | // * (-1) corrupted data. 401 | // * (0) more bytes needed. 402 | // * (1) successful parse. 403 | int NmqttMessage::parseHeader(char* buff, int len, uint32_t &msglen, int& idx) { 404 | // Assume the first byte is fine. This will be handled later during the full message parsing. 405 | // Second byte starts the variable byte integer. Check up to four bytes whether they have 406 | // bit 7 set. First three bytes means another byte follows, fourth byte means corrupted data. 407 | if (len < 2) { return -1; } 408 | 409 | idx = 1; 410 | 411 | if ((buff[idx++] >> 7) & 1UL) { 412 | if (len < 3) { return 0; } 413 | 414 | if ((buff[idx++] >> 7) & 1UL) { 415 | if (len < 4) { return 0; } 416 | 417 | if ((buff[idx++] >> 7) & 1UL) { 418 | if (len < 5) { return 0; } 419 | 420 | if ((buff[idx++] >> 7) & 1UL) { 421 | return -1; // Special bit on final byte should never be set. 422 | } 423 | } 424 | } 425 | 426 | } 427 | 428 | // Use ByteBauble to decode this variable byte integer. 429 | uint32_t pInt = *(uint32_t*) &buff[1]; // FIXME: limit? 430 | idx = ByteBauble::readPackedInt(pInt, msglen); 431 | idx++; 432 | 433 | return 1; 434 | } 435 | 436 | 437 | // --- SERIALIZE --- 438 | std::string NmqttMessage::serialize() { 439 | // First byte contains the command and any flags. 440 | // Second byte is a Variable Byte, indicating the length of the rest of the message. 441 | // We'll fill this one in later. 442 | 443 | // Next is the optional (variable) header section. 444 | // This is present for these types: 445 | // 446 | // PUBLISH Required 447 | 448 | // Finally the payload section. This is present for the following message types: 449 | // CONNECT Required 450 | // PUBLISH Optional 451 | // SUBSCRIBE Required 452 | // SUBACK Required 453 | // UNSUBSCRIBE Required 454 | // UNSUBACK Required 455 | uint8_t b0 = command; 456 | std::string varHeader; 457 | bytebauble.setGlobalEndianness(BB_BE); 458 | switch (command) { 459 | case MQTT_CONNECT: { 460 | // Fixed header has no flags. 461 | 462 | // The Variable Header for the CONNECT Packet contains the following fields in this 463 | // order: 464 | // * Protocol Name, 465 | // * Protocol Level, 466 | // * Connect Flags, 467 | // * Keep Alive, and 468 | // * Properties. (MQTT 5) 469 | bytebauble.setGlobalEndianness(BB_BE); 470 | uint16_t protNameLenHost = 0x0004; 471 | uint16_t protNameLenBE = bytebauble.toGlobal(protNameLenHost, bytebauble.getHostEndian()); 472 | varHeader.append((char*) &protNameLenBE, 2); 473 | varHeader += "MQTT"; // The fixed protocol name. 474 | 475 | uint8_t protVersion; 476 | if (mqttVersion == MQTT_PROTOCOL_VERSION_5) { 477 | protVersion = 5; 478 | } 479 | else { 480 | protVersion = 4; // Protocol version default is 4 (3.1.1). 481 | } 482 | 483 | varHeader.append((char*) &protVersion, 1); 484 | 485 | uint8_t connectFlags = 0; 486 | if (cleanSessionFlag) { connectFlags += (uint8_t) MQTT_CONNECT_CLEAN_START; } 487 | if (willFlag) { connectFlags += (uint8_t) MQTT_CONNECT_WILL; } 488 | if (willQoS1) { connectFlags += (uint8_t) MQTT_CONNECT_WILL_QOS_L1; } 489 | if (willQoS2) { connectFlags += (uint8_t) MQTT_CONNECT_WILL_QOS_L2; } 490 | if (willRetainFlag) { connectFlags += (uint8_t) MQTT_CONNECT_WILL_RETAIN; } 491 | if (passwordFlag) { connectFlags += (uint8_t) MQTT_CONNECT_PASSWORD; } 492 | if (usernameFlag) { connectFlags += (uint8_t) MQTT_CONNECT_USERNAME; } 493 | varHeader.append((char*) &connectFlags, 1); 494 | 495 | uint16_t keepAliveHost = 60; // In seconds. 496 | uint16_t keepAliveBE = bytebauble.toGlobal(keepAliveHost, bytebauble.getHostEndian()); 497 | varHeader.append((char*) &keepAliveBE, 2); 498 | 499 | if (mqttVersion == MQTT_PROTOCOL_VERSION_5) { 500 | uint8_t propertiesLen = 0x05; 501 | uint8_t sessionExpIntervalId = 0x11; 502 | uint32_t sessionExpInterval = 0x0A; 503 | varHeader.append((char*) &propertiesLen, 1); 504 | varHeader.append((char*) &sessionExpIntervalId, 1); 505 | varHeader.append((char*) &sessionExpInterval, 4); 506 | } 507 | 508 | // The payload section depends on previously set flags. 509 | // These fields, if present, MUST appear in the order Client Identifier, 510 | // Will Properties, Will Topic, Will Payload, User Name, Password. 511 | 512 | // UTF8-encoded string with 16-bit uint BE header indicating string length. 513 | uint16_t clientIdLenHost = clientId.length(); 514 | uint16_t clientIdLenBE = bytebauble.toGlobal(clientIdLenHost, bytebauble.getHostEndian()); 515 | payload.append((char*) &clientIdLenBE, 2); 516 | payload += clientId; 517 | 518 | // Will properties (MQTT 5), will topic, will payload. 519 | if (willFlag) { 520 | uint16_t willTopicLenBE = bytebauble.toGlobal(willTopic.length(), 521 | bytebauble.getHostEndian()); 522 | payload.append((char*) &willTopicLenBE, 2); 523 | payload += willTopic; 524 | 525 | uint16_t willLenBE = bytebauble.toGlobal(will.length(), bytebauble.getHostEndian()); 526 | payload.append((char*) &willLenBE, 2); 527 | payload += will; 528 | } 529 | 530 | // Username, password. 531 | if (usernameFlag) { 532 | uint16_t usernameLenBE = bytebauble.toGlobal(willTopic.length(), 533 | bytebauble.getHostEndian()); 534 | payload.append((char*) &usernameLenBE, 2); 535 | payload += username; 536 | } 537 | 538 | if (passwordFlag) { 539 | uint16_t passwordLenBE = bytebauble.toGlobal(password.length(), bytebauble.getHostEndian()); 540 | payload.append((char*) &passwordLenBE, 2); 541 | payload += password; 542 | } 543 | } 544 | 545 | break; 546 | case MQTT_CONNACK: { 547 | // Fixed header has no flags. 548 | 549 | // The Variable Header of the CONNACK Packet contains the following fields in the order: 550 | // * Connect Acknowledge Flags 551 | // * Connect Reason Code 552 | // * Properties. 553 | 554 | // Connect acknowledge flags. 1 byte. Bits 1-7 are reserved and set to 0. 555 | // Bit 0 is the session present flag. It's set to 0 if no existing session exists, or 556 | // the clean session flag was set in the Connect message. 557 | uint8_t connAckFlags = 0x0; // TODO: allow setting of this property. 558 | varHeader.append((char*) &connAckFlags, 1); 559 | 560 | // Connect reason code. 561 | // Single byte indicating the result of the connection attempt. 562 | uint8_t connRes = 0; // TODO: make settable. 563 | varHeader.append((char*) &connRes, 1); 564 | 565 | // Properties. 566 | // TODO: implement. 567 | } 568 | 569 | break; 570 | case MQTT_PUBLISH: { 571 | // Add flags as required. 572 | if (duplicateMessage) { b0 += 8; } 573 | if (QoS == MQTT_QOS_AT_LEAST_ONCE) { b0 += 2; } 574 | if (QoS == MQTT_QOS_EXACTLY_ONCE) { b0 += 4; } 575 | if (retainMessage) { b0 += 1; } 576 | 577 | // Variable header. 578 | // Get the length of the topic, convert it to big endian format. 579 | uint16_t topLenHost = topic.length(); 580 | uint16_t topLenBE = bytebauble.toGlobal(topLenHost, bytebauble.getHostEndian()); 581 | 582 | varHeader.append((char*) &topLenBE, 2); 583 | varHeader += topic; 584 | 585 | // Add packet identifier if QoS > 0. 586 | // TODO: keep track of unused IDs. 587 | if (QoS != MQTT_QOS_AT_MOST_ONCE) { 588 | uint16_t pIDBE = bytebauble.toGlobal(packetID, bytebauble.getHostEndian()); 589 | varHeader.append((char*) &pIDBE, 2); 590 | } 591 | 592 | // Set properties. 593 | // TODO: implement. Set to 0 for now. 594 | if (mqttVersion == MQTT_PROTOCOL_VERSION_5) { 595 | uint8_t properties = 0x00; 596 | varHeader.append((char*) &properties, 1); 597 | } 598 | } 599 | 600 | break; 601 | case MQTT_PUBACK: { 602 | // 603 | } 604 | 605 | break; 606 | case MQTT_PUBREC: { 607 | // 608 | } 609 | 610 | break; 611 | case MQTT_PUBREL: { 612 | // 613 | } 614 | 615 | break; 616 | case MQTT_PUBCOMP: { 617 | // 618 | } 619 | 620 | break; 621 | case MQTT_SUBSCRIBE: { 622 | // Fixed header has one required value: 0x2. 623 | b0 += 0x2; 624 | 625 | // Variable header. 626 | uint16_t packetIdHost = 10; 627 | uint16_t packetIdBE = bytebauble.toGlobal(packetIdHost, bytebauble.getHostEndian()); 628 | varHeader.append((char*) &packetIdBE, 2); 629 | 630 | if (mqttVersion == MQTT_PROTOCOL_VERSION_5) { 631 | uint8_t propLength = 0; 632 | varHeader.append((char*) &propLength, 1); 633 | } 634 | 635 | // Payload. 636 | uint16_t topicLenHost = topic.length(); 637 | uint16_t topicLenBE = bytebauble.toGlobal(topicLenHost, bytebauble.getHostEndian()); 638 | payload.append((char*) &topicLenBE, 2); 639 | payload += topic; 640 | 641 | // Subscribe flags. 642 | // TODO: implement settability. 643 | uint8_t subFlags = 0; 644 | payload.append((char*) &subFlags, 1); 645 | } 646 | 647 | break; 648 | case MQTT_SUBACK: { 649 | // 650 | } 651 | 652 | break; 653 | case MQTT_UNSUBSCRIBE: { 654 | // Client. 655 | // Fixed header has one required value: 0x2. 656 | b0 += 0x2; 657 | 658 | // Variable header. 659 | uint16_t packetIdHost = 10; 660 | uint16_t packetIdBE = bytebauble.toGlobal(packetIdHost, bytebauble.getHostEndian()); 661 | varHeader.append((char*) &packetIdBE, 2); 662 | 663 | // Set packet ID. 664 | // TODO: implement packet ID handling. 665 | uint16_t pIDBE = bytebauble.toGlobal(packetID, bytebauble.getHostEndian()); 666 | varHeader.append((char*) &pIDBE, 2); 667 | 668 | // Payload is the topic to unsubscribe from. 669 | uint16_t topicLenBE = bytebauble.toGlobal(topic.length(), bytebauble.getHostEndian()); 670 | payload.append((char*) &topicLenBE, 2); 671 | payload += topic; 672 | } 673 | 674 | break; 675 | case MQTT_UNSUBACK: { 676 | // 677 | } 678 | 679 | break; 680 | case MQTT_PINGREQ: { 681 | // Server. 682 | // This command has no variable header and no payload. 683 | // Generate a PINGRESP ping response packet to send back to the client. 684 | } 685 | 686 | break; 687 | case MQTT_PINGRESP: { 688 | // A ping response has no variable header and no payload. 689 | } 690 | 691 | break; 692 | case MQTT_DISCONNECT: { 693 | // A disconnect message has no variable header and no payload. 694 | } 695 | 696 | break; 697 | case MQTT_AUTH: { 698 | // MQTT 5. 699 | } 700 | 701 | break; 702 | }; 703 | 704 | // Calculate message length after the fixed header. Encode as packed integer. 705 | uint32_t msgLen = varHeader.length() + payload.length(); 706 | uint32_t msgLenPacked; 707 | uint32_t lenBytes = bytebauble.writePackedInt(msgLen, msgLenPacked); 708 | 709 | // Debug 710 | #ifdef DEBUG 711 | std::cout << "Message length: 0x" << std::hex << msgLen << std::endl; 712 | std::cout << "Message length (packed): 0x" << std::hex << msgLenPacked << std::endl; 713 | std::cout << "Message length bytes: 0x" << std::hex << lenBytes << std::endl; 714 | #endif 715 | 716 | std::string output; // TODO: preallocate size. 717 | output.append((char*) &b0, 1); 718 | output.append(((char*) &msgLenPacked), lenBytes); // FIXME: validate 719 | output += varHeader; 720 | output += payload; 721 | 722 | return output; 723 | 724 | } 725 | -------------------------------------------------------------------------------- /src/cpp/message.h: -------------------------------------------------------------------------------- 1 | /* 2 | message.h - Header for the NymphMQTT message class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/08 - Maya Posch 13 | */ 14 | 15 | 16 | #ifndef NMQTT_MESSAGE_H 17 | #define NMQTT_MESSAGE_H 18 | 19 | 20 | #include 21 | #include 22 | 23 | #include 24 | 25 | 26 | // MQTT version 27 | enum MqttProtocolVersion { 28 | MQTT_PROTOCOL_VERSION_4 = 0x04, 29 | MQTT_PROTOCOL_VERSION_5 = 0x05 30 | }; 31 | 32 | 33 | // Fixed header: first byte, bits [7-4]. 34 | enum MqttPacketType { 35 | MQTT_CONNECT = 0x10, 36 | MQTT_CONNACK = 0x20, 37 | MQTT_PUBLISH = 0x30, 38 | MQTT_PUBACK = 0x40, 39 | MQTT_PUBREC = 0x50, 40 | MQTT_PUBREL = 0x60, 41 | MQTT_PUBCOMP = 0x70, 42 | MQTT_SUBSCRIBE = 0x80, 43 | MQTT_SUBACK = 0x90, 44 | MQTT_UNSUBSCRIBE = 0xA0, 45 | MQTT_UNSUBACK = 0xB0, 46 | MQTT_PINGREQ = 0xC0, 47 | MQTT_PINGRESP = 0xD0, 48 | MQTT_DISCONNECT = 0xE0, 49 | MQTT_AUTH = 0xF0 // Since MQTT 5.0. 50 | }; 51 | 52 | 53 | // Fixed header: first byte, bits [3-0] 54 | // These are all required values ('reserved' in the MQTT 5.0 spec) without further explanation. 55 | // The Publish command is special, in that it sets the Duplicate, QoS and Retain flags. 56 | enum MqttPacketTypeFlags { 57 | MQTT_FLAGS_PUBREL = 0x02, 58 | MQTT_FLAGS_SUBSCRIBE = 0x02, 59 | MQTT_FLAGS_UNSUBSCRIBE = 0x02 60 | }; 61 | 62 | 63 | enum MqttQoS { 64 | MQTT_QOS_AT_MOST_ONCE = 0x0, 65 | MQTT_QOS_AT_LEAST_ONCE = 0x2, 66 | MQTT_QOS_EXACTLY_ONCE = 0x4 67 | }; 68 | 69 | 70 | enum MqttConnectFlags { 71 | MQTT_CONNECT_CLEAN_START = 0x02, 72 | MQTT_CONNECT_WILL = 0x04, 73 | MQTT_CONNECT_WILL_QOS_L1 = 0x08, 74 | MQTT_CONNECT_WILL_QOS_L2 = 0x10, 75 | MQTT_CONNECT_WILL_RETAIN = 0x20, 76 | MQTT_CONNECT_PASSWORD = 0x40, 77 | MQTT_CONNECT_USERNAME = 0x80 78 | }; 79 | 80 | 81 | // Code 4 marked reason codes are specific to MQTT v3.1.x. 82 | enum MqttReasonCodes { 83 | MQTT_CODE_SUCCESS = 0x0, 84 | MQTT_CODE_4_WRONG_PROTOCOL_VERSION = 0x01, 85 | MQTT_CODE_4_CLIENT_ID_REJECTED = 0x02, 86 | MQTT_CODE_4_SERVER_UNAVAILABLE = 0x03, 87 | MQTT_CODE_4_BAD_USERNAME_PASSWORD = 0x04, 88 | MQTT_CODE_4_NOT_AUTHORIZED = 0x05, 89 | MQTT_CODE_UNSPECIFIED = 0x80, 90 | MQTT_CODE_MALFORMED_PACKET = 0x81, 91 | MQTT_CODE_PROTOCOL_ERROR = 0x82, 92 | MQTT_CODE_RECEIVE_MAX_EXCEEDED = 0x93, 93 | MQTT_CODE_PACKAGE_TOO_LARGE = 0x95, 94 | MQTT_CODE_RETAIN_UNSUPPORTED = 0x9A, 95 | MQTT_CODE_QOS_UNSUPPORTED = 0x9B, 96 | MQTT_CODE_SHARED_SUB_UNSUPPORTED = 0x9E, 97 | MQTT_CODE_SUB_ID_UNSUPPORTED = 0xA1, 98 | MQTT_CODE_WILD_SUB_UNSUPPORTED = 0xA2 99 | }; 100 | 101 | 102 | class NmqttMessage { 103 | MqttProtocolVersion mqttVersion = MQTT_PROTOCOL_VERSION_4; 104 | MqttPacketType command; 105 | std::string loggerName = "NmqttMessage"; 106 | 107 | // For Publish message. 108 | bool duplicateMessage = false; 109 | MqttQoS QoS = MQTT_QOS_AT_MOST_ONCE; 110 | bool retainMessage = false; 111 | uint16_t packetID; 112 | 113 | // Fixed header. 114 | uint32_t messageLength; 115 | 116 | // Variable header. 117 | std::string topic; 118 | bool sessionPresent; 119 | MqttReasonCodes reasonCode; 120 | 121 | // Connect message. 122 | //MqttConnectFlags connectFlags; 123 | uint8_t connectFlags; 124 | bool cleanSessionFlag; 125 | bool willFlag; 126 | bool willRetainFlag; 127 | bool usernameFlag; 128 | bool passwordFlag; 129 | uint8_t willQoS; 130 | bool willQoS1; 131 | bool willQoS2; 132 | std::string will; 133 | std::string willTopic; 134 | std::string clientId; 135 | std::string username; 136 | std::string password; 137 | uint16_t keepAlive; 138 | 139 | // Status flags. 140 | bool empty = true; // Is this an empty message? 141 | bool parseGood = false; // Did the last binary message get parsed successfully? 142 | 143 | // Payload. 144 | std::string payload; 145 | 146 | ByteBauble bytebauble; 147 | 148 | public: 149 | NmqttMessage(); 150 | NmqttMessage(MqttPacketType type); 151 | NmqttMessage(std::string msg); 152 | ~NmqttMessage(); 153 | 154 | bool createMessage(MqttPacketType type); 155 | int parseMessage(std::string msg); 156 | int parseHeader(char* buff, int len, uint32_t &msglen, int& idx); 157 | bool valid() { return parseGood; } 158 | 159 | void setProtocolVersion(MqttProtocolVersion version) { mqttVersion = version; } 160 | 161 | // For Connect message. 162 | void setClientId(std::string id) { clientId = id; } 163 | void setCredentials(std::string &user, std::string &pass); 164 | void setWill(std::string topic, std::string will, uint8_t qos = 0, bool retain = false); 165 | 166 | // For Publish message. 167 | void setDuplicateMessage(bool dup) { duplicateMessage = dup; } 168 | void setQoS(MqttQoS q) { QoS = q; } 169 | void setRetain(bool retain) { retainMessage = retain; } 170 | 171 | void setTopic(std::string topic) { this->topic = topic; } 172 | void setPayload(std::string payload) { this->payload = payload; } 173 | 174 | MqttPacketType getCommand() { return command; } 175 | std::string getTopic() { return topic; } 176 | std::string getPayload() { return payload; } 177 | std::string getWill() { return will; } 178 | bool getSessionPresent() { return sessionPresent; } 179 | MqttReasonCodes getReasonCode() { return reasonCode; } 180 | 181 | std::string serialize(); 182 | }; 183 | 184 | 185 | #endif 186 | -------------------------------------------------------------------------------- /src/cpp/nymph_logger.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | nymph_logger.cpp - implementation for the NymphRPC Logger class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - 8 | 9 | Notes: 10 | - This class declares the main class to be used for logging. 11 | 12 | 2017/06/24, Maya Posch : Initial version. 13 | (c) Nyanko.ws 14 | */ 15 | 16 | #include "nymph_logger.h" 17 | 18 | #include 19 | #include 20 | //#include 21 | 22 | using namespace Poco; 23 | 24 | using namespace std; 25 | 26 | 27 | // >>> NYMPH LOGGER CHANNEL <<< 28 | // --- CONSTRUCTOR --- 29 | NymphLoggerChannel::NymphLoggerChannel(std::function function) { 30 | loggerFunction = function; 31 | } 32 | 33 | 34 | // --- DECONSTRUCTOR --- 35 | NymphLoggerChannel::~NymphLoggerChannel() { 36 | // 37 | } 38 | 39 | 40 | // --- CLOSE --- 41 | // Close the channel before discarding it. 42 | void NymphLoggerChannel::close() { 43 | // Nothing to do. 44 | } 45 | 46 | 47 | // --- LOG --- 48 | void NymphLoggerChannel::log(const Message &msg) { 49 | // Convert message to log string. 50 | int level; 51 | string logLevel; 52 | switch (msg.getPriority()) { 53 | case Message::PRIO_FATAL: 54 | level = 0; 55 | logLevel = "FATAL"; 56 | break; 57 | case Message::PRIO_CRITICAL: 58 | level = 1; 59 | logLevel = "CRITICAL"; 60 | break; 61 | case Message::PRIO_ERROR: 62 | level = 2; 63 | logLevel = "ERROR"; 64 | break; 65 | case Message::PRIO_WARNING: 66 | level = 3; 67 | logLevel = "WARNING"; 68 | break; 69 | case Message::PRIO_NOTICE: 70 | level = 4; 71 | logLevel = "NOTICE"; 72 | break; 73 | case Message::PRIO_INFORMATION: 74 | level = 5; 75 | logLevel = "INFO"; 76 | break; 77 | case Message::PRIO_DEBUG: 78 | level = 6; 79 | logLevel = "DEBUG"; 80 | break; 81 | case Message::PRIO_TRACE: 82 | level = 7; 83 | logLevel = "TRACE"; 84 | break; 85 | default: 86 | level = 7; 87 | logLevel = "UNKNOWN"; 88 | break; 89 | } 90 | 91 | string msgStr; 92 | //const string timeFormat = "%H:%M:%s"; // 24-hour, minutes, seconds & usecs. 93 | //msgStr = DateTimeFormatter::format(msg.getTimeStamp(), timeFormat); 94 | //msgStr = logLevel + "\t"; 95 | msgStr = NumberFormatter::format(msg.getPid()); 96 | msgStr += "." + NumberFormatter::format(msg.getTid()); 97 | msgStr += "\t" + msg.getSource() + "\t"; 98 | msgStr += NumberFormatter::format(msg.getSourceLine()) + "\t"; 99 | msgStr += msg.getText() + "\t\t- "; 100 | msgStr += msg.getSourceFile(); 101 | 102 | loggerFunction(level, msgStr); 103 | } 104 | 105 | 106 | // --- OPEN --- 107 | void NymphLoggerChannel::open() { 108 | // Nothing to do. 109 | } 110 | 111 | 112 | // >>> NYMPH LOGGER <<< 113 | // Static initialisations 114 | Message::Priority NymphLogger::priority; 115 | //Poco::Logger* NymphLogger::loggerRef; 116 | 117 | 118 | // --- SET LOGGER FUNCTION --- 119 | // Initialises the logger and associated logging channel. 120 | void NymphLogger::setLoggerFunction(std::function function) { 121 | AutoPtr nymphChannel(new NymphLoggerChannel(function)); 122 | Logger::root().setChannel(nymphChannel); 123 | //loggerRef = &Logger::get("NymphLogger"); 124 | } 125 | 126 | 127 | // --- SET LOG LEVEL --- 128 | void NymphLogger::setLogLevel(Poco::Message::Priority priority) { 129 | NymphLogger::priority = priority; 130 | Logger::root().setLevel(priority); 131 | 132 | } 133 | 134 | 135 | // --- LOGGER --- 136 | // Returns a reference to the logger instance. 137 | Logger& NymphLogger::logger() { 138 | //return *loggerRef; 139 | return Logger::get("NymphLogger"); 140 | } 141 | 142 | 143 | // Returns a reference to the logger instance using the provided name. 144 | Logger& NymphLogger::logger(string &name) { 145 | // TODO: cache the returned logger for future calls. 146 | return Logger::get(name); 147 | } 148 | -------------------------------------------------------------------------------- /src/cpp/nymph_logger.h: -------------------------------------------------------------------------------- 1 | /* 2 | nymph_logger.h - header file for the NymphRPC Logger class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - The central logging client for the Nymph library. 8 | 9 | 2017/06/24, Maya Posch : Initial version. 10 | (c) Nyanko.ws 11 | */ 12 | 13 | #pragma once 14 | #ifndef NYMPH_LOGGER_H 15 | #define NYMPH_LOGGER_H 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | #include 22 | #include 23 | 24 | 25 | enum NymphLogLevels { 26 | NYMPH_LOG_LEVEL_FATAL = 1, 27 | NYMPH_LOG_LEVEL_CRITICAL, 28 | NYMPH_LOG_LEVEL_ERROR, 29 | NYMPH_LOG_LEVEL_WARNING, 30 | NYMPH_LOG_LEVEL_NOTICE, 31 | NYMPH_LOG_LEVEL_INFO, 32 | NYMPH_LOG_LEVEL_DEBUG, 33 | NYMPH_LOG_LEVEL_TRACE 34 | }; 35 | 36 | 37 | #define NYMPH_LOG_FATAL(msg) \ 38 | if (NymphLogger::priority >= Poco::Message::PRIO_FATAL) { \ 39 | NymphLogger::logger(loggerName).fatal(msg, __FILE__, __LINE__);\ 40 | } 41 | #define NYMPH_LOG_CRITICAL(msg) \ 42 | if (NymphLogger::priority >= Poco::Message::PRIO_CRITICAL) { \ 43 | NymphLogger::logger(loggerName).critical(msg, __FILE__, __LINE__);\ 44 | } 45 | #define NYMPH_LOG_ERROR(msg) \ 46 | if (NymphLogger::priority >= Poco::Message::PRIO_ERROR) { \ 47 | NymphLogger::logger(loggerName).error(msg, __FILE__, __LINE__);\ 48 | } 49 | #define NYMPH_LOG_WARNING(msg) \ 50 | if (NymphLogger::priority >= Poco::Message::PRIO_WARNING) { \ 51 | NymphLogger::logger(loggerName).warning(msg, __FILE__, __LINE__);\ 52 | } 53 | #define NYMPH_LOG_NOTICE(msg) \ 54 | if (NymphLogger::priority >= Poco::Message::PRIO_NOTICE) { \ 55 | NymphLogger::logger(loggerName).notice(msg, __FILE__, __LINE__);\ 56 | } 57 | #define NYMPH_LOG_INFORMATION(msg) \ 58 | if (NymphLogger::priority >= Poco::Message::PRIO_INFORMATION) { \ 59 | NymphLogger::logger(loggerName).information(msg, __FILE__, __LINE__);\ 60 | } 61 | #define NYMPH_LOG_DEBUG(msg) \ 62 | if (NymphLogger::priority >= Poco::Message::PRIO_DEBUG) { \ 63 | NymphLogger::logger(loggerName).debug(msg, __FILE__, __LINE__);\ 64 | } 65 | #define NYMPH_LOG_TRACE(msg) \ 66 | if (NymphLogger::priority >= Poco::Message::PRIO_TRACE) { \ 67 | NymphLogger::logger(loggerName).trace(msg, __FILE__, __LINE__);\ 68 | } 69 | 70 | 71 | // Function pointer typedef for the function-based logger. 72 | //typedef void (*logFnc)(int, std::string); 73 | 74 | 75 | class NymphLoggerChannel : public Poco::Channel { 76 | std::function loggerFunction; 77 | 78 | public: 79 | NymphLoggerChannel(std::function function); 80 | ~NymphLoggerChannel(); 81 | 82 | void close(); 83 | //string getProperty(const string &name) { return string(); } 84 | void log(const Poco::Message &msg); 85 | void open(); 86 | //void setProperty(const string &name, const string &value) { } 87 | }; 88 | 89 | 90 | class NymphLogger { 91 | //static Poco::Logger* loggerRef; 92 | public: 93 | static Poco::Message::Priority priority; 94 | 95 | static void setLoggerFunction(std::function function); 96 | static void setLogLevel(Poco::Message::Priority priority); 97 | static Poco::Logger& logger(); 98 | static Poco::Logger& logger(std::string &name); 99 | //static void log(string message); 100 | }; 101 | 102 | #endif 103 | -------------------------------------------------------------------------------- /src/cpp/request.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | request.cpp - implementation of the Request class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - 8 | 9 | 2016/11/19, Maya Posch 10 | (c) Nyanko.ws 11 | */ 12 | 13 | 14 | #include "request.h" 15 | 16 | #include "nymph_logger.h" 17 | 18 | 19 | 20 | 21 | // Static initialisations 22 | /* std::atomic Semaphore::lvl = 0; 23 | std::condition_variable* Semaphore::cv = 0; 24 | std::mutex* Semaphore::mtx = 0; 25 | 26 | 27 | // --- INIT --- 28 | void Semaphore::init(int n, std::condition_variable* cv, std::mutex* mtx) { 29 | Semaphore::lvl = n; 30 | Semaphore::cv = cv; 31 | Semaphore::mtx = mtx; 32 | } 33 | 34 | 35 | // --- SIGNAL --- 36 | void Semaphore::signal() { 37 | ++Semaphore::lvl; 38 | 39 | if (Semaphore::lvl == 0 && cv) { 40 | cv->notify_one(); 41 | } 42 | } */ 43 | 44 | 45 | // --- PROCESS --- 46 | void Request::process() { 47 | NymphSocket* nymphSocket = NmqttConnections::getSocket(handle); 48 | 49 | if (msg.getCommand() == MQTT_PUBLISH) { 50 | NYMPH_LOG_DEBUG("Calling PUBLISH message handler..."); 51 | nymphSocket->handler(handle, msg.getTopic(), msg.getPayload()); 52 | } 53 | else if (msg.getCommand() == MQTT_CONNACK) { 54 | NYMPH_LOG_DEBUG("Calling CONNACK message handler..."); 55 | nymphSocket->connackHandler(handle, msg.getSessionPresent(), msg.getReasonCode()); 56 | } 57 | else if (msg.getCommand() == MQTT_PINGRESP) { 58 | NYMPH_LOG_DEBUG("Calling PINGRESP message handler..."); 59 | nymphSocket->pingrespHandler(handle); 60 | } 61 | 62 | // Signal that we are done. 63 | //Semaphore::signal(); 64 | } 65 | 66 | 67 | // --- FINISH --- 68 | void Request::finish() { 69 | // Call own destructor. 70 | delete this; 71 | } 72 | -------------------------------------------------------------------------------- /src/cpp/request.h: -------------------------------------------------------------------------------- 1 | /* 2 | request.h - header file for the Request class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - 8 | 9 | 2016/11/19, Maya Posch 10 | (c) Nyanko.ws 11 | */ 12 | 13 | 14 | #ifndef REQUEST_H 15 | #define REQUEST_H 16 | 17 | 18 | #include "abstract_request.h" 19 | #include "connections.h" 20 | #include "message.h" 21 | 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | using namespace std; 29 | 30 | 31 | /* class Semaphore { 32 | static std::atomic lvl; 33 | static std::condition_variable* cv; 34 | static std::mutex* mtx; 35 | 36 | public: 37 | static void init(int n, std::condition_variable* cv, std::mutex* mtx); 38 | static void wait() { --Semaphore::lvl; } 39 | static void signal(); 40 | static bool processing() { return (Semaphore::lvl == 0)? true : false; } 41 | }; */ 42 | 43 | 44 | //typedef void (*logFunction)(string text); 45 | 46 | 47 | class Request : public AbstractRequest { 48 | std::string value; 49 | int handle; 50 | NmqttMessage msg; 51 | std::string loggerName = "Request"; 52 | 53 | //logFunction outFnc; 54 | 55 | public: 56 | Request() { } 57 | void setValue(std::string value) { this->value = value; } 58 | void setMessage(int handle, NmqttMessage msg) { this->handle = handle; this->msg = msg; } 59 | //void setOutput(logFunction fnc) { outFnc = fnc; } 60 | void process(); 61 | void finish(); 62 | }; 63 | 64 | #endif 65 | -------------------------------------------------------------------------------- /src/cpp/server.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | server.cpp - Definition of the NymphMQTT server class. 3 | 4 | Revision 0. 5 | 6 | Features: 7 | - Provides public API for NymphMQTT server. 8 | 9 | Notes: 10 | - 11 | 12 | 2021/01/03, Maya Posch 13 | */ 14 | 15 | 16 | #include "server.h" 17 | 18 | #include "dispatcher.h" 19 | #include "nymph_logger.h" 20 | #include "session.h" 21 | #include "server_connections.h" 22 | 23 | #include 24 | #include 25 | 26 | 27 | // Static initialisations. 28 | long NmqttServer::timeout = 3000; 29 | string NmqttServer::loggerName = "NmqttServer"; 30 | Poco::Net::ServerSocket NmqttServer::ss; 31 | Poco::Net::TCPServer* NmqttServer::server; 32 | 33 | 34 | // --- CONSTRUCTOR --- 35 | NmqttServer::NmqttServer() { 36 | // 37 | } 38 | 39 | 40 | // --- DECONSTRUCTOR --- 41 | NmqttServer::~NmqttServer() { 42 | // 43 | } 44 | 45 | 46 | // --- INIT --- 47 | // Initialise the runtime. 48 | bool NmqttServer::init(std::function logger, int level, long timeout) { 49 | NmqttServer::timeout = timeout; 50 | setLogger(logger, level); 51 | 52 | // Set the function pointers for the command handlers. 53 | NmqttClientSocket ns; 54 | //using namespace std::placeholders; 55 | ns.connectHandler = &NmqttServer::connectHandler; //std::bind(&NmqttServer::connectHandler, this, _1); 56 | ns.pingreqHandler = &NmqttServer::pingreqHandler; //std::bind(&NmqttServer::pingreqHandler, this, _1); 57 | NmqttClientConnections::setCoreParameters(ns); 58 | 59 | // Start the dispatcher runtime. 60 | // Get the number of concurrent threads supported by the system we are running on. 61 | int numThreads = std::thread::hardware_concurrency(); 62 | 63 | // Initialise the Dispatcher with the maximum number of threads as worker count. 64 | Dispatcher::init(numThreads); 65 | 66 | return true; 67 | } 68 | 69 | 70 | // --- SET LOGGER --- 71 | // Sets the logger function to be used by the Nymph Logger class, along with the 72 | // desired maximum log level: 73 | // NYMPH_LOG_LEVEL_FATAL = 1, 74 | // NYMPH_LOG_LEVEL_CRITICAL, 75 | // NYMPH_LOG_LEVEL_ERROR, 76 | // NYMPH_LOG_LEVEL_WARNING, 77 | // NYMPH_LOG_LEVEL_NOTICE, 78 | // NYMPH_LOG_LEVEL_INFO, 79 | // NYMPH_LOG_LEVEL_DEBUG, 80 | // NYMPH_LOG_LEVEL_TRACE 81 | void NmqttServer::setLogger(std::function logger, int level) { 82 | NymphLogger::setLoggerFunction(logger); 83 | NymphLogger::setLogLevel((Poco::Message::Priority) level); 84 | } 85 | 86 | 87 | // --- START --- 88 | bool NmqttServer::start(int port) { 89 | try { 90 | // Create a server socket that listens on all interfaces, IPv4 and IPv6. 91 | // Assign it to the new TCPServer. 92 | ss.bind6(port, true, false); // Port, SO_REUSEADDR, IPv6-only. 93 | ss.listen(); 94 | server = new Poco::Net::TCPServer( 95 | new Poco::Net::TCPServerConnectionFactoryImpl(), ss); 96 | server->start(); 97 | } 98 | catch (Poco::Net::NetException& e) { 99 | NYMPH_LOG_ERROR("Error starting TCP server: " + e.message()); 100 | return false; 101 | } 102 | 103 | //running = true; 104 | return true; 105 | } 106 | 107 | 108 | // --- SEND MESSAGE --- 109 | // Private method for sending data to a remote broker. 110 | bool NmqttServer::sendMessage(uint64_t handle, std::string binMsg) { 111 | NmqttClientSocket* clientSocket = NmqttClientConnections::getSocket(handle); 112 | 113 | try { 114 | int ret = clientSocket->socket->sendBytes(((const void*) binMsg.c_str()), binMsg.length()); 115 | if (ret != binMsg.length()) { 116 | // Handle error. 117 | NYMPH_LOG_ERROR("Failed to send message. Not all bytes sent."); 118 | return false; 119 | } 120 | 121 | NYMPH_LOG_DEBUG("Sent " + Poco::NumberFormatter::format(ret) + " bytes."); 122 | } 123 | catch (Poco::Exception &e) { 124 | NYMPH_LOG_ERROR("Failed to send message: " + e.message()); 125 | return false; 126 | } 127 | 128 | return true; 129 | } 130 | 131 | 132 | // --- CONNECT HANDLER --- 133 | // Process connection. Return CONNACK response. 134 | void NmqttServer::connectHandler(uint64_t handle) { 135 | NmqttMessage msg(MQTT_CONNACK); 136 | sendMessage(handle, msg.serialize()); 137 | } 138 | 139 | 140 | // --- PINGREQ HANDLER --- 141 | // Reply to ping response from a client. 142 | void NmqttServer::pingreqHandler(uint64_t handle) { 143 | NmqttMessage msg(MQTT_PINGRESP); 144 | sendMessage(handle, msg.serialize()); 145 | } 146 | 147 | 148 | // --- SHUTDOWN --- 149 | // Shutdown the runtime. Close any open connections and clean up resources. 150 | bool NmqttServer::shutdown() { 151 | server->stop(); 152 | 153 | return true; 154 | } 155 | -------------------------------------------------------------------------------- /src/cpp/server.h: -------------------------------------------------------------------------------- 1 | /* 2 | server.h - Declaration of the NymphMQTT server class. 3 | 4 | Revision 0. 5 | 6 | Features: 7 | - Provides public API for NymphMQTT server. 8 | 9 | Notes: 10 | - 11 | 12 | 2021/01/03, Maya Posch 13 | */ 14 | 15 | 16 | #ifndef NMQTT_SERVER_H 17 | #define NMQTT_SERVER_H 18 | 19 | #include 20 | #include 21 | 22 | #include 23 | #include 24 | #include 25 | 26 | #include "nymph_logger.h" 27 | #include "message.h" 28 | 29 | 30 | class NmqttServer { 31 | static long timeout; 32 | static std::string loggerName; 33 | static Poco::Net::ServerSocket ss; 34 | static Poco::Net::TCPServer* server; 35 | 36 | static bool sendMessage(uint64_t handle, std::string binMsg); 37 | static void connectHandler(uint64_t handle); 38 | static void pingreqHandler(uint64_t handle); 39 | 40 | public: 41 | NmqttServer(); 42 | ~NmqttServer(); 43 | 44 | static bool init(std::function logger, int level = NYMPH_LOG_LEVEL_TRACE, long timeout = 3000); 45 | static void setLogger(std::function logger, int level); 46 | static bool start(int port = 4004); 47 | static bool shutdown(); 48 | }; 49 | 50 | #endif 51 | -------------------------------------------------------------------------------- /src/cpp/server_connections.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | server_connections.cpp - Implementation of the NymphMQTT Server Connections class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - Static class to enable the global management of client connections. 8 | 9 | Notes: 10 | - 11 | 12 | 2021/01/04 - Maya Posch 13 | */ 14 | 15 | 16 | #include "server_connections.h" 17 | 18 | 19 | // Static initialisations. 20 | std::map NmqttClientConnections::sockets; 21 | uint64_t NmqttClientConnections::lastHandle = 0; 22 | std::queue NmqttClientConnections::freeHandles; 23 | NmqttClientSocket NmqttClientConnections::coreCS; 24 | 25 | 26 | // --- ADD SOCKET --- 27 | uint64_t NmqttClientConnections::addSocket(NmqttClientSocket &ns) { 28 | // Add new instance to map, return either a new handle ID, or one from the FIFO. 29 | uint64_t handle; 30 | if (!freeHandles.empty()) { 31 | handle = freeHandles.front(); 32 | freeHandles.pop(); 33 | } 34 | else { 35 | handle = lastHandle++; 36 | } 37 | 38 | // Merge core and provided struct. 39 | NmqttClientSocket ts = coreCS; 40 | ts.socket = ns.socket; 41 | sockets.insert(std::pair(handle, ts)); 42 | 43 | return handle; 44 | } 45 | 46 | 47 | // --- GET SOCKET --- 48 | NmqttClientSocket* NmqttClientConnections::getSocket(uint64_t handle) { 49 | std::map::iterator it; 50 | it = sockets.find(handle); 51 | if (it == sockets.end()) { 52 | return 0; 53 | } 54 | 55 | return &it->second; 56 | } 57 | 58 | 59 | // --- REMOVE SOCKET --- 60 | void NmqttClientConnections::removeSocket(uint64_t handle) { 61 | std::map::iterator it; 62 | it = sockets.find(handle); 63 | if (it == sockets.end()) { 64 | return; 65 | } 66 | 67 | sockets.erase(it); 68 | freeHandles.push(handle); 69 | } 70 | 71 | 72 | void NmqttClientConnections::setCoreParameters(NmqttClientSocket &ns) { 73 | coreCS = ns; 74 | } 75 | -------------------------------------------------------------------------------- /src/cpp/server_connections.h: -------------------------------------------------------------------------------- 1 | /* 2 | server_connections.h - Header for the NymphMQTT Server Connections class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - Static class to enable the global management of client connections. 8 | 9 | Notes: 10 | - 11 | 12 | 2021/01/04 - Maya Posch 13 | */ 14 | 15 | 16 | #ifndef NMQTT_CLIENT_CONNECTIONS_H 17 | #define NMQTT_CLIENT_CONNECTIONS_H 18 | 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | #include "client.h" 29 | 30 | 31 | // TYPES 32 | struct NmqttClientSocket { 33 | //bool secure; // Are using an SSL/TLS connection or not? 34 | //Poco::Net::SecureStreamSocket* ssocket; // Pointer to a secure socket instance. 35 | //Poco::Net::Context::Ptr context; // The security context for TLS connections. 36 | Poco::Net::StreamSocket* socket; // Pointer to a non-secure socket instance. 37 | //Poco::Semaphore* semaphore; // Signals when it's safe to delete the socket. 38 | //std::function handler; // Publish message handler. 39 | std::function connectHandler; // CONNECT handler. 40 | std::function pingreqHandler; // PINGREQ handler. 41 | //void* data; // User data. 42 | //int handle; // The Nymph internal socket handle. 43 | bool username; 44 | bool password; 45 | bool willRetain; 46 | uint8_t qos; 47 | bool willFlag; 48 | bool cleanSession; 49 | }; 50 | 51 | 52 | class NmqttClientConnections { 53 | static std::map sockets; 54 | static uint64_t lastHandle; 55 | static std::queue freeHandles; 56 | static NmqttClientSocket coreCS; 57 | 58 | public: 59 | static uint64_t addSocket(NmqttClientSocket &ns); 60 | static NmqttClientSocket* getSocket(uint64_t handle); 61 | static void removeSocket(uint64_t handle); 62 | static void setCoreParameters(NmqttClientSocket &ns); 63 | }; 64 | 65 | 66 | #endif 67 | -------------------------------------------------------------------------------- /src/cpp/server_request.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | server_request.cpp - implementation of the Server Request class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - 8 | 9 | 2021/01/04, Maya Posch 10 | (c) Nyanko.ws 11 | */ 12 | 13 | 14 | #include "server_request.h" 15 | 16 | #include "nymph_logger.h" 17 | 18 | 19 | // --- PROCESS --- 20 | void NmqttServerRequest::process() { 21 | NmqttClientSocket* clientSocket = NmqttClientConnections::getSocket(handle); 22 | 23 | /* if (msg.getCommand() == MQTT_PUBLISH) { 24 | NYMPH_LOG_DEBUG("Calling PUBLISH message handler..."); 25 | clientSocket->handler(handle, msg.getTopic(), msg.getPayload()); 26 | } 27 | else */ if (msg.getCommand() == MQTT_CONNECT) { 28 | NYMPH_LOG_DEBUG("Calling CONNECT message handler..."); 29 | clientSocket->connectHandler(handle); 30 | } 31 | else if (msg.getCommand() == MQTT_PINGREQ) { 32 | NYMPH_LOG_DEBUG("Calling PINGREQ message handler..."); 33 | clientSocket->pingreqHandler(handle); 34 | } 35 | } 36 | 37 | 38 | // --- FINISH --- 39 | void NmqttServerRequest::finish() { 40 | // 41 | } 42 | -------------------------------------------------------------------------------- /src/cpp/server_request.h: -------------------------------------------------------------------------------- 1 | /* 2 | server_request.h - header file for the Server Request class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - 8 | 9 | 2021/01/04, Maya Posch 10 | (c) Nyanko.ws 11 | */ 12 | 13 | 14 | #ifndef SERVER_REQUEST_H 15 | #define SERVER_REQUEST_H 16 | 17 | 18 | #include "abstract_request.h" 19 | #include "server_connections.h" 20 | #include "message.h" 21 | 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | 28 | using namespace std; 29 | 30 | 31 | class NmqttServerRequest : public AbstractRequest { 32 | std::string value; 33 | int handle; 34 | NmqttMessage msg; 35 | std::string loggerName = "NmqttServerRequest"; 36 | 37 | public: 38 | NmqttServerRequest() { } 39 | void setValue(std::string value) { this->value = value; } 40 | void setMessage(uint64_t handle, NmqttMessage msg) { this->handle = handle; this->msg = msg; } 41 | void process(); 42 | void finish(); 43 | }; 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /src/cpp/session.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | session.cpp - Definition of the NMQTT Session class. 3 | 4 | Features: 5 | - Handles client connections. 6 | 7 | 2021/01/04, Maya Posch 8 | */ 9 | 10 | 11 | #include "session.h" 12 | 13 | #include "server_connections.h" 14 | #include "server_request.h" 15 | #include "dispatcher.h" 16 | #include "nymph_logger.h" 17 | 18 | #include 19 | 20 | 21 | // --- CONSTRUCTOR --- 22 | NmqttSession::NmqttSession(const Poco::Net::StreamSocket& socket) 23 | : Poco::Net::TCPServerConnection(socket) { 24 | loggerName = "NmqttSession"; 25 | listen = true; 26 | } 27 | 28 | 29 | // --- CONSTRUCTOR --- 30 | /* NmqttSession::NmqttSession(int handle, Condition* cnd, Mutex* mtx) { 31 | loggerName = "NmqttSession"; 32 | listen = true; 33 | init = true; 34 | this->nymphSocket = NmqttConnections::getSocket(handle); 35 | this->socket = nymphsocket.socket; 36 | this->readyCond = cnd; 37 | this->readyMutex = mtx; 38 | } */ 39 | 40 | 41 | // --- DECONSTRUCTOR --- 42 | NmqttSession::~NmqttSession() { 43 | // 44 | } 45 | 46 | 47 | // --- RUN --- 48 | void NmqttSession::run() { 49 | Poco::Net::StreamSocket& socket = this->socket(); 50 | 51 | // Add this connection to the list of client connections. 52 | NmqttClientSocket sk; 53 | sk.socket = &socket; 54 | uint64_t handle = NmqttClientConnections::addSocket(sk); 55 | 56 | Poco::Timespan timeout(0, 100); // 100 microsecond timeout 57 | 58 | NYMPH_LOG_INFORMATION("Start listening..."); 59 | 60 | char headerBuff[5]; 61 | while (listen) { 62 | if (socket.poll(timeout, Poco::Net::Socket::SELECT_READ)) { 63 | // Attempt to receive the entire message. 64 | // First validate the first two bytes. If it's an MQTT message this will contain the 65 | // command and the first byte of the message length. 66 | // 67 | // Unfortunately, MQTT's message length is a variable length integer, spanning 1-4 bytes. 68 | // Because of this, we have to read in the first byte, see whether a second one follows 69 | // by looking at the 8th bit of the byte, read that in, and so on. 70 | // 71 | // The smallest message we can receive is the Disconnect type, with just two bytes. 72 | int received = socket.receiveBytes((void*) &headerBuff, 2); 73 | if (received == 0) { 74 | // Remote disconnnected. Socket should be discarded. 75 | NYMPH_LOG_INFORMATION("Received remote disconnected notice. Terminating listener thread."); 76 | break; 77 | } 78 | else if (received < 2) { 79 | // TODO: try to wait for more bytes. 80 | NYMPH_LOG_WARNING("Received <2 bytes: " + Poco::NumberFormatter::format(received)); 81 | 82 | continue; 83 | } 84 | 85 | // Use the NmqttMessage class's validation feature to extract the message length from 86 | // the fixed header. 87 | NmqttMessage msg; 88 | uint32_t msglen = 0; 89 | int idx = 0; // Will be set to the index after the fixed header by the parse method. 90 | while (!msg.parseHeader((char*) &headerBuff, 5, msglen, idx)) { 91 | // Attempt to read more data. The index parameter is placed at the last valid 92 | // byte in the headerBuff array. Append new data after this. 93 | 94 | NYMPH_LOG_WARNING("Too few bytes to parse header. Aborting..."); 95 | 96 | // TODO: abort reading for now. 97 | continue; 98 | } 99 | 100 | NYMPH_LOG_DEBUG("Message length: " + Poco::NumberFormatter::format(msglen)); 101 | 102 | std::string binMsg; 103 | if (msglen > 0) { 104 | // Create new buffer for the rest of the message. 105 | char* buff = new char[msglen]; 106 | 107 | // Read the entire message into a string which is then used to 108 | // construct an NmqttMessage instance. 109 | received = socket.receiveBytes((void*) buff, msglen); 110 | binMsg.append(headerBuff, idx); 111 | binMsg.append(buff, received); 112 | if (received != msglen) { 113 | // Handle incomplete message. 114 | NYMPH_LOG_WARNING("Incomplete message: " 115 | + Poco::NumberFormatter::format(received) 116 | + " of " + Poco::NumberFormatter::format(msglen)); 117 | 118 | // Loop until the rest of the message has been received. 119 | // TODO: Set a maximum number of loops/timeout? Reset when 120 | // receiving data, timeout when poll times out N times? 121 | //binMsg->reserve(msglen); 122 | int unread = msglen - received; 123 | while (1) { 124 | if (socket.poll(timeout, Poco::Net::Socket::SELECT_READ)) { 125 | char* buff1 = new char[unread]; 126 | received = socket.receiveBytes((void*) buff1, unread); 127 | if (received == 0) { 128 | // Remote disconnnected. Socket should be discarded. 129 | NYMPH_LOG_INFORMATION("Received remote disconnected notice. Terminating listener thread."); 130 | delete[] buff1; 131 | break; 132 | } 133 | else if (received != unread) { 134 | binMsg.append((const char*) buff1, received); 135 | delete[] buff1; 136 | unread -= received; 137 | NYMPH_LOG_WARNING("Incomplete message: " 138 | + Poco::NumberFormatter::format(unread) + "/" 139 | + Poco::NumberFormatter::format(msglen) 140 | + " unread."); 141 | continue; 142 | } 143 | 144 | // Full message was read. Continue with processing. 145 | binMsg.append((const char*) buff1, received); 146 | delete[] buff1; 147 | break; 148 | } // if 149 | } //while 150 | } 151 | else { 152 | NYMPH_LOG_DEBUG("Read " + Poco::NumberFormatter::format(received) + " bytes."); 153 | } 154 | 155 | delete[] buff; 156 | } 157 | else { 158 | // 159 | binMsg.append(headerBuff, idx); 160 | } 161 | 162 | // Parse the string into an NmqttMessage instance. 163 | msg.parseMessage(binMsg); 164 | 165 | NYMPH_LOG_DEBUG("Got command: " + Poco::NumberFormatter::format(msg.getCommand())); 166 | 167 | // Call the message handler callback when one exists for this type of message. 168 | // TODO: refactor for Dispatcher. 169 | NmqttServerRequest* req = new NmqttServerRequest; 170 | req->setMessage(handle, msg); 171 | Dispatcher::addRequest(req); 172 | } 173 | 174 | // Check whether we're still initialising. 175 | /* if (init) { 176 | // Signal that this listener thread is ready. 177 | readyMutex->lock(); 178 | readyCond->signal(); 179 | readyMutex->unlock(); 180 | 181 | timeout.assign(1, 0); // Change timeout to 1 second. 182 | init = false; 183 | } */ 184 | } 185 | 186 | NYMPH_LOG_INFORMATION("Stopping thread..."); 187 | 188 | // Clean-up. 189 | // Remove this session from the list. 190 | NmqttClientConnections::removeSocket(handle); 191 | } 192 | 193 | 194 | // --- STOP --- 195 | void NmqttSession::stop() { 196 | listen = false; 197 | } 198 | -------------------------------------------------------------------------------- /src/cpp/session.h: -------------------------------------------------------------------------------- 1 | /* 2 | session.h - Declaration of NMQTT session class. 3 | 4 | 2021/01/04, Maya Posch 5 | */ 6 | 7 | 8 | #ifndef NMQTT_SESSION_H 9 | #define NMQTT_SESSION_H 10 | 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | 17 | class NmqttSession : public Poco::Net::TCPServerConnection { 18 | std::string loggerName; 19 | bool listen; 20 | bool init; 21 | Poco::Condition* readyCond; 22 | Poco::Mutex* readyMutex; 23 | 24 | public: 25 | NmqttSession(const Poco::Net::StreamSocket& socket); 26 | ~NmqttSession(); 27 | void run(); 28 | void stop(); 29 | }; 30 | 31 | #endif 32 | -------------------------------------------------------------------------------- /src/cpp/timer.h: -------------------------------------------------------------------------------- 1 | /* 2 | timer.h - Header for the NymphMQTT Timer class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - Allows one to set custom data to be passed along with the Poco Timer. 8 | 9 | Notes: 10 | - 11 | 12 | 2019/05/08 - Maya Posch 13 | */ 14 | 15 | 16 | #ifndef NMQTT_TIMER_H 17 | #define NMQTT_TIMER_H 18 | 19 | 20 | #include 21 | 22 | 23 | class NmqttTimer : public Poco::Timer { 24 | int handle; 25 | 26 | public: 27 | NmqttTimer(long startInterval = 0, long periodicInterval = 0) : Poco::Timer(startInterval, periodicInterval) { } 28 | void setHandle(int handle) { this->handle = handle; } 29 | int getHandle() { return handle; } 30 | }; 31 | 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /src/cpp/worker.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | worker.cpp - Implementation of the Worker class. 3 | 4 | Revision 0 5 | 6 | Features: 7 | - 8 | 9 | Notes: 10 | - 11 | 12 | 2016/11/19, Maya Posch 13 | (c) Nyanko.ws 14 | */ 15 | 16 | 17 | #include "worker.h" 18 | #include "dispatcher.h" 19 | 20 | #include 21 | 22 | using namespace std; 23 | 24 | 25 | // --- GET CONDITION --- 26 | void Worker::getCondition(condition_variable* &cv) { 27 | cv = &(this)->cv; 28 | } 29 | 30 | 31 | // --- GET MUTEX --- 32 | void Worker::getMutex(mutex* &mtx) { 33 | mtx = &(this)->mtx; 34 | } 35 | 36 | 37 | // --- RUN --- 38 | // Runs the worker instance. 39 | void Worker::run() { 40 | while (running) { 41 | if (ready) { 42 | // Execute the request. 43 | ready = false; 44 | request->process(); 45 | request->finish(); 46 | } 47 | 48 | // Add self to Dispatcher queue and execute next request or wait. 49 | if (Dispatcher::addWorker(this)) { 50 | // Use the ready loop to deal with spurious wake-ups. 51 | while (!ready && running) { 52 | unique_lock ulock(mtx); 53 | if (cv.wait_for(ulock, chrono::seconds(1)) == cv_status::timeout) { 54 | // We timed out, but we keep waiting unless the worker is 55 | // stopped by the dispatcher. 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/cpp/worker.h: -------------------------------------------------------------------------------- 1 | /* 2 | worker.h - Header file for the Worker class. 3 | 4 | Revision 0 5 | 6 | Notes: 7 | - 8 | 9 | 2016/11/19, Maya Posch 10 | (c) Nyanko.ws 11 | */ 12 | 13 | 14 | #pragma once 15 | #ifndef WORKER_H 16 | #define WORKER_H 17 | 18 | #include "abstract_request.h" 19 | 20 | #include 21 | #include 22 | 23 | using namespace std; 24 | 25 | 26 | class Worker { 27 | condition_variable cv; 28 | mutex mtx; 29 | /* unique_lock ulock; */ 30 | AbstractRequest* request; 31 | bool running; 32 | bool ready; 33 | 34 | public: 35 | Worker() { running = true; ready = false; /* ulock = unique_lock(mtx); */ } 36 | void run(); 37 | void stop() { running = false; } 38 | void setRequest(AbstractRequest* request) { this->request = request; ready = true; } 39 | void getCondition(condition_variable* &cv); 40 | void getMutex(mutex* &mtx); 41 | }; 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /src/server/nmqtt_server.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | nmqtt_server.cpp - Main file of the NMQTT Server application. 3 | 4 | Revision 0. 5 | 6 | 2021/01/04, Maya Posch 7 | */ 8 | 9 | 10 | #include "../cpp/server.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | 20 | 21 | Poco::Condition gCon; 22 | Poco::Mutex gMutex; 23 | 24 | 25 | void signal_handler(int signal) { 26 | gCon.signal(); 27 | } 28 | 29 | 30 | // --- LOG FUNCTION --- 31 | void logFunction(int level, std::string logStr) { 32 | std::cout << level << " - " << logStr << std::endl; 33 | } 34 | 35 | 36 | int main() { 37 | // Initialise the server instance. 38 | std::cout << "Initialising server..." << std::endl; 39 | long timeout = 5000; // 5 seconds. 40 | NmqttServer::init(logFunction, NYMPH_LOG_LEVEL_TRACE, timeout); 41 | 42 | // Install signal handler to terminate the server. 43 | signal(SIGINT, signal_handler); 44 | 45 | // Start server on port 1883. 46 | // FIXME: make configurable. Enable encrypted connections (8883). 47 | NmqttServer::start(1883); 48 | 49 | // Loop until the SIGINT signal has been received. 50 | gMutex.lock(); 51 | gCon.wait(gMutex); 52 | 53 | // Clean-up 54 | NmqttServer::shutdown(); 55 | 56 | // Wait before exiting, giving threads time to exit. 57 | Poco::Thread::sleep(2000); // 2 seconds. 58 | 59 | return 0; 60 | } 61 | --------------------------------------------------------------------------------