Compare commits

..

35 Commits

Author SHA1 Message Date
18fd850413 Improve logging of errors 2023-11-28 11:26:43 +01:00
f60cb3c05a Rework shutdown procedure 2023-11-28 11:20:29 +01:00
ab2a14e00b Create metric list and log start once 2023-11-28 08:46:26 +01:00
7f1b9a5d96 Log server start 2023-11-27 18:17:00 +01:00
a217db1945 Allow log path specification 2023-11-22 11:48:50 +01:00
9c85f955e4 Fix shutdown procedure 2023-11-10 15:08:33 +01:00
621d2af22b Close socket on shutdown 2023-11-10 15:04:20 +01:00
1369a621ad Attempt socket bugfix 2023-11-10 15:01:37 +01:00
9f20563877 Update logic to async 2023-11-10 13:45:42 +01:00
b8c7256b9d Simplify async scheduler 2023-11-09 13:12:32 +01:00
39766467e6 Remove uses of Task 2023-11-08 10:24:50 +01:00
037d146aba Fix error due to dependency update 2023-10-24 19:09:49 +02:00
00ac95df01 Update clairvoyant 2023-10-01 19:26:31 +02:00
810bff0eb3 Remove build script 2023-09-07 18:14:00 +02:00
fe5128fa8b Fix bug with socket 2023-09-07 16:06:42 +02:00
ef71e42341 Remove migration 2023-09-07 16:06:35 +02:00
44b776ef32 Wait to write metrics 2023-09-07 16:00:02 +02:00
253017c429 Fix decoding 2023-09-07 15:57:30 +02:00
c6b51c98fb Ignore invalid points 2023-09-07 15:49:00 +02:00
66a04781d3 Attempt fix 2023-09-07 15:33:10 +02:00
bc104a9040 Check bug 2023-09-07 15:30:35 +02:00
e52f44f168 Attempt metric log migration 2023-09-07 15:23:44 +02:00
04248b04af Update clairvoyant 2023-09-07 14:13:28 +02:00
e77efe795c Move socket operations to device manager 2023-09-07 14:05:41 +02:00
107b609aea Treat messages as data 2023-08-09 16:26:07 +02:00
5d4adf8b15 More logging 2023-08-08 16:32:25 +02:00
a53c12b02c Add logging 2023-08-08 16:06:41 +02:00
aad6e32082 Add build script 2023-08-08 16:06:28 +02:00
9dd0045c4b Update API with device id 2023-08-08 15:17:59 +02:00
23fd5055cd Move to newer metrics version 2023-02-17 00:09:51 +01:00
e96b85b1cc Log more metrics 2023-02-06 21:57:42 +01:00
b3c58ce4c7 Improve logging 2023-02-06 21:44:56 +01:00
790662a1ec Remove empty keys file 2023-01-31 19:16:38 +01:00
21a4f4ecae Add server status 2023-01-31 19:10:57 +01:00
52cb76d4c8 Read config from file 2023-01-31 19:10:33 +01:00
15 changed files with 339 additions and 394 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
Package.resolved
.swiftpm
.build
Resources/config.json

View File

