Add Client Certificate

This commit is contained in:
Djorkaeff Alexandre 2024-01-18 21:50:59 -03:00
parent 8f3bb1a99b
commit 0672f56f10
10 changed files with 309 additions and 128 deletions

View File

@ -11,9 +11,18 @@ protocol RocketChatClientProtocol {
func sendRead(rid: String) -> AnyPublisher<ReadResponse, RocketChatError> func sendRead(rid: String) -> AnyPublisher<ReadResponse, RocketChatError>
} }
final class RocketChatClient { final class RocketChatClient: NSObject {
private let server: Server private let server: Server
private lazy var session = URLSession(
configuration: .default,
delegate: URLSesionClientCertificateHandling(
certificate: server.certificate,
password: server.password
),
delegateQueue: nil
)
init(server: Server) { init(server: Server) {
self.server = server self.server = server
} }
@ -32,7 +41,7 @@ final class RocketChatClient {
urlRequest.httpMethod = request.method.rawValue urlRequest.httpMethod = request.method.rawValue
urlRequest.httpBody = request.body urlRequest.httpBody = request.body
return URLSession.shared.dataTaskPublisher(for: urlRequest) return session.dataTaskPublisher(for: urlRequest)
.tryMap { (data, response) in .tryMap { (data, response) in
guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else { guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
throw RocketChatError.unauthorized throw RocketChatError.unauthorized

View File

@ -0,0 +1,95 @@
// https://medium.com/@hamidptb/implementing-mtls-on-ios-using-urlsession-and-cloudflare-890b76aca66c
import Foundation
final class URLSesionClientCertificateHandling: NSObject, URLSessionDelegate {
private let certificate: Data?
private let password: String?
init(certificate: Data?, password: String?) {
self.certificate = certificate
self.password = password
}
public func urlSession(
_: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate else {
completionHandler(.performDefaultHandling, nil)
return
}
guard let credential = Credentials.urlCredential(certificate: certificate, password: password) else {
completionHandler(.performDefaultHandling, nil)
return
}
challenge.sender?.use(credential, for: challenge)
completionHandler(.useCredential, credential)
}
}
fileprivate typealias UserCertificate = (data: Data, password: String)
fileprivate final class Credentials {
static func urlCredential(certificate: Data?, password: String?) -> URLCredential? {
guard let certificate, let password else { return nil }
let p12Contents = PKCS12(pkcs12Data: certificate, password: password)
guard let identity = p12Contents.identity else {
return nil
}
return URLCredential(identity: identity, certificates: nil, persistence: .none)
}
}
fileprivate struct PKCS12 {
let label: String?
let keyID: NSData?
let trust: SecTrust?
let certChain: [SecTrust]?
let identity: SecIdentity?
public init(pkcs12Data: Data, password: String) {
let importPasswordOption: NSDictionary
= [kSecImportExportPassphrase as NSString: password]
var items: CFArray?
let secError: OSStatus
= SecPKCS12Import(pkcs12Data as NSData,
importPasswordOption, &items)
guard secError == errSecSuccess else {
if secError == errSecAuthFailed {
NSLog("Incorrect password?")
}
fatalError("Error trying to import PKCS12 data")
}
guard let theItemsCFArray = items else { fatalError() }
let theItemsNSArray: NSArray = theItemsCFArray as NSArray
guard let dictArray
= theItemsNSArray as? [[String: AnyObject]]
else {
fatalError()
}
label = dictArray.element(for: kSecImportItemLabel)
keyID = dictArray.element(for: kSecImportItemKeyID)
trust = dictArray.element(for: kSecImportItemTrust)
certChain = dictArray.element(for: kSecImportItemCertChain)
identity = dictArray.element(for: kSecImportItemIdentity)
}
}
fileprivate extension Array where Element == [String: AnyObject] {
func element<T>(for key: CFString) -> T? {
for dictElement in self {
if let value = dictElement[key as String] as? T {
return value
}
}
return nil
}
}

View File

@ -74,6 +74,8 @@ final class DefaultDatabase: ServersDatabase {
server.iconURL = updatedServer.iconURL server.iconURL = updatedServer.iconURL
server.useRealName = updatedServer.useRealName server.useRealName = updatedServer.useRealName
server.loggedUser = user(from: updatedServer.loggedUser) server.loggedUser = user(from: updatedServer.loggedUser)
server.certificate = updatedServer.clientSSL?.certificate
server.password = updatedServer.clientSSL?.password
} else { } else {
Server( Server(
context: viewContext, context: viewContext,
@ -81,7 +83,9 @@ final class DefaultDatabase: ServersDatabase {
name: updatedServer.name, name: updatedServer.name,
url: updatedServer.url, url: updatedServer.url,
useRealName: updatedServer.useRealName, useRealName: updatedServer.useRealName,
loggedUser: user(from: updatedServer.loggedUser) loggedUser: user(from: updatedServer.loggedUser),
certificate: updatedServer.clientSSL?.certificate,
password: updatedServer.clientSSL?.password
) )
} }

View File

@ -8,8 +8,10 @@
<relationship name="server" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Server" inverseName="loggedUser" inverseEntity="Server"/> <relationship name="server" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Server" inverseName="loggedUser" inverseEntity="Server"/>
</entity> </entity>
<entity name="Server" representedClassName=".Server" syncable="YES"> <entity name="Server" representedClassName=".Server" syncable="YES">
<attribute name="certificate" optional="YES" attributeType="Binary"/>
<attribute name="iconURL" optional="YES" attributeType="URI"/> <attribute name="iconURL" optional="YES" attributeType="URI"/>
<attribute name="name" optional="YES" attributeType="String"/> <attribute name="name" optional="YES" attributeType="String"/>
<attribute name="password" optional="YES" attributeType="String"/>
<attribute name="updatedSince" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="updatedSince" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="url" optional="YES" attributeType="URI"/> <attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="useRealName" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="useRealName" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>

View File

@ -13,6 +13,8 @@ public final class Server: NSManagedObject {
@NSManaged public var url: URL @NSManaged public var url: URL
@NSManaged public var useRealName: Bool @NSManaged public var useRealName: Bool
@NSManaged public var loggedUser: LoggedUser @NSManaged public var loggedUser: LoggedUser
@NSManaged public var certificate: Data?
@NSManaged public var password: String?
@available(*, unavailable) @available(*, unavailable)
init() { init() {
@ -36,7 +38,9 @@ public final class Server: NSManagedObject {
updatedSince: Date? = nil, updatedSince: Date? = nil,
url: URL, url: URL,
useRealName: Bool, useRealName: Bool,
loggedUser: LoggedUser loggedUser: LoggedUser,
certificate: Data? = nil,
password: String? = nil
) { ) {
let entity = NSEntityDescription.entity(forEntityName: "Server", in: context)! let entity = NSEntityDescription.entity(forEntityName: "Server", in: context)!
super.init(entity: entity, insertInto: context) super.init(entity: entity, insertInto: context)
@ -46,6 +50,8 @@ public final class Server: NSManagedObject {
self.url = url self.url = url
self.useRealName = useRealName self.useRealName = useRealName
self.loggedUser = loggedUser self.loggedUser = loggedUser
self.certificate = certificate
self.password = password
} }
} }

View File

@ -112,6 +112,9 @@
1E9A71692B59B6E100477BA2 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71682B59B6E100477BA2 /* MessageSender.swift */; }; 1E9A71692B59B6E100477BA2 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71682B59B6E100477BA2 /* MessageSender.swift */; };
1E9A716F2B59CBCA00477BA2 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A716E2B59CBCA00477BA2 /* AttachmentView.swift */; }; 1E9A716F2B59CBCA00477BA2 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A716E2B59CBCA00477BA2 /* AttachmentView.swift */; };
1E9A71712B59CC1300477BA2 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71702B59CC1300477BA2 /* Attachment.swift */; }; 1E9A71712B59CC1300477BA2 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71702B59CC1300477BA2 /* Attachment.swift */; };
1E9A71742B59F36E00477BA2 /* ClientSSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71722B59F34E00477BA2 /* ClientSSL.swift */; };
1E9A71752B59F36E00477BA2 /* ClientSSL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71722B59F34E00477BA2 /* ClientSSL.swift */; };
1E9A71772B59FCA900477BA2 /* URLSessionCertificateHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71762B59FCA900477BA2 /* URLSessionCertificateHandling.swift */; };
1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB375882B55DBFB00AEC3D7 /* Server.swift */; }; 1EB375892B55DBFB00AEC3D7 /* Server.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB375882B55DBFB00AEC3D7 /* Server.swift */; };
1EB8EF722510F1EE00F352B7 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB8EF712510F1EE00F352B7 /* Storage.swift */; }; 1EB8EF722510F1EE00F352B7 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB8EF712510F1EE00F352B7 /* Storage.swift */; };
1EC6ACB722CB9FC300A41C61 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EC6ACB522CB9FC300A41C61 /* MainInterface.storyboard */; }; 1EC6ACB722CB9FC300A41C61 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 1EC6ACB522CB9FC300A41C61 /* MainInterface.storyboard */; };
@ -398,6 +401,8 @@
1E9A71682B59B6E100477BA2 /* MessageSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSender.swift; sourceTree = "<group>"; }; 1E9A71682B59B6E100477BA2 /* MessageSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSender.swift; sourceTree = "<group>"; };
1E9A716E2B59CBCA00477BA2 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; }; 1E9A716E2B59CBCA00477BA2 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
1E9A71702B59CC1300477BA2 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; }; 1E9A71702B59CC1300477BA2 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
1E9A71722B59F34E00477BA2 /* ClientSSL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientSSL.swift; sourceTree = "<group>"; };
1E9A71762B59FCA900477BA2 /* URLSessionCertificateHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSessionCertificateHandling.swift; sourceTree = "<group>"; };
1EB375882B55DBFB00AEC3D7 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; }; 1EB375882B55DBFB00AEC3D7 /* Server.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Server.swift; sourceTree = "<group>"; };
1EB8EF712510F1EE00F352B7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; }; 1EB8EF712510F1EE00F352B7 /* Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storage.swift; sourceTree = "<group>"; };
1EC6ACB022CB9FC300A41C61 /* ShareRocketChatRN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareRocketChatRN.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 1EC6ACB022CB9FC300A41C61 /* ShareRocketChatRN.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareRocketChatRN.appex; sourceTree = BUILT_PRODUCTS_DIR; };
@ -593,6 +598,7 @@
1E29A2E92B585B070093C03C /* FailableDecodable.swift */, 1E29A2E92B585B070093C03C /* FailableDecodable.swift */,
1E29A2EA2B585B070093C03C /* HTTP */, 1E29A2EA2B585B070093C03C /* HTTP */,
1E29A2EE2B585B070093C03C /* RocketChatError.swift */, 1E29A2EE2B585B070093C03C /* RocketChatError.swift */,
1E9A71762B59FCA900477BA2 /* URLSessionCertificateHandling.swift */,
); );
path = Client; path = Client;
sourceTree = "<group>"; sourceTree = "<group>";
@ -699,6 +705,7 @@
1E470E822513A71E00E3DD1D /* RocketChat.swift */, 1E470E822513A71E00E3DD1D /* RocketChat.swift */,
1EB8EF712510F1EE00F352B7 /* Storage.swift */, 1EB8EF712510F1EE00F352B7 /* Storage.swift */,
1ED038A82B5090AD00C007D4 /* MMKV.swift */, 1ED038A82B5090AD00C007D4 /* MMKV.swift */,
1E9A71722B59F34E00477BA2 /* ClientSSL.swift */,
); );
path = RocketChat; path = RocketChat;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1797,6 +1804,7 @@
1E76CBD225152C730067298C /* Data+Extensions.swift in Sources */, 1E76CBD225152C730067298C /* Data+Extensions.swift in Sources */,
1E76CBD125152C710067298C /* Date+Extensions.swift in Sources */, 1E76CBD125152C710067298C /* Date+Extensions.swift in Sources */,
1E76CBD425152C790067298C /* Database.swift in Sources */, 1E76CBD425152C790067298C /* Database.swift in Sources */,
1E9A71742B59F36E00477BA2 /* ClientSSL.swift in Sources */,
1ED038AD2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */, 1ED038AD2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */,
1ED038A52B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 1ED038A52B50900800C007D4 /* Bundle+Extensions.swift in Sources */,
1E76CBC325152A460067298C /* String+Extensions.swift in Sources */, 1E76CBC325152A460067298C /* String+Extensions.swift in Sources */,
@ -1870,6 +1878,7 @@
1E29A2FF2B585B070093C03C /* TokenAdapter.swift in Sources */, 1E29A2FF2B585B070093C03C /* TokenAdapter.swift in Sources */,
1E29A3052B585B070093C03C /* Request.swift in Sources */, 1E29A3052B585B070093C03C /* Request.swift in Sources */,
1E6436242B59998A009F0CE1 /* ExtensionDelegate.swift in Sources */, 1E6436242B59998A009F0CE1 /* ExtensionDelegate.swift in Sources */,
1E9A71772B59FCA900477BA2 /* URLSessionCertificateHandling.swift in Sources */,
1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */, 1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */,
1E29A2FB2B585B070093C03C /* MessagesRequest.swift in Sources */, 1E29A2FB2B585B070093C03C /* MessagesRequest.swift in Sources */,
1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */, 1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */,
@ -1955,6 +1964,7 @@
7AAB3E23257E6A6E00707CF6 /* Data+Extensions.swift in Sources */, 7AAB3E23257E6A6E00707CF6 /* Data+Extensions.swift in Sources */,
7AAB3E24257E6A6E00707CF6 /* Date+Extensions.swift in Sources */, 7AAB3E24257E6A6E00707CF6 /* Date+Extensions.swift in Sources */,
7AAB3E25257E6A6E00707CF6 /* Database.swift in Sources */, 7AAB3E25257E6A6E00707CF6 /* Database.swift in Sources */,
1E9A71752B59F36E00477BA2 /* ClientSSL.swift in Sources */,
1ED038AF2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */, 1ED038AF2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */,
1ED038A72B50900800C007D4 /* Bundle+Extensions.swift in Sources */, 1ED038A72B50900800C007D4 /* Bundle+Extensions.swift in Sources */,
7AAB3E26257E6A6E00707CF6 /* String+Extensions.swift in Sources */, 7AAB3E26257E6A6E00707CF6 /* String+Extensions.swift in Sources */,

