diff --git a/Resources/config.json b/Resources/config.json index 5f23903..057eb42 100644 --- a/Resources/config.json +++ b/Resources/config.json @@ -1,4 +1,7 @@ { "logPath": "\/var\/log\/caps.log", - "serveFiles": true + "serveFiles": true, + "writers" : [ + "auth_key_1" + ] } diff --git a/Sources/App/CapServer.swift b/Sources/App/CapServer.swift index aa6a784..67df1c6 100644 --- a/Sources/App/CapServer.swift +++ b/Sources/App/CapServer.swift @@ -16,6 +16,8 @@ final class CapServer { private var saveImmediatelly = true + private var writers: Set + private var caps = [Int: Cap]() { didSet { guard saveImmediatelly else { @@ -26,12 +28,13 @@ final class CapServer { } var nextClassifierVersion: Int { - caps.values.map { $0.classifierVersion }.max() ?? 1 + caps.values.compactMap { $0.classifierVersion }.max() ?? 1 } - init(in folder: URL) throws { + init(in folder: URL, writers: [String]) throws { self.imageFolder = folder.appendingPathComponent("images") self.dbFile = folder.appendingPathComponent("caps.json") + self.writers = Set(writers) var isDirectory: ObjCBool = false guard fm.fileExists(atPath: folder.path, isDirectory: &isDirectory), @@ -65,6 +68,17 @@ final class CapServer { func file(of cap: Int, version: Int) -> URL { folder(of: cap).appendingPathComponent(String(format: "%04d-%02d.jpg", cap, version)) } + + // MARK: Authentication + + func hasAuthorization(for key: String) -> Bool { + // Note: This is not a constant-time compare, so there may be an opportunity + // for timing attack here. Sets perform hashed lookups, so this may be less of an issue, + // and we're not doing anything critical in this application. + // Worst case, an unauthorized person with a lot of free time and energy to hack this system + // is able to change contents of the database, which are backed up in any case. + writers.contains(key) + } // MARK: Counts diff --git a/Sources/App/Config.swift b/Sources/App/Config.swift index e1740cd..b361f61 100644 --- a/Sources/App/Config.swift +++ b/Sources/App/Config.swift @@ -2,15 +2,20 @@ import Foundation struct Config: Codable { + /// The path to the log file let logPath: String + /// Serve files in the Public directory using Vapor let serveFiles: Bool + /// Authentication tokens for remotes allowed to write + let writers: [String] + var logURL: URL { .init(fileURLWithPath: logPath) } static var `default`: Config { - .init(logPath: "/var/log/caps.log", serveFiles: true) + .init(logPath: "/var/log/caps.log", serveFiles: true, writers: []) } } diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift index c37b4f0..b620b89 100755 --- a/Sources/App/configure.swift +++ b/Sources/App/configure.swift @@ -30,7 +30,8 @@ public func configure(_ app: Application) throws { app.middleware.use(middleware) } - server = try CapServer(in: URL(fileURLWithPath: publicDirectory)) + server = try CapServer(in: URL(fileURLWithPath: publicDirectory), + writers: config.writers) // Register routes to the router try routes(app) diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 161df1f..c75d979 100755 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -1,5 +1,12 @@ import Vapor +private func authorize(_ request: Request) throws { + let key = try request.query.get(String.self, at: "key") + guard server.hasAuthorization(for: key) else { + throw Abort(.forbidden) + } +} + /// Register your application's routes here. /// /// [Learn More →](https://docs.vapor.codes/3.0/getting-started/structure/#routesswift) @@ -7,6 +14,7 @@ func routes(_ app: Application) throws { // Set the name of a cap app.postCatching("name", ":n") { request in + try authorize(request) guard let cap = request.parameters.get("n", as: Int.self) else { log("Invalid parameter for cap") throw Abort(.badRequest) @@ -20,6 +28,7 @@ func routes(_ app: Application) throws { // Upload an image app.postCatching("images", ":n") { request -> Data in + try authorize(request) guard let cap = request.parameters.get("n", as: Int.self) else { log("Invalid parameter for cap") throw Abort(.badRequest) @@ -35,6 +44,7 @@ func routes(_ app: Application) throws { // Set a different version as the main image app.getCatching("switch", ":n", ":v") { request in + try authorize(request) guard let cap = request.parameters.get("n", as: Int.self), cap >= 0 else { log("Invalid parameter for cap") throw Abort(.badRequest)