Compare commits

..

2 Commits

Author SHA1 Message Date
Christoph Hagen
f4864127f8 Update device authentication 2023-12-06 09:05:41 +01:00
Christoph Hagen
6117ae8305 Ignore more resources 2023-12-06 09:05:16 +01:00
4 changed files with 30 additions and 64 deletions

2
.gitignore vendored
View File

@ -3,3 +3,5 @@ Package.resolved
.swiftpm .swiftpm
.build .build
Resources/config.json Resources/config.json
Resources/logs
Resources/keys

View File

@ -3,18 +3,6 @@ import WebSocketKit
import Vapor import Vapor
import Clairvoyant import Clairvoyant
enum DeviceState: UInt8 {
case disconnected = 0
case connected = 1
case authenticated = 2
}
extension DeviceState: MetricValue {
static let valueType: MetricType = .customType(named: "DeviceState")
}
final class DeviceManager { final class DeviceManager {
/// The connection to the device /// The connection to the device
@ -26,28 +14,17 @@ final class DeviceManager {
/// The authentication token of the remote /// The authentication token of the remote
private let remoteKey: Data private let remoteKey: Data
/// Indicate that the socket is fully initialized with an authorized device
private var deviceIsAuthenticated = false
private let deviceTimeout: Int64 private let deviceTimeout: Int64
private let deviceStateMetric: Metric<DeviceState> private let deviceConnectedMetric: Metric<Bool>
private let messagesToDeviceMetric: Metric<Int> private let messagesToDeviceMetric: Metric<Int>
var deviceState: DeviceState {
guard let connection, !connection.isClosed else {
return .disconnected
}
guard deviceIsAuthenticated else {
return .connected
}
return .authenticated
}
/// Indicator for device availability
var deviceIsConnected: Bool { var deviceIsConnected: Bool {
deviceIsAuthenticated && !(connection?.isClosed ?? true) guard let connection, !connection.isClosed else {
return false
}
return true
} }
/// A promise to finish the request once the device responds or times out /// A promise to finish the request once the device responds or times out
@ -57,10 +34,10 @@ final class DeviceManager {
self.deviceKey = deviceKey self.deviceKey = deviceKey
self.remoteKey = remoteKey self.remoteKey = remoteKey
self.deviceTimeout = deviceTimeout self.deviceTimeout = deviceTimeout
self.deviceStateMetric = .init( self.deviceConnectedMetric = .init(
"sesame.device", "sesame.connected",
name: "Device status", name: "Device connection",
description: "Shows if the device is connected and authenticated via WebSocket") description: "Shows if the device is connected via WebSocket")
self.messagesToDeviceMetric = .init( self.messagesToDeviceMetric = .init(
"sesame.messages", "sesame.messages",
name: "Forwarded Messages", name: "Forwarded Messages",
@ -68,7 +45,7 @@ final class DeviceManager {
} }
func updateDeviceConnectionMetric() async { func updateDeviceConnectionMetric() async {
_ = try? await deviceStateMetric.update(deviceState) _ = try? await deviceConnectedMetric.update(deviceIsConnected)
} }
private func updateMessageCountMetric() async { private func updateMessageCountMetric() async {
@ -78,10 +55,6 @@ final class DeviceManager {
// MARK: API // MARK: API
private var deviceStatus: String {
"\(deviceState.rawValue)"
}
func sendMessageToDevice(_ message: Data, on eventLoop: EventLoop) async throws -> Data { func sendMessageToDevice(_ message: Data, on eventLoop: EventLoop) async throws -> Data {
guard let socket = connection, !socket.isClosed else { guard let socket = connection, !socket.isClosed else {
connection = nil connection = nil
@ -119,21 +92,6 @@ final class DeviceManager {
requestInProgress?.resume(throwing: result) requestInProgress?.resume(throwing: result)
requestInProgress = nil requestInProgress = nil
} }
func authenticateDevice(hash: String) async {
guard let key = Data(fromHexEncodedString: hash),
SHA256.hash(data: key) == self.deviceKey else {
log("Invalid device key")
await removeDeviceConnection()
return
}
guard let connection, !connection.isClosed else {
await updateDeviceConnectionMetric()
return
}
deviceIsAuthenticated = true
await updateDeviceConnectionMetric()
}
func authenticateRemote(_ token: Data) -> Bool { func authenticateRemote(_ token: Data) -> Bool {
let hash = SHA256.hash(data: token) let hash = SHA256.hash(data: token)
@ -150,32 +108,34 @@ final class DeviceManager {
} }
func didCloseDeviceSocket() { func didCloseDeviceSocket() {
deviceIsAuthenticated = false
connection = nil connection = nil
} }
func removeDeviceConnection() async { func removeDeviceConnection() async {
try? await connection?.close() try? await connection?.close()
connection = nil connection = nil
deviceIsAuthenticated = false
await updateDeviceConnectionMetric() await updateDeviceConnectionMetric()
} }
func createNewDeviceConnection(_ socket: WebSocket) async { func createNewDeviceConnection(socket: WebSocket, auth: String) async {
guard let key = Data(fromHexEncodedString: auth),
SHA256.hash(data: key) == self.deviceKey else {
log("Invalid device key")
return
}
await removeDeviceConnection() await removeDeviceConnection()
connection = socket
socket.eventLoop.execute { socket.eventLoop.execute {
socket.pingInterval = .seconds(10)
socket.onBinary { [weak self] _, data in socket.onBinary { [weak self] _, data in
self?.processDeviceResponse(data) self?.processDeviceResponse(data)
} }
socket.onText { [weak self] _, text async in socket.onClose.whenComplete { [weak self] _ in
await self?.authenticateDevice(hash: text)
}
_ = socket.onClose.always { [weak self] _ in
self?.didCloseDeviceSocket() self?.didCloseDeviceSocket()
} }
} }
connection = socket
await updateDeviceConnectionMetric() await updateDeviceConnectionMetric()
} }
} }

View File

@ -77,7 +77,11 @@ func routes(_ app: Application) throws {
- Returns: Nothing - Returns: Nothing
- Note: The first message from the device over the connection must be a valid auth token. - Note: The first message from the device over the connection must be a valid auth token.
*/ */
app.webSocket(RouteAPI.socket.path) { req, socket async in app.webSocket(RouteAPI.socket.path) { request, socket async in
await deviceManager.createNewDeviceConnection(socket) guard let authToken = request.headers.first(name: "Authorization") else {
try? await socket.close()
return
}
await deviceManager.createNewDeviceConnection(socket: socket, auth: authToken)
} }
} }

View File

@ -5,8 +5,8 @@ var env = Environment.production //.detect()
try LoggingSystem.bootstrap(from: &env) try LoggingSystem.bootstrap(from: &env)
let app = Application(env) let app = Application(env)
defer { defer {
app.shutdown()
shutdown() shutdown()
app.shutdown()
} }
try configure(app) try configure(app)