narutaro / note

0 stars 0 forks source link

CocoaMQTT with SwiftUI example #13

Open narutaro opened 1 year ago

narutaro commented 1 year ago

とりあえず動くコード

//
//  ContentView.swift
//  mqtt2
//
//  Created by Inoue, Masayuki on 2023/07/02.
//
import Foundation
import SwiftUI
import CocoaMQTT

struct ContentView: View {
    let mqtt = MQTTClient()

    var body: some View {
        VStack {
            Text("MQTT Control").font(.title)
            Button("Initialize"){ mqtt.initializeMQTT(host: "broker.emqx.io", identifier: "c1") }.padding()
            Button("Connect"){ mqtt.connect() }.padding()
            Button("Subscribe"){ mqtt.subscribe(topic: "some/topic" ) }.padding()
            Button("Publish"){ mqtt.publish(with: "Hello, finally")}.padding()
            Button("Disconnect"){ mqtt.disconnect() }.padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

class MQTTClient: ObservableObject, CocoaMQTTDelegate {

    var mqttClient: CocoaMQTT?
    var identifier = "client_1"
    var host = "broker.emqx.io"
    var topic = "some/topic"

    func initializeMQTT(host: String, identifier: String) {
        // If any previous instance exists then clean it
        if mqttClient != nil {
            mqttClient = nil
        }
        self.identifier = identifier
        self.host = host

        // TODO: Guard
        mqttClient = CocoaMQTT(clientID: identifier, host: host, port: 1883)
        mqttClient?.willMessage = CocoaMQTTMessage(topic: "/will", string: "dieout")
        mqttClient?.keepAlive = 60
        mqttClient?.delegate = self

        print("[Initialize] \(mqttClient as Any)")
    }

    func connect(){
        let result = mqttClient?.connect()
    }

    func publish(with message: String) {
        mqttClient?.publish(topic, withString: message, qos: .qos1)
    }

    func subscribe(topic: String) {
        self.topic = topic
        mqttClient?.subscribe(topic, qos: .qos1)
    }

    func unSubscribe(topic: String) {
        mqttClient?.unsubscribe(topic)
    }

    func disconnect(){
        mqttClient?.disconnect()
    }

    // MARK: CocoaMQTTDelegate
    func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
        print("[didConnectAck] ack: \(ack)")
    }

    func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
        print("[didPublishMessage] message: \(String(describing: message.string?.description)), id: \(id)")
    }

    func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) {
        //
    }

    func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) {
        print("[didReceiveMessage] message: \(String(describing: message.string?.description)), id: \(id), topic: message.topic")
    }

    func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
        print("[didSubscribeTopics] topic: \(success)")
    }

    func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) {
        //
    }

    func mqttDidPing(_ mqtt: CocoaMQTT) {
        //
    }

    func mqttDidReceivePong(_ mqtt: CocoaMQTT) {
        //
    }

    func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
        print("\(String(describing: err))")
        dump(err)
    }

}

証明書とTLS

AWS IoT Coreとの接続するので証明書やTLSを動かす。CocoaMQTTのreadmeにp12ファイルを使う例があるのでやってみる。

p12 file

p12ファイルについてはここ - p12ファイルことPKCS #12の証明書ファイルに関してざっくりまとめる

このコマンドでp12のバイナリファイルができる。

openssl pkcs12 -export -clcerts -in device.pem.crt -inkey private.pem.key -out client.p12
openssl pkcs12 -export -clcerts -in  device.pem.crt -inkey private.pem.key -certfile AmazonRootCA1.pem -out client.p12

p12ファイルの中身の確認方法

keytool -list -v -keystore client.p12 -storetype PKCS12 -storepass <pass>

⚠️ 上記で生成したp12ファイルはそのままでは使えなかった。具体的には以下のコードでインポートしようとするとERROR: SecPKCS12Import returned errSecAuthFailed. Incorrect password?となる。

    func getClientCertFromP12File(certName: String, certPassword: String) -> CFArray? {
        // get p12 file path
        let resourcePath = Bundle.main.path(forResource: certName, ofType: "p12")

        guard let filePath = resourcePath, let p12Data = NSData(contentsOfFile: filePath) else {
            print("Failed to open the certificate file: \(certName).p12")
            return nil
        }

        // create key dictionary for reading p12 file
        let key = kSecImportExportPassphrase as String
        let options : NSDictionary = [key: certPassword]

        var items : CFArray?
        let securityError = SecPKCS12Import(p12Data, options, &items)

        guard securityError == errSecSuccess else {
            if securityError == errSecAuthFailed {
                print("ERROR: SecPKCS12Import returned errSecAuthFailed. Incorrect password?")
                print(securityError)
            } else {
                print("Failed to open the certificate file: \(certName).p12")
            }
            return nil
        }

        guard let theArray = items, CFArrayGetCount(theArray) > 0 else {
            return nil
        }

        let dictionary = (theArray as NSArray).object(at: 0)
        guard let identity = (dictionary as AnyObject).value(forKey: kSecImportItemIdentity as String) else {
            return nil
        }
        let certArray = [identity] as CFArray

        return certArray
    }

💡 OpenSSL 3.1.1でp12ファイルを作成する際は-legacyオプションをつけることで読み込むことのできるファイルが生成された。どうも、OpenSSL3系ではこのオプションにより1系と互換性のあるファイルが生成されるようだ。

openssl pkcs12 -export -legacy -clcerts -in  device.pem.crt -inkey private.pem.key -certfile AmazonRootCA1.pem -out client.p12
narutaro commented 1 year ago

証明書(p12ファイル)とTLSで動くコード

