Skip to content

Commit

Permalink
Simplify OrdersDelegate
Browse files Browse the repository at this point in the history
  • Loading branch information
fpseverino committed Oct 17, 2024
1 parent 920c5ab commit 8a6cfc5
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 118 deletions.
33 changes: 19 additions & 14 deletions Sources/Orders/Orders.docc/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,6 @@ struct OrderJSONData: OrderJSON.Properties {
### Implement the Delegate

Create a delegate class that implements ``OrdersDelegate``.
In the ``OrdersDelegate/sslSigningFilesDirectory`` you specify there must be the `WWDR.pem`, `ordercertificate.pem` and `orderkey.pem` files.
If they are named like that you're good to go, otherwise you have to specify the custom name.

> Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). Those guides are for Wallet passes, but the process is similar for Wallet orders.
There are other fields available which have reasonable default values. See ``OrdersDelegate``'s documentation.

Because the files for your order's template and the method of encoding might vary by order type, you'll be provided the ``Order`` for those methods.
In the ``OrdersDelegate/encode(order:db:encoder:)`` method, you'll want to encode a `struct` that conforms to ``OrderJSON``.
Expand All @@ -127,10 +121,6 @@ import Fluent
import Orders

final class OrderDelegate: OrdersDelegate {
let sslSigningFilesDirectory = URL(fileURLWithPath: "Certificates/Orders/", isDirectory: true)

let pemPrivateKeyPassword: String? = Environment.get("ORDERS_PEM_PRIVATE_KEY_PASSWORD")!

func encode<O: OrderModel>(order: O, db: Database, encoder: JSONEncoder) async throws -> Data {
// The specific OrderData class you use here may vary based on the `order.orderTypeIdentifier`
// if you have multiple different types of orders, and thus multiple types of order data.
Expand All @@ -153,12 +143,14 @@ final class OrderDelegate: OrdersDelegate {
}
```

> Important: If you have an encrypted PEM private key, you **must** explicitly declare ``OrdersDelegate/pemPrivateKeyPassword`` as a `String?` or Swift will ignore it as it'll think it's a `String` instead.
### Initialize the Service

Next, initialize the ``OrdersService`` inside the `configure.swift` file.
This will implement all of the routes that Apple Wallet expects to exist on your server.
In the `signingFilesDirectory` you specify there must be the `WWDR.pem`, `certificate.pem` and `key.pem` files.
If they are named like that you're good to go, otherwise you have to specify the custom name.

> Tip: Obtaining the three certificates files could be a bit tricky. You could get some guidance from [this guide](https://github.com/alexandercerutti/passkit-generator/wiki/Generating-Certificates) and [this video](https://www.youtube.com/watch?v=rJZdPoXHtzI). Those guides are for Wallet passes, but the process is similar for Wallet orders.
```swift
import Fluent
Expand All @@ -169,7 +161,11 @@ let orderDelegate = OrderDelegate()

public func configure(_ app: Application) async throws {
...
let ordersService = try OrdersService(app: app, delegate: orderDelegate)
let ordersService = try OrdersService(
app: app,
delegate: orderDelegate,
signingFilesDirectory: "Certificates/Orders/"
)
}
```

Expand Down Expand Up @@ -199,7 +195,16 @@ let orderDelegate = OrderDelegate()

public func configure(_ app: Application) async throws {
...
let ordersService = try OrdersServiceCustom<MyOrderType, MyDeviceType, MyOrdersRegistrationType, MyErrorLogType>(app: app, delegate: orderDelegate)
let ordersService = try OrdersServiceCustom<
MyOrderType,
MyDeviceType,
MyOrdersRegistrationType,
MyErrorLogType
>(
app: app,
delegate: orderDelegate,
signingFilesDirectory: "Certificates/Orders/"
)
}
```

Expand Down
53 changes: 0 additions & 53 deletions Sources/Orders/OrdersDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,62 +51,9 @@ public protocol OrdersDelegate: AnyObject, Sendable {
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 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 }
}

extension OrdersDelegate {
public var wwdrCertificate: String {
return "WWDR.pem"
}

public var pemCertificate: String {
return "ordercertificate.pem"
}

public var pemPrivateKey: String {
return "orderkey.pem"
}

public var pemPrivateKeyPassword: String? {
return nil
}

public var sslBinary: URL {
return URL(fileURLWithPath: "/usr/bin/openssl")
}

public func generateSignatureFile(in root: URL) -> Bool {
return false
}
Expand Down
31 changes: 27 additions & 4 deletions Sources/Orders/OrdersService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,37 @@ public final class OrdersService: Sendable {
/// - Parameters:
/// - app: The `Vapor.Application` to use in route handlers and APNs.
/// - delegate: The ``OrdersDelegate`` to use for order generation.
/// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located.
/// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`.
/// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`.
/// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`.
/// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`.
/// - sslBinary: The location of the `openssl` command as a file path.
/// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
/// - logger: The `Logger` to use.
public init(
app: Application, delegate: any OrdersDelegate,
pushRoutesMiddleware: (any Middleware)? = nil, logger: Logger? = nil
app: Application,
delegate: any OrdersDelegate,
signingFilesDirectory: String,
wwdrCertificate: String = "WWDR.pem",
pemCertificate: String = "certificate.pem",
pemPrivateKey: String = "key.pem",
pemPrivateKeyPassword: String? = nil,
sslBinary: String = "/usr/bin/openssl",
pushRoutesMiddleware: (any Middleware)? = nil,
logger: Logger? = nil
) throws {
service = try .init(
app: app, delegate: delegate, pushRoutesMiddleware: pushRoutesMiddleware, logger: logger
self.service = try .init(
app: app,
delegate: delegate,
signingFilesDirectory: signingFilesDirectory,
wwdrCertificate: wwdrCertificate,
pemCertificate: pemCertificate,
pemPrivateKey: pemPrivateKey,
pemPrivateKeyPassword: pemPrivateKeyPassword,
sslBinary: sslBinary,
pushRoutesMiddleware: pushRoutesMiddleware,
logger: logger
)
}

Expand Down
58 changes: 39 additions & 19 deletions Sources/Orders/OrdersServiceCustom.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,39 +26,59 @@ public final class OrdersServiceCustom<O, D, R: OrdersRegistrationModel, E: Erro
where O == R.OrderType, D == R.DeviceType {
private unowned let app: Application
private unowned let delegate: any OrdersDelegate
private let signingFilesDirectory: URL
private let wwdrCertificate: String
private let pemCertificate: String
private let pemPrivateKey: String
private let pemPrivateKeyPassword: String?
private let sslBinary: URL
private let logger: Logger?

/// Initializes the service and registers all the routes required for Apple Wallet to work.
///
/// - Parameters:
/// - app: The `Vapor.Application` to use in route handlers and APNs.
/// - delegate: The ``OrdersDelegate`` to use for order generation.
/// - signingFilesDirectory: The path of the directory where the signing files (`wwdrCertificate`, `pemCertificate`, `pemPrivateKey`) are located.
/// - wwdrCertificate: The name of Apple's WWDR.pem certificate as contained in `signingFilesDirectory` path. Defaults to `WWDR.pem`.
/// - pemCertificate: The name of the PEM Certificate for signing the pass as contained in `signingFilesDirectory` path. Defaults to `certificate.pem`.
/// - pemPrivateKey: The name of the PEM Certificate's private key for signing the pass as contained in `signingFilesDirectory` path. Defaults to `key.pem`.
/// - pemPrivateKeyPassword: The password to the private key file. If the key is not encrypted it must be `nil`. Defaults to `nil`.
/// - sslBinary: The location of the `openssl` command as a file path.
/// - pushRoutesMiddleware: The `Middleware` to use for push notification routes. If `nil`, push routes will not be registered.
/// - logger: The `Logger` to use.
public init(
app: Application,
delegate: any OrdersDelegate,
signingFilesDirectory: String,
wwdrCertificate: String = "WWDR.pem",
pemCertificate: String = "certificate.pem",
pemPrivateKey: String = "key.pem",
pemPrivateKeyPassword: String? = nil,
sslBinary: String = "/usr/bin/openssl",
pushRoutesMiddleware: (any Middleware)? = nil,
logger: Logger? = nil
) throws {
self.app = app
self.delegate = delegate
self.signingFilesDirectory = URL(fileURLWithPath: signingFilesDirectory, isDirectory: true)
self.wwdrCertificate = wwdrCertificate
self.pemCertificate = pemCertificate
self.pemPrivateKey = pemPrivateKey
self.pemPrivateKeyPassword = pemPrivateKeyPassword
self.sslBinary = URL(fileURLWithPath: sslBinary)
self.logger = logger

let privateKeyPath = URL(
fileURLWithPath: delegate.pemPrivateKey, relativeTo: delegate.sslSigningFilesDirectory
).path
let privateKeyPath = URL(fileURLWithPath: pemPrivateKey, relativeTo: self.signingFilesDirectory).path
guard FileManager.default.fileExists(atPath: privateKeyPath) else {
throw OrdersError.pemPrivateKeyMissing
}
let pemPath = URL(
fileURLWithPath: delegate.pemCertificate, relativeTo: delegate.sslSigningFilesDirectory
).path
let pemPath = URL(fileURLWithPath: pemCertificate, relativeTo: self.signingFilesDirectory).path
guard FileManager.default.fileExists(atPath: pemPath) else {
throw OrdersError.pemCertificateMissing
}
let apnsConfig: APNSClientConfiguration
if let password = delegate.pemPrivateKeyPassword {
if let password = pemPrivateKeyPassword {
apnsConfig = APNSClientConfiguration(
authenticationMethod: try .tls(
privateKey: .privateKey(
Expand Down Expand Up @@ -395,20 +415,20 @@ extension OrdersServiceCustom {
if delegate.generateSignatureFile(in: root) { return }

// Swift Crypto doesn't support encrypted PEM private keys, so we have to use OpenSSL for that.
if let password = delegate.pemPrivateKeyPassword {
let sslBinary = delegate.sslBinary
if let password = self.pemPrivateKeyPassword {
let sslBinary = self.sslBinary
guard FileManager.default.fileExists(atPath: sslBinary.path) else {
throw OrdersError.opensslBinaryMissing
}

let proc = Process()
proc.currentDirectoryURL = delegate.sslSigningFilesDirectory
proc.currentDirectoryURL = self.signingFilesDirectory
proc.executableURL = sslBinary
proc.arguments = [
"smime", "-binary", "-sign",
"-certfile", delegate.wwdrCertificate,
"-signer", delegate.pemCertificate,
"-inkey", delegate.pemPrivateKey,
"-certfile", self.wwdrCertificate,
"-signer", self.pemCertificate,
"-inkey", self.pemPrivateKey,
"-in", root.appendingPathComponent("manifest.json").path,
"-out", root.appendingPathComponent("signature").path,
"-outform", "DER",
Expand All @@ -425,21 +445,21 @@ extension OrdersServiceCustom {
additionalIntermediateCertificates: [
Certificate(
pemEncoded: String(
contentsOf: delegate.sslSigningFilesDirectory
.appendingPathComponent(delegate.wwdrCertificate)
contentsOf: self.signingFilesDirectory
.appendingPathComponent(self.wwdrCertificate)
)
)
],
certificate: Certificate(
pemEncoded: String(
contentsOf: delegate.sslSigningFilesDirectory
.appendingPathComponent(delegate.pemCertificate)
contentsOf: self.signingFilesDirectory
.appendingPathComponent(self.pemCertificate)
)
),
privateKey: .init(
pemEncoded: String(
contentsOf: delegate.sslSigningFilesDirectory
.appendingPathComponent(delegate.pemPrivateKey)
contentsOf: self.signingFilesDirectory
.appendingPathComponent(self.pemPrivateKey)
)
),
signingTime: Date()
Expand Down
10 changes: 0 additions & 10 deletions Tests/OrdersTests/EncryptedOrdersDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,6 @@ import Orders
import Vapor

final class EncryptedOrdersDelegate: OrdersDelegate {
let sslSigningFilesDirectory = URL(
fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/",
isDirectory: true
)

let pemCertificate = "encryptedcert.pem"
let pemPrivateKey = "encryptedkey.pem"

let pemPrivateKeyPassword: String? = "password"

func encode<O: OrderModel>(
order: O, db: any Database, encoder: JSONEncoder
) async throws -> Data {
Expand Down
8 changes: 1 addition & 7 deletions Tests/OrdersTests/OrdersTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -397,13 +397,7 @@ struct OrdersTests {

@Test("Default OrdersDelegate Properties")
func defaultDelegate() {
let delegate = DefaultOrdersDelegate()
#expect(delegate.wwdrCertificate == "WWDR.pem")
#expect(delegate.pemCertificate == "ordercertificate.pem")
#expect(delegate.pemPrivateKey == "orderkey.pem")
#expect(delegate.pemPrivateKeyPassword == nil)
#expect(delegate.sslBinary == URL(fileURLWithPath: "/usr/bin/openssl"))
#expect(!delegate.generateSignatureFile(in: URL(fileURLWithPath: "")))
#expect(!DefaultOrdersDelegate().generateSignatureFile(in: URL(fileURLWithPath: "")))
}
}

Expand Down
8 changes: 0 additions & 8 deletions Tests/OrdersTests/TestOrdersDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,6 @@ import Orders
import Vapor

final class TestOrdersDelegate: OrdersDelegate {
let sslSigningFilesDirectory = URL(
fileURLWithPath: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/",
isDirectory: true
)

let pemCertificate = "certificate.pem"
let pemPrivateKey = "key.pem"

func encode<O: OrderModel>(
order: O, db: any Database, encoder: JSONEncoder
) async throws -> Data {
Expand Down
11 changes: 8 additions & 3 deletions Tests/OrdersTests/withApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Zip

func withApp(
delegate: some OrdersDelegate,
useEncryptedKey: Bool = false,
_ body: (Application, OrdersService) async throws -> Void
) async throws {
let app = try await Application.make(.testing)
Expand All @@ -18,20 +19,24 @@ func withApp(

OrdersService.register(migrations: app.migrations)
app.migrations.add(CreateOrderData())
let passesService = try OrdersService(
let ordersService = try OrdersService(
app: app,
delegate: delegate,
signingFilesDirectory: "\(FileManager.default.currentDirectoryPath)/Tests/Certificates/",
pemCertificate: useEncryptedKey ? "encryptedcert.pem" : "certificate.pem",
pemPrivateKey: useEncryptedKey ? "encryptedkey.pem" : "key.pem",
pemPrivateKeyPassword: useEncryptedKey ? "password" : nil,
pushRoutesMiddleware: SecretMiddleware(secret: "foo"),
logger: app.logger
)

app.databases.middleware.use(OrderDataMiddleware(service: passesService), on: .sqlite)
app.databases.middleware.use(OrderDataMiddleware(service: ordersService), on: .sqlite)

try await app.autoMigrate()

Zip.addCustomFileExtension("order")

try await body(app, passesService)
try await body(app, ordersService)

try await app.autoRevert()
try await app.asyncShutdown()
Expand Down

0 comments on commit 8a6cfc5

Please sign in to comment.