Extract shared code

This commit is contained in:
Christoph Hagen 2022-04-13 14:56:47 +02:00
parent 863eb730b3
commit bf755b4d50
8 changed files with 168 additions and 91 deletions

View File

@ -1,7 +0,0 @@
import Foundation
enum PublicAPI: String {
case getDeviceStatus = "status"
case postMessage = "message"
case socket = "listen"
}

View File

@ -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])

View File

@ -1,5 +1,10 @@
import Foundation
#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
extension Message {

View File

@ -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<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
self.time = UInt32(data: data.prefix(MemoryLayout<UInt32>.size))
self.id = UInt32(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
}
/// 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<T: Sequence>(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)
}
}
}

View File

@ -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"
}

View File

@ -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<T: Sequence>(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<UInt32>.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<T: Sequence>(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)
}
}
}

View File

@ -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)
}