Compare commits
33 Commits
1.0.0
...
ab2a14e00b
Author | SHA1 | Date | |
---|---|---|---|
ab2a14e00b | |||
7f1b9a5d96 | |||
a217db1945 | |||
9c85f955e4 | |||
621d2af22b | |||
1369a621ad | |||
9f20563877 | |||
b8c7256b9d | |||
39766467e6 | |||
037d146aba | |||
00ac95df01 | |||
810bff0eb3 | |||
fe5128fa8b | |||
ef71e42341 | |||
44b776ef32 | |||
253017c429 | |||
c6b51c98fb | |||
66a04781d3 | |||
bc104a9040 | |||
e52f44f168 | |||
04248b04af | |||
e77efe795c | |||
107b609aea | |||
5d4adf8b15 | |||
a53c12b02c | |||
aad6e32082 | |||
9dd0045c4b | |||
23fd5055cd | |||
e96b85b1cc | |||
b3c58ce4c7 | |||
790662a1ec | |||
21a4f4ecae | |||
52cb76d4c8 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
Package.resolved
|
Package.resolved
|
||||||
.swiftpm
|
.swiftpm
|
||||||
.build
|
.build
|
||||||
|
Resources/config.json
|
||||||
|
@ -4,16 +4,22 @@ import PackageDescription
|
|||||||
let package = Package(
|
let package = Package(
|
||||||
name: "SesameServer",
|
name: "SesameServer",
|
||||||
platforms: [
|
platforms: [
|
||||||
.macOS(.v10_15)
|
.macOS(.v12)
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
|
.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: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "App",
|
name: "App",
|
||||||
dependencies: [
|
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: [
|
swiftSettings: [
|
||||||
// Enable better optimizations when building in Release configuration. Despite the use of
|
// Enable better optimizations when building in Release configuration. Despite the use of
|
||||||
|
6
Resources/config_example.json
Normal file
6
Resources/config_example.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"port": 6003,
|
||||||
|
"keyFileName": "keys",
|
||||||
|
"deviceTimeout": 20,
|
||||||
|
"authenticationTokens" : [],
|
||||||
|
}
|
@ -1,2 +0,0 @@
|
|||||||
0000000000000000000000000000000000000000000000000000000000000000
|
|
||||||
0000000000000000000000000000000000000000000000000000000000000000
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,7 +3,7 @@ import Foundation
|
|||||||
/**
|
/**
|
||||||
A result from sending a key to the device.
|
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
|
/// Text content was received, although binary data was expected
|
||||||
case textReceived = 1
|
case textReceived = 1
|
||||||
@ -11,8 +11,8 @@ enum MessageResult: UInt8 {
|
|||||||
/// A socket event on the device was unexpected (not binary data)
|
/// A socket event on the device was unexpected (not binary data)
|
||||||
case unexpectedSocketEvent = 2
|
case unexpectedSocketEvent = 2
|
||||||
|
|
||||||
/// The size of the payload (i.e. message) was invalid, or the data could not be read
|
/// The size of the payload (i.e. message) was invalid
|
||||||
case invalidMessageData = 3
|
case invalidMessageSize = 3
|
||||||
|
|
||||||
/// The transmitted message could not be authenticated using the key
|
/// The transmitted message could not be authenticated using the key
|
||||||
case messageAuthenticationFailed = 4
|
case messageAuthenticationFailed = 4
|
||||||
@ -26,6 +26,9 @@ enum MessageResult: UInt8 {
|
|||||||
/// The key was accepted by the device, and the door will be opened
|
/// The key was accepted by the device, and the door will be opened
|
||||||
case messageAccepted = 7
|
case messageAccepted = 7
|
||||||
|
|
||||||
|
/// The device id is invalid
|
||||||
|
case messageDeviceInvalid = 8
|
||||||
|
|
||||||
|
|
||||||
/// The request did not contain body data with the key
|
/// The request did not contain body data with the key
|
||||||
case noBodyData = 10
|
case noBodyData = 10
|
||||||
@ -41,6 +44,12 @@ enum MessageResult: UInt8 {
|
|||||||
|
|
||||||
/// The device is connected
|
/// The device is connected
|
||||||
case deviceConnected = 15
|
case deviceConnected = 15
|
||||||
|
|
||||||
|
case invalidUrlParameter = 20
|
||||||
|
|
||||||
|
case invalidResponseAuthentication = 21
|
||||||
|
|
||||||
|
case invalidDeviceResponse = 22
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MessageResult: CustomStringConvertible {
|
extension MessageResult: CustomStringConvertible {
|
||||||
@ -51,7 +60,7 @@ extension MessageResult: CustomStringConvertible {
|
|||||||
return "The device received unexpected text"
|
return "The device received unexpected text"
|
||||||
case .unexpectedSocketEvent:
|
case .unexpectedSocketEvent:
|
||||||
return "Unexpected socket event for the device"
|
return "Unexpected socket event for the device"
|
||||||
case .invalidMessageData:
|
case .invalidMessageSize:
|
||||||
return "Invalid message data"
|
return "Invalid message data"
|
||||||
case .messageAuthenticationFailed:
|
case .messageAuthenticationFailed:
|
||||||
return "Message authentication failed"
|
return "Message authentication failed"
|
||||||
@ -61,6 +70,8 @@ extension MessageResult: CustomStringConvertible {
|
|||||||
return "Message counter invalid"
|
return "Message counter invalid"
|
||||||
case .messageAccepted:
|
case .messageAccepted:
|
||||||
return "Message accepted"
|
return "Message accepted"
|
||||||
|
case .messageDeviceInvalid:
|
||||||
|
return "Invalid device ID"
|
||||||
case .noBodyData:
|
case .noBodyData:
|
||||||
return "No body data included in the request"
|
return "No body data included in the request"
|
||||||
case .deviceNotConnected:
|
case .deviceNotConnected:
|
||||||
@ -71,6 +82,19 @@ extension MessageResult: CustomStringConvertible {
|
|||||||
return "Another operation is in progress"
|
return "Another operation is in progress"
|
||||||
case .deviceConnected:
|
case .deviceConnected:
|
||||||
return "The device is connected"
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -11,11 +11,11 @@ struct ServerMessage {
|
|||||||
|
|
||||||
static let authTokenSize = SHA256.byteCount
|
static let authTokenSize = SHA256.byteCount
|
||||||
|
|
||||||
static let length = authTokenSize + Message.length
|
static let maxLength = authTokenSize + 200
|
||||||
|
|
||||||
let authToken: Data
|
let authToken: Data
|
||||||
|
|
||||||
let message: Message
|
let message: Data
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Decode a message from a byte buffer.
|
Decode a message from a byte buffer.
|
||||||
@ -23,15 +23,16 @@ struct ServerMessage {
|
|||||||
- Parameter buffer: The buffer containing the bytes.
|
- Parameter buffer: The buffer containing the bytes.
|
||||||
*/
|
*/
|
||||||
init?(decodeFrom buffer: ByteBuffer) {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
|
self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
|
||||||
self.message = Message(decodeFrom: Data(data.dropFirst(ServerMessage.authTokenSize)))
|
self.message = Data(data.dropFirst(ServerMessage.authTokenSize))
|
||||||
}
|
|
||||||
|
|
||||||
var encoded: Data {
|
|
||||||
authToken + message.encoded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static func token(from buffer: ByteBuffer) -> Data? {
|
static func token(from buffer: ByteBuffer) -> Data? {
|
||||||
|
@ -3,11 +3,57 @@ import Foundation
|
|||||||
struct Config {
|
struct Config {
|
||||||
|
|
||||||
/// The port where the server runs
|
/// 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
|
/// 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
|
/// 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 {
|
||||||
|
log("No configuration file found at \(url.path)")
|
||||||
|
fatalError("No configuration file found")
|
||||||
|
}
|
||||||
|
let data: Data
|
||||||
|
do {
|
||||||
|
data = try Data(contentsOf: url)
|
||||||
|
} catch {
|
||||||
|
log("Failed to read config data: \(error)")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
self = try JSONDecoder().decode(Config.self, from: data)
|
||||||
|
} catch {
|
||||||
|
log("Failed to decode config data: \(error)")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,19 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import WebSocketKit
|
import WebSocketKit
|
||||||
import Vapor
|
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 {
|
final class DeviceManager {
|
||||||
|
|
||||||
@ -14,9 +27,23 @@ final class DeviceManager {
|
|||||||
private let remoteKey: Data
|
private let remoteKey: Data
|
||||||
|
|
||||||
/// Indicate that the socket is fully initialized with an authorized device
|
/// 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
|
/// Indicator for device availability
|
||||||
var deviceIsConnected: Bool {
|
var deviceIsConnected: Bool {
|
||||||
@ -24,49 +51,88 @@ final class DeviceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
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.deviceKey = deviceKey
|
||||||
self.remoteKey = remoteKey
|
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
|
// MARK: API
|
||||||
|
|
||||||
var deviceStatus: String {
|
private var deviceStatus: String {
|
||||||
deviceIsConnected ? "1" : "0"
|
"\(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 {
|
guard let socket = connection, !socket.isClosed else {
|
||||||
connection = nil
|
connection = nil
|
||||||
return eventLoop.makeSucceededFuture(.deviceNotConnected)
|
throw MessageResult.deviceNotConnected
|
||||||
}
|
}
|
||||||
guard requestInProgress == nil else {
|
guard requestInProgress == nil else {
|
||||||
return eventLoop.makeSucceededFuture(.operationInProgress)
|
throw MessageResult.operationInProgress
|
||||||
}
|
}
|
||||||
requestInProgress = eventLoop.makePromise(of: DeviceResponse.self)
|
do {
|
||||||
socket.send(message.bytes, promise: nil)
|
try await socket.send(Array(message))
|
||||||
eventLoop.scheduleTask(in: .seconds(Config.deviceTimeout)) { [weak self] in
|
await updateMessageCountMetric()
|
||||||
guard let promise = self?.requestInProgress else {
|
} catch {
|
||||||
return
|
throw MessageResult.deviceNotConnected
|
||||||
}
|
}
|
||||||
self?.requestInProgress = nil
|
startTimeoutForDeviceRequest(on: eventLoop)
|
||||||
promise.succeed(.deviceTimedOut)
|
|
||||||
|
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),
|
guard let key = Data(fromHexEncodedString: hash),
|
||||||
SHA256.hash(data: key) == self.deviceKey else {
|
SHA256.hash(data: key) == self.deviceKey else {
|
||||||
print("Invalid device key")
|
log("Invalid device key")
|
||||||
_ = connection?.close()
|
await removeDeviceConnection()
|
||||||
deviceIsAuthenticated = false
|
return
|
||||||
|
}
|
||||||
|
guard let connection, !connection.isClosed else {
|
||||||
|
await updateDeviceConnectionMetric()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
print("Device authenticated")
|
|
||||||
deviceIsAuthenticated = true
|
deviceIsAuthenticated = true
|
||||||
|
await updateDeviceConnectionMetric()
|
||||||
}
|
}
|
||||||
|
|
||||||
func authenticateRemote(_ token: Data) -> Bool {
|
func authenticateRemote(_ token: Data) -> Bool {
|
||||||
@ -74,42 +140,42 @@ final class DeviceManager {
|
|||||||
return hash == remoteKey
|
return hash == remoteKey
|
||||||
}
|
}
|
||||||
|
|
||||||
func processDeviceResponse(_ data: ByteBuffer) {
|
func processDeviceResponse(_ buffer: ByteBuffer) {
|
||||||
guard let promise = requestInProgress else {
|
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
|
return
|
||||||
}
|
}
|
||||||
defer { requestInProgress = nil }
|
self.resumeDeviceRequest(with: data)
|
||||||
promise.succeed(DeviceResponse(data) ?? .unexpectedSocketEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func didCloseDeviceSocket() {
|
func didCloseDeviceSocket() {
|
||||||
guard !isOpeningNewConnection else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
deviceIsAuthenticated = false
|
deviceIsAuthenticated = false
|
||||||
guard connection != nil else {
|
|
||||||
print("Socket closed, but no connection anyway")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
connection = nil
|
connection = nil
|
||||||
print("Socket closed")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeDeviceConnection() {
|
func removeDeviceConnection() async {
|
||||||
deviceIsAuthenticated = false
|
try? await connection?.close()
|
||||||
guard let socket = connection else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try? socket.close().wait()
|
|
||||||
connection = nil
|
connection = nil
|
||||||
print("Removed device connection")
|
deviceIsAuthenticated = false
|
||||||
|
await updateDeviceConnectionMetric()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNewDeviceConnection(_ socket: WebSocket) {
|
func createNewDeviceConnection(_ socket: WebSocket) async {
|
||||||
isOpeningNewConnection = true
|
await removeDeviceConnection()
|
||||||
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
|
connection = socket
|
||||||
print("Socket connected")
|
await updateDeviceConnectionMetric()
|
||||||
isOpeningNewConnection = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
Sources/App/EventLoopScheduler.swift
Normal file
10
Sources/App/EventLoopScheduler.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,21 @@
|
|||||||
import Vapor
|
import Vapor
|
||||||
|
import Clairvoyant
|
||||||
|
import ClairvoyantVapor
|
||||||
|
import ClairvoyantBinaryCodable
|
||||||
|
|
||||||
var deviceManager: DeviceManager!
|
var deviceManager: DeviceManager!
|
||||||
|
|
||||||
|
private var provider: VaporMetricProvider!
|
||||||
|
|
||||||
|
private var asyncScheduler = MultiThreadedEventLoopGroup(numberOfThreads: 2)
|
||||||
|
|
||||||
|
private let df: DateFormatter = {
|
||||||
|
let df = DateFormatter()
|
||||||
|
df.dateStyle = .short
|
||||||
|
df.timeStyle = .short
|
||||||
|
return df
|
||||||
|
}()
|
||||||
|
|
||||||
enum ServerError: Error {
|
enum ServerError: Error {
|
||||||
case invalidAuthenticationFileContent
|
case invalidAuthenticationFileContent
|
||||||
case invalidAuthenticationToken
|
case invalidAuthenticationToken
|
||||||
@ -9,11 +23,60 @@ enum ServerError: Error {
|
|||||||
|
|
||||||
// configures your application
|
// configures your application
|
||||||
public func configure(_ app: Application) throws {
|
public func configure(_ app: Application) throws {
|
||||||
app.http.server.configuration.port = Config.port
|
|
||||||
|
|
||||||
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
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
|
||||||
|
|
||||||
|
let status = Metric<ServerStatus>("sesame.status")
|
||||||
|
asyncScheduler.schedule {
|
||||||
|
_ = try await status.update(.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()
|
||||||
|
|
||||||
|
asyncScheduler.schedule {
|
||||||
|
_ = try await status.update(.nominal)
|
||||||
|
await deviceManager.updateDeviceConnectionMetric()
|
||||||
|
}
|
||||||
|
|
||||||
|
log("[\(df.string(from: Date()))] Server started")
|
||||||
|
|
||||||
|
// Gracefully shut down by closing potentially open socket
|
||||||
|
// Must be done after app is running, otherwise error is thrown
|
||||||
|
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {
|
||||||
|
_ = app.server.onShutdown.always { _ in
|
||||||
|
print("[\(df.string(from: Date()))] Server shutdown")
|
||||||
|
asyncScheduler.schedule {
|
||||||
|
await deviceManager.removeDeviceConnection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func shutdown() {
|
||||||
|
try? asyncScheduler.syncShutdownGracefully()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadKeys(at url: URL) throws -> (deviceKey: Data, remoteKey: Data) {
|
||||||
|
let authContent: [Data] = try String(contentsOf: url)
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
.components(separatedBy: "\n")
|
.components(separatedBy: "\n")
|
||||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
@ -29,15 +92,15 @@ public func configure(_ app: Application) throws {
|
|||||||
guard authContent.count == 2 else {
|
guard authContent.count == 2 else {
|
||||||
throw ServerError.invalidAuthenticationFileContent
|
throw ServerError.invalidAuthenticationFileContent
|
||||||
}
|
}
|
||||||
let deviceKey = authContent[0]
|
return (deviceKey: authContent[0], remoteKey: authContent[1])
|
||||||
let remoteKey = authContent[1]
|
}
|
||||||
deviceManager = DeviceManager(deviceKey: deviceKey, remoteKey: remoteKey)
|
|
||||||
try routes(app)
|
|
||||||
|
|
||||||
// Gracefully shut down by closing potentially open socket
|
func log(_ message: String) {
|
||||||
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {
|
guard let observer = MetricObserver.standard else {
|
||||||
_ = app.server.onShutdown.always { _ in
|
print(message)
|
||||||
deviceManager.removeDeviceConnection()
|
return
|
||||||
}
|
}
|
||||||
|
asyncScheduler.schedule {
|
||||||
|
await observer.log(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
guard let body = req.body.data else {
|
||||||
return req.eventLoop.makeSucceededFuture(.noBodyData)
|
throw MessageResult.noBodyData
|
||||||
}
|
}
|
||||||
guard let message = ServerMessage(decodeFrom: body) else {
|
guard let message = ServerMessage(decodeFrom: body) else {
|
||||||
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
|
throw MessageResult.invalidMessageSize
|
||||||
}
|
}
|
||||||
guard deviceManager.authenticateRemote(message.authToken) else {
|
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 {
|
guard let body = req.body.data else {
|
||||||
return req.eventLoop.makeSucceededFuture(.noBodyData)
|
return .noBodyData
|
||||||
}
|
}
|
||||||
guard let authToken = ServerMessage.token(from: body) else {
|
guard let authToken = ServerMessage.token(from: body) else {
|
||||||
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
|
return .invalidMessageSize
|
||||||
}
|
}
|
||||||
guard deviceManager.authenticateRemote(authToken) else {
|
guard deviceManager.authenticateRemote(authToken) else {
|
||||||
return req.eventLoop.makeSucceededFuture(.invalidMessageData)
|
return .messageAuthenticationFailed
|
||||||
}
|
}
|
||||||
guard deviceManager.deviceIsConnected else {
|
guard deviceManager.deviceIsConnected else {
|
||||||
return req.eventLoop.makeSucceededFuture(.deviceNotConnected)
|
return .deviceNotConnected
|
||||||
}
|
}
|
||||||
return req.eventLoop.makeSucceededFuture(.deviceConnected)
|
return .deviceConnected
|
||||||
}
|
}
|
||||||
|
|
||||||
func routes(_ app: Application) throws {
|
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 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`.
|
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
|
app.post(RouteAPI.getDeviceStatus.path) { request in
|
||||||
deviceStatus(req).map {
|
let result = deviceStatus(request)
|
||||||
Response(status: .ok, body: .init(data: $0.encoded))
|
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`,
|
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`.
|
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
|
app.post(RouteAPI.postMessage.path) { request async throws in
|
||||||
messageTransmission(req).map {
|
do {
|
||||||
Response(status: .ok, body: .init(data: $0.encoded))
|
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
|
- 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 in
|
app.webSocket(RouteAPI.socket.path) { req, socket async in
|
||||||
socket.onBinary { _, data in
|
await deviceManager.createNewDeviceConnection(socket)
|
||||||
deviceManager.processDeviceResponse(data)
|
|
||||||
}
|
|
||||||
socket.onText { _, text in
|
|
||||||
deviceManager.authenticateDevice(hash: text)
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = socket.onClose.always { _ in
|
|
||||||
deviceManager.didCloseDeviceSocket()
|
|
||||||
}
|
|
||||||
deviceManager.createNewDeviceConnection(socket)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import App
|
import App
|
||||||
import Vapor
|
import Vapor
|
||||||
|
|
||||||
var env = try Environment.detect()
|
var env = Environment.production //.detect()
|
||||||
try LoggingSystem.bootstrap(from: &env)
|
try LoggingSystem.bootstrap(from: &env)
|
||||||
let app = Application(env)
|
let app = Application(env)
|
||||||
defer { app.shutdown() }
|
defer {
|
||||||
|
app.shutdown()
|
||||||
|
shutdown()
|
||||||
|
}
|
||||||
|
|
||||||
try configure(app)
|
try configure(app)
|
||||||
try app.run()
|
try app.run()
|
||||||
|
@ -10,44 +10,4 @@ final class AppTests: XCTestCase {
|
|||||||
XCTAssertEqual(input, output)
|
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?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user