Update to challenge-response system
This commit is contained in:
parent
ac22fcd4eb
commit
e76029270a
@ -11,6 +11,7 @@ let package = Package(
|
||||
.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"),
|
||||
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"),
|
||||
],
|
||||
targets: [
|
||||
.executableTarget(
|
||||
@ -20,6 +21,7 @@ let package = Package(
|
||||
.product(name: "Clairvoyant", package: "Clairvoyant"),
|
||||
.product(name: "ClairvoyantVapor", package: "ClairvoyantVapor"),
|
||||
.product(name: "ClairvoyantBinaryCodable", package: "ClairvoyantBinaryCodable"),
|
||||
.product(name: "Crypto", package: "swift-crypto"),
|
||||
]
|
||||
)
|
||||
]
|
||||
|
5
Sources/App/API Extensions/Message.swift
Normal file
5
Sources/App/API Extensions/Message.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
enum Message {
|
||||
|
||||
}
|
9
Sources/App/API Extensions/SesameRoute+Path.swift
Normal file
9
Sources/App/API Extensions/SesameRoute+Path.swift
Normal file
@ -0,0 +1,9 @@
|
||||
import Foundation
|
||||
import RoutingKit
|
||||
|
||||
extension SesameRoute {
|
||||
|
||||
var path: PathComponent {
|
||||
.init(stringLiteral: rawValue)
|
||||
}
|
||||
}
|
5
Sources/App/API Extensions/SignedMessage.swift
Normal file
5
Sources/App/API Extensions/SignedMessage.swift
Normal file
@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
enum SignedMessage {
|
||||
|
||||
}
|
17
Sources/App/API/Extensions/Data+Coding.swift
Normal file
17
Sources/App/API/Extensions/Data+Coding.swift
Normal file
@ -0,0 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
|
||||
func convert<T>(into value: T) -> T {
|
||||
withUnsafeBytes {
|
||||
$0.baseAddress!.load(as: T.self)
|
||||
}
|
||||
}
|
||||
|
||||
init<T>(from value: T) {
|
||||
var target = value
|
||||
self = Swift.withUnsafeBytes(of: &target) {
|
||||
Data($0)
|
||||
}
|
||||
}
|
||||
}
|
@ -40,20 +40,3 @@ extension Data {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Data {
|
||||
|
||||
|
||||
func convert<T>(into value: T) -> T {
|
||||
withUnsafeBytes {
|
||||
$0.baseAddress!.load(as: T.self)
|
||||
}
|
||||
}
|
||||
|
||||
init<T>(from value: T) {
|
||||
var target = value
|
||||
self = Swift.withUnsafeBytes(of: &target) {
|
||||
Data($0)
|
||||
}
|
||||
}
|
||||
}
|
@ -16,4 +16,7 @@ extension UInt32 {
|
||||
var encoded: Data {
|
||||
Data(from: CFSwapInt32HostToLittle(self))
|
||||
}
|
||||
|
||||
/// The size of a `UInt32` when converted to data
|
||||
static let byteSize = MemoryLayout<UInt32>.size
|
||||
}
|
8
Sources/App/API/Message+Size.swift
Normal file
8
Sources/App/API/Message+Size.swift
Normal file
@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
extension Message {
|
||||
|
||||
/// The byte length of an encoded message content
|
||||
static let size: Int = 2 + 2 * UInt32.byteSize
|
||||
|
||||
}
|
@ -3,98 +3,212 @@ import Foundation
|
||||
/**
|
||||
A result from sending a key to the device.
|
||||
*/
|
||||
enum MessageResult: UInt8, Error {
|
||||
enum MessageResult: UInt8 {
|
||||
|
||||
/// Text content was received, although binary data was expected
|
||||
/// The message was accepted.
|
||||
case messageAccepted = 0
|
||||
|
||||
/// The web socket received text while waiting for binary data.
|
||||
case textReceived = 1
|
||||
|
||||
/// A socket event on the device was unexpected (not binary data)
|
||||
/// An unexpected socket event occured while performing the exchange.
|
||||
case unexpectedSocketEvent = 2
|
||||
|
||||
/// The size of the payload (i.e. message) was invalid
|
||||
case invalidMessageSize = 3
|
||||
/// The received message size is invalid.
|
||||
case invalidMessageSizeFromRemote = 3
|
||||
|
||||
/// The transmitted message could not be authenticated using the key
|
||||
case messageAuthenticationFailed = 4
|
||||
/// The message signature was incorrect.
|
||||
case invalidSignatureByRemote = 4
|
||||
|
||||
/// The message time was not within the acceptable bounds
|
||||
case messageTimeMismatch = 5
|
||||
/// The server challenge of the message did not match previous messages
|
||||
case serverChallengeMismatch = 5
|
||||
|
||||
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
|
||||
case messageCounterInvalid = 6
|
||||
/// The client challenge of the message did not match previous messages
|
||||
case clientChallengeMismatchFromRemote = 6
|
||||
|
||||
/// The key was accepted by the device, and the door will be opened
|
||||
case messageAccepted = 7
|
||||
/// An unexpected or unsupported message type was received
|
||||
case invalidMessageTypeFromRemote = 7
|
||||
|
||||
/// The device id is invalid
|
||||
case messageDeviceInvalid = 8
|
||||
|
||||
|
||||
/// The request did not contain body data with the key
|
||||
case noBodyData = 10
|
||||
|
||||
/// The device is not connected
|
||||
case deviceNotConnected = 12
|
||||
|
||||
/// The device did not respond within the timeout
|
||||
case deviceTimedOut = 13
|
||||
|
||||
/// Another message is being processed by the device
|
||||
case operationInProgress = 14
|
||||
|
||||
/// The device is connected
|
||||
case deviceConnected = 15
|
||||
|
||||
case invalidUrlParameter = 20
|
||||
|
||||
case invalidResponseAuthentication = 21
|
||||
/// A message is already being processed
|
||||
case tooManyRequests = 8
|
||||
|
||||
/// The received message result was not ``messageAccepted``
|
||||
case invalidMessageResultFromRemote = 9
|
||||
|
||||
/// An invalid Url parameter was set sending a message to the device over a local connection
|
||||
case invalidUrlParameter = 10
|
||||
|
||||
/// The request took too long to complete
|
||||
case deviceTimedOut = 20
|
||||
|
||||
case noOrInvalidBodyDataInServerRequest = 21
|
||||
|
||||
/// The device is not connected to the server via web socket
|
||||
case deviceNotConnected = 22
|
||||
case serverNotReached = 23
|
||||
case serverUrlInvalid = 24
|
||||
case invalidDeviceResponseSize = 25
|
||||
case invalidSignatureByDevice = 26
|
||||
case noKeyAvailable = 27
|
||||
case unlocked = 28
|
||||
case unknownMessageResultFromDevice = 29
|
||||
|
||||
/// The device sent a message with an invalid client challenge
|
||||
case clientChallengeMismatchFromDevice = 30
|
||||
|
||||
/// A valid server challenge was received
|
||||
case deviceAvailable = 31
|
||||
|
||||
case invalidMessageTypeFromDevice = 32
|
||||
|
||||
/// The url session request returned an unknown response
|
||||
case unexpectedUrlResponseType = 33
|
||||
|
||||
/// The request to the server returned an unhandled HTTP code
|
||||
case unexpectedServerResponseCode = 34
|
||||
|
||||
/// The server produced an internal error (500)
|
||||
case internalServerError = 35
|
||||
|
||||
/// The Sesame server behind the proxy could not be found (502)
|
||||
case serviceBehindProxyUnavailable = 36
|
||||
|
||||
/// The server url could not be found (404)
|
||||
case pathOnServerNotFound = 37
|
||||
|
||||
/// The header with the authentication token was missing or invalid (not a hex string) from a server request.
|
||||
case missingOrInvalidAuthenticationHeader = 38
|
||||
|
||||
/// The authentication token for the server was invalid
|
||||
case invalidServerAuthentication = 39
|
||||
|
||||
/// The device sent a response of invalid size
|
||||
case invalidMessageSizeFromDevice = 40
|
||||
}
|
||||
|
||||
extension MessageResult: Error {
|
||||
|
||||
case invalidDeviceResponse = 22
|
||||
}
|
||||
|
||||
extension MessageResult: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .messageAccepted:
|
||||
return "Message accepted"
|
||||
case .textReceived:
|
||||
return "The device received unexpected text"
|
||||
case .unexpectedSocketEvent:
|
||||
return "Unexpected socket event for the device"
|
||||
case .invalidMessageSize:
|
||||
return "Invalid message data"
|
||||
case .messageAuthenticationFailed:
|
||||
case .invalidMessageSizeFromRemote:
|
||||
return "Invalid message data from remote"
|
||||
case .invalidSignatureByRemote:
|
||||
return "Message authentication failed"
|
||||
case .messageTimeMismatch:
|
||||
return "Message time invalid"
|
||||
case .messageCounterInvalid:
|
||||
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 .noOrInvalidBodyDataInServerRequest:
|
||||
return "Invalid body data in server request"
|
||||
case .deviceNotConnected:
|
||||
return "Device not connected"
|
||||
return "Device not connected to server"
|
||||
case .deviceTimedOut:
|
||||
return "The device did not respond"
|
||||
case .operationInProgress:
|
||||
return "Another operation is in progress"
|
||||
case .deviceConnected:
|
||||
return "The device is connected"
|
||||
case .serverChallengeMismatch:
|
||||
return "Server challenge mismatch"
|
||||
case .clientChallengeMismatchFromRemote:
|
||||
return "Wrong client challenge sent"
|
||||
case .invalidMessageTypeFromRemote:
|
||||
return "Message type from remote invalid"
|
||||
case .tooManyRequests:
|
||||
return "Device busy"
|
||||
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"
|
||||
case .invalidMessageResultFromRemote:
|
||||
return "Invalid message result"
|
||||
case .serverNotReached:
|
||||
return "Server unavailable"
|
||||
case .serverUrlInvalid:
|
||||
return "Invalid server url"
|
||||
case .invalidDeviceResponseSize:
|
||||
return "Invalid Response size"
|
||||
case .invalidSignatureByDevice:
|
||||
return "Invalid device signature"
|
||||
case .noKeyAvailable:
|
||||
return "No key available"
|
||||
case .unlocked:
|
||||
return "Unlocked"
|
||||
case .unknownMessageResultFromDevice:
|
||||
return "Unknown message result"
|
||||
case .deviceAvailable:
|
||||
return "Device available"
|
||||
case .clientChallengeMismatchFromDevice:
|
||||
return "Device sent invalid client challenge"
|
||||
case .invalidMessageTypeFromDevice:
|
||||
return "Message type from device invalid"
|
||||
case .unexpectedUrlResponseType:
|
||||
return "Unexpected URL response"
|
||||
case .unexpectedServerResponseCode:
|
||||
return "Unexpected server response code"
|
||||
case .internalServerError:
|
||||
return "Internal server error"
|
||||
case .serviceBehindProxyUnavailable:
|
||||
return "Service behind proxy not found"
|
||||
case .pathOnServerNotFound:
|
||||
return "Invalid server path"
|
||||
case .missingOrInvalidAuthenticationHeader:
|
||||
return "Invalid server token format"
|
||||
case .invalidServerAuthentication:
|
||||
return "Invalid server token"
|
||||
case .invalidMessageSizeFromDevice:
|
||||
return "Invalid device message size"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageResult: Codable {
|
||||
|
||||
}
|
||||
|
||||
extension MessageResult {
|
||||
|
||||
var encoded: Data {
|
||||
Data([rawValue])
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageResult {
|
||||
|
||||
init(httpCode: Int) {
|
||||
switch httpCode {
|
||||
case 200: self = .messageAccepted
|
||||
case 204: self = .noOrInvalidBodyDataInServerRequest
|
||||
case 403: self = .invalidServerAuthentication
|
||||
case 404: self = .pathOnServerNotFound
|
||||
case 408: self = .deviceTimedOut
|
||||
case 412: self = .deviceNotConnected
|
||||
case 413: self = .invalidMessageSizeFromDevice
|
||||
case 422: self = .missingOrInvalidAuthenticationHeader
|
||||
case 429: self = .tooManyRequests
|
||||
case 500: self = .internalServerError
|
||||
case 501: self = .unexpectedServerResponseCode
|
||||
case 502: self = .serviceBehindProxyUnavailable
|
||||
default: self = .unexpectedServerResponseCode
|
||||
}
|
||||
}
|
||||
|
||||
var statusCode: Int {
|
||||
switch self {
|
||||
case .messageAccepted: return 200 // ok
|
||||
case .noOrInvalidBodyDataInServerRequest: return 204 // noContent
|
||||
case .invalidServerAuthentication: return 403 // forbidden
|
||||
case .pathOnServerNotFound: return 404 // notFound
|
||||
case .deviceTimedOut: return 408 // requestTimeout
|
||||
case .invalidMessageSizeFromRemote: return 411 // lengthRequired
|
||||
case .deviceNotConnected: return 412 // preconditionFailed
|
||||
case .invalidMessageSizeFromDevice: return 413 // payloadTooLarge
|
||||
case .missingOrInvalidAuthenticationHeader: return 422 // unprocessableEntity
|
||||
case .tooManyRequests: return 429 // tooManyRequests
|
||||
case .internalServerError: return 500 // internalServerError
|
||||
case .unexpectedServerResponseCode: return 501 // notImplemented
|
||||
case .serviceBehindProxyUnavailable: return 502 // badGateway
|
||||
default: return 501 // == unexpectedServerResponseCode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +0,0 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
struct ServerMessage {
|
||||
|
||||
static let authTokenSize = SHA256.byteCount
|
||||
|
||||
static let maxLength = authTokenSize + 200
|
||||
|
||||
let authToken: Data
|
||||
|
||||
let message: Data
|
||||
|
||||
/**
|
||||
Decode a message from a byte buffer.
|
||||
The buffer must contain at least `ServerMessage.length` bytes, or it will return `nil`.
|
||||
- Parameter buffer: The buffer containing the bytes.
|
||||
*/
|
||||
init?(decodeFrom buffer: ByteBuffer) {
|
||||
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 = Data(data.dropFirst(ServerMessage.authTokenSize))
|
||||
}
|
||||
|
||||
static func token(from buffer: ByteBuffer) -> Data? {
|
||||
guard buffer.readableBytes == authTokenSize else {
|
||||
return nil
|
||||
}
|
||||
guard let bytes = buffer.getBytes(at: 0, length: authTokenSize) else {
|
||||
return nil
|
||||
}
|
||||
return Data(bytes)
|
||||
}
|
||||
}
|
14
Sources/App/API/SesameHeader.swift
Normal file
14
Sources/App/API/SesameHeader.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
enum SesameHeader {
|
||||
|
||||
static let authenticationHeader = "Authentication"
|
||||
|
||||
static let serverAuthenticationTokenSize = SHA256.byteCount
|
||||
|
||||
}
|
@ -3,10 +3,7 @@ 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"
|
||||
enum SesameRoute: String {
|
||||
|
||||
/// Send a message to the server, to relay to the device
|
||||
case postMessage = "message"
|
15
Sources/App/API/SignedMessage+Size.swift
Normal file
15
Sources/App/API/SignedMessage+Size.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
extension SignedMessage {
|
||||
|
||||
/// The length of a message in bytes
|
||||
static var size: Int {
|
||||
SHA256.byteCount + Message.size
|
||||
}
|
||||
}
|
@ -20,6 +20,8 @@ final class DeviceManager {
|
||||
|
||||
private let messagesToDeviceMetric: Metric<Int>
|
||||
|
||||
let serverStatus: Metric<ServerStatus>
|
||||
|
||||
var deviceIsConnected: Bool {
|
||||
guard let connection, !connection.isClosed else {
|
||||
return false
|
||||
@ -30,7 +32,7 @@ final class DeviceManager {
|
||||
/// A promise to finish the request once the device responds or times out
|
||||
private var requestInProgress: CheckedContinuation<Data, Error>?
|
||||
|
||||
init(deviceKey: Data, remoteKey: Data, deviceTimeout: Int64) {
|
||||
init(deviceKey: Data, remoteKey: Data, deviceTimeout: Int64, serverStatus: Metric<ServerStatus>) {
|
||||
self.deviceKey = deviceKey
|
||||
self.remoteKey = remoteKey
|
||||
self.deviceTimeout = deviceTimeout
|
||||
@ -42,9 +44,15 @@ final class DeviceManager {
|
||||
"sesame.messages",
|
||||
name: "Forwarded Messages",
|
||||
description: "The number of messages transmitted to the device")
|
||||
self.serverStatus = serverStatus
|
||||
}
|
||||
|
||||
func updateMetricsAfterSystemStart() async {
|
||||
_ = try? await serverStatus.update(deviceIsConnected ? .nominal : .reducedFunctionality)
|
||||
await updateDeviceConnectionMetric()
|
||||
}
|
||||
|
||||
func updateDeviceConnectionMetric() async {
|
||||
private func updateDeviceConnectionMetric() async {
|
||||
_ = try? await deviceConnectedMetric.update(deviceIsConnected)
|
||||
}
|
||||
|
||||
@ -55,13 +63,19 @@ final class DeviceManager {
|
||||
|
||||
// MARK: API
|
||||
|
||||
func sendMessageToDevice(_ message: Data, on eventLoop: EventLoop) async throws -> Data {
|
||||
func sendMessageToDevice(_ message: Data, authToken: Data, on eventLoop: EventLoop) async throws -> Data {
|
||||
guard message.count == SignedMessage.size else {
|
||||
throw MessageResult.invalidMessageSizeFromDevice
|
||||
}
|
||||
guard SHA256.hash(data: authToken) == remoteKey else {
|
||||
throw MessageResult.invalidServerAuthentication
|
||||
}
|
||||
guard let socket = connection, !socket.isClosed else {
|
||||
connection = nil
|
||||
throw MessageResult.deviceNotConnected
|
||||
}
|
||||
guard requestInProgress == nil else {
|
||||
throw MessageResult.operationInProgress
|
||||
throw MessageResult.tooManyRequests
|
||||
}
|
||||
do {
|
||||
try await socket.send(Array(message))
|
||||
@ -99,9 +113,10 @@ final class DeviceManager {
|
||||
}
|
||||
|
||||
func processDeviceResponse(_ buffer: ByteBuffer) {
|
||||
guard let data = buffer.getData(at: 0, length: buffer.readableBytes) else {
|
||||
guard let data = buffer.getData(at: 0, length: buffer.readableBytes),
|
||||
data.count == SignedMessage.size else {
|
||||
log("Failed to get data buffer received from device")
|
||||
self.resumeDeviceRequest(with: .invalidDeviceResponse)
|
||||
self.resumeDeviceRequest(with: .invalidMessageSizeFromDevice)
|
||||
return
|
||||
}
|
||||
self.resumeDeviceRequest(with: data)
|
||||
|
@ -6,7 +6,6 @@ import ClairvoyantBinaryCodable
|
||||
var deviceManager: DeviceManager!
|
||||
|
||||
private var provider: VaporMetricProvider!
|
||||
private var status: Metric<ServerStatus>!
|
||||
|
||||
private var asyncScheduler = MultiThreadedEventLoopGroup(numberOfThreads: 2)
|
||||
|
||||
@ -29,7 +28,7 @@ public func configure(_ app: Application) async throws {
|
||||
let monitor = MetricObserver(logFileFolder: logFolder, logMetricId: "sesame.log")
|
||||
MetricObserver.standard = monitor
|
||||
|
||||
status = Metric<ServerStatus>("sesame.status")
|
||||
let status = Metric<ServerStatus>("sesame.status")
|
||||
try await status.update(.initializing)
|
||||
|
||||
app.http.server.configuration.port = config.port
|
||||
@ -37,19 +36,17 @@ public func configure(_ app: Application) async throws {
|
||||
let keyFile = storageFolder.appendingPathComponent(config.keyFileName)
|
||||
|
||||
let (deviceKey, remoteKey) = try loadKeys(at: keyFile)
|
||||
deviceManager = DeviceManager(deviceKey: deviceKey, remoteKey: remoteKey, deviceTimeout: config.deviceTimeout)
|
||||
deviceManager = DeviceManager(deviceKey: deviceKey, remoteKey: remoteKey, deviceTimeout: config.deviceTimeout, serverStatus: status)
|
||||
|
||||
try routes(app)
|
||||
routes(app)
|
||||
|
||||
provider = .init(observer: monitor, accessManager: config.authenticationTokens)
|
||||
provider.asyncScheduler = asyncScheduler
|
||||
provider.registerRoutes(app)
|
||||
monitor.saveCurrentListOfMetricsToLogFolder()
|
||||
|
||||
try await status.update(.nominal)
|
||||
|
||||
// Update the metric of the device status to ensure that it is accurate
|
||||
await deviceManager.updateDeviceConnectionMetric()
|
||||
// Update the metric of the device and server status
|
||||
await deviceManager.updateMetricsAfterSystemStart()
|
||||
|
||||
log("[\(df.string(from: Date()))] Server started")
|
||||
}
|
||||
|
@ -1,84 +1,45 @@
|
||||
import Vapor
|
||||
|
||||
extension RouteAPI {
|
||||
|
||||
var path: PathComponent {
|
||||
.init(stringLiteral: rawValue)
|
||||
}
|
||||
|
||||
var pathParameter: PathComponent {
|
||||
.parameter(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
private func messageTransmission(_ req: Request) async throws -> Data {
|
||||
guard let body = req.body.data else {
|
||||
throw MessageResult.noBodyData
|
||||
}
|
||||
guard let message = ServerMessage(decodeFrom: body) else {
|
||||
throw MessageResult.invalidMessageSize
|
||||
}
|
||||
guard deviceManager.authenticateRemote(message.authToken) else {
|
||||
throw MessageResult.messageAuthenticationFailed
|
||||
}
|
||||
return try await deviceManager.sendMessageToDevice(message.message, on: req.eventLoop)
|
||||
}
|
||||
|
||||
private func deviceStatus(_ req: Request) -> MessageResult {
|
||||
guard let body = req.body.data else {
|
||||
return .noBodyData
|
||||
}
|
||||
guard let authToken = ServerMessage.token(from: body) else {
|
||||
return .invalidMessageSize
|
||||
}
|
||||
guard deviceManager.authenticateRemote(authToken) else {
|
||||
return .messageAuthenticationFailed
|
||||
}
|
||||
guard deviceManager.deviceIsConnected else {
|
||||
return .deviceNotConnected
|
||||
}
|
||||
return .deviceConnected
|
||||
}
|
||||
|
||||
func routes(_ app: Application) throws {
|
||||
|
||||
/**
|
||||
Get the connection status of the device.
|
||||
|
||||
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`, `invalidMessageSize`, `deviceNotConnected`, `deviceConnected`.
|
||||
*/
|
||||
app.post(RouteAPI.getDeviceStatus.path) { request in
|
||||
let result = deviceStatus(request)
|
||||
return Response(status: .ok, body: .init(data: result.encoded))
|
||||
}
|
||||
func routes(_ app: Application) {
|
||||
|
||||
/**
|
||||
Post a message to the device for unlocking.
|
||||
|
||||
The expects a `ServerMessage` in the body data of the POST request, containing the valid remote authentication token and the message to send to the device.
|
||||
The expects a `Message` in the body data of the POST request, containing the message to send to the device.
|
||||
Expects a header ``RouteAPI.authenticationHeader`` with the hexencoded authentication token with binary length ``ServerMessage.authTokenSize``.
|
||||
|
||||
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`.
|
||||
The request returns the ``ServerMessage.messageSize`` bytes of data constituting the device response,
|
||||
or a status code corresponding to a ``MessageResult``.
|
||||
This request does not complete until either the device responds or the request times out.
|
||||
The timeout is specified by the configuration parameter `deviceTimeout`.
|
||||
*/
|
||||
app.post(RouteAPI.postMessage.path) { request async throws in
|
||||
app.post(SesameRoute.postMessage.path) { request async throws in
|
||||
do {
|
||||
let result = try await messageTransmission(request)
|
||||
return Response(status: .ok, body: .init(data: result))
|
||||
guard let authString = request.headers.first(name: SesameHeader.authenticationHeader),
|
||||
let authToken = Data(fromHexEncodedString: authString),
|
||||
authToken.count == SesameHeader.serverAuthenticationTokenSize else {
|
||||
throw MessageResult.missingOrInvalidAuthenticationHeader
|
||||
}
|
||||
|
||||
guard let body = request.body.data,
|
||||
let message = body.getData(at: 0, length: body.readableBytes) else {
|
||||
throw MessageResult.noOrInvalidBodyDataInServerRequest
|
||||
}
|
||||
|
||||
let responseMessage = try await deviceManager.sendMessageToDevice(message, authToken: authToken, on: request.eventLoop)
|
||||
return Response(status: .ok, body: .init(data: responseMessage))
|
||||
} catch let error as MessageResult {
|
||||
return Response(status: .ok, body: .init(data: error.encoded))
|
||||
return Response(status: .init(statusCode: error.statusCode))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Start a new websocket connection for the device to receive messages from the server
|
||||
- Returns: Nothing
|
||||
- Note: The first message from the device over the connection must be a valid auth token.
|
||||
Start a new websocket connection for the device to receive messages from the server.
|
||||
|
||||
The request must contain a header ``RouteAPI.socketAuthenticationHeader`` with a valid authentication token.
|
||||
*/
|
||||
app.webSocket(RouteAPI.socket.path) { request, socket async in
|
||||
guard let authToken = request.headers.first(name: "Authorization") else {
|
||||
app.webSocket(SesameRoute.socket.path) { request, socket async in
|
||||
guard let authToken = request.headers.first(name: SesameHeader.authenticationHeader) else {
|
||||
try? await socket.close()
|
||||
return
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user