View File

@ -0,0 +1,28 @@
import Foundation
struct ClientSSL: Codable {
let path: String
let password: String
}
extension MMKV {
func clientSSL(for url: URL) -> ClientSSL? {
guard let host = url.host else {
return nil
}
guard let rawClientSSL = string(forKey: host) else {
return nil
}
guard let data = rawClientSSL.data(using: .utf8) else {
return nil
}
guard let clientSSL = try? JSONDecoder().decode(ClientSSL.self, from: data) else {
return nil
}
return clientSSL
}
}

View File

@ -1,40 +1,40 @@
import Foundation import Foundation
extension MMKV { extension MMKV {
static func build() -> MMKV { static func build() -> MMKV {
let password = SecureStorage().getSecureKey("com.MMKV.default".toHex()) let password = SecureStorage().getSecureKey("com.MMKV.default".toHex())
let groupDir = FileManager.default.groupDir() let groupDir = FileManager.default.groupDir()
MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: MMKVLogLevel.none) MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: MMKVLogLevel.none)
guard let mmkv = MMKV(mmapID: "default", cryptKey: password?.data(using: .utf8), mode: MMKVMode.multiProcess) else { guard let mmkv = MMKV(mmapID: "default", cryptKey: password?.data(using: .utf8), mode: MMKVMode.multiProcess) else {
fatalError("Could not initialize MMKV instance.") fatalError("Could not initialize MMKV instance.")
} }
return mmkv return mmkv
} }
func userToken(for userId: String) -> String { func userToken(for userId: String) -> String {
guard let userToken = string(forKey: "reactnativemeteor_usertoken-\(userId)") else { guard let userToken = string(forKey: "reactnativemeteor_usertoken-\(userId)") else {
fatalError("userToken is nil for userId \(userId)") fatalError("userToken is nil for userId \(userId)")
} }
return userToken return userToken
} }
func userId(for server: String) -> String { func userId(for server: String) -> String {
guard let userId = string(forKey: "reactnativemeteor_usertoken-\(server)") else { guard let userId = string(forKey: "reactnativemeteor_usertoken-\(server)") else {
fatalError("userId is nil for server \(server)") fatalError("userId is nil for server \(server)")
} }
return userId return userId
} }
func privateKey(for server: String) -> String { func privateKey(for server: String) -> String {
guard let privateKey = string(forKey: "\(server)-RC_E2E_PRIVATE_KEY") else { guard let privateKey = string(forKey: "\(server)-RC_E2E_PRIVATE_KEY") else {
fatalError("privateKey is nil for server \(server)") fatalError("privateKey is nil for server \(server)")
} }
return privateKey return privateKey
} }
} }

