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/Clairvoyant", from: "0.13.0"),
|
||||||
.package(url: "https://github.com/christophhagen/ClairvoyantVapor", from: "0.5.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/christophhagen/ClairvoyantBinaryCodable", from: "0.3.1"),
|
||||||
|
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "4.0.0"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.executableTarget(
|
.executableTarget(
|
||||||
@ -20,6 +21,7 @@ let package = Package(
|
|||||||
.product(name: "Clairvoyant", package: "Clairvoyant"),
|
.product(name: "Clairvoyant", package: "Clairvoyant"),
|
||||||
.product(name: "ClairvoyantVapor", package: "ClairvoyantVapor"),
|
.product(name: "ClairvoyantVapor", package: "ClairvoyantVapor"),
|
||||||
.product(name: "ClairvoyantBinaryCodable", package: "ClairvoyantBinaryCodable"),
|
.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 {
|
var encoded: Data {
|
||||||
Data(from: CFSwapInt32HostToLittle(self))
|
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.
|
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
|
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
|
case unexpectedSocketEvent = 2
|
||||||
|
|
||||||
/// The size of the payload (i.e. message) was invalid
|
/// The received message size is invalid.
|
||||||
case invalidMessageSize = 3
|
case invalidMessageSizeFromRemote = 3
|
||||||
|
|
||||||
/// The transmitted message could not be authenticated using the key
|
/// The message signature was incorrect.
|
||||||
case messageAuthenticationFailed = 4
|
case invalidSignatureByRemote = 4
|
||||||
|
|
||||||
/// The message time was not within the acceptable bounds
|
/// The server challenge of the message did not match previous messages
|
||||||
case messageTimeMismatch = 5
|
case serverChallengeMismatch = 5
|
||||||
|
|
||||||
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
|
/// The client challenge of the message did not match previous messages
|
||||||
case messageCounterInvalid = 6
|
case clientChallengeMismatchFromRemote = 6
|
||||||
|
|
||||||
/// The key was accepted by the device, and the door will be opened
|
/// An unexpected or unsupported message type was received
|
||||||
case messageAccepted = 7
|
case invalidMessageTypeFromRemote = 7
|
||||||
|
|
||||||
/// The device id is invalid
|
/// A message is already being processed
|
||||||
case messageDeviceInvalid = 8
|
case tooManyRequests = 8
|
||||||
|
|
||||||
|
/// The received message result was not ``messageAccepted``
|
||||||
|
case invalidMessageResultFromRemote = 9
|
||||||
|
|
||||||
/// The request did not contain body data with the key
|
/// An invalid Url parameter was set sending a message to the device over a local connection
|
||||||
case noBodyData = 10
|
case invalidUrlParameter = 10
|
||||||
|
|
||||||
/// The device is not connected
|
/// The request took too long to complete
|
||||||
case deviceNotConnected = 12
|
case deviceTimedOut = 20
|
||||||
|
|
||||||
/// The device did not respond within the timeout
|
case noOrInvalidBodyDataInServerRequest = 21
|
||||||
case deviceTimedOut = 13
|
|
||||||
|
|
||||||
/// Another message is being processed by the device
|
/// The device is not connected to the server via web socket
|
||||||
case operationInProgress = 14
|
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 is connected
|
/// The device sent a message with an invalid client challenge
|
||||||
case deviceConnected = 15
|
case clientChallengeMismatchFromDevice = 30
|
||||||
|
|
||||||
case invalidUrlParameter = 20
|
/// A valid server challenge was received
|
||||||
|
case deviceAvailable = 31
|
||||||
|
|
||||||
case invalidResponseAuthentication = 21
|
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 {
|
extension MessageResult: CustomStringConvertible {
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .messageAccepted:
|
||||||
|
return "Message accepted"
|
||||||
case .textReceived:
|
case .textReceived:
|
||||||
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 .invalidMessageSize:
|
case .invalidMessageSizeFromRemote:
|
||||||
return "Invalid message data"
|
return "Invalid message data from remote"
|
||||||
case .messageAuthenticationFailed:
|
case .invalidSignatureByRemote:
|
||||||
return "Message authentication failed"
|
return "Message authentication failed"
|
||||||
case .messageTimeMismatch:
|
case .noOrInvalidBodyDataInServerRequest:
|
||||||
return "Message time invalid"
|
return "Invalid body data in server request"
|
||||||
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 .deviceNotConnected:
|
case .deviceNotConnected:
|
||||||
return "Device not connected"
|
return "Device not connected to server"
|
||||||
case .deviceTimedOut:
|
case .deviceTimedOut:
|
||||||
return "The device did not respond"
|
return "The device did not respond"
|
||||||
case .operationInProgress:
|
case .serverChallengeMismatch:
|
||||||
return "Another operation is in progress"
|
return "Server challenge mismatch"
|
||||||
case .deviceConnected:
|
case .clientChallengeMismatchFromRemote:
|
||||||
return "The device is connected"
|
return "Wrong client challenge sent"
|
||||||
|
case .invalidMessageTypeFromRemote:
|
||||||
|
return "Message type from remote invalid"
|
||||||
|
case .tooManyRequests:
|
||||||
|
return "Device busy"
|
||||||
case .invalidUrlParameter:
|
case .invalidUrlParameter:
|
||||||
return "The url parameter could not be found"
|
return "The url parameter could not be found"
|
||||||
case .invalidResponseAuthentication:
|
case .invalidMessageResultFromRemote:
|
||||||
return "The response could not be authenticated"
|
return "Invalid message result"
|
||||||
case .invalidDeviceResponse:
|
case .serverNotReached:
|
||||||
return "The device responded with invalid data"
|
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 {
|
extension MessageResult {
|
||||||
|
|
||||||
var encoded: Data {
|
var encoded: Data {
|
||||||
Data([rawValue])
|
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
|
The active urls on the server, for the device and the remote to connect
|
||||||
*/
|
*/
|
||||||
enum RouteAPI: String {
|
enum SesameRoute: String {
|
||||||
|
|
||||||
/// Check the device status
|
|
||||||
case getDeviceStatus = "status"
|
|
||||||
|
|
||||||
/// Send a message to the server, to relay to the device
|
/// Send a message to the server, to relay to the device
|
||||||
case postMessage = "message"
|
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>
|
private let messagesToDeviceMetric: Metric<Int>
|
||||||
|
|
||||||
|
let serverStatus: Metric<ServerStatus>
|
||||||
|
|
||||||
var deviceIsConnected: Bool {
|
var deviceIsConnected: Bool {
|
||||||
guard let connection, !connection.isClosed else {
|
guard let connection, !connection.isClosed else {
|
||||||
return false
|
return false
|
||||||
@ -30,7 +32,7 @@ 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: CheckedContinuation<Data, Error>?
|
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.deviceKey = deviceKey
|
||||||
self.remoteKey = remoteKey
|
self.remoteKey = remoteKey
|
||||||
self.deviceTimeout = deviceTimeout
|
self.deviceTimeout = deviceTimeout
|
||||||
@ -42,9 +44,15 @@ final class DeviceManager {
|
|||||||
"sesame.messages",
|
"sesame.messages",
|
||||||
name: "Forwarded Messages",
|
name: "Forwarded Messages",
|
||||||
description: "The number of messages transmitted to the device")
|
description: "The number of messages transmitted to the device")
|
||||||
|
self.serverStatus = serverStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateDeviceConnectionMetric() async {
|
func updateMetricsAfterSystemStart() async {
|
||||||
|
_ = try? await serverStatus.update(deviceIsConnected ? .nominal : .reducedFunctionality)
|
||||||
|
await updateDeviceConnectionMetric()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateDeviceConnectionMetric() async {
|
||||||
_ = try? await deviceConnectedMetric.update(deviceIsConnected)
|
_ = try? await deviceConnectedMetric.update(deviceIsConnected)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,13 +63,19 @@ final class DeviceManager {
|
|||||||
|
|
||||||
// MARK: API
|
// 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 {
|
guard let socket = connection, !socket.isClosed else {
|
||||||
connection = nil
|
connection = nil
|
||||||
throw MessageResult.deviceNotConnected
|
throw MessageResult.deviceNotConnected
|
||||||
}
|
}
|
||||||
guard requestInProgress == nil else {
|
guard requestInProgress == nil else {
|
||||||
throw MessageResult.operationInProgress
|
throw MessageResult.tooManyRequests
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
try await socket.send(Array(message))
|
try await socket.send(Array(message))
|
||||||
@ -99,9 +113,10 @@ final class DeviceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func processDeviceResponse(_ buffer: ByteBuffer) {
|
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")
|
log("Failed to get data buffer received from device")
|
||||||
self.resumeDeviceRequest(with: .invalidDeviceResponse)
|
self.resumeDeviceRequest(with: .invalidMessageSizeFromDevice)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.resumeDeviceRequest(with: data)
|
self.resumeDeviceRequest(with: data)
|
||||||
|
@ -6,7 +6,6 @@ import ClairvoyantBinaryCodable
|
|||||||
var deviceManager: DeviceManager!
|
var deviceManager: DeviceManager!
|
||||||
|
|
||||||
private var provider: VaporMetricProvider!
|
private var provider: VaporMetricProvider!
|
||||||
private var status: Metric<ServerStatus>!
|
|
||||||
|
|
||||||
private var asyncScheduler = MultiThreadedEventLoopGroup(numberOfThreads: 2)
|
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")
|
let monitor = MetricObserver(logFileFolder: logFolder, logMetricId: "sesame.log")
|
||||||
MetricObserver.standard = monitor
|
MetricObserver.standard = monitor
|
||||||
|
|
||||||
status = Metric<ServerStatus>("sesame.status")
|
let status = Metric<ServerStatus>("sesame.status")
|
||||||
try await status.update(.initializing)
|
try await status.update(.initializing)
|
||||||
|
|
||||||
app.http.server.configuration.port = config.port
|
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 keyFile = storageFolder.appendingPathComponent(config.keyFileName)
|
||||||
|
|
||||||
let (deviceKey, remoteKey) = try loadKeys(at: keyFile)
|
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 = .init(observer: monitor, accessManager: config.authenticationTokens)
|
||||||
provider.asyncScheduler = asyncScheduler
|
provider.asyncScheduler = asyncScheduler
|
||||||
provider.registerRoutes(app)
|
provider.registerRoutes(app)
|
||||||
monitor.saveCurrentListOfMetricsToLogFolder()
|
monitor.saveCurrentListOfMetricsToLogFolder()
|
||||||
|
|
||||||
try await status.update(.nominal)
|
// Update the metric of the device and server status
|
||||||
|
await deviceManager.updateMetricsAfterSystemStart()
|
||||||
// Update the metric of the device status to ensure that it is accurate
|
|
||||||
await deviceManager.updateDeviceConnectionMetric()
|
|
||||||
|
|
||||||
log("[\(df.string(from: Date()))] Server started")
|
log("[\(df.string(from: Date()))] Server started")
|
||||||
}
|
}
|
||||||
|
@ -1,84 +1,45 @@
|
|||||||
import Vapor
|
import Vapor
|
||||||
|
|
||||||
extension RouteAPI {
|
func routes(_ app: Application) {
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Post a message to the device for unlocking.
|
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`,
|
The request returns the ``ServerMessage.messageSize`` bytes of data constituting the device response,
|
||||||
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`.
|
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 {
|
do {
|
||||||
let result = try await messageTransmission(request)
|
guard let authString = request.headers.first(name: SesameHeader.authenticationHeader),
|
||||||
return Response(status: .ok, body: .init(data: result))
|
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 {
|
} 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
|
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.
|
The request must contain a header ``RouteAPI.socketAuthenticationHeader`` with a valid authentication token.
|
||||||
*/
|
*/
|
||||||
app.webSocket(RouteAPI.socket.path) { request, socket async in
|
app.webSocket(SesameRoute.socket.path) { request, socket async in
|
||||||
guard let authToken = request.headers.first(name: "Authorization") else {
|
guard let authToken = request.headers.first(name: SesameHeader.authenticationHeader) else {
|
||||||
try? await socket.close()
|
try? await socket.close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user