@ -4,16 +4,22 @@ import PackageDescription
let package = Package(
name: "SesameServer",
platforms: [
.macOS(.v10_15)
.macOS(.v12)
],
dependencies: [
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
.package(url: "https://github.com/christophhagen/Clairvoyant", from: "0.13.0"),
.package(url: "https://github.com/christophhagen/ClairvoyantVapor", from: "0.5.0"),
.package(url: "https://github.com/christophhagen/ClairvoyantBinaryCodable", from: "0.3.1"),
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Vapor", package: "vapor")
.product(name: "Vapor", package: "vapor"),
.product(name: "Clairvoyant", package: "Clairvoyant"),
.product(name: "ClairvoyantVapor", package: "ClairvoyantVapor"),
.product(name: "ClairvoyantBinaryCodable", package: "ClairvoyantBinaryCodable"),
],
swiftSettings: [
// Enable better optimizations when building in Release configuration. Despite the use of

View File

@ -0,0 +1,6 @@
{
"port": 6003,
"keyFileName": "keys",
"deviceTimeout": 20,
"authenticationTokens" : [],
}

View File

@ -1,2 +0,0 @@
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000

View File

@ -1,90 +0,0 @@
import Foundation
import NIOCore
/**
Encapsulates a response from a device.
*/
struct DeviceResponse {
/// Shorthand property for a timeout event.
static var deviceTimedOut: DeviceResponse {
.init(event: .deviceTimedOut)
}
/// Shorthand property for a disconnected event.
static var deviceNotConnected: DeviceResponse {
.init(event: .deviceNotConnected)
}
/// Shorthand property for a connected event.
static var deviceConnected: DeviceResponse {
.init(event: .deviceConnected)
}
/// Shorthand property for an unexpected socket event.
static var unexpectedSocketEvent: DeviceResponse {
.init(event: .unexpectedSocketEvent)
}
/// Shorthand property for an invalid message.
static var invalidMessageData: DeviceResponse {
.init(event: .invalidMessageData)
}
/// Shorthand property for missing body data.
static var noBodyData: DeviceResponse {
.init(event: .noBodyData)
}
/// Shorthand property for a busy connection
static var operationInProgress: DeviceResponse {
.init(event: .operationInProgress)
}
/// The response to a key from the server
let event: MessageResult
/// The index of the next key to use
let response: Message?
/**
Decode a message from a buffer.
The buffer must contain `Message.length+1` bytes. The first byte denotes the event type,
the remaining bytes contain the message.
- Parameter buffer: The buffer where the message bytes are stored
*/
init?(_ buffer: ByteBuffer) {
guard let byte = buffer.getBytes(at: 0, length: 1) else {
print("No bytes received from device")
return nil
}
guard let event = MessageResult(rawValue: byte[0]) else {
print("Unknown response \(byte[0]) received from device")
return nil
}
self.event = event
guard let data = buffer.getSlice(at: 1, length: Message.length) else {
self.response = nil
return
}
self.response = Message(decodeFrom: data)
}
/**
Create a response from an event without a message from the device.
- Parameter event: The response from the device.
*/
init(event: MessageResult) {
self.event = event
self.response = nil
}
/// Get the reponse encoded in bytes.
var encoded: Data {
guard let message = response else {
return Data([event.rawValue])
}
return Data([event.rawValue]) + message.encoded
}
}

View File

@ -1,151 +0,0 @@
import Foundation
import NIOCore
#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
/**
An authenticated message to or from the device.
*/
struct Message: Equatable, Hashable {
/// The message authentication code for the message (32 bytes)
let mac: Data
/// The message content
let content: Content
/**
Create an authenticated message
- Parameter mac: The message authentication code
- Parameter content: The message content
*/
init(mac: Data, content: Content) {
self.mac = mac
self.content = content
}
}
extension Message {
/**
The message content without authentication.
*/
struct Content: Equatable, Hashable {
/// The time of message creation, in UNIX time (seconds since 1970)
let time: UInt32
/// The counter of the message (for freshness)
let id: UInt32
/**
Create new message content.
- Parameter time: The time of message creation,
- Parameter id: The counter of the message
*/
init(time: UInt32, id: UInt32) {
self.time = time
self.id = id
}
/**
Decode message content from data.
The data consists of two `UInt32` encoded in little endian format
- Warning: The sequence must contain at least 8 bytes, or the function will crash.
- Parameter data: The sequence containing the bytes.
*/
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
self.time = UInt32(data: Data(data.prefix(MemoryLayout<UInt32>.size)))
self.id = UInt32(data: Data(data.dropFirst(MemoryLayout<UInt32>.size)))
}
/// The byte length of an encoded message content
static var length: Int {
MemoryLayout<UInt32>.size * 2
}
/// The message content encoded to data
var encoded: Data {
time.encoded + id.encoded
}
}
}
extension Message {
/// The length of a message in bytes
static var length: Int {
SHA256.byteCount + Content.length
}
/**
Decode a message from a byte buffer.
The buffer must contain at least `Message.length` bytes, or it will return `nil`.
- Parameter buffer: The buffer containing the bytes.
*/
init?(decodeFrom buffer: ByteBuffer) {
guard let data = buffer.getBytes(at: 0, length: Message.length) else {
return nil
}
self.init(decodeFrom: data)
}
/// The message encoded to data
var encoded: Data {
mac + content.encoded
}
var bytes: [UInt8] {
Array(encoded)
}
/**
Create a message from received bytes.
- Parameter data: The sequence of bytes
- Note: The sequence must contain at least `Message.length` bytes, or the function will crash.
*/
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
let count = SHA256.byteCount
self.mac = Data(data.prefix(count))
self.content = .init(decodeFrom: Array(data.dropFirst(count)))
}
/**
Check if the message contains a valid authentication code
- Parameter key: The key used to sign the message.
- Returns: `true`, if the message is valid.
*/
func isValid(using key: SymmetricKey) -> Bool {
HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: content.encoded, using: key)
}
}
extension Message.Content {
/**
Calculate an authentication code for the message content.
- Parameter key: The key to use to sign the content.
- Returns: The new message signed with the key.
*/
func authenticate(using key: SymmetricKey) -> Message {
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
return .init(mac: Data(mac.map { $0 }), content: self)
}
/**
Calculate an authentication code for the message content and convert everything to data.
- Parameter key: The key to use to sign the content.
- Returns: The new message signed with the key, serialized to bytes.
*/
func authenticateAndSerialize(using key: SymmetricKey) -> Data {
let encoded = self.encoded
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
return Data(mac.map { $0 }) + encoded
}
}

