2022-06-07 14:03:33 +02:00
|
|
|
import Foundation
|
2022-06-09 13:52:17 +02:00
|
|
|
import PushMessageDefinitions
|
2022-06-07 14:03:33 +02:00
|
|
|
|
2022-06-09 15:40:40 +02:00
|
|
|
// Use crypto replacement on Linux
|
2022-06-07 14:03:33 +02:00
|
|
|
#if canImport(CryptoKit)
|
|
|
|
import CryptoKit
|
|
|
|
#else
|
|
|
|
import Crypto
|
|
|
|
#endif
|
|
|
|
|
2022-06-09 15:40:40 +02:00
|
|
|
// Import network types on Linux
|
|
|
|
#if canImport(FoundationNetworking)
|
|
|
|
import FoundationNetworking
|
|
|
|
#endif
|
|
|
|
|
2022-06-07 14:03:33 +02:00
|
|
|
/**
|
|
|
|
A client to interact with a push server.
|
|
|
|
*/
|
|
|
|
public final class PushClient {
|
|
|
|
|
|
|
|
/**
|
|
|
|
The bas url to reach the push server
|
|
|
|
*/
|
|
|
|
public let server: URL
|
|
|
|
|
|
|
|
/**
|
|
|
|
The application id of the service for which this client is used.
|
|
|
|
*/
|
|
|
|
public let application: ApplicationId
|
|
|
|
|
|
|
|
private static let encoder = JSONEncoder()
|
|
|
|
private static let decoder = JSONDecoder()
|
|
|
|
|
|
|
|
/**
|
|
|
|
Create a new client.
|
|
|
|
- Parameter server: The base url of the push server.
|
|
|
|
- Parameter application: The id of the application
|
|
|
|
*/
|
|
|
|
public init(server: URL, application: ApplicationId) {
|
|
|
|
self.server = server
|
|
|
|
self.application = application
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Register the device with the push server.
|
|
|
|
- Parameter token: The APNs token of the device
|
|
|
|
- Parameter name: The optional device name for easier identification
|
|
|
|
- Returns: The new authentication token associated with the device
|
|
|
|
- Note: Depending on the application, the device must be approved by an administrator.
|
|
|
|
*/
|
|
|
|
public func register(token: PushToken, name: String? = nil) async -> AuthenticationToken? {
|
|
|
|
let device = DeviceRegistration(
|
|
|
|
pushToken: token,
|
|
|
|
application: application,
|
|
|
|
name: name ?? "")
|
|
|
|
return await post(.registerNewDevice, body: device)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Get the list of all devices in the same application.
|
|
|
|
- Parameter pushToken: The APNs token of the registered device.
|
|
|
|
- Parameter authToken: The authentication token of the device.
|
|
|
|
- Returns: The list of registered and approved devices in the application
|
|
|
|
*/
|
|
|
|
public func getDeviceList(pushToken: PushToken, authToken: AuthenticationToken) async -> [DeviceRegistration]? {
|
|
|
|
let device = DeviceAuthentication(pushToken: pushToken, authentication: authToken)
|
|
|
|
guard let data = await post(.listDevicesInApplication, body: device) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
return try PushClient.decoder.decode([DeviceRegistration].self, from: data)
|
|
|
|
} catch {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Get a list of unapproved devices for an administrator key.
|
|
|
|
- Parameter masterKey: The master key of the administrator
|
|
|
|
- Returns: A list of registered devices which need to be approved or rejected.
|
|
|
|
*/
|
|
|
|
public func getUnapprovedDevices(masterKey: String) async -> [DeviceRegistration]? {
|
|
|
|
let hash = hash(masterKey)
|
2022-06-07 17:21:47 +02:00
|
|
|
let request = AdminRequest(keyHash: hash, application: application)
|
|
|
|
guard let data = await post(.listUnapprovedDevices, body: request) else {
|
2022-06-07 14:03:33 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
do {
|
|
|
|
return try PushClient.decoder.decode([DeviceRegistration].self, from: data)
|
|
|
|
} catch {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Approve a device using the administrator key.
|
|
|
|
- Parameter pushToken: The push token of the approved device.
|
|
|
|
- Parameter masterKey: The administrator master key.
|
|
|
|
- Returns: `true`, if the device has been approved
|
|
|
|
*/
|
|
|
|
public func approve(pushToken: PushToken, with masterKey: String) async -> Bool {
|
|
|
|
let hash = hash(masterKey)
|
|
|
|
let device = DeviceDecision(pushToken: pushToken, masterKeyHash: hash)
|
|
|
|
return await post(.approveDevice, body: device) != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Reject a device using the administrator key.
|
|
|
|
- Parameter pushToken: The push token of the rejected device.
|
|
|
|
- Parameter masterKey: The administrator master key.
|
|
|
|
- Returns: `true`, if the device has been rejected
|
|
|
|
*/
|
|
|
|
public func reject(pushToken: PushToken, with masterKey: String) async -> Bool {
|
|
|
|
let hash = hash(masterKey)
|
|
|
|
let device = DeviceDecision(pushToken: pushToken, masterKeyHash: hash)
|
|
|
|
return await post(.rejectDevice, body: device) != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Check if the device has been approved.
|
|
|
|
- Parameter token: The APNs token of the device.
|
|
|
|
- Parameter authToken: The authentication token of the device.
|
|
|
|
- Returns: `true`, if the device is approved, `false` on error
|
|
|
|
*/
|
|
|
|
public func isConfirmed(token: PushToken, authentication: AuthenticationToken) async -> Bool {
|
|
|
|
let device = DeviceAuthentication(pushToken: token, authentication: authentication)
|
|
|
|
return await post(.isDeviceApproved, body: device) != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
Send a push notification.
|
|
|
|
- Parameter message: The push message to send
|
|
|
|
- Returns: `true` if the message was sent
|
|
|
|
*/
|
|
|
|
public func send(push message: AuthenticatedPushMessage) async -> Bool {
|
|
|
|
await post(.sendPushNotification, body: message) != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
private func post<T>(_ route: Route, body: T) async -> Data? where T: Encodable {
|
|
|
|
let bodyData = try! PushClient.encoder.encode(body)
|
|
|
|
return await post(route, bodyData: bodyData)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func post(_ route: Route, bodyData: Data) async -> Data? {
|
|
|
|
var request = URLRequest(url: server.appendingPathComponent(route.rawValue))
|
|
|
|
request.httpBody = bodyData
|
|
|
|
request.httpMethod = "POST"
|
2022-06-09 16:27:37 +02:00
|
|
|
return await post(request)
|
2022-06-09 16:00:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private func post(_ request: URLRequest) async -> Data? {
|
2022-06-07 14:03:33 +02:00
|
|
|
do {
|
2022-06-09 16:27:37 +02:00
|
|
|
let (data, response) : (Data, URLResponse) = try await data(for: request)
|
2022-06-07 14:03:33 +02:00
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
guard httpResponse.statusCode == 200 else {
|
|
|
|
print("Failed with code: \(httpResponse.statusCode)")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return data
|
|
|
|
} catch {
|
|
|
|
print("Failed with error: \(error)")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-09 16:27:37 +02:00
|
|
|
private func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
|
|
|
#if !canImport(FoundationNetworking)
|
|
|
|
if #available(iOS 15.0, *) {
|
|
|
|
return try await URLSession.shared.data(for: request)
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
return try await URLSession.shared.dataRequest(request)
|
|
|
|
}
|
|
|
|
|
2022-06-07 14:03:33 +02:00
|
|
|
private func hash(_ masterKey: String) -> Data {
|
|
|
|
Data(SHA256.hash(data: masterKey.data(using: .utf8)!))
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2022-06-09 16:27:37 +02:00
|
|
|
|
|
|
|
private extension URLSession {
|
|
|
|
|
|
|
|
func dataRequest(_ request: URLRequest) async throws -> (Data, URLResponse) {
|
|
|
|
try await withCheckedThrowingContinuation { continuation in
|
|
|
|
dataTask(with: request) { data, response, error in
|
|
|
|
if let error = error {
|
|
|
|
print("Failed with error: \(error)")
|
|
|
|
continuation.resume(throwing: error)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
guard let data = data, let response = response else {
|
|
|
|
continuation.resume(throwing: URLError(.unknown))
|
|
|
|
return
|
|
|
|
}
|
|
|
|
continuation.resume(returning: (data, response))
|
|
|
|
}.resume()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|