First version
This commit is contained in:
10
Sources/App/API.swift
Normal file
10
Sources/App/API.swift
Normal file
@@ -0,0 +1,10 @@
|
||||
import Foundation
|
||||
|
||||
enum PublicAPI: String {
|
||||
case getDeviceResponse = "response"
|
||||
case getDeviceStatus = "status"
|
||||
case clearKeyRequest = "clear"
|
||||
case postKey = "key"
|
||||
case postKeyIdParameter = "id"
|
||||
case socket = "listen"
|
||||
}
|
124
Sources/App/KeyManagement.swift
Normal file
124
Sources/App/KeyManagement.swift
Normal file
@@ -0,0 +1,124 @@
|
||||
import Foundation
|
||||
import WebSocketKit
|
||||
import Vapor
|
||||
|
||||
final class KeyManagement {
|
||||
|
||||
/// The security parameter for the keys (in bits)
|
||||
private static let keySecurity = 128
|
||||
|
||||
/// The size of the individual keys in bytes
|
||||
static let keySize = keySecurity / 8
|
||||
|
||||
/// The connection to the device
|
||||
private var connection: WebSocket?
|
||||
|
||||
private let deviceKey: String
|
||||
|
||||
var deviceIsAuthenticated = false
|
||||
|
||||
/// Indicator for device availability
|
||||
var deviceIsConnected: Bool {
|
||||
!(connection?.isClosed ?? true) && deviceIsAuthenticated
|
||||
}
|
||||
|
||||
/// The id of the key which was sent to the device
|
||||
private var keyInTransit: UInt16?
|
||||
|
||||
/// The result transmitted by the device for the sent key
|
||||
var keyResult: KeyResult = .none
|
||||
|
||||
init(deviceKey: String) {
|
||||
self.deviceKey = deviceKey
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
var deviceResponse: String {
|
||||
guard let keyId = keyInTransit else {
|
||||
return "No key"
|
||||
}
|
||||
return "\(keyId):\(keyResult.rawValue)"
|
||||
}
|
||||
|
||||
var deviceStatus: String {
|
||||
deviceIsConnected ? "1" : "0"
|
||||
}
|
||||
|
||||
func clearClientRequest() {
|
||||
keyInTransit = nil
|
||||
keyResult = .none
|
||||
}
|
||||
|
||||
func sendKeyToDevice(_ key: Data, keyId: UInt16) -> KeyPostResponse {
|
||||
guard key.count == KeyManagement.keySize else {
|
||||
return .invalidKeySize
|
||||
}
|
||||
guard let socket = connection, !socket.isClosed else {
|
||||
connection = nil
|
||||
return .deviceNotConnected
|
||||
}
|
||||
let keyIdData = [UInt8(keyId >> 8), UInt8(keyId & 0xFF)]
|
||||
keyInTransit = keyId
|
||||
socket.send(keyIdData + key, promise: nil)
|
||||
return .success
|
||||
}
|
||||
|
||||
func authenticateDevice(psk: String) {
|
||||
guard psk == self.deviceKey else {
|
||||
print("Invalid device key")
|
||||
_ = connection?.close()
|
||||
deviceIsAuthenticated = false
|
||||
return
|
||||
}
|
||||
print("Device authenticated")
|
||||
deviceIsAuthenticated = true
|
||||
}
|
||||
|
||||
func processDeviceResponse(_ data: ByteBuffer) {
|
||||
guard data.readableBytes == 1 else {
|
||||
print("Unexpected number of bytes received from device")
|
||||
keyInTransit = nil
|
||||
keyResult = .unexpectedSocketEvent
|
||||
return
|
||||
}
|
||||
guard let rawValue = data.getBytes(at: 0, length: 1)?.first else {
|
||||
print("Unreadable data received from device")
|
||||
keyInTransit = nil
|
||||
keyResult = .unexpectedSocketEvent
|
||||
return
|
||||
}
|
||||
guard let response = KeyResult(rawValue: rawValue) else {
|
||||
print("Unknown response \(rawValue) received from device")
|
||||
keyInTransit = nil
|
||||
keyResult = .unexpectedSocketEvent
|
||||
return
|
||||
}
|
||||
guard keyInTransit != nil else {
|
||||
print("No key in transit for response \(response)")
|
||||
return
|
||||
}
|
||||
keyResult = response
|
||||
}
|
||||
|
||||
func didCloseDeviceSocket() {
|
||||
deviceIsAuthenticated = false
|
||||
guard connection != nil else {
|
||||
return
|
||||
}
|
||||
connection = nil
|
||||
print("Socket closed")
|
||||
}
|
||||
|
||||
func removeDeviceConnection() {
|
||||
_ = connection?.close()
|
||||
connection = nil
|
||||
}
|
||||
|
||||
func createNewDeviceConnection(_ socket: WebSocket) {
|
||||
removeDeviceConnection()
|
||||
connection = socket
|
||||
deviceIsAuthenticated = false
|
||||
print("Socket connected")
|
||||
}
|
||||
}
|
64
Sources/App/Response.swift
Normal file
64
Sources/App/Response.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
A result from sending a key to the device.
|
||||
*/
|
||||
enum KeyResult: UInt8 {
|
||||
|
||||
/// No result from the device, or state not applicable
|
||||
case none = 0
|
||||
|
||||
/// Text content was received, although binary data was expected
|
||||
case textReceived = 1
|
||||
|
||||
/// A socket event on the device was unexpected (not binary data)
|
||||
case unexpectedSocketEvent = 2
|
||||
|
||||
/// The size of the payload (key id + key data) was invalid
|
||||
case invalidPayloadSize = 3
|
||||
|
||||
/// The index of the key was out of bounds
|
||||
case invalidKeyIndex = 4
|
||||
|
||||
/// The transmitted key data did not match the expected key
|
||||
case invalidKey = 5
|
||||
|
||||
/// The key has been previously used and is no longer valid
|
||||
case keyAlreadyUsed = 6
|
||||
|
||||
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
|
||||
case keyWasSkipped = 7
|
||||
|
||||
/// The key was accepted by the device, and the door will be opened
|
||||
case keyAccepted = 8
|
||||
|
||||
/// The device produced an unknown error
|
||||
case uknownDeviceError = 9
|
||||
|
||||
/// The device is not connected through the socket
|
||||
case notConnected = 10
|
||||
}
|
||||
|
||||
/**
|
||||
A response from the server to a key request.
|
||||
*/
|
||||
enum KeyPostResponse: Int {
|
||||
|
||||
/// The key will be transmitted to the device
|
||||
case success = 0
|
||||
|
||||
/// The key id is out of bounds or otherwise invalid
|
||||
case invalidKeyId = 1
|
||||
|
||||
/// The request did not contain body data with the key
|
||||
case noBodyData = 2
|
||||
|
||||
/// The key contained in the body data has an invalid size
|
||||
case invalidKeySize = 3
|
||||
|
||||
/// The body data could not be read
|
||||
case corruptkeyData = 4
|
||||
|
||||
/// The device is not connected
|
||||
case deviceNotConnected = 5
|
||||
}
|
@@ -1,7 +1,22 @@
|
||||
import Vapor
|
||||
|
||||
var keyManager: KeyManagement!
|
||||
|
||||
// configures your application
|
||||
public func configure(_ app: Application) throws {
|
||||
app.http.server.configuration.port = 10000
|
||||
|
||||
let storageFolder = URL(fileURLWithPath: app.directory.resourcesDirectory)
|
||||
let keyFile = storageFolder.appendingPathComponent("device.key")
|
||||
let deviceKey = try String(contentsOf: keyFile)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
keyManager = KeyManagement(deviceKey: deviceKey)
|
||||
try routes(app)
|
||||
|
||||
// Gracefully shut down by closing potentially open socket
|
||||
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + .seconds(5)) {
|
||||
_ = app.server.onShutdown.always { _ in
|
||||
keyManager.removeDeviceConnection()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,74 @@
|
||||
import Vapor
|
||||
|
||||
var connection: WebSocket?
|
||||
extension PublicAPI {
|
||||
|
||||
var path: PathComponent {
|
||||
.init(stringLiteral: rawValue)
|
||||
}
|
||||
|
||||
var pathParameter: PathComponent {
|
||||
.parameter(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
private func handleKeyPost(_ req: Request) -> KeyPostResponse {
|
||||
guard let keyId = req.parameters.get(PublicAPI.postKeyIdParameter.rawValue, as: UInt16.self) else {
|
||||
return .invalidKeyId
|
||||
}
|
||||
guard let body = req.body.data else {
|
||||
return .noBodyData
|
||||
}
|
||||
guard body.readableBytes == KeyManagement.keySize else {
|
||||
return .invalidKeySize
|
||||
}
|
||||
guard let key = body.getData(at: 0, length: KeyManagement.keySize) else {
|
||||
return .corruptkeyData
|
||||
}
|
||||
return keyManager.sendKeyToDevice(key, keyId: keyId)
|
||||
}
|
||||
|
||||
func routes(_ app: Application) throws {
|
||||
|
||||
app.get { req in
|
||||
return "It works!"
|
||||
/**
|
||||
Get the connection status of the device.
|
||||
|
||||
The response is a string of either "1" (connected) or "0" (disconnected)
|
||||
*/
|
||||
app.get(PublicAPI.getDeviceStatus.path) { req -> String in
|
||||
keyManager.deviceStatus
|
||||
}
|
||||
|
||||
/**
|
||||
Get the response from the device.
|
||||
|
||||
The response is a string of an integer `rawValue` of a `KeyResult`
|
||||
*/
|
||||
app.get(PublicAPI.getDeviceResponse.path) { req -> String in
|
||||
keyManager.deviceResponse
|
||||
}
|
||||
|
||||
/**
|
||||
Post a request to remove the information about the last key transmission.
|
||||
|
||||
- The request always succeeds and returns the string "Success"
|
||||
*/
|
||||
app.post(PublicAPI.clearKeyRequest.path) { req -> String in
|
||||
keyManager.clearClientRequest()
|
||||
return "Success"
|
||||
}
|
||||
|
||||
/**
|
||||
Post a key to the device for unlocking.
|
||||
|
||||
The corresponding integer key id for the key data must be contained in the url path.
|
||||
|
||||
The request returns a string containing a `rawValue` of a `KeyPostResponse`
|
||||
A success of this method does not yet signal successful unlocking.
|
||||
The client should request the status by inquiring the device response.
|
||||
*/
|
||||
app.post(PublicAPI.postKey.path, PublicAPI.postKeyIdParameter.pathParameter) { req -> String in
|
||||
let result = handleKeyPost(req)
|
||||
return String(result.rawValue)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -13,19 +76,17 @@ func routes(_ app: Application) throws {
|
||||
- Returns: Nothing
|
||||
- Note: The first (and only) message from the client over the connection must be a valid session token.
|
||||
*/
|
||||
app.webSocket("listen") { req, socket in
|
||||
socket.onBinary { socket, data in
|
||||
print("\(data)")
|
||||
app.webSocket(PublicAPI.socket.path) { req, socket in
|
||||
socket.onBinary { _, data in
|
||||
keyManager.processDeviceResponse(data)
|
||||
}
|
||||
socket.onText { socket, text in
|
||||
print(text)
|
||||
socket.onText { _, text in
|
||||
keyManager.authenticateDevice(psk: text)
|
||||
}
|
||||
|
||||
_ = socket.onClose.always { result in
|
||||
connection = nil
|
||||
print("Socket closed")
|
||||
_ = socket.onClose.always { _ in
|
||||
keyManager.didCloseDeviceSocket()
|
||||
}
|
||||
connection = socket
|
||||
print("Socket connected")
|
||||
keyManager.createNewDeviceConnection(socket)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user