Compare commits

..

No commits in common. "master" and "f454cc80e4de1e59d134950f29d7c138f2db33c7" have entirely different histories.

8 changed files with 90 additions and 157 deletions

View File

@ -29,7 +29,7 @@ let package = Package(
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
] ]
), ),
.executableTarget(name: "FlurSchnaps-Server", dependencies: [.target(name: "App")]), .executableTarget(name: "Run", dependencies: [.target(name: "App")]),
.testTarget(name: "AppTests", dependencies: [ .testTarget(name: "AppTests", dependencies: [
.target(name: "App"), .target(name: "App"),
.product(name: "XCTVapor", package: "vapor"), .product(name: "XCTVapor", package: "vapor"),

View File

@ -1,89 +0,0 @@
import Foundation
import APNSwift
import NIO
import Crypto
struct APNSInterface {
private struct Payload: Codable {}
private var apnsConfiguration: APNSClientConfiguration!
private let title: String
private let messages: [String]
private let topic: String
private let apnsEventGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
private let apnsRequestEncoder = JSONEncoder()
private let apnsResponseDecoder = JSONDecoder()
init?(_ config: ServerConfiguration) {
guard let key = load(keyAtPath: config.privateKeyFilePath) else {
return nil
}
let authentication = APNSClientConfiguration.AuthenticationMethod.jwt(
privateKey: key,
keyIdentifier: config.keyIdentifier,
teamIdentifier: config.teamIdentifier)
apnsConfiguration = APNSClientConfiguration(
authenticationMethod: authentication,
environment: .sandbox)
title = config.messageTitle
messages = config.messages
topic = config.topic
}
func sendPush(to tokens: Set<String>) async {
let client = APNSClient(
configuration: apnsConfiguration,
eventLoopGroupProvider: .shared(apnsEventGroup),
responseDecoder: apnsResponseDecoder,
requestEncoder: apnsRequestEncoder)
log(info: "Client created")
let alert = APNSAlertNotificationContent(
title: .raw(title),
body: .raw(messages.randomElement()!))
let apnsNotification = APNSAlertNotification(
alert: alert,
expiration: .none,
priority: .immediately,
topic: topic,
payload: Payload(),
sound: .default)
do {
for token in tokenStorage.tokens {
log(info: "Sending push to \(token.prefix(6))...")
try await client.sendAlertNotification(
apnsNotification,
deviceToken: token,
deadline: .now() + .seconds(10))
log(info: "Sent push to \(token.prefix(6))...")
}
} catch let error as APNSError {
print(error)
} catch {
log(error: error.localizedDescription)
}
do {
log("Closing client")
try client.syncShutdown()
} catch {
log(error: error.localizedDescription)
}
}
}
private func load(keyAtPath path: String) -> P256.Signing.PrivateKey? {
do {
guard let key = try P256.Signing.PrivateKey.loadFrom(filePath: path) else {
print("Failed to read key \(path)")
return nil
}
return key
} catch {
print("Failed to load key \(error)")
return nil
}
}

View File

@ -16,9 +16,11 @@ struct ServerConfiguration: Codable {
let messageTitle: String let messageTitle: String
let messages: [String] let messageBody: String
let buttonPin: Int let buttonPin: Int
let bounceTime: Double let bounceTime: Double
let tokens: [String]
} }

View File

@ -9,6 +9,4 @@ enum ServerStatus: Int, Codable {
case pushFailed = 2 case pushFailed = 2
case starting = 3 case starting = 3
case failedToStart = 4
} }

View File

@ -1,47 +0,0 @@
import Foundation
import BinaryCodable
struct TokenStorage {
private(set) var tokens: Set<String>
private let fileUrl: URL
private let encoder = BinaryEncoder()
init(in folder: URL) {
tokens = []
fileUrl = folder.appendingPathComponent("tokens")
loadTokensFromDisk()
}
private mutating func loadTokensFromDisk() {
guard FileManager.default.fileExists(atPath: fileUrl.path) else {
log(info: "No tokens loaded")
return
}
do {
let data = try Data(contentsOf: fileUrl)
tokens = try BinaryDecoder().decode(from: data)
log(info: "\(tokens.count) tokens loaded")
} catch {
log(error: "Failed to read token file: \(error)")
}
}
mutating func add(_ tokenUpload: TokenUpload) {
if let oldToken = tokenUpload.previousToken {
tokens.remove(oldToken.hex)
}
tokens.insert(tokenUpload.currentToken.hex)
do {
let data = try encoder.encode(tokens)
try data.write(to: fileUrl)
} catch {
log(error: "Failed to write token file: \(error)")
}
}
}

View File

@ -1,8 +1,16 @@
import Vapor import Vapor
import SwiftyGPIO import SwiftyGPIO
import APNSwift
import NIO
import Crypto
var apns: APNSInterface? private struct Payload: Codable {}
var tokenStorage: TokenStorage!
private var apnsConfiguration: APNSClientConfiguration!
private var apnsNotification: APNSAlertNotification<Payload>!
private let apnsEventGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
private let apnsRequestEncoder = JSONEncoder()
private let apnsResponseDecoder = JSONDecoder()
// configures your application // configures your application
public func configure(_ app: Application) throws { public func configure(_ app: Application) throws {
@ -15,18 +23,13 @@ public func configure(_ app: Application) throws {
let data = try Data(contentsOf: configUrl) let data = try Data(contentsOf: configUrl)
config = try JSONDecoder().decode(from: data) config = try JSONDecoder().decode(from: data)
} catch { } catch {
print("Failed to load config from \(configUrl.path): \(error)") print("Failed to load config: \(error)")
return return
} }
Log.set(logFile: URL(fileURLWithPath: config.logPath))
tokenStorage = .init(in: resourcesFolderUrl)
app.http.server.configuration.port = config.port app.http.server.configuration.port = config.port
apns = .init(config) guard configureAPNS(config) else {
guard apns != nil else {
serverStatus = .failedToStart
return return
} }
@ -36,7 +39,6 @@ public func configure(_ app: Application) throws {
try routes(app) try routes(app)
serverStatus = .running serverStatus = .running
log(info: "Server is running")
} }
private extension JSONDecoder { private extension JSONDecoder {
@ -46,6 +48,47 @@ private extension JSONDecoder {
} }
} }
private func load(keyAtPath path: String) -> P256.Signing.PrivateKey? {
do {
guard let key = try P256.Signing.PrivateKey.loadFrom(filePath: path) else {
print("Failed to read key \(path)")
return nil
}
return key
} catch {
print("Failed to load key \(error)")
return nil
}
}
private func configureAPNS(_ config: ServerConfiguration) -> Bool {
guard let key = load(keyAtPath: config.privateKeyFilePath) else {
return false
}
let authentication = APNSClientConfiguration.AuthenticationMethod.jwt(
privateKey: key,
keyIdentifier: config.keyIdentifier,
teamIdentifier: config.teamIdentifier)
apnsConfiguration = APNSClientConfiguration(
authenticationMethod: authentication,
environment: .sandbox)
let alert = APNSAlertNotificationContent(
title: .raw(config.messageTitle),
body: .raw(config.messageBody))
apnsNotification = APNSAlertNotification(
alert: alert,
expiration: .none,
priority: .immediately,
topic: config.topic,
payload: Payload(),
sound: .default)
return true
}
private func configureGPIO(_ config: ServerConfiguration) { private func configureGPIO(_ config: ServerConfiguration) {
let gpio = RaspberryGPIO( let gpio = RaspberryGPIO(
name: "GPIO\(config.buttonPin)", name: "GPIO\(config.buttonPin)",
@ -53,7 +96,7 @@ private func configureGPIO(_ config: ServerConfiguration) {
baseAddr: 0x7E000000) baseAddr: 0x7E000000)
gpio.direction = .IN gpio.direction = .IN
gpio.pull = .neither gpio.pull = .down
gpio.bounceTime = config.bounceTime gpio.bounceTime = config.bounceTime
gpio.onChange { _ in gpio.onChange { _ in
log(info: "Push detected") log(info: "Push detected")
@ -63,11 +106,32 @@ private func configureGPIO(_ config: ServerConfiguration) {
} }
private func sendPush() { private func sendPush() {
guard !tokenStorage.tokens.isEmpty else {
log(info: "No tokens registered to send push")
return
}
Task(priority: .userInitiated) { Task(priority: .userInitiated) {
await apns?.sendPush(to: tokenStorage.tokens) let client = APNSClient(
configuration: apnsConfiguration,
eventLoopGroupProvider: .shared(apnsEventGroup),
responseDecoder: apnsResponseDecoder,
requestEncoder: apnsRequestEncoder)
log(info: "Client created")
do {
for token in knownTokens {
log(info: "Sending push to \(token.prefix(6))...")
try await client.sendAlertNotification(
apnsNotification,
deviceToken: token,
deadline: .now() + .seconds(10))
log(info: "Sent push to \(token.prefix(6))...")
}
} catch let error as APNSError {
print(error)
} catch {
log(error: error.localizedDescription)
}
do {
log("Closing client")
try client.syncShutdown()
} catch {
log(error: error.localizedDescription)
}
} }
} }

View File

@ -6,6 +6,8 @@ private let requestDecoder = BinaryDecoder()
var serverStatus: ServerStatus = .starting var serverStatus: ServerStatus = .starting
var knownTokens = Set<String>()
func routes(_ app: Application) throws { func routes(_ app: Application) throws {
app.post("token") { req async throws -> HTTPResponseStatus in app.post("token") { req async throws -> HTTPResponseStatus in
@ -13,7 +15,10 @@ func routes(_ app: Application) throws {
return .badRequest return .badRequest
} }
let tokenUpload: TokenUpload = try requestDecoder.decode(from: data) let tokenUpload: TokenUpload = try requestDecoder.decode(from: data)
tokenStorage.add(tokenUpload) if let oldToken = tokenUpload.previousToken {
knownTokens.remove(oldToken.hex)
}
knownTokens.insert(tokenUpload.currentToken.hex)
return .ok return .ok
} }