View File

@ -4,79 +4,100 @@ import WatchConnectivity
@objc @objc
final class WatchConnection: NSObject { final class WatchConnection: NSObject {
private let database = WatermelonDB.Database(name: "default") private let database = WatermelonDB.Database(name: "default")
private let mmkv = MMKV.build() private let mmkv = MMKV.build()
private let session: WCSession private let session: WCSession
@objc init(session: WCSession) { @objc init(session: WCSession) {
self.session = session self.session = session
super.init() super.init()
if WCSession.isSupported() { if WCSession.isSupported() {
session.delegate = self session.delegate = self
session.activate() session.activate()
} }
} }
private func getMessage() -> WatchMessage { private func getClientSSL(from clientSSL: ClientSSL?) -> WatchMessage.Server.ClientSSL? {
let serversQuery = database.query(raw: "select * from servers") as [DBServer] guard let clientSSL else {
return nil
}
let servers = serversQuery.compactMap { item -> WatchMessage.Server? in guard FileManager.default.fileExists(atPath: clientSSL.path) else {
let userId = mmkv.userId(for: item.identifier) return nil
let userToken = mmkv.userToken(for: userId) }
let usersQuery = database.query(raw: "select * from users where token == ? limit 1", [userToken]) as [DBUser] guard let certificate = NSData(contentsOfFile: clientSSL.path) else {
return nil
}
guard let user = usersQuery.first else { return .init(
return nil certificate: Data(referencing: certificate),
} password: clientSSL.password
)
}
return WatchMessage.Server( private func getMessage() -> WatchMessage {
url: item.url, let serversQuery = database.query(raw: "select * from servers") as [DBServer]
name: item.name,
iconURL: item.iconURL,
useRealName: item.useRealName == 1 ? true : false,
loggedUser: .init(
id: userId,
token: userToken,
name: user.name,
username: user.username
)
)
}
return WatchMessage(servers: servers) let servers = serversQuery.compactMap { item -> WatchMessage.Server? in
} let userId = mmkv.userId(for: item.identifier)
let userToken = mmkv.userToken(for: userId)
let clientSSL = mmkv.clientSSL(for: item.url)
private func encodedMessage() -> [String: Any] { let usersQuery = database.query(raw: "select * from users where token == ? limit 1", [userToken]) as [DBUser]
do {
let data = try JSONEncoder().encode(getMessage())
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else { guard let user = usersQuery.first else {
fatalError("Could not serialize message: \(getMessage())") return nil
} }
return dictionary return WatchMessage.Server(
} catch { url: item.url,
fatalError("Could not encode message: \(getMessage())") name: item.name,
} iconURL: item.iconURL,
} useRealName: item.useRealName == 1 ? true : false,
loggedUser: .init(
id: userId,
token: userToken,
name: user.name,
username: user.username
),
clientSSL: getClientSSL(from: clientSSL)
)
}
return WatchMessage(servers: servers)
}
private func encodedMessage() -> [String: Any] {
do {
let data = try JSONEncoder().encode(getMessage())
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] else {
fatalError("Could not serialize message: \(getMessage())")
}
return dictionary
} catch {
fatalError("Could not encode message: \(getMessage())")
}
}
} }
extension WatchConnection: WCSessionDelegate { extension WatchConnection: WCSessionDelegate {
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
} }
func sessionDidBecomeInactive(_ session: WCSession) { func sessionDidBecomeInactive(_ session: WCSession) {
session.activate() session.activate()
} }
func sessionDidDeactivate(_ session: WCSession) { func sessionDidDeactivate(_ session: WCSession) {
session.activate() session.activate()
} }
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) { func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
replyHandler(encodedMessage()) replyHandler(encodedMessage())
} }
} }

View File

@ -1,20 +1,26 @@
import Foundation import Foundation
struct WatchMessage: Codable { struct WatchMessage: Codable {
let servers: [Server] let servers: [Server]
struct Server: Codable { struct Server: Codable {
let url: URL let url: URL
let name: String let name: String
let iconURL: URL let iconURL: URL
let useRealName: Bool let useRealName: Bool
let loggedUser: LoggedUser let loggedUser: LoggedUser
let clientSSL: ClientSSL?
struct LoggedUser: Codable { struct LoggedUser: Codable {
let id: String let id: String
let token: String let token: String
let name: String let name: String
let username: String let username: String
} }
}
struct ClientSSL: Codable {
let certificate: Data
let password: String
}
}
} }