├── server ├── mosquitto │ ├── .gitignore │ ├── password.txt │ ├── Dockerfile │ ├── config │ │ └── mosquitto.conf │ └── docker-entrypoint.sh ├── docker-compose.yml └── gen_certs.sh ├── .gitignore ├── Tests ├── LinuxMain.swift └── MQTTTests │ ├── XCTestManifests.swift │ ├── RemainLengthTests.swift │ └── MQTTTests.swift ├── Sources ├── MQTT │ ├── Packet │ │ ├── PING │ │ │ ├── PingResp.swift │ │ │ └── PingReq.swift │ │ ├── DISCONNECT │ │ │ └── Disconnect.swift │ │ ├── FixedHeader.swift │ │ ├── QoS.swift │ │ ├── PUBLISH │ │ │ ├── PubAck.swift │ │ │ ├── PubRec.swift │ │ │ ├── PubComp.swift │ │ │ ├── PubRel.swift │ │ │ └── Publish.swift │ │ ├── UNSUBSCRIBE │ │ │ ├── UnsubAck.swift │ │ │ └── Unsubscribe.swift │ │ ├── MQTTPacketType.swift │ │ ├── MQTTPacket.swift │ │ ├── CONNECT │ │ │ ├── ConnAck.swift │ │ │ └── Connect.swift │ │ └── SUBSCRIBE │ │ │ ├── Subscribe.swift │ │ │ └── SubAck.swift │ ├── DataEncodable.swift │ ├── Error.swift │ ├── MQTTChannelHandler.swift │ ├── Extension │ │ └── Data.swift │ ├── MQTTDecoder.swift │ └── MQTTClient.swift └── SampleClient │ └── main.swift ├── Package.resolved ├── Package.swift ├── LICENSE └── README.md /server/mosquitto/.gitignore: -------------------------------------------------------------------------------- 1 | certs/ 2 | -------------------------------------------------------------------------------- /server/mosquitto/password.txt: -------------------------------------------------------------------------------- 1 | swift-mqtt:swift-mqtt 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | 7 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import swift_mqttTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += swift_mqttTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/PING/PingResp.swift: -------------------------------------------------------------------------------- 1 | /// # Reference 2 | /// [PINGRESP – PING response](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718086) 3 | public final class PingRespPacket: MQTTPacket {} 4 | -------------------------------------------------------------------------------- /server/mosquitto/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-mosquitto 2 | 3 | COPY docker-entrypoint.sh / 4 | 5 | COPY password.txt / 6 | 7 | ENTRYPOINT ["sh", "./docker-entrypoint.sh"] 8 | 9 | CMD ["/usr/sbin/mosquitto", "-c", "/mosquitto/config/mosquitto.conf"] 10 | -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mosquitto: 5 | build: ./mosquitto 6 | volumes: 7 | - ./mosquitto/config:/mosquitto/config 8 | - ./mosquitto/certs:/mosquitto/certs 9 | ports: 10 | - 8883:8883 11 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/PING/PingReq.swift: -------------------------------------------------------------------------------- 1 | /// # Reference 2 | /// [PINGREQ – PING request](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718081) 3 | final class PingReqPacket: MQTTPacket { 4 | init() { 5 | super.init(packetType: .pingreq, flags: 0) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Tests/MQTTTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(MQTTTests.allTests), 7 | testCase(RemainLengthTests.allTests), 8 | ] 9 | } 10 | #endif 11 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/DISCONNECT/Disconnect.swift: -------------------------------------------------------------------------------- 1 | /// # Reference 2 | /// [DISCONNECT – Disconnect notification](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718090) 3 | final class DisconnectPacket: MQTTPacket { 4 | init() { 5 | super.init(packetType: .disconnect, flags: 0) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /server/mosquitto/config/mosquitto.conf: -------------------------------------------------------------------------------- 1 | log_type debug 2 | 3 | port 8883 4 | 5 | # SSL 6 | cafile /mosquitto/certs/ca/ca_cert.pem 7 | keyfile /mosquitto/certs/server/private/server_key.pem 8 | certfile /mosquitto/certs/server/server_cert.pem 9 | 10 | # user auth 11 | allow_anonymous false 12 | password_file password.txt 13 | 14 | -------------------------------------------------------------------------------- /server/mosquitto/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/ash 2 | set -e 3 | 4 | MOSQUITTO_PASSWORDFILE="password.txt" 5 | 6 | if [ ! -f "${MOSQUITTO_PASSWORDFILE}" ] 7 | then 8 | echo "$0: File '${MOSQUITTO_PASSWORDFILE}' not found." 9 | exit 1 10 | fi 11 | 12 | # Convert the password file. 13 | mosquitto_passwd -U $MOSQUITTO_PASSWORDFILE 14 | 15 | exec "$@" 16 | -------------------------------------------------------------------------------- /Sources/MQTT/DataEncodable.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public protocol DataEncodable { 4 | func encode() -> Data 5 | } 6 | 7 | extension Data: DataEncodable { 8 | public func encode() -> Data { 9 | self 10 | } 11 | } 12 | 13 | extension String: DataEncodable { 14 | public func encode() -> Data { 15 | Data(utf8) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/FixedHeader.swift: -------------------------------------------------------------------------------- 1 | /// # Reference 2 | /// [Fixed header](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718020) 3 | public struct FixedHeader { 4 | let packetType: MQTTPacketType 5 | let flags: UInt8 6 | 7 | var byte1: UInt8 { 8 | (packetType.rawValue << 4) | (flags & 0b0000_1111) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/MQTT/Error.swift: -------------------------------------------------------------------------------- 1 | public enum MQTTError: Error { 2 | case channelConnectPending 3 | case brokerConnectPending 4 | case notConnected 5 | case connectTimeout 6 | case decodeError(DecodeError) 7 | } 8 | 9 | public enum DecodeError: Error { 10 | case malformedData 11 | case malformedQoS 12 | case malformedPacketType 13 | case malformedConnAckReturnCode 14 | case malformedSubAckReturnCode 15 | } 16 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/QoS.swift: -------------------------------------------------------------------------------- 1 | /// # Reference 2 | /// [Quality of Service levels and protocol flows](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718099) 3 | public enum QoS: UInt8 { 4 | case atMostOnce = 0 5 | case atLeastOnce = 1 6 | case exactlyOnce = 2 7 | 8 | init(value: UInt8) throws { 9 | guard let qos = QoS(rawValue: value) else { 10 | throw DecodeError.malformedQoS 11 | } 12 | self = qos 13 | } 14 | } 15 | 16 | public let QOS = (QoS.atMostOnce, QoS.atLeastOnce, QoS.exactlyOnce) 17 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "swift-nio", 6 | "repositoryURL": "https://github.com/apple/swift-nio.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "acf5465b5e7fb9aeda54a34d16fb44c31a399715", 10 | "version": "2.20.2" 11 | } 12 | }, 13 | { 14 | "package": "swift-nio-ssl", 15 | "repositoryURL": "https://github.com/apple/swift-nio-ssl.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "ea1dfd64193bf5af4490635a4a44c4fb43b1e1ae", 19 | "version": "2.9.1" 20 | } 21 | } 22 | ] 23 | }, 24 | "version": 1 25 | } 26 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/PUBLISH/PubAck.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [PUBACK – Publish acknowledgement](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718043) 5 | public final class PubAckPacket: MQTTPacket { 6 | private let variableHeader: VariableHeader 7 | 8 | public var identifier: UInt16 { 9 | variableHeader.identifier 10 | } 11 | 12 | init(fixedHeader: FixedHeader, data: inout Data) throws { 13 | guard data.hasSize(2) else { 14 | throw DecodeError.malformedData 15 | } 16 | variableHeader = VariableHeader(identifier: data.read2BytesInt()) 17 | super.init(fixedHeader: fixedHeader) 18 | } 19 | } 20 | 21 | extension PubAckPacket { 22 | struct VariableHeader { 23 | let identifier: UInt16 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/UNSUBSCRIBE/UnsubAck.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [UNSUBACK – Unsubscribe acknowledgement](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718077) 5 | final class UnsubAckPacket: MQTTPacket { 6 | private let variableHeader: VariableHeader 7 | 8 | var identifier: UInt16 { 9 | variableHeader.identifier 10 | } 11 | 12 | init(fixedHeader: FixedHeader, data: inout Data) throws { 13 | guard data.hasSize(2) else { 14 | throw DecodeError.malformedData 15 | } 16 | variableHeader = VariableHeader(identifier: data.read2BytesInt()) 17 | super.init(fixedHeader: fixedHeader) 18 | } 19 | } 20 | 21 | extension UnsubAckPacket { 22 | struct VariableHeader { 23 | let identifier: UInt16 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/PUBLISH/PubRec.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [PUBREC – Publish received (QoS 2 publish received, part 1)](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718048) 5 | public final class PubRecPacket: MQTTPacket { 6 | private let variableHeader: VariableHeader 7 | 8 | public var identifier: UInt16 { 9 | variableHeader.identifier 10 | } 11 | 12 | init(fixedHeader: FixedHeader, data: inout Data) throws { 13 | guard data.hasSize(2) else { 14 | throw DecodeError.malformedData 15 | } 16 | variableHeader = VariableHeader(identifier: data.read2BytesInt()) 17 | super.init(fixedHeader: fixedHeader) 18 | } 19 | } 20 | 21 | extension PubRecPacket { 22 | struct VariableHeader { 23 | let identifier: UInt16 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/PUBLISH/PubComp.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [PUBCOMP – Publish complete (QoS 2 publish received, part 3)](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718058) 5 | public final class PubCompPacket: MQTTPacket { 6 | private let variableHeader: VariableHeader 7 | 8 | public var identifier: UInt16 { 9 | variableHeader.identifier 10 | } 11 | 12 | init(fixedHeader: FixedHeader, data: inout Data) throws { 13 | guard data.hasSize(2) else { 14 | throw DecodeError.malformedData 15 | } 16 | variableHeader = VariableHeader(identifier: data.read2BytesInt()) 17 | super.init(fixedHeader: fixedHeader) 18 | } 19 | } 20 | 21 | extension PubCompPacket { 22 | struct VariableHeader { 23 | let identifier: UInt16 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/PUBLISH/PubRel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [PUBREL – Publish release (QoS 2 publish received, part 2)](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718053) 5 | final class PubRelPacket: MQTTPacket { 6 | let variableHeader: VariableHeader 7 | 8 | var identifier: UInt16 { 9 | variableHeader.identifier 10 | } 11 | 12 | init(identifier: UInt16) { 13 | variableHeader = VariableHeader(identifier: identifier) 14 | super.init(packetType: .pubrel, flags: 0b0010) 15 | } 16 | 17 | override func encode() -> Data { 18 | encode(variableHeader: variableHeader, payload: nil) 19 | } 20 | } 21 | 22 | extension PubRelPacket { 23 | struct VariableHeader: DataEncodable { 24 | let identifier: UInt16 25 | 26 | func encode() -> Data { 27 | var data = Data() 28 | data.write(identifier) 29 | return data 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-mqtt", 8 | products: [ 9 | .library(name: "MQTT", targets: ["MQTT"]), 10 | ], 11 | dependencies: [ 12 | .package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"), 13 | .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"), 14 | ], 15 | targets: [ 16 | .target( 17 | name: "MQTT", 18 | dependencies: [ 19 | .product(name: "NIO", package: "swift-nio"), 20 | .product(name: "NIOSSL", package: "swift-nio-ssl"), 21 | ] 22 | ), 23 | .testTarget( 24 | name: "MQTTTests", 25 | dependencies: ["MQTT"] 26 | ), 27 | .target( 28 | name: "SampleClient", 29 | dependencies: ["MQTT"] 30 | ), 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Yuma Matsune 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/MQTTPacketType.swift: -------------------------------------------------------------------------------- 1 | /// # Reference 2 | /// [MQTT Control Packet Type](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718021) 3 | public enum MQTTPacketType: UInt8 { 4 | /// Client request to connect to Server 5 | case connect = 0x01 6 | /// Connect acknowledgment 7 | case connack = 0x02 8 | /// Publish message 9 | case publish = 0x03 10 | /// Publish acknowledgment 11 | case puback = 0x04 12 | /// Publish received (assured delivery part 1) 13 | case pubrec = 0x05 14 | /// Publish release (assured delivery part 2) 15 | case pubrel = 0x06 16 | /// Publish complete (assured delivery part 3) 17 | case pubcomp = 0x07 18 | /// Client subscribe request 19 | case subscribe = 0x08 20 | /// Subscribe acknowledgment 21 | case suback = 0x09 22 | /// Unsubscribe request 23 | case unsubscribe = 0x0A 24 | /// Unsubscribe acknowledgment 25 | case unsuback = 0x0B 26 | /// PING request 27 | case pingreq = 0x0C 28 | /// PING response 29 | case pingresp = 0x0D 30 | /// Client is disconnecting 31 | case disconnect = 0x0E 32 | 33 | init(packet: UInt8) throws { 34 | guard let type = MQTTPacketType(rawValue: packet) else { 35 | throw DecodeError.malformedPacketType 36 | } 37 | self = type 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/UNSUBSCRIBE/Unsubscribe.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [UNSUBSCRIBE – Unsubscribe from topics](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718072) 5 | class UnsubscribePacket: MQTTPacket { 6 | let variableHeader: VariableHeader 7 | let payload: Payload 8 | 9 | var identifier: UInt16 { 10 | variableHeader.identifier 11 | } 12 | 13 | var topicFilters: [String] { 14 | payload.topicFilters 15 | } 16 | 17 | init(identifier: UInt16, topicFilters: [String]) { 18 | variableHeader = VariableHeader(identifier: identifier) 19 | payload = Payload(topicFilters: topicFilters) 20 | super.init(packetType: .unsubscribe, flags: 0b0010) 21 | } 22 | 23 | override func encode() -> Data { 24 | encode(variableHeader: variableHeader, payload: payload) 25 | } 26 | } 27 | 28 | extension UnsubscribePacket { 29 | struct VariableHeader: DataEncodable { 30 | let identifier: UInt16 31 | 32 | func encode() -> Data { 33 | var data = Data() 34 | data.write(identifier) 35 | return data 36 | } 37 | } 38 | 39 | struct Payload: DataEncodable { 40 | let topicFilters: [String] 41 | 42 | func encode() -> Data { 43 | var data = Data() 44 | topicFilters.forEach { data.write($0) } 45 | return data 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/MQTT/MQTTChannelHandler.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | 4 | protocol MQTTChannelHandlerDelegate: AnyObject { 5 | func didReceive(packet: MQTTPacket) 6 | func didCatch(decodeError error: DecodeError) 7 | func channelActive(channel: Channel) 8 | func channelInactive() 9 | } 10 | 11 | final class MQTTChannelHandler: ChannelInboundHandler { 12 | typealias InboundIn = ByteBuffer 13 | 14 | private let decoder: MQTTDecoder 15 | weak var delegate: MQTTChannelHandlerDelegate? 16 | 17 | init(decoder: MQTTDecoder = MQTTDecoder()) { 18 | self.decoder = decoder 19 | } 20 | 21 | func channelRead(context _: ChannelHandlerContext, data: NIOAny) { 22 | var buf = unwrapInboundIn(data) 23 | if let bytes = buf.readBytes(length: buf.readableBytes) { 24 | var data = Data(bytes) 25 | do { 26 | let packet = try decoder.decode(data: &data) 27 | delegate?.didReceive(packet: packet) 28 | } catch let error as DecodeError { 29 | delegate?.didCatch(decodeError: error) 30 | } catch { 31 | // unhandled error 32 | fatalError("Error while decoding packet data: \(error.localizedDescription)") 33 | } 34 | } 35 | } 36 | 37 | func channelActive(context: ChannelHandlerContext) { 38 | delegate?.channelActive(channel: context.channel) 39 | } 40 | 41 | func channelInactive(context _: ChannelHandlerContext) { 42 | delegate?.channelInactive() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/MQTTPacket.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// MQTT Control Packet 4 | /// 5 | /// # Reference 6 | /// [MQTT Control Packet format](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718018) 7 | public class MQTTPacket { 8 | let fixedHeader: FixedHeader 9 | 10 | init(packetType: MQTTPacketType, flags: UInt8) { 11 | fixedHeader = FixedHeader(packetType: packetType, flags: flags) 12 | } 13 | 14 | init(fixedHeader: FixedHeader) { 15 | self.fixedHeader = fixedHeader 16 | } 17 | 18 | func encode() -> Data { 19 | encode(variableHeader: nil, payload: nil) 20 | } 21 | 22 | func encode(variableHeader: DataEncodable?, payload: DataEncodable?) -> Data { 23 | var body = Data() 24 | if let variableHeader = variableHeader { 25 | body.append(variableHeader.encode()) 26 | } 27 | if let payload = payload { 28 | body.append(payload.encode()) 29 | } 30 | var data = Data() 31 | data.append(fixedHeader.byte1) 32 | data.append(encodeRemainLen(body.count)) 33 | data.append(body) 34 | return data 35 | } 36 | } 37 | 38 | internal func encodeRemainLen(_ length: Int) -> Data { 39 | var data = Data(capacity: 4) 40 | var x = length 41 | var encodedByte: UInt8 = 0 42 | repeat { 43 | encodedByte = UInt8(x % 128) 44 | x /= 128 45 | if x > 0 { 46 | encodedByte |= 128 47 | } 48 | data.append(encodedByte) 49 | } while x > 0 50 | return data 51 | } 52 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/CONNECT/ConnAck.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [CONNACK – Acknowledge connection request](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718033) 5 | public final class ConnAckPacket: MQTTPacket { 6 | private let variableHeader: VariableHeader 7 | 8 | public var returnCode: ReturnCode { 9 | variableHeader.returnCode 10 | } 11 | 12 | public var sessionPresent: Bool { 13 | variableHeader.sessionPresent 14 | } 15 | 16 | init(fixedHeader: FixedHeader, data: inout Data) throws { 17 | guard data.hasSize(2) else { 18 | throw DecodeError.malformedData 19 | } 20 | let sessionPresent = (data.read1ByteInt() & 1) == 1 21 | let returnCode = try ReturnCode(code: data.read1ByteInt()) 22 | variableHeader = VariableHeader(sessionPresent: sessionPresent, returnCode: returnCode) 23 | super.init(fixedHeader: fixedHeader) 24 | } 25 | } 26 | 27 | extension ConnAckPacket { 28 | struct VariableHeader { 29 | let sessionPresent: Bool 30 | let returnCode: ReturnCode 31 | } 32 | 33 | public enum ReturnCode: UInt8 { 34 | case accepted = 0 35 | case unaceptable = 1 36 | case identifierRejected = 2 37 | case serverUnavailable = 3 38 | case badUserCredential = 4 39 | case notAuthorized = 5 40 | 41 | init(code: UInt8) throws { 42 | guard let returnCode = ReturnCode(rawValue: code) else { 43 | throw DecodeError.malformedConnAckReturnCode 44 | } 45 | self = returnCode 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/SUBSCRIBE/Subscribe.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [SUBSCRIBE - Subscribe to topics](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718063) 5 | final class SubscribePacket: MQTTPacket { 6 | let variableHeader: VariableHeader 7 | let payload: Payload 8 | 9 | var identifier: UInt16 { 10 | variableHeader.identifier 11 | } 12 | 13 | var topicFilters: [TopicFilter] { 14 | payload.topicFilters 15 | } 16 | 17 | init(identifier: UInt16, topicFilters: [TopicFilter]) { 18 | variableHeader = VariableHeader(identifier: identifier) 19 | payload = Payload(topicFilters: topicFilters) 20 | super.init(packetType: .subscribe, flags: 0b0010) 21 | } 22 | 23 | override func encode() -> Data { 24 | encode(variableHeader: variableHeader, payload: payload) 25 | } 26 | } 27 | 28 | extension SubscribePacket { 29 | struct VariableHeader: DataEncodable { 30 | let identifier: UInt16 31 | 32 | func encode() -> Data { 33 | var data = Data() 34 | data.write(identifier) 35 | return data 36 | } 37 | } 38 | 39 | struct Payload: DataEncodable { 40 | let topicFilters: [TopicFilter] 41 | 42 | func encode() -> Data { 43 | var data = Data() 44 | topicFilters.forEach { data.append($0.encode()) } 45 | return data 46 | } 47 | } 48 | } 49 | 50 | public struct TopicFilter { 51 | let topic: String 52 | let qos: QoS 53 | 54 | func encode() -> Data { 55 | var data = Data() 56 | data.write(topic) 57 | data.write(qos.rawValue) 58 | return data 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server/gen_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # ref: https://gist.github.com/zapstar/4b51d7cfa74c7e709fcdaace19233443 4 | 5 | DIR="mosquitto/certs" 6 | 7 | mkdir $DIR 8 | cd $DIR 9 | 10 | # Generate a self signed certificate for the CA along with a key. 11 | mkdir -p ca/private 12 | chmod 700 ca/private 13 | # NOTE: I'm using -nodes, this means that once anybody gets 14 | # their hands on this particular key, they can become this CA. 15 | openssl req \ 16 | -x509 \ 17 | -nodes \ 18 | -days 3650 \ 19 | -newkey rsa:4096 \ 20 | -keyout ca/private/ca_key.pem \ 21 | -out ca/ca_cert.pem \ 22 | -subj "/C=US/ST=Acme State/L=Acme City/O=Acme Inc./CN=example.com" 23 | 24 | # Create server private key and certificate request 25 | mkdir -p server/private 26 | chmod 700 ca/private 27 | openssl genrsa -out server/private/server_key.pem 4096 28 | openssl req -new \ 29 | -key server/private/server_key.pem \ 30 | -out server/server.csr \ 31 | -subj "/C=US/ST=Acme State/L=Acme City/O=Acme Inc./CN=server.example.com" 32 | 33 | # Create client private key and certificate request 34 | mkdir -p client/private 35 | chmod 700 client/private 36 | openssl genrsa -out client/private/client_key.pem 4096 37 | openssl req -new \ 38 | -key client/private/client_key.pem \ 39 | -out client/client.csr \ 40 | -subj "/C=US/ST=Acme State/L=Acme City/O=Acme Inc./CN=client.example.com" 41 | 42 | # Generate certificates 43 | openssl x509 -req -days 1460 -in server/server.csr \ 44 | -CA ca/ca_cert.pem -CAkey ca/private/ca_key.pem \ 45 | -CAcreateserial -out server/server_cert.pem 46 | openssl x509 -req -days 1460 -in client/client.csr \ 47 | -CA ca/ca_cert.pem -CAkey ca/private/ca_key.pem \ 48 | -CAcreateserial -out client/client_cert.pem 49 | 50 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/SUBSCRIBE/SubAck.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [SUBACK – Subscribe acknowledgement](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718068) 5 | public final class SubAckPacket: MQTTPacket { 6 | private let variableHeader: VariableHeader 7 | 8 | public let returnCodes: [ReturnCode] 9 | 10 | public var identifier: UInt16 { 11 | variableHeader.identifier 12 | } 13 | 14 | init(fixedHeader: FixedHeader, data: inout Data) throws { 15 | guard data.hasSize(2) else { 16 | throw DecodeError.malformedData 17 | } 18 | variableHeader = VariableHeader(identifier: data.read2BytesInt()) 19 | returnCodes = try data.map { try ReturnCode(code: $0) } 20 | super.init(fixedHeader: fixedHeader) 21 | } 22 | } 23 | 24 | extension SubAckPacket { 25 | public enum ReturnCode: Equatable { 26 | case success(QoS) 27 | case failure 28 | 29 | init(code: UInt8) throws { 30 | switch code { 31 | case 0x00, 0x01, 0x02: 32 | self = .success(try QoS(value: code)) 33 | case 0x80: 34 | self = .failure 35 | default: 36 | throw DecodeError.malformedSubAckReturnCode 37 | } 38 | } 39 | 40 | public static func == (lhs: ReturnCode, rhs: ReturnCode) -> Bool { 41 | switch (lhs, rhs) { 42 | case (.failure, .failure): 43 | return true 44 | case let (.success(qosL), .success(qosR)): 45 | return qosL == qosR 46 | default: 47 | return false 48 | } 49 | } 50 | } 51 | 52 | struct VariableHeader { 53 | let identifier: UInt16 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/MQTTTests/RemainLengthTests.swift: -------------------------------------------------------------------------------- 1 | @testable import MQTT 2 | import XCTest 3 | 4 | final class RemainLengthTests: XCTestCase { 5 | func testEncodeRemainLen() { 6 | XCTAssert(encodeRemainLen(0) == Data([0])) 7 | XCTAssert(encodeRemainLen(127) == Data([0x7F])) 8 | XCTAssert(encodeRemainLen(128) == Data([0x80, 0x01])) 9 | XCTAssert(encodeRemainLen(16383) == Data([0xFF, 0x7F])) 10 | XCTAssert(encodeRemainLen(16384) == Data([0x80, 0x80, 0x01])) 11 | XCTAssert(encodeRemainLen(2_097_151) == Data([0xFF, 0xFF, 0x7F])) 12 | XCTAssert(encodeRemainLen(2_097_152) == Data([0x80, 0x80, 0x80, 0x01])) 13 | XCTAssert(encodeRemainLen(268_435_455) == Data([0xFF, 0xFF, 0xFF, 0x7F])) 14 | } 15 | 16 | func testDecodeRemainingLength() { 17 | var data = Data([0x00]) 18 | XCTAssert(try decodeRemainLen(data: &data) == 0) 19 | data = Data([0x7F]) 20 | XCTAssert(try decodeRemainLen(data: &data) == 127) 21 | data = Data([0x80, 0x01]) 22 | XCTAssert(try decodeRemainLen(data: &data) == 128) 23 | data = Data([0xFF, 0x7F]) 24 | XCTAssert(try decodeRemainLen(data: &data) == 16383) 25 | data = Data([0x80, 0x80, 0x01]) 26 | XCTAssert(try decodeRemainLen(data: &data) == 16384) 27 | data = Data([0xFF, 0xFF, 0x7F]) 28 | XCTAssert(try decodeRemainLen(data: &data) == 2_097_151) 29 | data = Data([0x80, 0x80, 0x80, 0x01]) 30 | XCTAssert(try decodeRemainLen(data: &data) == 2_097_152) 31 | data = Data([0xFF, 0xFF, 0xFF, 0x7F]) 32 | XCTAssert(try decodeRemainLen(data: &data) == 268_435_455) 33 | } 34 | 35 | static var allTests = [ 36 | ("testEncodeRemainLen", testEncodeRemainLen), 37 | ("testDecodeRemainingLength", testDecodeRemainingLength), 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /Sources/MQTT/Extension/Data.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Data { 4 | /// Append 8bit data. 5 | mutating func write(_ data: UInt8) { 6 | var data = data 7 | append(&data, count: 1) 8 | } 9 | 10 | /// Append 16 bits integer value in big-endian order. 11 | mutating func write(_ data: UInt16) { 12 | let msb = UInt8(data / 256) 13 | let lsb = UInt8(data % 256) 14 | append(contentsOf: [msb, lsb]) 15 | } 16 | 17 | /// Append string value encoded as UTF-8 prefixed with 16 bits length field. 18 | mutating func write(_ string: String) { 19 | write(UInt16(string.count)) 20 | append(contentsOf: string.utf8) 21 | } 22 | 23 | mutating func advance(_ bytes: Int) { 24 | if count <= bytes { 25 | self = Data() 26 | } else { 27 | self = advanced(by: bytes) 28 | } 29 | } 30 | 31 | mutating func read1ByteInt() -> UInt8 { 32 | let value = self[0] 33 | advance(1) 34 | return value 35 | } 36 | 37 | /// Read 16 bits integer value ordered by big-endian and advance 2 bytes. 38 | mutating func read2BytesInt() -> UInt16 { 39 | let value = UInt16(self[0]) << 8 | UInt16(self[1]) 40 | advance(2) 41 | return value 42 | } 43 | 44 | /// Read and advance n-bytes. 45 | mutating func read(bytes: Int) -> Data { 46 | let m = Swift.min(bytes, count) 47 | let data = subdata(in: 0 ..< m) 48 | advance(bytes) 49 | return data 50 | } 51 | 52 | /// Read and advance n-bytes. 53 | mutating func read(bytes: UInt16) -> Data { 54 | return read(bytes: Int(bytes)) 55 | } 56 | 57 | /// Check remain size before read and advance. 58 | func hasSize(_ size: UInt16) -> Bool { 59 | count >= size 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/MQTT/MQTTDecoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class MQTTDecoder { 4 | func decode(data: inout Data) throws -> MQTTPacket { 5 | guard data.hasSize(1) else { 6 | throw DecodeError.malformedData 7 | } 8 | let byte1 = data.read1ByteInt() 9 | let packetType = try MQTTPacketType(packet: byte1 >> 4) 10 | let flags = byte1 & 0x0F 11 | let fixedHeader = FixedHeader(packetType: packetType, flags: flags) 12 | let remainLen = try decodeRemainLen(data: &data) 13 | if data.count < remainLen { 14 | throw DecodeError.malformedData 15 | } 16 | data = data[.. Int { 41 | var multiplier = 1 42 | var value = 0 43 | var encodedByte: UInt8 = 0 44 | repeat { 45 | guard data.hasSize(1) else { 46 | throw DecodeError.malformedData 47 | } 48 | encodedByte = data.read1ByteInt() 49 | value += (Int(encodedByte) & 127) * multiplier 50 | multiplier *= 128 51 | if multiplier > 128 * 128 * 128 { 52 | break 53 | } 54 | } while (encodedByte & 128) != 0 55 | return value 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-mqtt 2 | 3 | Asynchronous MQTT client library using [SwiftNIO](https://github.com/apple/swift-nio) for networking layer. 4 | 5 | - Based on [MQTT Version 3.1.1 Specification](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028). 6 | - Supports SSL/TLS connection 7 | 8 | ## Usage 9 | 10 | Create an instance of `MQTTClient` with parameters for connection. 11 | 12 | ```swift 13 | let client = MQTTClient( 14 | host: "localhost", 15 | port: 1883, 16 | clientID: "swift-mqtt client", 17 | cleanSession: true, 18 | keepAlive: 30, 19 | willMessage: PublishMessage(topic: "will", payload: "will msg", retain: false, qos: .atMostOnce), 20 | ) 21 | client.connect() 22 | ``` 23 | 24 | You can handle events with delegate methods. 25 | 26 | ```swift 27 | client.delegate = self 28 | 29 | ... 30 | 31 | // MQTTClientDelegate 32 | 33 | func mqttClient(_ client: MQTTClient, didReceive packet: MQTTPacket) { 34 | ... 35 | } 36 | 37 | func mqttClient(_ client: MQTTClient, didChange state: ConnectionState) { 38 | ... 39 | } 40 | 41 | func mqttClient(_ client: MQTTClient, didCatchError error: Error) { 42 | ... 43 | } 44 | ``` 45 | 46 | ### Publish 47 | 48 | ```swift 49 | client.publish(topic: "topic", retain: false, qos: QOS.0, payload: "payload") 50 | ``` 51 | 52 | ### Subscribe 53 | 54 | ```swift 55 | client.subscribe(topic: "topic", qos: QOS.0) 56 | ``` 57 | 58 | ### Unsubscribe 59 | 60 | ```swift 61 | client.unsubscribe(topic: "topic") 62 | ``` 63 | 64 | ### Disconnect 65 | 66 | ```swift 67 | client.disconnect() 68 | ``` 69 | 70 | ## SSL/TLS connection 71 | 72 | This library uses [SwiftNIO SSL](https://github.com/apple/swift-nio-ssl) for SSL connection. You can configure settings of a client. 73 | 74 | ```swift 75 | let caCert = "./server/certs/ca/ca_cert.pem" 76 | let clientCert = "./server/certs/client/client_cert.pem" 77 | let keyCert = "./server/certs/client/private/client_key.pem" 78 | let tlsConfiguration = try? TLSConfiguration.forClient( 79 | minimumTLSVersion: .tlsv11, 80 | maximumTLSVersion: .tlsv12, 81 | certificateVerification: .noHostnameVerification, 82 | trustRoots: NIOSSLTrustRoots.certificates(NIOSSLCertificate.fromPEMFile(caCert)), 83 | certificateChain: NIOSSLCertificate.fromPEMFile(clientCert).map { .certificate($0) }, 84 | privateKey: .privateKey(.init(file: keyCert, format: .pem)) 85 | ) 86 | 87 | client.tlsConfiguration = tlsConfiguration 88 | ``` 89 | -------------------------------------------------------------------------------- /Sources/SampleClient/main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import MQTT 3 | import NIOSSL 4 | 5 | let base: String 6 | if CommandLine.arguments.count > 1 { 7 | base = CommandLine.arguments[1] 8 | } else { 9 | base = "." 10 | } 11 | 12 | let sem = DispatchSemaphore(value: 0) 13 | let queue = DispatchQueue(label: "a", qos: .background) 14 | 15 | class App: MQTTClientDelegate { 16 | let client: MQTTClient 17 | 18 | var delegateDispatchQueue: DispatchQueue { 19 | queue 20 | } 21 | 22 | init() { 23 | let caCert = "\(base)/server/mosquitto/certs/ca/ca_cert.pem" 24 | let clientCert = "\(base)/server/mosquitto/certs/client/client_cert.pem" 25 | let keyCert = "\(base)/server/mosquitto/certs/client/private/client_key.pem" 26 | let tlsConfiguration = try! TLSConfiguration.forClient(minimumTLSVersion: .tlsv11, 27 | maximumTLSVersion: .tlsv12, 28 | certificateVerification: .noHostnameVerification, 29 | trustRoots: NIOSSLTrustRoots.certificates(NIOSSLCertificate.fromPEMFile(caCert)), 30 | certificateChain: NIOSSLCertificate.fromPEMFile(clientCert).map { .certificate($0) }, 31 | privateKey: .privateKey(.init(file: keyCert, format: .pem))) 32 | client = MQTTClient( 33 | host: "localhost", 34 | port: 8883, 35 | clientID: "swift-mqtt client", 36 | cleanSession: true, 37 | keepAlive: 30, 38 | willMessage: PublishMessage(topic: "will", payload: "will msg", retain: false, qos: .atMostOnce), 39 | username: "swift-mqtt", 40 | password: "swift-mqtt", 41 | tlsConfiguration: tlsConfiguration 42 | ) 43 | client.tlsConfiguration = tlsConfiguration 44 | client.delegate = self 45 | } 46 | 47 | func run() throws { 48 | client.connect() 49 | } 50 | 51 | func mqttClient(_ client: MQTTClient, didReceive packet: MQTTPacket) { 52 | switch packet { 53 | case let packet as ConnAckPacket: 54 | print("Connack \(packet)") 55 | client.subscribe(topic: "a", qos: QOS.0) 56 | client.publish(topic: "a", retain: false, qos: QOS.0, payload: "abc") 57 | queue.asyncAfter(deadline: .now() + 40) { 58 | self.client.disconnect() 59 | } 60 | default: 61 | print(packet) 62 | } 63 | } 64 | 65 | func mqttClient(_: MQTTClient, didChange state: ConnectionState) { 66 | if state == .disconnected { 67 | sem.signal() 68 | } 69 | print(state) 70 | } 71 | 72 | func mqttClient(_: MQTTClient, didCatchError error: Error) { 73 | print("Error: \(error)") 74 | } 75 | } 76 | 77 | let app = App() 78 | try app.run() 79 | 80 | sem.wait() 81 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/PUBLISH/Publish.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | /// # Reference 4 | /// [PUBLISH – Publish message](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718037) 5 | public final class PublishPacket: MQTTPacket { 6 | public let variableHeader: VariableHeader 7 | public let payload: Data 8 | 9 | public var qos: QoS { 10 | QoS(rawValue: fixedHeader.flags >> 1 & 0b11)! 11 | } 12 | 13 | public var topic: String { 14 | variableHeader.topic 15 | } 16 | 17 | public var identifier: UInt16? { 18 | variableHeader.identifier 19 | } 20 | 21 | init(topic: String, identifier: UInt16?, retain: Bool, qos: QoS, payload: DataEncodable) { 22 | var flags: UInt8 = 0 23 | flags |= qos.rawValue << 1 24 | flags |= retain ? 1 : 0 25 | if qos == .atMostOnce { 26 | // QoS = 0 27 | if identifier != nil { 28 | fatalError("A PUBLISH Packet MUST NOT contain a Packet Identifier if its QoS value is set to 0") 29 | } 30 | } else { 31 | // QoS > 0 32 | guard let identifier = identifier, identifier > 0 else { 33 | fatalError("A PUBLISH Packet with QoS > 0 Control Packets MUST contain a non-zero 16-bit Packet Identifier ") 34 | } 35 | } 36 | variableHeader = VariableHeader(topic: topic, identifier: identifier) 37 | self.payload = payload.encode() 38 | super.init(fixedHeader: FixedHeader(packetType: .publish, flags: flags)) 39 | } 40 | 41 | init(fixedHeader: FixedHeader, data: inout Data) throws { 42 | let qos = try QoS(value: (fixedHeader.flags >> 1) & 0b11) 43 | guard data.hasSize(2) else { 44 | throw DecodeError.malformedData 45 | } 46 | let topicLen = data.read2BytesInt() 47 | guard data.hasSize(topicLen) else { 48 | throw DecodeError.malformedData 49 | } 50 | let topic = String(data: data.read(bytes: topicLen), encoding: .utf8) ?? "" 51 | 52 | let identifier: UInt16? 53 | if qos == .atMostOnce { 54 | identifier = nil 55 | } else { 56 | guard data.hasSize(2) else { 57 | throw DecodeError.malformedData 58 | } 59 | identifier = data.read2BytesInt() 60 | } 61 | variableHeader = VariableHeader(topic: topic, identifier: identifier) 62 | payload = data 63 | super.init(fixedHeader: fixedHeader) 64 | } 65 | 66 | override func encode() -> Data { 67 | encode(variableHeader: variableHeader, payload: payload) 68 | } 69 | } 70 | 71 | extension PublishPacket { 72 | public struct VariableHeader: DataEncodable { 73 | public let topic: String 74 | public let identifier: UInt16? 75 | 76 | public func encode() -> Data { 77 | var data = Data() 78 | data.write(topic) 79 | if let identifier = identifier { 80 | data.write(identifier) 81 | } 82 | return data 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/MQTT/Packet/CONNECT/Connect.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct PublishMessage { 4 | let topic: String 5 | let payload: DataEncodable 6 | let retain: Bool 7 | let qos: QoS 8 | 9 | public init(topic: String, payload: DataEncodable, retain: Bool, qos: QoS) { 10 | self.topic = topic 11 | self.payload = payload 12 | self.retain = retain 13 | self.qos = qos 14 | } 15 | } 16 | 17 | /// # Reference 18 | /// [CONNECT – Client requests a connection to a Server](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718028) 19 | final class ConnectPacket: MQTTPacket { 20 | let clientID: String 21 | let cleanSession: Bool 22 | let will: PublishMessage? 23 | let username: String? 24 | let password: String? 25 | let keepAlive: UInt16 26 | 27 | init( 28 | clientID: String, 29 | cleanSession: Bool, 30 | will: PublishMessage?, 31 | username: String?, 32 | password: String?, 33 | keepAlive: UInt16 34 | ) { 35 | self.clientID = clientID 36 | self.cleanSession = cleanSession 37 | self.will = will 38 | self.username = username 39 | self.password = password 40 | self.keepAlive = keepAlive 41 | super.init(packetType: .connect, flags: 0) 42 | } 43 | 44 | var variableHeader: VariableHeader { 45 | var flags: VariableHeader.Flags = [] 46 | if cleanSession { 47 | flags.insert(.cleanSession) 48 | } 49 | if let will = will { 50 | flags.insert(.will) 51 | flags.insert(.qos(will.qos)) 52 | if will.retain { 53 | flags.insert(.willRetain) 54 | } 55 | } 56 | if username != nil { 57 | flags.insert(.hasUsername) 58 | } 59 | if password != nil { 60 | flags.insert(.hasPassword) 61 | } 62 | return VariableHeader(flags: flags, keepAlive: keepAlive) 63 | } 64 | 65 | var payload: Payload { 66 | Payload(clientID: clientID, will: will, username: username, password: password) 67 | } 68 | 69 | override func encode() -> Data { 70 | encode(variableHeader: variableHeader, payload: payload) 71 | } 72 | } 73 | 74 | extension ConnectPacket { 75 | struct VariableHeader: DataEncodable { 76 | struct Flags: OptionSet { 77 | let rawValue: UInt8 78 | 79 | static let cleanSession = Flags(rawValue: 1 << 1) 80 | static let will = Flags(rawValue: 1 << 2) 81 | static func qos(_ qos: QoS) -> Flags { 82 | Flags(rawValue: qos.rawValue << 3) 83 | } 84 | 85 | static let willRetain = Flags(rawValue: 1 << 5) 86 | static let hasUsername = Flags(rawValue: 1 << 6) 87 | static let hasPassword = Flags(rawValue: 1 << 7) 88 | } 89 | 90 | let protocolName: String = "MQTT" 91 | let protocolLevel: UInt8 = 0x04 92 | let flags: Flags 93 | let keepAlive: UInt16 94 | 95 | func encode() -> Data { 96 | var data = Data() 97 | data.write(protocolName) 98 | data.write(protocolLevel) 99 | data.write(flags.rawValue) 100 | data.write(keepAlive) 101 | return data 102 | } 103 | } 104 | 105 | struct Payload: DataEncodable { 106 | let clientID: String 107 | let will: PublishMessage? 108 | let username: String? 109 | let password: String? 110 | 111 | func encode() -> Data { 112 | var data = Data() 113 | data.write(clientID) 114 | if let will = will { 115 | data.write(will.topic) 116 | let payload = will.payload.encode() 117 | data.write(UInt16(payload.count)) 118 | data.append(payload) 119 | } 120 | if let username = username { 121 | data.write(username) 122 | } 123 | if let password = password { 124 | data.write(password) 125 | } 126 | return data 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /Tests/MQTTTests/MQTTTests.swift: -------------------------------------------------------------------------------- 1 | @testable import MQTT 2 | import NIOSSL 3 | import XCTest 4 | 5 | final class MQTTTests: XCTestCase { 6 | private var client: MQTTClient! 7 | 8 | private var didChangeStateCallback: (ConnectionState) -> Void = { _ in } 9 | private var didReceivePacketCallback: (MQTTPacket) -> Void = { _ in } 10 | 11 | override func setUp() { 12 | super.setUp() 13 | let caCert = "./server/mosquitto/certs/ca/ca_cert.pem" 14 | let clientCert = "./server/mosquitto/certs/client/client_cert.pem" 15 | let keyCert = "./server/mosquitto/certs/client/private/client_key.pem" 16 | let tlsConfiguration = try! TLSConfiguration.forClient(minimumTLSVersion: .tlsv11, 17 | maximumTLSVersion: .tlsv12, 18 | certificateVerification: .noHostnameVerification, 19 | trustRoots: NIOSSLTrustRoots.certificates(NIOSSLCertificate.fromPEMFile(caCert)), 20 | certificateChain: NIOSSLCertificate.fromPEMFile(clientCert).map { .certificate($0) }, 21 | privateKey: .privateKey(.init(file: keyCert, format: .pem))) 22 | client = MQTTClient(host: "localhost", 23 | port: 8883, 24 | clientID: "MQTTTests", 25 | cleanSession: true, 26 | keepAlive: 60, 27 | willMessage: nil, 28 | username: "swift-mqtt", 29 | password: "swift-mqtt", 30 | tlsConfiguration: tlsConfiguration, 31 | connectTimeout: 5) 32 | client.delegate = self 33 | 34 | let connect = expectation(description: "connect") 35 | didChangeStateCallback = { state in 36 | if state == .connected { 37 | connect.fulfill() 38 | } 39 | } 40 | client.connect() 41 | wait(for: [connect], timeout: 5) 42 | } 43 | 44 | override func tearDown() { 45 | super.tearDown() 46 | client.disconnect() 47 | client = nil 48 | } 49 | 50 | func testSubscribe() { 51 | let subscribe = expectation(description: "subscribe") 52 | let identifier: UInt16 = 10 53 | didReceivePacketCallback = { packet in 54 | switch packet { 55 | case let packet as SubAckPacket: 56 | XCTAssert(packet.identifier == identifier, "identifier, \(packet.identifier) != \(identifier)") 57 | let returnCodes: [SubAckPacket.ReturnCode] = [.success(.atMostOnce)] 58 | XCTAssert(packet.returnCodes == returnCodes, "return codes, \(packet.returnCodes) != \(returnCodes)") 59 | subscribe.fulfill() 60 | default: 61 | XCTFail() 62 | } 63 | } 64 | client.subscribe(topic: "test", qos: .atMostOnce, identifier: identifier) 65 | wait(for: [subscribe], timeout: 5) 66 | } 67 | 68 | func testPublish() { 69 | let subscribe = expectation(description: "subscribe") 70 | let publish = expectation(description: "publish") 71 | let identifier: UInt16 = 10 72 | let topic = "test" 73 | let payload = "test payload" 74 | didReceivePacketCallback = { packet in 75 | switch packet { 76 | case let packet as PublishPacket: 77 | XCTAssert(packet.topic == topic) 78 | XCTAssert(packet.payload == Data(payload.utf8)) 79 | publish.fulfill() 80 | case let packet as SubAckPacket: 81 | XCTAssert(packet.identifier == identifier) 82 | XCTAssert(packet.returnCodes == [.success(.atMostOnce)]) 83 | subscribe.fulfill() 84 | default: 85 | XCTFail() 86 | } 87 | } 88 | client.subscribe(topic: topic, qos: .atMostOnce, identifier: identifier) 89 | wait(for: [subscribe], timeout: 5) 90 | client.publish(topic: topic, retain: false, qos: .atMostOnce, payload: payload, identifier: identifier) 91 | wait(for: [publish], timeout: 5) 92 | } 93 | 94 | static var allTests = [ 95 | ("testSubscribe", testSubscribe), 96 | ("testPublish", testPublish), 97 | ] 98 | } 99 | 100 | extension MQTTTests: MQTTClientDelegate { 101 | func mqttClient(_: MQTTClient, didChange state: ConnectionState) { 102 | didChangeStateCallback(state) 103 | } 104 | 105 | func mqttClient(_: MQTTClient, didCatchError error: Error) { 106 | XCTFail("\(error)") 107 | } 108 | 109 | func mqttClient(_: MQTTClient, didReceive packet: MQTTPacket) { 110 | didReceivePacketCallback(packet) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/MQTT/MQTTClient.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIO 3 | import NIOSSL 4 | 5 | public enum ConnectionState { 6 | case disconnected 7 | case connectingChannel 8 | case connectingBroker 9 | case connected 10 | } 11 | 12 | public protocol MQTTClientDelegate: AnyObject { 13 | var delegateDispatchQueue: DispatchQueue { get } 14 | 15 | func mqttClient(_ client: MQTTClient, didReceive packet: MQTTPacket) 16 | func mqttClient(_ client: MQTTClient, didChange state: ConnectionState) 17 | func mqttClient(_ client: MQTTClient, didCatchError error: Error) 18 | } 19 | 20 | public extension MQTTClientDelegate { 21 | var delegateDispatchQueue: DispatchQueue { 22 | .main 23 | } 24 | 25 | func mqttClient(_: MQTTClient, didReceive _: MQTTPacket) {} 26 | 27 | func mqttClient(_: MQTTClient, didChange _: ConnectionState) {} 28 | 29 | func mqttClient(_: MQTTClient, didCatchError _: Error) {} 30 | } 31 | 32 | public class MQTTClient { 33 | public var host: String 34 | public var port: Int 35 | public var clientID: String 36 | public var cleanSession: Bool 37 | public var keepAlive: UInt16 38 | public var willMessage: PublishMessage? 39 | public var username: String? 40 | public var password: String? 41 | public var tlsConfiguration: TLSConfiguration? 42 | public var connectTimeout: Int64 43 | 44 | public weak var delegate: MQTTClientDelegate? 45 | 46 | private let group: EventLoopGroup 47 | 48 | private var _nextPacketIdentifier: UInt16 = 0 49 | private let lockQueue = DispatchQueue(label: "_nextPacketIdentifier") 50 | 51 | private var timeoutSchedule: Scheduled? 52 | private var pingTask: RepeatedTask? 53 | 54 | public init( 55 | host: String, 56 | port: Int, 57 | clientID: String = "", 58 | cleanSession: Bool, 59 | keepAlive: UInt16, 60 | willMessage: PublishMessage? = nil, 61 | username: String? = nil, 62 | password: String? = nil, 63 | tlsConfiguration: TLSConfiguration? = nil, 64 | connectTimeout: Int64 = 5 65 | ) { 66 | self.host = host 67 | self.port = port 68 | self.clientID = clientID 69 | self.cleanSession = cleanSession 70 | self.keepAlive = keepAlive 71 | self.willMessage = willMessage 72 | self.username = username 73 | self.password = password 74 | self.connectTimeout = connectTimeout 75 | self.tlsConfiguration = tlsConfiguration 76 | group = MultiThreadedEventLoopGroup(numberOfThreads: 1) 77 | } 78 | 79 | deinit { 80 | try? group.syncShutdownGracefully() 81 | } 82 | 83 | private var channelState: ChannelState = .disconnected { 84 | didSet { 85 | switch channelState { 86 | case .disconnected: 87 | cancelPingTimer() 88 | case .connectingChannel: 89 | break 90 | case .connectingBroker: 91 | break 92 | case .connected: 93 | cancelTimeoutTimer() 94 | startPingTimer() 95 | } 96 | let connectionState = channelState.connectionState 97 | delegate?.delegateDispatchQueue.async { 98 | self.delegate?.mqttClient(self, didChange: connectionState) 99 | } 100 | } 101 | } 102 | 103 | public var state: ConnectionState { 104 | channelState.connectionState 105 | } 106 | 107 | private func nextPacketIdentifier() -> UInt16 { 108 | lockQueue.sync { 109 | if _nextPacketIdentifier == 0xFFFF { 110 | _nextPacketIdentifier = 0 111 | } 112 | _nextPacketIdentifier += 1 113 | return _nextPacketIdentifier 114 | } 115 | } 116 | 117 | private func createSSLHandler() throws -> NIOSSLClientHandler? { 118 | guard let tlsConfiguration = tlsConfiguration else { 119 | return nil 120 | } 121 | let sslContext = try NIOSSLContext(configuration: tlsConfiguration) 122 | return try NIOSSLClientHandler(context: sslContext, serverHostname: host) 123 | } 124 | 125 | private func createMQTTHandler() -> MQTTChannelHandler { 126 | let mqttHandler = MQTTChannelHandler() 127 | mqttHandler.delegate = self 128 | return mqttHandler 129 | } 130 | 131 | private func connectChannnel() -> EventLoopFuture { 132 | do { 133 | let mqttHandler = createMQTTHandler() 134 | let handlers: [ChannelHandler] 135 | if let sslHandler = try createSSLHandler() { 136 | handlers = [sslHandler, mqttHandler] 137 | } else { 138 | handlers = [mqttHandler] 139 | } 140 | return ClientBootstrap(group: group) 141 | .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) 142 | .channelInitializer { $0.pipeline.addHandlers(handlers) } 143 | .connect(host: host, port: port) 144 | } catch { 145 | return group.next().makeFailedFuture(error) 146 | } 147 | } 148 | 149 | private func connectBroker(channel: Channel) -> EventLoopFuture { 150 | let packet = ConnectPacket(clientID: clientID, 151 | cleanSession: cleanSession, 152 | will: willMessage, 153 | username: username, 154 | password: password, 155 | keepAlive: keepAlive) 156 | return writeAndFlush(channel: channel, packet: packet) 157 | } 158 | 159 | private func startPingTimer() { 160 | pingTask = group.next() 161 | .scheduleRepeatedTask(initialDelay: .seconds(Int64(keepAlive)), delay: .seconds(Int64(keepAlive))) { [weak self] _ in 162 | guard let self = self else { return } 163 | self.send(packet: PingReqPacket()).whenFailure { 164 | self.throwErrorToDelegate($0, disconnect: true) 165 | } 166 | } 167 | } 168 | 169 | private func cancelPingTimer() { 170 | pingTask?.cancel() 171 | pingTask = nil 172 | } 173 | 174 | /// Check connection and send MQTT packet to connecting channel. 175 | /// Fail if channel is not connected. 176 | private func send(packet: MQTTPacket) -> EventLoopFuture { 177 | switch channelState { 178 | case let .connected(channel): 179 | return writeAndFlush(channel: channel, packet: packet) 180 | default: 181 | return group.next().makeFailedFuture(MQTTError.notConnected) 182 | } 183 | } 184 | 185 | private func writeAndFlush(channel: Channel, packet: MQTTPacket) -> EventLoopFuture { 186 | let bytes = packet.encode() 187 | return channel.writeAndFlush(ByteBuffer(bytes: bytes)) 188 | } 189 | 190 | public func connect() { 191 | switch channelState { 192 | case .connected: 193 | break 194 | case .connectingChannel: 195 | throwErrorToDelegate(MQTTError.channelConnectPending, disconnect: true) 196 | case .connectingBroker: 197 | throwErrorToDelegate(MQTTError.brokerConnectPending, disconnect: true) 198 | case .disconnected: 199 | startTimeoutTimer() 200 | channelState = .connectingChannel 201 | connectChannnel() 202 | .whenFailure { 203 | self.throwErrorToDelegate($0, disconnect: true) 204 | } 205 | } 206 | } 207 | 208 | private func startTimeoutTimer() { 209 | timeoutSchedule = group.next() 210 | .scheduleTask(deadline: .now() + .seconds(connectTimeout)) { [weak self] in 211 | self?.throwErrorToDelegate(MQTTError.connectTimeout, disconnect: true) 212 | } 213 | } 214 | 215 | private func cancelTimeoutTimer() { 216 | timeoutSchedule?.cancel() 217 | timeoutSchedule = nil 218 | } 219 | 220 | private func throwErrorToDelegate(_ error: Error, disconnect: Bool) { 221 | delegate?.delegateDispatchQueue.async { 222 | self.delegate?.mqttClient(self, didCatchError: error) 223 | } 224 | if disconnect { 225 | self.disconnect() 226 | } 227 | } 228 | 229 | public func disconnect() { 230 | switch channelState { 231 | case .connectingChannel: 232 | channelState = .disconnected 233 | case let .connectingBroker(channel): 234 | channel.close() 235 | .whenComplete { 236 | self.channelState = .disconnected 237 | if case let .failure(error) = $0 { 238 | self.throwErrorToDelegate(error, disconnect: false) 239 | } 240 | } 241 | case let .connected(channel): 242 | writeAndFlush(channel: channel, packet: DisconnectPacket()) 243 | .flatMap { channel.close() } 244 | .whenComplete { 245 | self.channelState = .disconnected 246 | if case let .failure(error) = $0 { 247 | self.throwErrorToDelegate(error, disconnect: false) 248 | } 249 | } 250 | case .disconnected: 251 | break 252 | } 253 | } 254 | 255 | public func publish(message: PublishMessage, identifier: UInt16? = nil) { 256 | publish(topic: message.topic, retain: message.retain, qos: message.qos, payload: message.payload, identifier: identifier) 257 | } 258 | 259 | public func publish(topic: String, retain: Bool, qos: QoS, payload: DataEncodable, identifier: UInt16? = nil) { 260 | let id: UInt16? 261 | if qos == .atMostOnce { 262 | id = nil 263 | } else { 264 | id = identifier ?? nextPacketIdentifier() 265 | } 266 | let packet = PublishPacket(topic: topic, identifier: id, retain: retain, qos: qos, payload: payload.encode()) 267 | send(packet: packet).whenFailure { 268 | self.throwErrorToDelegate($0, disconnect: true) 269 | } 270 | } 271 | 272 | public func subscribe(topic: String, qos: QoS, identifier: UInt16? = nil) { 273 | subscribe(topicFilter: TopicFilter(topic: topic, qos: qos), identifier: identifier) 274 | } 275 | 276 | public func subscribe(topicFilter: TopicFilter, identifier: UInt16? = nil) { 277 | subscribe(topicFilters: [topicFilter], identifier: identifier) 278 | } 279 | 280 | public func subscribe(topicFilters: [TopicFilter], identifier: UInt16? = nil) { 281 | let packet = SubscribePacket(identifier: identifier ?? nextPacketIdentifier(), 282 | topicFilters: topicFilters) 283 | send(packet: packet).whenFailure { 284 | self.throwErrorToDelegate($0, disconnect: true) 285 | } 286 | } 287 | 288 | public func unsubscribe(topic: String, identifier: UInt16? = nil) { 289 | unsubscribe(topics: [topic], identifier: identifier) 290 | } 291 | 292 | public func unsubscribe(topics: [String], identifier: UInt16? = nil) { 293 | let packet = UnsubscribePacket(identifier: identifier ?? nextPacketIdentifier(), topicFilters: topics) 294 | send(packet: packet).whenFailure { 295 | self.throwErrorToDelegate($0, disconnect: true) 296 | } 297 | } 298 | } 299 | 300 | extension MQTTClient: MQTTChannelHandlerDelegate { 301 | func didReceive(packet: MQTTPacket) { 302 | switch packet { 303 | case let packet as ConnAckPacket: 304 | if case let .connectingBroker(channel) = channelState { 305 | if packet.returnCode == .accepted { 306 | self.channelState = .connected(channel) 307 | } else { 308 | disconnect() 309 | } 310 | } 311 | case let packet as PubRecPacket: 312 | let identifier = packet.identifier 313 | send(packet: PubRelPacket(identifier: identifier)) 314 | .whenFailure { error in 315 | print("failed to send PubRel packet: \(error.localizedDescription)") 316 | } 317 | default: 318 | break 319 | } 320 | delegate?.delegateDispatchQueue.async { 321 | self.delegate?.mqttClient(self, didReceive: packet) 322 | } 323 | } 324 | 325 | func didCatch(decodeError error: DecodeError) { 326 | throwErrorToDelegate(MQTTError.decodeError(error), disconnect: false) 327 | } 328 | 329 | func channelActive(channel: Channel) { 330 | switch channelState { 331 | case .connectingChannel: 332 | channelState = .connectingBroker(channel) 333 | connectBroker(channel: channel).whenFailure { self.throwErrorToDelegate($0, disconnect: true) } 334 | default: 335 | break 336 | } 337 | } 338 | 339 | func channelInactive() { 340 | disconnect() 341 | } 342 | } 343 | 344 | extension MQTTClient { 345 | private enum ChannelState { 346 | case disconnected 347 | case connectingChannel 348 | case connectingBroker(Channel) 349 | case connected(Channel) 350 | 351 | var connectionState: ConnectionState { 352 | switch self { 353 | case .disconnected: 354 | return .disconnected 355 | case .connectingChannel: 356 | return .connectingChannel 357 | case .connectingBroker: 358 | return .connectingBroker 359 | case .connected: 360 | return .connected 361 | } 362 | } 363 | } 364 | } 365 | --------------------------------------------------------------------------------