Add Client Certificate
This commit is contained in:
parent
8f3bb1a99b
commit
0672f56f10
|
@ -11,9 +11,18 @@ protocol RocketChatClientProtocol {
|
|||
func sendRead(rid: String) -> AnyPublisher<ReadResponse, RocketChatError>
|
||||
}
|
||||
|
||||
final class RocketChatClient {
|
||||
final class RocketChatClient: NSObject {
|
||||
private let server: Server
|
||||
|
||||
private lazy var session = URLSession(
|
||||
configuration: .default,
|
||||
delegate: URLSesionClientCertificateHandling(
|
||||
certificate: server.certificate,
|
||||
password: server.password
|
||||
),
|
||||
delegateQueue: nil
|
||||
)
|
||||
|
||||
init(server: Server) {
|
||||
self.server = server
|
||||
}
|
||||
|
@ -32,7 +41,7 @@ final class RocketChatClient {
|
|||
urlRequest.httpMethod = request.method.rawValue
|
||||
urlRequest.httpBody = request.body
|
||||
|
||||
return URLSession.shared.dataTaskPublisher(for: urlRequest)
|
||||
return session.dataTaskPublisher(for: urlRequest)
|
||||
.tryMap { (data, response) in
|
||||
guard let httpResponse = response as? HTTPURLResponse, 200...299 ~= httpResponse.statusCode else {
|
||||
throw RocketChatError.unauthorized
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -74,6 +74,8 @@ final class DefaultDatabase: ServersDatabase {
|
|||
server.iconURL = updatedServer.iconURL
|
||||
server.useRealName = updatedServer.useRealName
|
||||
server.loggedUser = user(from: updatedServer.loggedUser)
|
||||
server.certificate = updatedServer.clientSSL?.certificate
|
||||
server.password = updatedServer.clientSSL?.password
|
||||
} else {
|
||||
Server(
|
||||
context: viewContext,
|
||||
|
@ -81,7 +83,9 @@ final class DefaultDatabase: ServersDatabase {
|
|||
name: updatedServer.name,
|
||||
url: updatedServer.url,
|
||||
useRealName: updatedServer.useRealName,
|
||||
loggedUser: user(from: updatedServer.loggedUser)
|
||||
loggedUser: user(from: updatedServer.loggedUser),
|
||||
certificate: updatedServer.clientSSL?.certificate,
|
||||
password: updatedServer.clientSSL?.password
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,8 +8,10 @@
|
|||
<relationship name="server" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Server" inverseName="loggedUser" inverseEntity="Server"/>
|
||||
</entity>
|
||||
<entity name="Server" representedClassName=".Server" syncable="YES">
|
||||
<attribute name="certificate" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="iconURL" optional="YES" attributeType="URI"/>
|
||||
<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="url" optional="YES" attributeType="URI"/>
|
||||
<attribute name="useRealName" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
|
|
|
@ -13,6 +13,8 @@ public final class Server: NSManagedObject {
|
|||
@NSManaged public var url: URL
|
||||
@NSManaged public var useRealName: Bool
|
||||
@NSManaged public var loggedUser: LoggedUser
|
||||
@NSManaged public var certificate: Data?
|
||||
@NSManaged public var password: String?
|
||||
|
||||
@available(*, unavailable)
|
||||
init() {
|
||||
|
@ -36,7 +38,9 @@ public final class Server: NSManagedObject {
|
|||
updatedSince: Date? = nil,
|
||||
url: URL,
|
||||
useRealName: Bool,
|
||||
loggedUser: LoggedUser
|
||||
loggedUser: LoggedUser,
|
||||
certificate: Data? = nil,
|
||||
password: String? = nil
|
||||
) {
|
||||
let entity = NSEntityDescription.entity(forEntityName: "Server", in: context)!
|
||||
super.init(entity: entity, insertInto: context)
|
||||
|
@ -46,6 +50,8 @@ public final class Server: NSManagedObject {
|
|||
self.url = url
|
||||
self.useRealName = useRealName
|
||||
self.loggedUser = loggedUser
|
||||
self.certificate = certificate
|
||||
self.password = password
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -112,6 +112,9 @@
|
|||
1E9A71692B59B6E100477BA2 /* MessageSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A71682B59B6E100477BA2 /* MessageSender.swift */; };
|
||||
1E9A716F2B59CBCA00477BA2 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9A716E2B59CBCA00477BA2 /* AttachmentView.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 */; };
|
||||
1EB8EF722510F1EE00F352B7 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB8EF712510F1EE00F352B7 /* Storage.swift */; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
|
@ -593,6 +598,7 @@
|
|||
1E29A2E92B585B070093C03C /* FailableDecodable.swift */,
|
||||
1E29A2EA2B585B070093C03C /* HTTP */,
|
||||
1E29A2EE2B585B070093C03C /* RocketChatError.swift */,
|
||||
1E9A71762B59FCA900477BA2 /* URLSessionCertificateHandling.swift */,
|
||||
);
|
||||
path = Client;
|
||||
sourceTree = "<group>";
|
||||
|
@ -699,6 +705,7 @@
|
|||
1E470E822513A71E00E3DD1D /* RocketChat.swift */,
|
||||
1EB8EF712510F1EE00F352B7 /* Storage.swift */,
|
||||
1ED038A82B5090AD00C007D4 /* MMKV.swift */,
|
||||
1E9A71722B59F34E00477BA2 /* ClientSSL.swift */,
|
||||
);
|
||||
path = RocketChat;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1797,6 +1804,7 @@
|
|||
1E76CBD225152C730067298C /* Data+Extensions.swift in Sources */,
|
||||
1E76CBD125152C710067298C /* Date+Extensions.swift in Sources */,
|
||||
1E76CBD425152C790067298C /* Database.swift in Sources */,
|
||||
1E9A71742B59F36E00477BA2 /* ClientSSL.swift in Sources */,
|
||||
1ED038AD2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */,
|
||||
1ED038A52B50900800C007D4 /* Bundle+Extensions.swift in Sources */,
|
||||
1E76CBC325152A460067298C /* String+Extensions.swift in Sources */,
|
||||
|
@ -1870,6 +1878,7 @@
|
|||
1E29A2FF2B585B070093C03C /* TokenAdapter.swift in Sources */,
|
||||
1E29A3052B585B070093C03C /* Request.swift in Sources */,
|
||||
1E6436242B59998A009F0CE1 /* ExtensionDelegate.swift in Sources */,
|
||||
1E9A71772B59FCA900477BA2 /* URLSessionCertificateHandling.swift in Sources */,
|
||||
1E29A2EF2B585B070093C03C /* RocketChatClient.swift in Sources */,
|
||||
1E29A2FB2B585B070093C03C /* MessagesRequest.swift in Sources */,
|
||||
1E29A31D2B5871B60093C03C /* Date+Extensions.swift in Sources */,
|
||||
|
@ -1955,6 +1964,7 @@
|
|||
7AAB3E23257E6A6E00707CF6 /* Data+Extensions.swift in Sources */,
|
||||
7AAB3E24257E6A6E00707CF6 /* Date+Extensions.swift in Sources */,
|
||||
7AAB3E25257E6A6E00707CF6 /* Database.swift in Sources */,
|
||||
1E9A71752B59F36E00477BA2 /* ClientSSL.swift in Sources */,
|
||||
1ED038AF2B50927B00C007D4 /* WatermelonDB+Extensions.swift in Sources */,
|
||||
1ED038A72B50900800C007D4 /* Bundle+Extensions.swift in Sources */,
|
||||
7AAB3E26257E6A6E00707CF6 /* String+Extensions.swift in Sources */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -1,40 +1,40 @@
|
|||
import Foundation
|
||||
|
||||
extension MMKV {
|
||||
static func build() -> MMKV {
|
||||
let password = SecureStorage().getSecureKey("com.MMKV.default".toHex())
|
||||
let groupDir = FileManager.default.groupDir()
|
||||
|
||||
MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: MMKVLogLevel.none)
|
||||
|
||||
guard let mmkv = MMKV(mmapID: "default", cryptKey: password?.data(using: .utf8), mode: MMKVMode.multiProcess) else {
|
||||
fatalError("Could not initialize MMKV instance.")
|
||||
}
|
||||
|
||||
return mmkv
|
||||
}
|
||||
|
||||
func userToken(for userId: String) -> String {
|
||||
guard let userToken = string(forKey: "reactnativemeteor_usertoken-\(userId)") else {
|
||||
fatalError("userToken is nil for userId \(userId)")
|
||||
}
|
||||
|
||||
return userToken
|
||||
}
|
||||
|
||||
func userId(for server: String) -> String {
|
||||
guard let userId = string(forKey: "reactnativemeteor_usertoken-\(server)") else {
|
||||
fatalError("userId is nil for server \(server)")
|
||||
}
|
||||
|
||||
return userId
|
||||
}
|
||||
|
||||
func privateKey(for server: String) -> String {
|
||||
guard let privateKey = string(forKey: "\(server)-RC_E2E_PRIVATE_KEY") else {
|
||||
fatalError("privateKey is nil for server \(server)")
|
||||
}
|
||||
|
||||
return privateKey
|
||||
}
|
||||
static func build() -> MMKV {
|
||||
let password = SecureStorage().getSecureKey("com.MMKV.default".toHex())
|
||||
let groupDir = FileManager.default.groupDir()
|
||||
|
||||
MMKV.initialize(rootDir: nil, groupDir: groupDir, logLevel: MMKVLogLevel.none)
|
||||
|
||||
guard let mmkv = MMKV(mmapID: "default", cryptKey: password?.data(using: .utf8), mode: MMKVMode.multiProcess) else {
|
||||
fatalError("Could not initialize MMKV instance.")
|
||||
}
|
||||
|
||||
return mmkv
|
||||
}
|
||||
|
||||
func userToken(for userId: String) -> String {
|
||||
guard let userToken = string(forKey: "reactnativemeteor_usertoken-\(userId)") else {
|
||||
fatalError("userToken is nil for userId \(userId)")
|
||||
}
|
||||
|
||||
return userToken
|
||||
}
|
||||
|
||||
func userId(for server: String) -> String {
|
||||
guard let userId = string(forKey: "reactnativemeteor_usertoken-\(server)") else {
|
||||
fatalError("userId is nil for server \(server)")
|
||||
}
|
||||
|
||||
return userId
|
||||
}
|
||||
|
||||
func privateKey(for server: String) -> String {
|
||||
guard let privateKey = string(forKey: "\(server)-RC_E2E_PRIVATE_KEY") else {
|
||||
fatalError("privateKey is nil for server \(server)")
|
||||
}
|
||||
|
||||
return privateKey
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,79 +4,100 @@ import WatchConnectivity
|
|||
|
||||
@objc
|
||||
final class WatchConnection: NSObject {
|
||||
private let database = WatermelonDB.Database(name: "default")
|
||||
private let mmkv = MMKV.build()
|
||||
private let session: WCSession
|
||||
|
||||
@objc init(session: WCSession) {
|
||||
self.session = session
|
||||
super.init()
|
||||
|
||||
if WCSession.isSupported() {
|
||||
session.delegate = self
|
||||
session.activate()
|
||||
}
|
||||
}
|
||||
|
||||
private func getMessage() -> WatchMessage {
|
||||
let serversQuery = database.query(raw: "select * from servers") as [DBServer]
|
||||
|
||||
let servers = serversQuery.compactMap { item -> WatchMessage.Server? in
|
||||
let userId = mmkv.userId(for: item.identifier)
|
||||
let userToken = mmkv.userToken(for: userId)
|
||||
|
||||
let usersQuery = database.query(raw: "select * from users where token == ? limit 1", [userToken]) as [DBUser]
|
||||
|
||||
guard let user = usersQuery.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return WatchMessage.Server(
|
||||
url: item.url,
|
||||
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)
|
||||
}
|
||||
|
||||
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())")
|
||||
}
|
||||
}
|
||||
private let database = WatermelonDB.Database(name: "default")
|
||||
private let mmkv = MMKV.build()
|
||||
private let session: WCSession
|
||||
|
||||
@objc init(session: WCSession) {
|
||||
self.session = session
|
||||
super.init()
|
||||
|
||||
if WCSession.isSupported() {
|
||||
session.delegate = self
|
||||
session.activate()
|
||||
}
|
||||
}
|
||||
|
||||
private func getClientSSL(from clientSSL: ClientSSL?) -> WatchMessage.Server.ClientSSL? {
|
||||
guard let clientSSL else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard FileManager.default.fileExists(atPath: clientSSL.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let certificate = NSData(contentsOfFile: clientSSL.path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return .init(
|
||||
certificate: Data(referencing: certificate),
|
||||
password: clientSSL.password
|
||||
)
|
||||
}
|
||||
|
||||
private func getMessage() -> WatchMessage {
|
||||
let serversQuery = database.query(raw: "select * from servers") as [DBServer]
|
||||
|
||||
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)
|
||||
|
||||
let usersQuery = database.query(raw: "select * from users where token == ? limit 1", [userToken]) as [DBUser]
|
||||
|
||||
guard let user = usersQuery.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return WatchMessage.Server(
|
||||
url: item.url,
|
||||
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 {
|
||||
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||
|
||||
}
|
||||
|
||||
func sessionDidBecomeInactive(_ session: WCSession) {
|
||||
session.activate()
|
||||
}
|
||||
|
||||
func sessionDidDeactivate(_ session: WCSession) {
|
||||
session.activate()
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
|
||||
replyHandler(encodedMessage())
|
||||
}
|
||||
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
|
||||
|
||||
}
|
||||
|
||||
func sessionDidBecomeInactive(_ session: WCSession) {
|
||||
session.activate()
|
||||
}
|
||||
|
||||
func sessionDidDeactivate(_ session: WCSession) {
|
||||
session.activate()
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
|
||||
replyHandler(encodedMessage())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,26 @@
|
|||
import Foundation
|
||||
|
||||
struct WatchMessage: Codable {
|
||||
let servers: [Server]
|
||||
|
||||
struct Server: Codable {
|
||||
let url: URL
|
||||
let name: String
|
||||
let iconURL: URL
|
||||
let useRealName: Bool
|
||||
let loggedUser: LoggedUser
|
||||
|
||||
struct LoggedUser: Codable {
|
||||
let id: String
|
||||
let token: String
|
||||
let name: String
|
||||
let username: String
|
||||
}
|
||||
}
|
||||
let servers: [Server]
|
||||
|
||||
struct Server: Codable {
|
||||
let url: URL
|
||||
let name: String
|
||||
let iconURL: URL
|
||||
let useRealName: Bool
|
||||
let loggedUser: LoggedUser
|
||||
let clientSSL: ClientSSL?
|
||||
|
||||
struct LoggedUser: Codable {
|
||||
let id: String
|
||||
let token: String
|
||||
let name: String
|
||||
let username: String
|
||||
}
|
||||
|
||||
struct ClientSSL: Codable {
|
||||
let certificate: Data
|
||||
let password: String
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue