Преглед изворни кода

Add iOS client implementation with SoftEther protocol handshake support

Ahmad Reza пре 5 месеци
родитељ
комит
62c71ebe5c

+ 118 - 0
SoftEtherVPN-iOS/SoftEtherVPN-iOS/Protocol/SecureConnection.swift

@@ -0,0 +1,118 @@
+import Foundation
+import Network
+import Security
+
+/// SecureConnection handles the TLS connection with the SoftEther VPN server
+class SecureConnection {
+    
+    // MARK: - Properties
+    
+    private var connection: NWConnection?
+    private let host: String
+    private let port: UInt16
+    private let queue = DispatchQueue(label: "com.softether.connection", qos: .userInitiated)
+    
+    // MARK: - Initialization
+    
+    /// Initialize a secure connection
+    /// - Parameters:
+    ///   - host: Server hostname or IP address
+    ///   - port: Server port number
+    init(host: String, port: UInt16) {
+        self.host = host
+        self.port = port
+    }
+    
+    // MARK: - Public Methods
+    
+    /// Connect to the server using TLS
+    /// - Parameter completion: Callback with connection result
+    func connect(completion: @escaping (Bool, Error?) -> Void) {
+        let hostEndpoint = NWEndpoint.Host(host)
+        let portEndpoint = NWEndpoint.Port(rawValue: port)!
+        
+        // Create TLS parameters
+        let tlsOptions = NWProtocolTLS.Options()
+        
+        // Configure TLS for maximum compatibility with SoftEther
+        let securityOptions = tlsOptions.securityProtocolOptions
+        sec_protocol_options_set_tls_min_version(securityOptions, .TLSv12)
+        sec_protocol_options_set_tls_max_version(securityOptions, .TLSv13)
+        
+        // Allow all cipher suites for compatibility
+        sec_protocol_options_set_cipher_suites(securityOptions, nil, 0)
+        
+        // Disable certificate validation for initial development (ENABLE IN PRODUCTION)
+        sec_protocol_options_set_verify_block(securityOptions, { (_, _, trustResult, _) in
+            return true // Accept all certificates for testing
+        }, queue)
+        
+        // Create TCP options with TLS
+        let tcpOptions = NWProtocolTCP.Options()
+        tcpOptions.enableKeepalive = true
+        tcpOptions.keepaliveIdle = 30
+        
+        // Create connection parameters
+        let parameters = NWParameters(tls: tlsOptions, tcp: tcpOptions)
+        
+        // Create the connection
+        connection = NWConnection(host: hostEndpoint, port: portEndpoint, using: parameters)
+        
+        // Set up state handling
+        connection?.stateUpdateHandler = { [weak self] state in
+            switch state {
+            case .ready:
+                completion(true, nil)
+            case .failed(let error):
+                self?.disconnect()
+                completion(false, error)
+            case .cancelled:
+                completion(false, NSError(domain: "SoftEtherError", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Connection cancelled"]))
+            default:
+                break
+            }
+        }
+        
+        // Start the connection
+        connection?.start(queue: queue)
+    }
+    
+    /// Disconnect from the server
+    func disconnect() {
+        connection?.cancel()
+        connection = nil
+    }
+    
+    /// Send data to the server
+    /// - Parameters:
+    ///   - data: Data to send
+    ///   - completion: Callback with error if any
+    func send(data: Data, completion: @escaping (Error?) -> Void) {
+        guard let connection = connection, connection.state == .ready else {
+            completion(NSError(domain: "SoftEtherError", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Connection not ready"]))
+            return
+        }
+        
+        connection.send(content: data, completion: .contentProcessed { error in
+            completion(error)
+        })
+    }
+    
+    /// Receive data from the server
+    /// - Parameter completion: Callback with received data and error if any
+    func receive(completion: @escaping (Data?, Error?) -> Void) {
+        guard let connection = connection, connection.state == .ready else {
+            completion(nil, NSError(domain: "SoftEtherError", code: 1001, userInfo: [NSLocalizedDescriptionKey: "Connection not ready"]))
+            return
+        }
+        
+        connection.receive(minimumIncompleteLength: 1, maximumLength: 65536) { data, _, isComplete, error in
+            completion(data, error)
+            
+            if isComplete {
+                // Connection was closed by the peer
+                self.disconnect()
+            }
+        }
+    }
+}

+ 90 - 0
SoftEtherVPN-iOS/SoftEtherVPN-iOS/Protocol/SoftEtherClientSignature.swift

@@ -0,0 +1,90 @@
+import Foundation
+
+/// Handles the specific client signature format that SoftEther expects
+class SoftEtherClientSignature {
+    
+    // MARK: - Constants
+    
+    private enum Constants {
+        static let clientBuildNumber: UInt32 = 5187
+        static let clientVersion: UInt32 = 5_02_0000 + clientBuildNumber
+        static let clientString = "SoftEther VPN Client"
+        static let softEtherMagic: [UInt8] = [0x5E, 0x68] // 'Se' in hex
+        
+        // Protocol identification constants from SoftEther source
+        static let cedar = "CEDAR"
+        static let sessionKey = "sessionkey"
+        static let protocol1 = "PROTOCOL"
+        static let protocol2 = "PROTOCOL2"
+    }
+    
+    // MARK: - Public Methods
+    
+    /// Generate the client signature packet that identifies this client as a legitimate SoftEther VPN client
+    /// - Returns: Data containing the formatted client signature
+    static func generateSignature() -> Data {
+        var data = Data()
+        
+        // 1. Add SoftEther magic bytes
+        data.append(contentsOf: Constants.softEtherMagic)
+        
+        // 2. Add client version in network byte order (big endian)
+        data.appendUInt32(Constants.clientVersion)
+        
+        // 3. Add client build number in network byte order
+        data.appendUInt32(Constants.clientBuildNumber)
+        
+        // 4. Add cedar protocol identifier
+        if let cedarData = Constants.cedar.data(using: .ascii) {
+            data.append(cedarData)
+            data.append(0) // null terminator
+        }
+        
+        // 5. Add client string with null terminator
+        if let clientString = (Constants.clientString + "\0").data(using: .ascii) {
+            data.append(clientString)
+        }
+        
+        // 6. Add protocol identifiers
+        if let protocolData = (Constants.protocol1 + "\0").data(using: .ascii) {
+            data.append(protocolData)
+        }
+        
+        if let protocol2Data = (Constants.protocol2 + "\0").data(using: .ascii) {
+            data.append(protocol2Data)
+        }
+        
+        // 7. Add session key marker
+        if let sessionKeyData = (Constants.sessionKey + "\0").data(using: .ascii) {
+            data.append(sessionKeyData)
+        }
+        
+        // 8. Add random data for session key (typically 20 bytes)
+        let randomSessionKey = SoftEtherCrypto.randomBytes(count: 20)
+        data.append(randomSessionKey)
+        
+        // 9. Calculate and append SHA-1 hash of the entire data for integrity verification
+        let hash = SoftEtherCrypto.sha1(data)
+        data.append(hash)
+        
+        return data
+    }
+    
+    /// Verify a server response to the client signature
+    /// - Parameter data: Response data from server
+    /// - Returns: True if valid response, false otherwise
+    static func verifyServerResponse(_ data: Data) -> Bool {
+        // Basic validation - a real implementation would parse and validate the server response format
+        // This is a minimal check to see if we have enough data and it starts with the magic bytes
+        guard data.count >= 8 else {
+            return false
+        }
+        
+        // Check if response starts with SoftEther magic bytes
+        if data[0] == Constants.softEtherMagic[0] && data[1] == Constants.softEtherMagic[1] {
+            return true
+        }
+        
+        return false
+    }
+}

+ 97 - 0
SoftEtherVPN-iOS/SoftEtherVPN-iOS/Protocol/SoftEtherCrypto.swift

@@ -0,0 +1,97 @@
+import Foundation
+import CryptoKit
+
+/// Handles encryption operations for SoftEther protocol
+class SoftEtherCrypto {
+    
+    // MARK: - Constants
+    
+    private enum Constants {
+        static let sha1Size = 20
+        static let md5Size = 16
+    }
+    
+    // MARK: - Public Methods
+    
+    /// Generate secure random bytes
+    /// - Parameter count: Number of random bytes to generate
+    /// - Returns: Data containing random bytes
+    static func randomBytes(count: Int) -> Data {
+        var data = Data(count: count)
+        _ = data.withUnsafeMutableBytes { 
+            SecRandomCopyBytes(kSecRandomDefault, count, $0.baseAddress!)
+        }
+        return data
+    }
+    
+    /// Calculate SHA-1 hash
+    /// - Parameter data: Input data
+    /// - Returns: SHA-1 hash of the input data
+    static func sha1(_ data: Data) -> Data {
+        let digest = SHA1.hash(data: data)
+        return Data(digest)
+    }
+    
+    /// Calculate MD5 hash
+    /// - Parameter data: Input data
+    /// - Returns: MD5 hash of the input data
+    static func md5(_ data: Data) -> Data {
+        let digest = Insecure.MD5.hash(data: data)
+        return Data(digest)
+    }
+    
+    /// Encrypt data using RC4 algorithm (for SoftEther compatibility)
+    /// - Parameters:
+    ///   - data: Data to encrypt
+    ///   - key: Encryption key
+    /// - Returns: Encrypted data
+    static func rc4Encrypt(data: Data, key: Data) -> Data {
+        let rc4 = RC4(key: key)
+        return rc4.process(data)
+    }
+    
+    /// Decrypt data using RC4 algorithm (for SoftEther compatibility)
+    /// - Parameters:
+    ///   - data: Data to decrypt
+    ///   - key: Decryption key
+    /// - Returns: Decrypted data
+    static func rc4Decrypt(data: Data, key: Data) -> Data {
+        // RC4 is symmetric, so encryption and decryption are the same operation
+        return rc4Encrypt(data: data, key: key)
+    }
+}
+
+/// Simple RC4 implementation for SoftEther compatibility
+/// Note: RC4 is considered insecure, but SoftEther uses it in parts of its protocol
+private class RC4 {
+    private var state: [UInt8]
+    
+    init(key: Data) {
+        state = Array(0...255)
+        var j: Int = 0
+        
+        // Key scheduling algorithm
+        for i in 0..<256 {
+            let keyByte = key[i % key.count]
+            j = (j + Int(state[i]) + Int(keyByte)) & 0xFF
+            state.swapAt(i, j)
+        }
+    }
+    
+    func process(_ data: Data) -> Data {
+        var result = Data(count: data.count)
+        var i: Int = 0
+        var j: Int = 0
+        
+        // Generate keystream and XOR with plaintext
+        for k in 0..<data.count {
+            i = (i + 1) & 0xFF
+            j = (j + Int(state[i])) & 0xFF
+            state.swapAt(i, j)
+            let keyStreamByte = state[(Int(state[i]) + Int(state[j])) & 0xFF]
+            result[k] = data[k] ^ keyStreamByte
+        }
+        
+        return result
+    }
+}

+ 123 - 0
SoftEtherVPN-iOS/SoftEtherVPN-iOS/Protocol/SoftEtherPacket.swift

@@ -0,0 +1,123 @@
+import Foundation
+
+/// Handles the SoftEther packet structure for communication
+class SoftEtherPacket {
+    
+    // MARK: - Constants
+    
+    private enum PacketType: UInt32 {
+        case clientSignature = 0x01
+        case serverResponse = 0x02
+        case sessionRequest = 0x03
+        case sessionResponse = 0x04
+        case data = 0x05
+        case keepAlive = 0x06
+    }
+    
+    private enum Constants {
+        static let headerSize: UInt32 = 16
+        static let maxPacketSize: UInt32 = 1024 * 1024 // 1MB
+    }
+    
+    // MARK: - Properties
+    
+    private var packetType: PacketType
+    private var packetId: UInt32
+    private var packetData: Data
+    
+    // MARK: - Initialization
+    
+    /// Initialize a packet with type, ID and data
+    /// - Parameters:
+    ///   - type: Packet type
+    ///   - id: Packet ID
+    ///   - data: Packet payload
+    init(type: UInt32, id: UInt32, data: Data) {
+        self.packetType = PacketType(rawValue: type) ?? .data
+        self.packetId = id
+        self.packetData = data
+    }
+    
+    /// Initialize a packet from raw data
+    /// - Parameter data: Raw packet data
+    init?(fromData data: Data) {
+        guard data.count >= Int(Constants.headerSize) else {
+            return nil
+        }
+        
+        // Parse header
+        let typeValue = data.readUInt32(at: 0)
+        self.packetId = data.readUInt32(at: 4)
+        let dataSize = data.readUInt32(at: 8)
+        
+        // Validate packet
+        guard let type = PacketType(rawValue: typeValue),
+              dataSize <= Constants.maxPacketSize,
+              data.count >= Int(Constants.headerSize + dataSize) else {
+            return nil
+        }
+        
+        self.packetType = type
+        
+        // Extract payload
+        let startIndex = Int(Constants.headerSize)
+        let endIndex = startIndex + Int(dataSize)
+        self.packetData = data.subdata(in: startIndex..<endIndex)
+    }
+    
+    // MARK: - Public Methods
+    
+    /// Serialize the packet to binary data format
+    /// - Returns: Serialized packet data
+    func serialize() -> Data {
+        var result = Data(capacity: Int(Constants.headerSize) + packetData.count)
+        
+        // Write header
+        result.appendUInt32(packetType.rawValue)
+        result.appendUInt32(packetId)
+        result.appendUInt32(UInt32(packetData.count))
+        result.appendUInt32(0) // Reserved
+        
+        // Write payload
+        result.append(packetData)
+        
+        return result
+    }
+    
+    /// Get the packet type
+    /// - Returns: Packet type
+    func getType() -> UInt32 {
+        return packetType.rawValue
+    }
+    
+    /// Get the packet ID
+    /// - Returns: Packet ID
+    func getId() -> UInt32 {
+        return packetId
+    }
+    
+    /// Get the packet payload
+    /// - Returns: Packet payload data
+    func getData() -> Data {
+        return packetData
+    }
+}
+
+// MARK: - Extensions
+
+extension Data {
+    /// Read a UInt32 value from the data at specified offset
+    /// - Parameter offset: Offset to read from
+    /// - Returns: UInt32 value in big-endian order
+    func readUInt32(at offset: Int) -> UInt32 {
+        let slice = self.subdata(in: offset..<(offset + 4))
+        return slice.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
+    }
+    
+    /// Append a UInt32 value to the data in big-endian order
+    /// - Parameter value: UInt32 value to append
+    mutating func appendUInt32(_ value: UInt32) {
+        var bigEndian = value.bigEndian
+        append(UnsafeBufferPointer(start: &bigEndian, count: 1))
+    }
+}

+ 184 - 0
SoftEtherVPN-iOS/SoftEtherVPN-iOS/Protocol/SoftEtherProtocol.swift

@@ -0,0 +1,184 @@
+import Foundation
+import Network
+import Security
+import CryptoKit
+
+/// SoftEtherProtocol manages the communication between iOS client and SoftEther VPN server
+class SoftEtherProtocol {
+    
+    // MARK: - Properties
+    
+    private var secureConnection: SecureConnection?
+    private var isConnected = false
+    private var host: String = ""
+    private var port: UInt16 = 443
+    private var nextPacketId: UInt32 = 1
+    
+    // MARK: - Public Methods
+    
+    /// Connect to a SoftEther VPN server
+    /// - Parameters:
+    ///   - host: The server hostname or IP address
+    ///   - port: The server port (default: 443)
+    ///   - completion: Callback with connection result
+    public func connect(to host: String, port: UInt16 = 443, completion: @escaping (Bool, Error?) -> Void) {
+        self.host = host
+        self.port = port
+        
+        // Create a secure connection
+        secureConnection = SecureConnection(host: host, port: port)
+        
+        // Connect using TLS
+        secureConnection?.connect { [weak self] success, error in
+            guard let self = self, success else {
+                completion(false, error ?? NSError(domain: "SoftEtherError", code: 1, userInfo: [NSLocalizedDescriptionKey: "TLS connection failed"]))
+                return
+            }
+            
+            // After successful TLS connection, send the client signature
+            self.sendClientSignature { success, error in
+                if success {
+                    self.isConnected = true
+                }
+                completion(success, error)
+            }
+        }
+    }
+    
+    /// Disconnect from the server
+    public func disconnect() {
+        secureConnection?.disconnect()
+        isConnected = false
+    }
+    
+    // MARK: - Private Methods
+    
+    /// Send the SoftEther client signature to identify as a legitimate client
+    /// - Parameter completion: Callback with result
+    private func sendClientSignature(completion: @escaping (Bool, Error?) -> Void) {
+        // Generate client signature using our specialized class
+        let signatureData = SoftEtherClientSignature.generateSignature()
+        
+        // Create a packet with the signature data
+        let packetId = self.nextPacketId
+        self.nextPacketId += 1
+        
+        let packet = SoftEtherPacket(type: 0x01, id: packetId, data: signatureData)
+        let packetData = packet.serialize()
+        
+        print("Sending client signature packet: \(packetData.count) bytes")
+        
+        // Send the packet
+        secureConnection?.send(data: packetData) { [weak self] error in
+            guard let self = self else { return }
+            
+            if let error = error {
+                print("Error sending client signature: \(error)")
+                completion(false, error)
+                return
+            }
+            
+            // After sending signature, wait for server response
+            self.receiveServerResponse { success, error in
+                completion(success, error)
+            }
+        }
+    }
+    
+    /// Receive and process server response after sending signature
+    /// - Parameter completion: Callback with result
+    private func receiveServerResponse(completion: @escaping (Bool, Error?) -> Void) {
+        secureConnection?.receive { data, error in
+            if let error = error {
+                print("Error receiving server response: \(error)")
+                completion(false, error)
+                return
+            }
+            
+            guard let data = data, data.count > 4 else {
+                let error = NSError(domain: "SoftEtherError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid server response"])
+                print("Invalid server response: insufficient data")
+                completion(false, error)
+                return
+            }
+            
+            print("Received server response: \(data.count) bytes")
+            
+            // Parse the response packet
+            guard let packet = SoftEtherPacket(fromData: data) else {
+                let error = NSError(domain: "SoftEtherError", code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid packet format"])
+                print("Could not parse server response packet")
+                completion(false, error)
+                return
+            }
+            
+            // Verify the response
+            let packetData = packet.getData()
+            let isValid = SoftEtherClientSignature.verifyServerResponse(packetData)
+            
+            if isValid {
+                print("Server accepted our client signature")
+                completion(true, nil)
+            } else {
+                print("Server rejected our client signature")
+                let error = NSError(domain: "SoftEtherError", code: 4, userInfo: [NSLocalizedDescriptionKey: "Server rejected client signature"])
+                completion(false, error)
+            }
+        }
+    }
+    
+    /// Send a data packet to the server
+    /// - Parameters:
+    ///   - data: Data to send
+    ///   - completion: Callback with result
+    func sendData(data: Data, completion: @escaping (Bool, Error?) -> Void) {
+        guard isConnected else {
+            completion(false, NSError(domain: "SoftEtherError", code: 5, userInfo: [NSLocalizedDescriptionKey: "Not connected to server"]))
+            return
+        }
+        
+        let packetId = self.nextPacketId
+        self.nextPacketId += 1
+        
+        let packet = SoftEtherPacket(type: 0x05, id: packetId, data: data)
+        let packetData = packet.serialize()
+        
+        secureConnection?.send(data: packetData) { error in
+            if let error = error {
+                completion(false, error)
+                return
+            }
+            
+            completion(true, nil)
+        }
+    }
+    
+    /// Receive data from the server
+    /// - Parameter completion: Callback with received data and result
+    func receiveData(completion: @escaping (Data?, Bool, Error?) -> Void) {
+        guard isConnected else {
+            completion(nil, false, NSError(domain: "SoftEtherError", code: 5, userInfo: [NSLocalizedDescriptionKey: "Not connected to server"]))
+            return
+        }
+        
+        secureConnection?.receive { data, error in
+            if let error = error {
+                completion(nil, false, error)
+                return
+            }
+            
+            guard let data = data, data.count > 4 else {
+                completion(nil, false, NSError(domain: "SoftEtherError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Invalid server response"]))
+                return
+            }
+            
+            // Parse the packet
+            guard let packet = SoftEtherPacket(fromData: data) else {
+                completion(nil, false, NSError(domain: "SoftEtherError", code: 3, userInfo: [NSLocalizedDescriptionKey: "Invalid packet format"]))
+                return
+            }
+            
+            completion(packet.getData(), true, nil)
+        }
+    }
+}

+ 149 - 0
SoftEtherVPN-iOS/SoftEtherVPN-iOS/SoftEtherVPNClient.swift

@@ -0,0 +1,149 @@
+import Foundation
+import UIKit
+
+/// SoftEtherVPNClient provides a simple interface for connecting to SoftEther VPN servers
+public class SoftEtherVPNClient {
+    
+    // MARK: - Properties
+    
+    private let protocol: SoftEtherProtocol
+    private var connectionState: ConnectionState = .disconnected
+    
+    // MARK: - Public Types
+    
+    /// Connection states for the VPN client
+    public enum ConnectionState {
+        case disconnected
+        case connecting
+        case connected
+        case disconnecting
+        case error(Error)
+    }
+    
+    /// Connection delegate to receive state updates
+    public protocol ConnectionDelegate: AnyObject {
+        func connectionStateDidChange(_ state: ConnectionState)
+    }
+    
+    /// Weak reference to the delegate
+    public weak var delegate: ConnectionDelegate?
+    
+    // MARK: - Initialization
+    
+    public init() {
+        self.protocol = SoftEtherProtocol()
+    }
+    
+    // MARK: - Public Methods
+    
+    /// Connect to a SoftEther VPN server
+    /// - Parameters:
+    ///   - host: Server hostname or IP address
+    ///   - port: Server port (default: 443)
+    ///   - completion: Optional completion handler
+    public func connect(to host: String, port: UInt16 = 443, completion: ((Bool, Error?) -> Void)? = nil) {
+        // Update state
+        connectionState = .connecting
+        delegate?.connectionStateDidChange(connectionState)
+        
+        // Connect using the protocol implementation
+        protocol.connect(to: host, port: port) { [weak self] success, error in
+            guard let self = self else { return }
+            
+            if success {
+                self.connectionState = .connected
+            } else if let error = error {
+                self.connectionState = .error(error)
+            } else {
+                self.connectionState = .disconnected
+            }
+            
+            self.delegate?.connectionStateDidChange(self.connectionState)
+            completion?(success, error)
+        }
+    }
+    
+    /// Disconnect from the server
+    /// - Parameter completion: Optional completion handler
+    public func disconnect(completion: (() -> Void)? = nil) {
+        // Update state
+        connectionState = .disconnecting
+        delegate?.connectionStateDidChange(connectionState)
+        
+        // Disconnect
+        protocol.disconnect()
+        
+        // Update state again
+        connectionState = .disconnected
+        delegate?.connectionStateDidChange(connectionState)
+        
+        completion?()
+    }
+    
+    /// Get the current connection state
+    /// - Returns: Current ConnectionState
+    public func getConnectionState() -> ConnectionState {
+        return connectionState
+    }
+    
+    /// Check if currently connected
+    /// - Returns: True if connected, false otherwise
+    public func isConnected() -> Bool {
+        if case .connected = connectionState {
+            return true
+        }
+        return false
+    }
+    
+    // MARK: - Example Usage
+    
+    /// Example showing how to use this class in a view controller
+    public static func exampleUsage() -> String {
+        return """
+        // In your view controller:
+        
+        private let vpnClient = SoftEtherVPNClient()
+        
+        override func viewDidLoad() {
+            super.viewDidLoad()
+            
+            // Set delegate
+            vpnClient.delegate = self
+        }
+        
+        @IBAction func connectButtonTapped(_ sender: UIButton) {
+            if vpnClient.isConnected() {
+                vpnClient.disconnect()
+            } else {
+                vpnClient.connect(to: "vpn.example.com") { success, error in
+                    if !success {
+                        print("Failed to connect: \\(error?.localizedDescription ?? "Unknown error")")
+                    }
+                }
+            }
+        }
+        
+        // MARK: - ConnectionDelegate
+        
+        extension YourViewController: SoftEtherVPNClient.ConnectionDelegate {
+            func connectionStateDidChange(_ state: SoftEtherVPNClient.ConnectionState) {
+                switch state {
+                case .connected:
+                    connectButton.setTitle("Disconnect", for: .normal)
+                    statusLabel.text = "Connected"
+                case .connecting:
+                    statusLabel.text = "Connecting..."
+                case .disconnecting:
+                    statusLabel.text = "Disconnecting..."
+                case .disconnected:
+                    connectButton.setTitle("Connect", for: .normal)
+                    statusLabel.text = "Disconnected"
+                case .error(let error):
+                    statusLabel.text = "Error: \\(error.localizedDescription)"
+                    connectButton.setTitle("Connect", for: .normal)
+                }
+            }
+        }
+        """
+    }
+}