//
//  ContentView.swift
//  mqtt2
//
//  Created by Inoue, Masayuki on 2023/07/02.
//
import Foundation
import SwiftUI
import CocoaMQTT

struct ContentView: View {
    let mqtt = MQTTClient()

    var body: some View {
        VStack {
            Text("MQTT Control").font(.title)
            Button("Initialize"){ mqtt.initialize(host: mqtt.host, identifier: mqtt.identifier) }.padding()
            Button("Connect"){ mqtt.connect() }.padding()
            Button("Subscribe"){ mqtt.subscribe(topic: "some/topic") }.padding()
            Button("Publish"){ mqtt.publish(with: "{\"message\":\"hello world!\"}")}.padding()
            Button("Disconnect"){ mqtt.disconnect() }.padding()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

class MQTTClient: ObservableObject, CocoaMQTTDelegate {

    var mqttClient: CocoaMQTT?
    var identifier = "client_1"

    //var host = "broker.emqx.io"
    var host = "<id>-ats.iot.ap-northeast-1.amazonaws.com"
    var topic = "some/topic"

    func getClientCertFromP12File(certName: String, certPassword: String) -> CFArray? {
        // get p12 file path
        let resourcePath = Bundle.main.path(forResource: certName, ofType: "p12")

        guard let filePath = resourcePath, let p12Data = NSData(contentsOfFile: filePath) else {
            print("Failed to open the certificate file: \(certName).p12")
            return nil
        }

        // create key dictionary for reading p12 file
        let key = kSecImportExportPassphrase as String
        let options : NSDictionary = [key: certPassword]

        var items : CFArray?
        let securityError = SecPKCS12Import(p12Data, options, &items)

        guard securityError == errSecSuccess else {
            if securityError == errSecAuthFailed {
                print("ERROR: SecPKCS12Import returned errSecAuthFailed. Incorrect password?")
                print(securityError)
            } else {
                print("Failed to open the certificate file: \(certName).p12")
            }
            return nil
        }

        guard let theArray = items, CFArrayGetCount(theArray) > 0 else {
            return nil
        }

        let dictionary = (theArray as NSArray).object(at: 0)
        guard let identity = (dictionary as AnyObject).value(forKey: kSecImportItemIdentity as String) else {
            return nil
        }
        let certArray = [identity] as CFArray

        return certArray
    }

    func initialize(host: String, identifier: String) {
        // If any previous instance exists then clean it
        if mqttClient != nil {
            mqttClient = nil
        }
        self.identifier = identifier
        self.host = host

        // TODO: Guard
        mqttClient = CocoaMQTT(clientID: identifier, host: host, port: 8883)
        mqttClient?.willMessage = CocoaMQTTMessage(topic: "/will", string: "dieout")
        mqttClient?.keepAlive = 60
        mqttClient?.delegate = self

        // TLS
        mqttClient?.enableSSL = true
        // mqttClient?.allowUntrustCACertificate = true

        let clientCertArray = getClientCertFromP12File(certName: "client-legacy", certPassword: "<pass>")

        var sslSettings: [String: NSObject] = [:]
        sslSettings[kCFStreamSSLCertificates as String] = clientCertArray
        mqttClient?.sslSettings = sslSettings

        print("[Initialize] \(mqttClient! as Any)")
    }

    func connect(){
        _ = mqttClient?.connect()
    }

    func publish(with message: String) {
        let publishProperties = MqttPublishProperties()
        publishProperties.contentType = "JSON"
        mqttClient?.publish(topic, withString: message, qos: .qos1)
    }

    func subscribe(topic: String) {
        self.topic = topic
        mqttClient?.subscribe(topic, qos: .qos1)
    }

    func unSubscribe(topic: String) {
        mqttClient?.unsubscribe(topic)
    }

    func disconnect(){
        mqttClient?.disconnect()
    }

    // MARK: CocoaMQTTDelegate
    func mqtt(_ mqtt: CocoaMQTT, didConnectAck ack: CocoaMQTTConnAck) {
        TRACE("ack: \(ack)")
    }

    func mqtt(_ mqtt: CocoaMQTT, didPublishMessage message: CocoaMQTTMessage, id: UInt16) {
        TRACE("message: \(message.string.description), id: \(id)")
    }

    func mqtt(_ mqtt: CocoaMQTT, didPublishAck id: UInt16) {
        TRACE("id: \(id)")
    }

    func mqtt(_ mqtt: CocoaMQTT, didReceiveMessage message: CocoaMQTTMessage, id: UInt16) {
        TRACE("message: \(message.string.description), id: \(id), topic: \(message.topic)")
    }

    func mqtt(_ mqtt: CocoaMQTT, didSubscribeTopics success: NSDictionary, failed: [String]) {
        TRACE("subscribed: \(success), failed: \(failed)")
    }

    func mqtt(_ mqtt: CocoaMQTT, didUnsubscribeTopics topics: [String]) {
        TRACE("topic: \(topics)")
    }

    func mqttDidPing(_ mqtt: CocoaMQTT) {
        TRACE()
    }

    func mqttDidReceivePong(_ mqtt: CocoaMQTT) {
        TRACE()
    }

    func mqttDidDisconnect(_ mqtt: CocoaMQTT, withError err: Error?) {
        TRACE("\(err.description)")
    }

    func TRACE(_ message: String = "", fun: String = #function) {
        let names = fun.components(separatedBy: ":")
        var prettyName: String
        if names.count == 2 {
            prettyName = names[0]
        } else {
            prettyName = names[1]
        }

        if fun == "mqttDidDisconnect(_:withError:)" {
            prettyName = "didDisconnect"
        }

        print("[TRACE] [\(prettyName)]: \(message)")
    }
}