2022-01-24 17:17:06 +01:00
|
|
|
import Foundation
|
|
|
|
import WebSocketKit
|
|
|
|
import Vapor
|
2023-02-06 21:57:42 +01:00
|
|
|
import Clairvoyant
|
2022-01-24 17:17:06 +01:00
|
|
|
|
2022-04-07 23:53:25 +02:00
|
|
|
final class DeviceManager {
|
2022-01-24 17:17:06 +01:00
|
|
|
|
|
|
|
/// The connection to the device
|
|
|
|
private var connection: WebSocket?
|
2022-04-07 23:53:25 +02:00
|
|
|
|
|
|
|
/// The authentication token of the device for the socket connection
|
2022-05-01 13:28:06 +02:00
|
|
|
private let deviceKey: Data
|
2022-04-07 23:53:25 +02:00
|
|
|
|
2022-05-01 13:12:16 +02:00
|
|
|
/// The authentication token of the remote
|
|
|
|
private let remoteKey: Data
|
|
|
|
|
2022-04-07 23:53:25 +02:00
|
|
|
/// Indicate that the socket is fully initialized with an authorized device
|
2023-09-07 14:05:41 +02:00
|
|
|
private var deviceIsAuthenticated = false
|
2022-04-08 23:38:22 +02:00
|
|
|
|
|
|
|
private var isOpeningNewConnection = false
|
2023-01-31 19:10:33 +01:00
|
|
|
|
|
|
|
private let deviceTimeout: Int64
|
2023-02-06 21:57:42 +01:00
|
|
|
|
|
|
|
private let deviceConnectedMetric: Metric<Bool>
|
|
|
|
|
|
|
|
private let messagesToDeviceMetric: Metric<Int>
|
2022-01-24 17:17:06 +01:00
|
|
|
|
|
|
|
/// Indicator for device availability
|
|
|
|
var deviceIsConnected: Bool {
|
2022-04-08 23:38:22 +02:00
|
|
|
deviceIsAuthenticated && !(connection?.isClosed ?? true)
|
2022-01-24 17:17:06 +01:00
|
|
|
}
|
|
|
|
|
2022-04-07 23:53:25 +02:00
|
|
|
/// A promise to finish the request once the device responds or times out
|
2023-08-09 16:26:07 +02:00
|
|
|
private var requestInProgress: EventLoopPromise<Data>?
|
2022-01-24 17:17:06 +01:00
|
|
|
|
2023-02-17 00:09:51 +01:00
|
|
|
init(deviceKey: Data, remoteKey: Data, deviceTimeout: Int64) async {
|
2022-01-24 17:17:06 +01:00
|
|
|
self.deviceKey = deviceKey
|
2022-05-01 13:12:16 +02:00
|
|
|
self.remoteKey = remoteKey
|
2023-01-31 19:10:33 +01:00
|
|
|
self.deviceTimeout = deviceTimeout
|
2023-02-17 00:09:51 +01:00
|
|
|
self.deviceConnectedMetric = try! await .init(
|
2023-02-06 21:57:42 +01:00
|
|
|
"sesame.connected",
|
|
|
|
name: "Device connected",
|
|
|
|
description: "Shows if the device is connected via WebSocket")
|
2023-02-17 00:09:51 +01:00
|
|
|
self.messagesToDeviceMetric = try! await .init(
|
2023-02-06 21:57:42 +01:00
|
|
|
"sesame.messages",
|
|
|
|
name: "Forwarded Messages",
|
|
|
|
description: "The number of messages transmitted to the device")
|
|
|
|
}
|
|
|
|
|
|
|
|
private func updateDeviceConnectionMetric() {
|
2023-02-17 00:09:51 +01:00
|
|
|
Task {
|
|
|
|
try? await deviceConnectedMetric.update(deviceIsConnected)
|
|
|
|
}
|
2023-02-06 21:57:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private func updateMessageCountMetric() {
|
2023-02-17 00:09:51 +01:00
|
|
|
Task {
|
|
|
|
let lastValue = await messagesToDeviceMetric.lastValue()?.value ?? 0
|
2023-09-07 14:13:28 +02:00
|
|
|
_ = try? await messagesToDeviceMetric.update(lastValue + 1)
|
2023-02-17 00:09:51 +01:00
|
|
|
}
|
2022-01-24 17:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: API
|
|
|
|
|
2023-09-07 14:05:41 +02:00
|
|
|
private var deviceStatus: String {
|
2022-01-24 17:17:06 +01:00
|
|
|
deviceIsConnected ? "1" : "0"
|
|
|
|
}
|
|
|
|
|
2023-08-09 16:26:07 +02:00
|
|
|
func sendMessageToDevice(_ message: Data, on eventLoop: EventLoop) -> EventLoopFuture<Data> {
|
2022-01-24 17:17:06 +01:00
|
|
|
guard let socket = connection, !socket.isClosed else {
|
|
|
|
connection = nil
|
2023-08-09 16:26:07 +02:00
|
|
|
return eventLoop.makeSucceededFuture(MessageResult.deviceNotConnected.encoded)
|
2022-01-24 17:17:06 +01:00
|
|
|
}
|
2022-04-07 23:53:25 +02:00
|
|
|
guard requestInProgress == nil else {
|
2023-08-09 16:26:07 +02:00
|
|
|
return eventLoop.makeSucceededFuture(MessageResult.operationInProgress.encoded)
|
2022-04-07 23:53:25 +02:00
|
|
|
}
|
2023-08-09 16:26:07 +02:00
|
|
|
let result = eventLoop.makePromise(of: Data.self)
|
2023-02-17 00:09:51 +01:00
|
|
|
self.requestInProgress = result
|
2023-08-09 16:26:07 +02:00
|
|
|
socket.send(Array(message), promise: nil)
|
2023-02-06 21:57:42 +01:00
|
|
|
updateMessageCountMetric()
|
2023-01-31 19:10:33 +01:00
|
|
|
eventLoop.scheduleTask(in: .seconds(deviceTimeout)) { [weak self] in
|
2022-04-07 23:53:25 +02:00
|
|
|
guard let promise = self?.requestInProgress else {
|
2022-01-29 10:26:30 +01:00
|
|
|
return
|
|
|
|
}
|
2022-04-07 23:53:25 +02:00
|
|
|
self?.requestInProgress = nil
|
2023-08-09 16:26:07 +02:00
|
|
|
log("Timed out waiting for device response")
|
|
|
|
promise.succeed(MessageResult.deviceTimedOut.encoded)
|
2022-01-29 10:26:30 +01:00
|
|
|
}
|
2023-02-17 00:09:51 +01:00
|
|
|
return result.futureResult
|
2022-01-24 17:17:06 +01:00
|
|
|
}
|
|
|
|
|
2022-05-01 13:28:06 +02:00
|
|
|
func authenticateDevice(hash: String) {
|
2023-02-06 21:57:42 +01:00
|
|
|
defer { updateDeviceConnectionMetric() }
|
2022-05-01 13:28:06 +02:00
|
|
|
guard let key = Data(fromHexEncodedString: hash),
|
|
|
|
SHA256.hash(data: key) == self.deviceKey else {
|
2023-02-06 21:44:56 +01:00
|
|
|
log("Invalid device key")
|
2022-01-24 17:17:06 +01:00
|
|
|
_ = connection?.close()
|
|
|
|
deviceIsAuthenticated = false
|
|
|
|
return
|
|
|
|
}
|
2023-02-06 21:44:56 +01:00
|
|
|
log("Device authenticated")
|
2022-01-24 17:17:06 +01:00
|
|
|
deviceIsAuthenticated = true
|
|
|
|
}
|
|
|
|
|
2022-05-01 13:12:16 +02:00
|
|
|
func authenticateRemote(_ token: Data) -> Bool {
|
|
|
|
let hash = SHA256.hash(data: token)
|
|
|
|
return hash == remoteKey
|
|
|
|
}
|
|
|
|
|
2023-08-08 16:32:25 +02:00
|
|
|
func processDeviceResponse(_ buffer: ByteBuffer) {
|
|
|
|
guard let data = buffer.getData(at: 0, length: buffer.readableBytes) else {
|
|
|
|
log("Failed to get data buffer received from device")
|
|
|
|
return
|
|
|
|
}
|
2022-04-07 23:53:25 +02:00
|
|
|
guard let promise = requestInProgress else {
|
2023-08-08 16:32:25 +02:00
|
|
|
log("Received device response \(data) without an active request")
|
2022-01-24 17:17:06 +01:00
|
|
|
return
|
|
|
|
}
|
2022-04-07 23:53:25 +02:00
|
|
|
defer { requestInProgress = nil }
|
2023-08-08 16:32:25 +02:00
|
|
|
log("Device response received")
|
2023-08-09 16:26:07 +02:00
|
|
|
promise.succeed(data)
|
2022-01-24 17:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func didCloseDeviceSocket() {
|
2023-02-06 21:57:42 +01:00
|
|
|
defer { updateDeviceConnectionMetric() }
|
2022-04-08 23:38:22 +02:00
|
|
|
guard !isOpeningNewConnection else {
|
|
|
|
return
|
|
|
|
}
|
2022-01-24 17:17:06 +01:00
|
|
|
deviceIsAuthenticated = false
|
|
|
|
guard connection != nil else {
|
2023-02-06 21:44:56 +01:00
|
|
|
log("Socket closed, but no connection anyway")
|
2022-01-24 17:17:06 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
connection = nil
|
2023-02-06 21:44:56 +01:00
|
|
|
log("Socket closed")
|
2022-01-24 17:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func removeDeviceConnection() {
|
2023-02-06 21:57:42 +01:00
|
|
|
defer { updateDeviceConnectionMetric() }
|
2022-04-08 23:38:22 +02:00
|
|
|
deviceIsAuthenticated = false
|
|
|
|
guard let socket = connection else {
|
|
|
|
return
|
|
|
|
}
|
2023-09-07 16:06:42 +02:00
|
|
|
socket.close().whenSuccess { log("Socket closed") }
|
2022-01-24 17:17:06 +01:00
|
|
|
connection = nil
|
2023-02-06 21:44:56 +01:00
|
|
|
log("Removed device connection")
|
2022-01-24 17:17:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func createNewDeviceConnection(_ socket: WebSocket) {
|
2023-02-06 21:57:42 +01:00
|
|
|
defer { updateDeviceConnectionMetric() }
|
2023-09-07 14:05:41 +02:00
|
|
|
|
|
|
|
socket.onBinary { _, data in
|
|
|
|
self.processDeviceResponse(data)
|
|
|
|
}
|
|
|
|
socket.onText { _, text in
|
|
|
|
self.authenticateDevice(hash: text)
|
|
|
|
}
|
|
|
|
|
|
|
|
_ = socket.onClose.always { _ in
|
|
|
|
self.didCloseDeviceSocket()
|
|
|
|
}
|
|
|
|
|
2022-04-08 23:38:22 +02:00
|
|
|
isOpeningNewConnection = true
|
2022-01-24 17:17:06 +01:00
|
|
|
removeDeviceConnection()
|
|
|
|
connection = socket
|
2023-02-06 21:44:56 +01:00
|
|
|
log("Socket connected")
|
2022-04-08 23:38:22 +02:00
|
|
|
isOpeningNewConnection = false
|
2022-01-24 17:17:06 +01:00
|
|
|
}
|
|
|
|
}
|