-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c358a9b
commit 50db67c
Showing
4 changed files
with
306 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// | ||
// OrdersDelegate.swift | ||
// PassKit | ||
// | ||
// Created by Francesco Paolo Severino on 01/07/24. | ||
// | ||
|
||
import Foundation | ||
import FluentKit | ||
|
||
/// The delegate which is responsible for generating the order files. | ||
public protocol OrdersDelegate: AnyObject, Sendable { | ||
/// Should return a `URL` which points to the template data for the order. | ||
/// | ||
/// The URL should point to a directory containing all the images and localizations for the generated `.order` archive but should *not* contain any of these items: | ||
/// - `manifest.json` | ||
/// - `order.json` | ||
/// - `signature` | ||
/// | ||
/// - Parameters: | ||
/// - for: The order data from the SQL server. | ||
/// - db: The SQL database to query against. | ||
/// | ||
/// - Returns: A `URL` which points to the template data for the order. | ||
/// | ||
/// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` constructor. | ||
func template<O: OrderModel>(for: O, db: any Database) async throws -> URL | ||
|
||
/// Generates the SSL `signature` file. | ||
/// | ||
/// If you need to implement custom S/Mime signing you can use this | ||
/// method to do so. You must generate a detached DER signature of the `manifest.json` file. | ||
/// | ||
/// - Parameter root: The location of the `manifest.json` and where to write the `signature` to. | ||
/// - Returns: Return `true` if you generated a custom `signature`, otherwise `false`. | ||
func generateSignatureFile(in root: URL) -> Bool | ||
|
||
/// Encode the order into JSON. | ||
/// | ||
/// This method should generate the entire order JSON. You are provided with | ||
/// the order data from the SQL database and you should return a properly | ||
/// formatted order file encoding. | ||
/// | ||
/// - Parameters: | ||
/// - order: The order data from the SQL server | ||
/// - db: The SQL database to query against. | ||
/// - encoder: The `JSONEncoder` which you should use. | ||
/// - Returns: The encoded order JSON data. | ||
/// | ||
/// > Tip: See the [`Order`](https://developer.apple.com/documentation/walletorders/order) object to understand the keys. | ||
func encode<O: OrderModel>(order: O, db: any Database, encoder: JSONEncoder) async throws -> Data | ||
|
||
/// Should return a `URL` which points to the template data for the order. | ||
/// | ||
/// The URL should point to a directory containing the files specified by these keys: | ||
/// - `wwdrCertificate` | ||
/// - `pemCertificate` | ||
/// - `pemPrivateKey` | ||
/// | ||
/// > Important: Be sure to use the `URL(fileURLWithPath:isDirectory:)` initializer! | ||
var sslSigningFilesDirectory: URL { get } | ||
|
||
/// The location of the `openssl` command as a file URL. | ||
/// | ||
/// > Important: Be sure to use the `URL(fileURLWithPath:)` constructor. | ||
var sslBinary: URL { get } | ||
|
||
/// The full path to the `zip` command as a file URL. | ||
/// | ||
/// > Important: Be sure to use the `URL(fileURLWithPath:)` constructor. | ||
var zipBinary: URL { get } | ||
|
||
/// The name of Apple's WWDR.pem certificate as contained in `sslSigningFiles` path. | ||
/// | ||
/// Defaults to `WWDR.pem` | ||
var wwdrCertificate: String { get } | ||
|
||
/// The name of the PEM Certificate for signing the order as contained in `sslSigningFiles` path. | ||
/// | ||
/// Defaults to `ordercertificate.pem` | ||
var pemCertificate: String { get } | ||
|
||
/// The name of the PEM Certificate's private key for signing the order as contained in `sslSigningFiles` path. | ||
/// | ||
/// Defaults to `orderkey.pem` | ||
var pemPrivateKey: String { get } | ||
|
||
/// The password to the private key file. | ||
var pemPrivateKeyPassword: String? { get } | ||
} | ||
|
||
public extension OrdersDelegate { | ||
var wwdrCertificate: String { | ||
get { return "WWDR.pem" } | ||
} | ||
|
||
var pemCertificate: String { | ||
get { return "ordercertificate.pem" } | ||
} | ||
|
||
var pemPrivateKey: String { | ||
get { return "orderkey.pem" } | ||
} | ||
|
||
var pemPrivateKeyPassword: String? { | ||
get { return nil } | ||
} | ||
|
||
var sslBinary: URL { | ||
get { return URL(fileURLWithPath: "/usr/bin/openssl") } | ||
} | ||
|
||
var zipBinary: URL { | ||
get { return URL(fileURLWithPath: "/usr/bin/zip") } | ||
} | ||
|
||
func generateSignatureFile(in root: URL) -> Bool { | ||
return false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
// | ||
// OrdersService.swift | ||
// PassKit | ||
// | ||
// Created by Francesco Paolo Severino on 01/07/24. | ||
// | ||
|
||
import Vapor | ||
import FluentKit | ||
|
||
/// The main class that handles Wallet orders. | ||
public final class OrdersService: Sendable { | ||
private let service: OrdersServiceCustom<Order, OrdersDevice, OrdersRegistration, OrdersErrorLog> | ||
|
||
public init(app: Application, delegate: any OrdersDelegate, logger: Logger? = nil) { | ||
service = .init(app: app, delegate: delegate, logger: logger) | ||
} | ||
|
||
/// Generates the order content bundle for a given order. | ||
/// | ||
/// - Parameters: | ||
/// - order: The order to generate the content for. | ||
/// - db: The `Database` to use. | ||
/// - Returns: The generated order content. | ||
public func generatePassContent(for order: Order, on db: any Database) async throws -> Data { | ||
try await service.generateOrderContent(for: order, on: db) | ||
} | ||
|
||
/// Adds the migrations for Wallet orders models. | ||
/// | ||
/// - Parameter migrations: The `Migrations` object to add the migrations to. | ||
public static func register(migrations: Migrations) { | ||
migrations.add(Order()) | ||
migrations.add(OrdersDevice()) | ||
migrations.add(OrdersRegistration()) | ||
migrations.add(OrdersErrorLog()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
// | ||
// OrdersServiceCustom.swift | ||
// PassKit | ||
// | ||
// Created by Francesco Paolo Severino on 01/07/24. | ||
// | ||
|
||
import Vapor | ||
import APNS | ||
import VaporAPNS | ||
@preconcurrency import APNSCore | ||
import Fluent | ||
import NIOSSL | ||
import PassKit | ||
|
||
/// Class to handle `OrdersService`. | ||
/// | ||
/// The generics should be passed in this order: | ||
/// - Order Type | ||
/// - Device Type | ||
/// - Registration Type | ||
/// - Error Log Type | ||
public final class OrdersServiceCustom<O, D, R: OrdersRegistrationModel, E: ErrorLogModel>: Sendable where O == R.OrderType, D == R.DeviceType { | ||
public unowned let delegate: any OrdersDelegate | ||
private unowned let app: Application | ||
|
||
private let v1: FakeSendable<any RoutesBuilder> | ||
private let logger: Logger? | ||
|
||
public init(app: Application, delegate: any OrdersDelegate, logger: Logger? = nil) { | ||
self.delegate = delegate | ||
self.logger = logger | ||
self.app = app | ||
|
||
v1 = FakeSendable(value: app.grouped("api", "orders", "v1")) | ||
} | ||
} | ||
|
||
// MARK: - order file generation | ||
extension OrdersServiceCustom { | ||
private static func generateManifestFile(using encoder: JSONEncoder, in root: URL) throws { | ||
var manifest: [String: String] = [:] | ||
|
||
let paths = try FileManager.default.subpathsOfDirectory(atPath: root.unixPath()) | ||
try paths.forEach { relativePath in | ||
let file = URL(fileURLWithPath: relativePath, relativeTo: root) | ||
guard !file.hasDirectoryPath else { | ||
return | ||
} | ||
|
||
let data = try Data(contentsOf: file) | ||
let hash = SHA256.hash(data: data) | ||
manifest[relativePath] = hash.map { "0\(String($0, radix: 16))".suffix(2) }.joined() | ||
} | ||
|
||
let encoded = try encoder.encode(manifest) | ||
try encoded.write(to: root.appendingPathComponent("manifest.json")) | ||
} | ||
|
||
private func generateSignatureFile(in root: URL) throws { | ||
if delegate.generateSignatureFile(in: root) { | ||
// If the caller's delegate generated a file we don't have to do it. | ||
return | ||
} | ||
|
||
let sslBinary = delegate.sslBinary | ||
|
||
guard FileManager.default.fileExists(atPath: sslBinary.unixPath()) else { | ||
throw OrdersError.opensslBinaryMissing | ||
} | ||
|
||
let proc = Process() | ||
proc.currentDirectoryURL = delegate.sslSigningFilesDirectory | ||
proc.executableURL = sslBinary | ||
|
||
proc.arguments = [ | ||
"smime", "-binary", "-sign", | ||
"-certfile", delegate.wwdrCertificate, | ||
"-signer", delegate.pemCertificate, | ||
"-inkey", delegate.pemPrivateKey, | ||
"-in", root.appendingPathComponent("manifest.json").unixPath(), | ||
"-out", root.appendingPathComponent("signature").unixPath(), | ||
"-outform", "DER" | ||
] | ||
|
||
if let pwd = delegate.pemPrivateKeyPassword { | ||
proc.arguments!.append(contentsOf: ["-passin", "pass:\(pwd)"]) | ||
} | ||
|
||
try proc.run() | ||
|
||
proc.waitUntilExit() | ||
} | ||
|
||
private func zip(directory: URL, to: URL) throws { | ||
let zipBinary = delegate.zipBinary | ||
guard FileManager.default.fileExists(atPath: zipBinary.unixPath()) else { | ||
throw OrdersError.zipBinaryMissing | ||
} | ||
|
||
let proc = Process() | ||
proc.currentDirectoryURL = directory | ||
proc.executableURL = zipBinary | ||
|
||
proc.arguments = [ to.unixPath(), "-r", "-q", "." ] | ||
|
||
try proc.run() | ||
proc.waitUntilExit() | ||
} | ||
|
||
public func generateOrderContent(for order: O, on db: any Database) async throws -> Data { | ||
let tmp = FileManager.default.temporaryDirectory | ||
let root = tmp.appendingPathComponent(UUID().uuidString, isDirectory: true) | ||
let zipFile = tmp.appendingPathComponent("\(UUID().uuidString).zip") | ||
let encoder = JSONEncoder() | ||
|
||
let src = try await delegate.template(for: order, db: db) | ||
guard (try? src.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false else { | ||
throw OrdersError.templateNotDirectory | ||
} | ||
|
||
let encoded = try await self.delegate.encode(order: order, db: db, encoder: encoder) | ||
|
||
do { | ||
try FileManager.default.copyItem(at: src, to: root) | ||
|
||
defer { | ||
_ = try? FileManager.default.removeItem(at: root) | ||
} | ||
|
||
try encoded.write(to: root.appendingPathComponent("order.json")) | ||
|
||
try Self.generateManifestFile(using: encoder, in: root) | ||
try self.generateSignatureFile(in: root) | ||
|
||
try self.zip(directory: root, to: zipFile) | ||
|
||
defer { | ||
_ = try? FileManager.default.removeItem(at: zipFile) | ||
} | ||
|
||
return try Data(contentsOf: zipFile) | ||
} catch { | ||
throw error | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters