From bf755b4d502fa302a3ece263030c3f555949e613 Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Wed, 13 Apr 2022 14:56:47 +0200 Subject: [PATCH] Extract shared code --- Sources/App/API.swift | 7 - Sources/App/{ => API}/DeviceResponse.swift | 22 +++- .../App/{ => API}/Message+Extensions.swift | 5 + Sources/App/API/Message.swift | 122 ++++++++++++++++++ Sources/App/{ => API}/MessageResult.swift | 0 Sources/App/API/RouteAPI.swift | 16 +++ Sources/App/Message.swift | 79 ------------ Sources/App/routes.swift | 8 +- 8 files changed, 168 insertions(+), 91 deletions(-) delete mode 100644 Sources/App/API.swift rename Sources/App/{ => API}/DeviceResponse.swift (67%) rename Sources/App/{ => API}/Message+Extensions.swift (94%) create mode 100644 Sources/App/API/Message.swift rename Sources/App/{ => API}/MessageResult.swift (100%) create mode 100644 Sources/App/API/RouteAPI.swift delete mode 100644 Sources/App/Message.swift diff --git a/Sources/App/API.swift b/Sources/App/API.swift deleted file mode 100644 index 2af6ad6..0000000 --- a/Sources/App/API.swift +++ /dev/null @@ -1,7 +0,0 @@ -import Foundation - -enum PublicAPI: String { - case getDeviceStatus = "status" - case postMessage = "message" - case socket = "listen" -} diff --git a/Sources/App/DeviceResponse.swift b/Sources/App/API/DeviceResponse.swift similarity index 67% rename from Sources/App/DeviceResponse.swift rename to Sources/App/API/DeviceResponse.swift index 78cda5b..10e3749 100644 --- a/Sources/App/DeviceResponse.swift +++ b/Sources/App/API/DeviceResponse.swift @@ -1,29 +1,37 @@ 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 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) } @@ -34,6 +42,13 @@ struct DeviceResponse { /// 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") @@ -51,11 +66,16 @@ struct DeviceResponse { 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]) diff --git a/Sources/App/Message+Extensions.swift b/Sources/App/API/Message+Extensions.swift similarity index 94% rename from Sources/App/Message+Extensions.swift rename to Sources/App/API/Message+Extensions.swift index 4bc58a1..38fb2b9 100644 --- a/Sources/App/Message+Extensions.swift +++ b/Sources/App/API/Message+Extensions.swift @@ -1,5 +1,10 @@ import Foundation + +#if canImport(CryptoKit) +import CryptoKit +#else import Crypto +#endif extension Message { diff --git a/Sources/App/API/Message.swift b/Sources/App/API/Message.swift new file mode 100644 index 0000000..60ede14 --- /dev/null +++ b/Sources/App/API/Message.swift @@ -0,0 +1,122 @@ +import Foundation +import NIOCore + +/** + An authenticated message to or from the device. + */ +struct Message: Equatable, Hashable { + + /** + 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 big endian format (MSB at index 0) + - Warning: The sequence must contain at least 8 bytes, or the function will crash. + - Parameter data: The sequence containing the bytes. + */ + init(decodeFrom data: T) where T.Element == UInt8 { + self.time = UInt32(data: data.prefix(MemoryLayout.size)) + self.id = UInt32(data: data.dropFirst(MemoryLayout.size)) + } + + /// The byte length of an encoded message content + static var length: Int { + MemoryLayout.size * 2 + } + + /// The message content encoded to data + var encoded: Data { + time.encoded + id.encoded + } + + /// The message content encoded to bytes + var bytes: [UInt8] { + time.bytes + id.bytes + } + } + + /// 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 + } + + /** + 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 + } + + /// The message encoded to bytes + var bytes: [UInt8] { + Array(mac) + content.bytes + } +} + + extension UInt32 { + + /** + Create a value from a big-endian data representation (MSB first) + - Note: The data must contain exactly four bytes. + */ + init(data: T) where T.Element == UInt8 { + self = data + .reversed() + .enumerated() + .map { UInt32($0.element) << ($0.offset * 8) } + .reduce(0, +) + } + + /// The value encoded to a big-endian representation + var encoded: Data { + .init(bytes) + } + + /// The value encoded to a big-endian byte array + var bytes: [UInt8] { + (0..<4).reversed().map { + UInt8((self >> ($0*8)) & 0xFF) + } + } +} diff --git a/Sources/App/MessageResult.swift b/Sources/App/API/MessageResult.swift similarity index 100% rename from Sources/App/MessageResult.swift rename to Sources/App/API/MessageResult.swift diff --git a/Sources/App/API/RouteAPI.swift b/Sources/App/API/RouteAPI.swift new file mode 100644 index 0000000..22e90f0 --- /dev/null +++ b/Sources/App/API/RouteAPI.swift @@ -0,0 +1,16 @@ +import Foundation + +/** + The active urls on the server, for the device and the remote to connect + */ +enum RouteAPI: String { + + /// Check the device status + case getDeviceStatus = "status" + + /// Send a message to the server, to relay to the device + case postMessage = "message" + + /// Open a socket between the device and the server + case socket = "listen" +} diff --git a/Sources/App/Message.swift b/Sources/App/Message.swift deleted file mode 100644 index 6891833..0000000 --- a/Sources/App/Message.swift +++ /dev/null @@ -1,79 +0,0 @@ -import Foundation -import NIOCore - -struct Message: Equatable, Hashable { - - struct Content: Equatable, Hashable { - - let time: UInt32 - - let id: UInt32 - - init(time: UInt32, id: UInt32) { - self.time = time - self.id = id - } - - init(decodeFrom data: T) where T.Element == UInt8 { - self.time = UInt32(data: data.prefix(4)) - self.id = UInt32(data: data.dropFirst(4)) - } - - static var length: Int { - MemoryLayout.size * 2 - } - - var encoded: Data { - time.encoded + id.encoded - } - - var bytes: [UInt8] { - time.bytes + id.bytes - } - } - - let mac: Data - - let content: Content - - init(mac: Data, content: Content) { - self.mac = mac - self.content = content - } - - init?(decodeFrom buffer: ByteBuffer) { - guard let data = buffer.getBytes(at: 0, length: Message.length) else { - return nil - } - self.init(decodeFrom: data) - } - - var encoded: Data { - mac + content.encoded - } - - var bytes: [UInt8] { - Array(mac) + content.bytes - } -} - - extension UInt32 { - - init(data: T) where T.Element == UInt8 { - self = data - .reversed() - .enumerated() - .map { UInt32($0.element) << ($0.offset * 8) } - .reduce(0, +) - } - - var encoded: Data { - .init(bytes) - } - - var bytes: [UInt8] { - (0..<4).reversed().map { - UInt8((self >> ($0*8)) & 0xFF) - } - } -} diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index c4c7ccf..7d50c0e 100755 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -1,6 +1,6 @@ import Vapor -extension PublicAPI { +extension RouteAPI { var path: PathComponent { .init(stringLiteral: rawValue) @@ -28,7 +28,7 @@ func routes(_ app: Application) throws { The response is a string of either "1" (connected) or "0" (disconnected) */ - app.get(PublicAPI.getDeviceStatus.path) { req -> String in + app.get(RouteAPI.getDeviceStatus.path) { req -> String in deviceManager.deviceStatus } @@ -38,7 +38,7 @@ 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(PublicAPI.postMessage.path) { req in + app.post(RouteAPI.postMessage.path) { req in messageTransmission(req).map { Response(status: .ok, body: .init(data: $0.encoded)) } @@ -49,7 +49,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(PublicAPI.socket.path) { req, socket in + app.webSocket(RouteAPI.socket.path) { req, socket in socket.onBinary { _, data in deviceManager.processDeviceResponse(data) }