View File

@ -3,7 +3,7 @@ import Foundation
/**
A result from sending a key to the device.
*/
enum MessageResult: UInt8 {
enum MessageResult: UInt8, Error {
/// Text content was received, although binary data was expected
case textReceived = 1
@ -11,8 +11,8 @@ enum MessageResult: UInt8 {
/// A socket event on the device was unexpected (not binary data)
case unexpectedSocketEvent = 2
/// The size of the payload (i.e. message) was invalid, or the data could not be read
case invalidMessageData = 3
/// The size of the payload (i.e. message) was invalid
case invalidMessageSize = 3
/// The transmitted message could not be authenticated using the key
case messageAuthenticationFailed = 4
@ -26,6 +26,9 @@ enum MessageResult: UInt8 {
/// The key was accepted by the device, and the door will be opened
case messageAccepted = 7
/// The device id is invalid
case messageDeviceInvalid = 8
/// The request did not contain body data with the key
case noBodyData = 10
@ -41,6 +44,12 @@ enum MessageResult: UInt8 {
/// The device is connected
case deviceConnected = 15
case invalidUrlParameter = 20
case invalidResponseAuthentication = 21
case invalidDeviceResponse = 22
}
extension MessageResult: CustomStringConvertible {
@ -51,7 +60,7 @@ extension MessageResult: CustomStringConvertible {
return "The device received unexpected text"
case .unexpectedSocketEvent:
return "Unexpected socket event for the device"
case .invalidMessageData:
case .invalidMessageSize:
return "Invalid message data"
case .messageAuthenticationFailed:
return "Message authentication failed"
@ -61,6 +70,8 @@ extension MessageResult: CustomStringConvertible {
return "Message counter invalid"
case .messageAccepted:
return "Message accepted"
case .messageDeviceInvalid:
return "Invalid device ID"
case .noBodyData:
return "No body data included in the request"
case .deviceNotConnected:
@ -71,6 +82,19 @@ extension MessageResult: CustomStringConvertible {
return "Another operation is in progress"
case .deviceConnected:
return "The device is connected"
case .invalidUrlParameter:
return "The url parameter could not be found"
case .invalidResponseAuthentication:
return "The response could not be authenticated"
case .invalidDeviceResponse:
return "The device responded with invalid data"
}
}
}
extension MessageResult {
var encoded: Data {
Data([rawValue])
}
}

View File

@ -11,11 +11,11 @@ struct ServerMessage {
static let authTokenSize = SHA256.byteCount
static let length = authTokenSize + Message.length
static let maxLength = authTokenSize + 200
let authToken: Data
let message: Message
let message: Data
/**
Decode a message from a byte buffer.
@ -23,15 +23,16 @@ struct ServerMessage {
- Parameter buffer: The buffer containing the bytes.
*/
init?(decodeFrom buffer: ByteBuffer) {
guard let data = buffer.getBytes(at: 0, length: ServerMessage.length) else {
guard buffer.readableBytes < ServerMessage.maxLength else {
log("Received invalid message with \(buffer.readableBytes) bytes")
return nil
}
guard let data = buffer.getBytes(at: 0, length: buffer.readableBytes) else {
log("Failed to read bytes of received message")
return nil
}
self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
self.message = Message(decodeFrom: Data(data.dropFirst(ServerMessage.authTokenSize)))
}
var encoded: Data {
authToken + message.encoded
self.message = Data(data.dropFirst(ServerMessage.authTokenSize))
}
static func token(from buffer: ByteBuffer) -> Data? {

View File

@ -3,11 +3,57 @@ import Foundation
struct Config {
/// The port where the server runs
static let port = 6003
let port: Int
/// The name of the file in the `Resources` folder containing the device authentication token
static let keyFileName = "keys"
let keyFileName: String
/// The seconds to wait for a response from the device
static let deviceTimeout: Int64 = 20
let deviceTimeout: Int64
/// The authentication tokens to use for monitoring of the service
let authenticationTokens: Set<String>
/// The path to the folder where the metric logs are stored
///
/// If no path is provided, then a folder `logs` in the resources directory is created
/// If the path is relative, then it is assumed relative to the resources directory
let logPath: String?
func logURL(possiblyRelativeTo resourcesDirectory: URL) -> URL {
guard let logPath else {
return resourcesDirectory.appendingPathComponent("logs")
}
guard !logPath.hasPrefix("/") else {
return .init(fileURLWithPath: logPath)
}
return resourcesDirectory.appendingPathComponent(logPath)
}
}
extension Config: Codable {
}
extension Config {
init(loadFrom url: URL) throws {
guard FileManager.default.fileExists(atPath: url.path) else {
print("No configuration file found at \(url.path)")
fatalError("No configuration file found")
}
let data: Data
do {
data = try Data(contentsOf: url)
} catch {
print("Failed to read config data: \(error)")
throw error
}
do {
self = try JSONDecoder().decode(Config.self, from: data)
} catch {
print("Failed to decode config data: \(error)")
throw error
}
}
}

View File

@ -1,6 +1,19 @@
import Foundation
import WebSocketKit
import Vapor
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 {
@ -14,9 +27,23 @@ final class DeviceManager {
private let remoteKey: Data
/// Indicate that the socket is fully initialized with an authorized device
var deviceIsAuthenticated = false
private var deviceIsAuthenticated = false
private var isOpeningNewConnection = false
private let deviceTimeout: Int64
private let deviceStateMetric: Metric<DeviceState>
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 {
@ -24,49 +51,88 @@ final class DeviceManager {
}
/// A promise to finish the request once the device responds or times out
private var requestInProgress: EventLoopPromise<DeviceResponse>?
private var requestInProgress: CheckedContinuation<Data, Error>?
init(deviceKey: Data, remoteKey: Data) {
init(deviceKey: Data, remoteKey: Data, deviceTimeout: Int64) {
self.deviceKey = deviceKey
self.remoteKey = remoteKey
self.deviceTimeout = deviceTimeout
self.deviceStateMetric = .init(
"sesame.device",
name: "Device status",
description: "Shows if the device is connected and authenticated via WebSocket")
self.messagesToDeviceMetric = .init(
"sesame.messages",
name: "Forwarded Messages",
description: "The number of messages transmitted to the device")
}
func updateDeviceConnectionMetric() async {
_ = try? await deviceStateMetric.update(deviceState)
}
private func updateMessageCountMetric() async {
let lastValue = await messagesToDeviceMetric.lastValue()?.value ?? 0
_ = try? await messagesToDeviceMetric.update(lastValue + 1)
}
// MARK: API
var deviceStatus: String {
deviceIsConnected ? "1" : "0"
private var deviceStatus: String {
"\(deviceState.rawValue)"
}
func sendMessageToDevice(_ message: Message, on eventLoop: EventLoop) -> EventLoopFuture<DeviceResponse> {
func sendMessageToDevice(_ message: Data, on eventLoop: EventLoop) async throws -> Data {
guard let socket = connection, !socket.isClosed else {
connection = nil
return eventLoop.makeSucceededFuture(.deviceNotConnected)
throw MessageResult.deviceNotConnected
}
guard requestInProgress == nil else {
return eventLoop.makeSucceededFuture(.operationInProgress)
throw MessageResult.operationInProgress
}
requestInProgress = eventLoop.makePromise(of: DeviceResponse.self)
socket.send(message.bytes, promise: nil)
eventLoop.scheduleTask(in: .seconds(Config.deviceTimeout)) { [weak self] in
guard let promise = self?.requestInProgress else {
return
do {
try await socket.send(Array(message))
await updateMessageCountMetric()
} catch {
throw MessageResult.deviceNotConnected
}
self?.requestInProgress = nil
promise.succeed(.deviceTimedOut)
startTimeoutForDeviceRequest(on: eventLoop)
let result: Data = try await withCheckedThrowingContinuation { continuation in
self.requestInProgress = continuation
}
return requestInProgress!.futureResult
return result
}
func authenticateDevice(hash: String) {
private func startTimeoutForDeviceRequest(on eventLoop: EventLoop) {
eventLoop.scheduleTask(in: .seconds(deviceTimeout)) { [weak self] in
self?.resumeDeviceRequest(with: .deviceTimedOut)
}
}
private func resumeDeviceRequest(with data: Data) {
requestInProgress?.resume(returning: data)
requestInProgress = nil
}
private func resumeDeviceRequest(with result: MessageResult) {
requestInProgress?.resume(throwing: result)
requestInProgress = nil
}
func authenticateDevice(hash: String) async {
guard let key = Data(fromHexEncodedString: hash),
SHA256.hash(data: key) == self.deviceKey else {
print("Invalid device key")
_ = connection?.close()
deviceIsAuthenticated = false
log("Invalid device key")
await removeDeviceConnection()
return
}
guard let connection, !connection.isClosed else {
await updateDeviceConnectionMetric()
return
}
print("Device authenticated")
deviceIsAuthenticated = true
await updateDeviceConnectionMetric()
}
func authenticateRemote(_ token: Data) -> Bool {
@ -74,42 +140,42 @@ final class DeviceManager {
return hash == remoteKey
}
func processDeviceResponse(_ data: ByteBuffer) {
guard let promise = requestInProgress else {
func processDeviceResponse(_ buffer: ByteBuffer) {
guard let data = buffer.getData(at: 0, length: buffer.readableBytes) else {
log("Failed to get data buffer received from device")
self.resumeDeviceRequest(with: .invalidDeviceResponse)
return
}
defer { requestInProgress = nil }
promise.succeed(DeviceResponse(data) ?? .unexpectedSocketEvent)
self.resumeDeviceRequest(with: data)
}
func didCloseDeviceSocket() {
guard !isOpeningNewConnection else {
return
}
deviceIsAuthenticated = false
guard connection != nil else {
print("Socket closed, but no connection anyway")
return
}
connection = nil
print("Socket closed")
}
func removeDeviceConnection() {
deviceIsAuthenticated = false
guard let socket = connection else {
return
}
try? socket.close().wait()
func removeDeviceConnection() async {
try? await connection?.close()
connection = nil
print("Removed device connection")
deviceIsAuthenticated = false
await updateDeviceConnectionMetric()
}
func createNewDeviceConnection(_ socket: WebSocket) {
isOpeningNewConnection = true
removeDeviceConnection()
func createNewDeviceConnection(_ socket: WebSocket) async {
await removeDeviceConnection()
socket.eventLoop.execute {
socket.onBinary { [weak self] _, data in
self?.processDeviceResponse(data)
}
socket.onText { [weak self] _, text async in
await self?.authenticateDevice(hash: text)
}
_ = socket.onClose.always { [weak self] _ in
self?.didCloseDeviceSocket()
}
}
connection = socket
print("Socket connected")
isOpeningNewConnection = false
await updateDeviceConnectionMetric()
}
}

View File

@ -0,0 +1,10 @@
import Foundation
import Vapor
import Clairvoyant
extension MultiThreadedEventLoopGroup: AsyncScheduler {
public func schedule(asyncJob: @escaping @Sendable () async throws -> Void) {
_ = any().makeFutureWithTask(asyncJob)
}
}

View File

@ -1,19 +1,91 @@
import Vapor
import Clairvoyant
import ClairvoyantVapor
import ClairvoyantBinaryCodable
var deviceManager: DeviceManager!
private var provider: VaporMetricProvider!
private var status: Metric<ServerStatus>!
private var asyncScheduler = MultiThreadedEventLoopGroup(numberOfThreads: 2)
private let df: DateFormatter = {
let df = DateFormatter()
df.dateStyle = .short
df.timeStyle = .short
return df
}()
enum ServerError: Error {
case invalidAuthenticationFileContent
case invalidAuthenticationToken
}
private func updateStatus(_ newStatus: ServerStatus) {
asyncScheduler.schedule {
do {
try await status.update(newStatus)
} catch {
print("Failed to update server status: \(error)")
}
}
}
// configures your application
public func configure(_ app: Application) throws {
app.http.server.configuration.port = Config.port
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
let keyFile = storageFolder.appendingPathComponent(Config.keyFileName)
let authContent: [Data] = try String(contentsOf: keyFile)
let configUrl = storageFolder.appendingPathComponent("config.json")
let config = try Config(loadFrom: configUrl)
let logFolder = config.logURL(possiblyRelativeTo: storageFolder)
let monitor = MetricObserver(logFileFolder: logFolder, logMetricId: "sesame.log")
MetricObserver.standard = monitor
status = Metric<ServerStatus>("sesame.status")
updateStatus(.initializing)
app.http.server.configuration.port = config.port
let keyFile = storageFolder.appendingPathComponent(config.keyFileName)
let (deviceKey, remoteKey) = try loadKeys(at: keyFile)
deviceManager = DeviceManager(deviceKey: deviceKey, remoteKey: remoteKey, deviceTimeout: config.deviceTimeout)
try routes(app)
provider = .init(observer: monitor, accessManager: config.authenticationTokens)
provider.asyncScheduler = asyncScheduler
provider.registerRoutes(app)
monitor.saveCurrentListOfMetricsToLogFolder()
updateStatus(.nominal)
// Update the metric of the device status to ensure that it is accurate
asyncScheduler.schedule {
await deviceManager.updateDeviceConnectionMetric()
}
log("[\(df.string(from: Date()))] Server started")
}
public func shutdown() {
print("[\(df.string(from: Date()))] Server shutdown")
asyncScheduler.schedule {
// Gracefully shut down by closing potentially open socket
await deviceManager.removeDeviceConnection()
do {
try await asyncScheduler.shutdownGracefully()
} catch {
print("Failed to shut down MultiThreadedEventLoopGroup: \(error)")
}
}
}
private func loadKeys(at url: URL) throws -> (deviceKey: Data, remoteKey: Data) {
let authContent: [Data] = try String(contentsOf: url)
.trimmingCharacters(in: .whitespacesAndNewlines)
.components(separatedBy: "\n")
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
@ -29,15 +101,15 @@ public func configure(_ app: Application) throws {
guard authContent.count == 2 else {
throw ServerError.invalidAuthenticationFileContent
}
let deviceKey = authContent[0]
let remoteKey = authContent[1]
deviceManager = DeviceManager(deviceKey: deviceKey, remoteKey: remoteKey)
try routes(app)
return (deviceKey: authContent[0], remoteKey: authContent[1])
}
// Gracefully shut down by closing potentially open socket
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {
_ = app.server.onShutdown.always { _ in
deviceManager.removeDeviceConnection()
func log(_ message: String) {
guard let observer = MetricObserver.standard else {
print(message)
return
}
asyncScheduler.schedule {
await observer.log(message)
}
}

View File

@ -11,33 +11,33 @@ extension RouteAPI {
}
}
private func messageTransmission(_ req: Request) -> EventLoopFuture<DeviceResponse> {
private func messageTransmission(_ req: Request) async throws -> Data {
guard let body = req.body.data else {
return req.eventLoop.makeSucceededFuture(.noBodyData)
throw MessageResult.noBodyData
}
guard let message = ServerMessage(decodeFrom: body) else {
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
throw MessageResult.invalidMessageSize
}
guard deviceManager.authenticateRemote(message.authToken) else {
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
throw MessageResult.messageAuthenticationFailed
}
return deviceManager.sendMessageToDevice(message.message, on: req.eventLoop)
return try await deviceManager.sendMessageToDevice(message.message, on: req.eventLoop)
}
private func deviceStatus(_ req: Request) -> EventLoopFuture<DeviceResponse> {
private func deviceStatus(_ req: Request) -> MessageResult {
guard let body = req.body.data else {
return req.eventLoop.makeSucceededFuture(.noBodyData)
return .noBodyData
}
guard let authToken = ServerMessage.token(from: body) else {
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
return .invalidMessageSize
}
guard deviceManager.authenticateRemote(authToken) else {
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
return .messageAuthenticationFailed
}
guard deviceManager.deviceIsConnected else {
return req.eventLoop.makeSucceededFuture(.deviceNotConnected)
return .deviceNotConnected
}
return req.eventLoop.makeSucceededFuture(.deviceConnected)
return .deviceConnected
}
func routes(_ app: Application) throws {
@ -48,12 +48,11 @@ func routes(_ app: Application) throws {
The request expects the authentication token of the remote in the body data of the POST request.
The request returns one byte of data, which is the raw value of a `MessageResult`.
Possible results are `noBodyData`, `invalidMessageData`, `deviceNotConnected`, `deviceConnected`.
Possible results are `noBodyData`, `invalidMessageSize`, `deviceNotConnected`, `deviceConnected`.
*/
app.post(RouteAPI.getDeviceStatus.path) { req in
deviceStatus(req).map {
Response(status: .ok, body: .init(data: $0.encoded))
}
app.post(RouteAPI.getDeviceStatus.path) { request in
let result = deviceStatus(request)
return Response(status: .ok, body: .init(data: result.encoded))
}
/**
@ -64,9 +63,12 @@ func routes(_ app: Application) throws {
The request returns one or `Message.length+1` bytes of data, where the first byte is the raw value of a `MessageResult`,
and the optional following bytes contain the response message of the device. This request does not complete until either the device responds or the request times out. The timeout is specified by `KeyManagement.deviceTimeout`.
*/
app.post(RouteAPI.postMessage.path) { req in
messageTransmission(req).map {
Response(status: .ok, body: .init(data: $0.encoded))
app.post(RouteAPI.postMessage.path) { request async throws in
do {
let result = try await messageTransmission(request)
return Response(status: .ok, body: .init(data: result))
} catch let error as MessageResult {
return Response(status: .ok, body: .init(data: error.encoded))
}
}
@ -75,17 +77,7 @@ func routes(_ app: Application) throws {
- Returns: Nothing
- Note: The first message from the device over the connection must be a valid auth token.
*/
app.webSocket(RouteAPI.socket.path) { req, socket in
socket.onBinary { _, data in
deviceManager.processDeviceResponse(data)
}
socket.onText { _, text in
deviceManager.authenticateDevice(hash: text)
}
_ = socket.onClose.always { _ in
deviceManager.didCloseDeviceSocket()
}
deviceManager.createNewDeviceConnection(socket)
app.webSocket(RouteAPI.socket.path) { req, socket async in
await deviceManager.createNewDeviceConnection(socket)
}
}

View File

@ -1,9 +1,13 @@
import App
import Vapor
var env = try Environment.detect()
var env = Environment.production //.detect()
try LoggingSystem.bootstrap(from: &env)
let app = Application(env)
defer { app.shutdown() }
defer {
app.shutdown()
shutdown()
}
try configure(app)
try app.run()

View File

@ -10,44 +10,4 @@ final class AppTests: XCTestCase {
XCTAssertEqual(input, output)
}
func testEncodingContent() {
let input = Message.Content(time: 1234567890, id: 23456789)
let data = Array(input.encoded)
let output = Message.Content(decodeFrom: data)
XCTAssertEqual(input, output)
let data2 = [42, 42] + data
let output2 = Message.Content(decodeFrom: data2[2...])
XCTAssertEqual(input, output2)
}
func testEncodingMessage() {
let input = Message(mac: Data(repeating: 42, count: 32),
content: Message.Content(time: 1234567890, id: 23456789))
let data = input.encoded
let buffer = ByteBuffer(data: data)
let output = Message(decodeFrom: buffer)
XCTAssertEqual(input, output)
}
func testSigning() throws {
let key = SymmetricKey(size: .bits256)
let content = Message.Content(time: 1234567890, id: 23456789)
let input = content.authenticate(using: key)
XCTAssertTrue(input.isValid(using: key))
let data = content.authenticateAndSerialize(using: key)
let decoded = Message(decodeFrom: ByteBuffer(data: data))
XCTAssertNotNil(decoded)
XCTAssertTrue(decoded!.isValid(using: key))
XCTAssertEqual(decoded!, input)
XCTAssertEqual(content, input.content)
}
func testMessageTransmission() throws {
let app = Application(.testing)
defer { app.shutdown() }
try configure(app)
// How to open a socket via request?
}
}