NIO-based Apple Push Notification Service
- Proposal: SSWG-0006
- Authors: Kyle Browning
- Sponsors: TBD
- Review Manager: TBD
- Status: Implemented
- Pitch: Server/Pitches/APNS Implementation
- Implementation: kylebrowning/swift-nio-http2-apns
Package Description
Apple push notification service implementation built on Swift NIO.
Package name | nio-apns |
Proposed Maturity Level | Sandbox |
License | Apache 2 |
Dependencies | SwiftNIO 2.x, SwiftNIOSSL 2.x, SwiftNIOHTTP2 1.x |
Introduction
NIOAPNS
is a module thats gives server side swift applications the ability to use the Apple push notification service.
Motivation
APNS is used to push billions of pushes a day, (7 billion per day in 2012). Many of us using Swift on the backend are using it to power our iOS applications. Having a community supported APNS implementation would go a long way to making it the fastest, free-ist, and simplest solution that exists.
All of the currently maintained libraries either have framework specific dependencies, are not built with NIO, or do not provide enough extensibility while providing "out of the box" capabilities.
Existing Solutions
Proposed Solution
NIOApns
provides the essential types for interacting with APNS Server (both production and sandbox).
What it does do
- Provides an API for handling connection to Apples HTTP2 APNS server
- Provides proper error messages that APNS might respond with.
- Uses custom/non dependency implementations of JSON Web Token specific to APNS (using rfc7519
- Imports OpenSSL for SHA256 and ES256
- Provides an interface for signing your Push Notifications
- Signs your token request
- Sends push notifications to a specific device.
- Adheres to guidelines Apple Provides.
What it won't do.
- Store/register device tokens
- Build an HTTP2 generic client
- Google Cloud Message
- Refresh your token no more than once every 20 minutes and no less than once every 60 minutes. (up to connection handler)
Future considerations and dependencies
- BoringSSL
- Common connection handler options
- Swift-Log
- Swift-Metric
- Swift-JWT?
APNSConfiguration
APNSConfiguration
is a structure that provides the system with common configuration.
public struct APNSConfiguration {
public let keyIdentifier: String
public let teamIdentifier: String
public let signingMode: SigningMode
public let topic: String
public let environment: APNSEnvironment
public let tlsConfiguration: TLSConfiguration
public var url: URL {
switch environment {
case .production:
return URL(string: "https://api.push.apple.com")!
case .sandbox:
return URL(string: "https://api.development.push.apple.com")!
}
}
}
Example APNSConfiguration
let apnsConfig = try APNSConfiguration(keyIdentifier: "9UC9ZLQ8YW",
teamIdentifier: "ABBM6U9RM5",
signingMode: .file(path: "/Users/kylebrowning/Downloads/AuthKey_9UC9ZLQ8YW.p8"),
topic: "com.grasscove.Fern",
environment: .sandbox)
SigningMode
SigningMode
provides a method by which engineers can choose how their certificates are signed. Since security is important keeping we extracted this logic into three options. file
, data
, or custom
.
public struct SigningMode {
public let signer: APNSSigner
init(signer: APNSSigner) {
self.signer = signer
}
}
extension SigningMode {
public static func file(path: String) throws -> SigningMode {
return .init(signer: try FileSigner(url: URL(fileURLWithPath: path)))
}
public static func data(data: Data) throws -> SigningMode {
return .init(signer: try DataSigner(data: data))
}
public static func custom(signer: APNSSigner) -> SigningMode {
return .init(signer: signer)
}
}
Example Custom SigningMode that uses AWS for private keystorage
public class CustomSigner: APNSSigner {
public func sign(digest: Data) throws -> Data {
return try AWSKeyStore.sign(digest: digest)
}
public func verify(digest: Data, signature: Data) -> Bool {
// verification
}
}
let customSigner = CustomSigner()
let apnsConfig = APNSConfig(keyId: "9UC9ZLQ8YW",
teamId: "ABBM6U9RM5",
signingMode: .custom(signer: customSigner),
topic: "com.grasscove.Fern",
environment: .sandbox)
APNSConnection
APNSConnection
is a class with methods thats provides a wrapper to NIO's ClientBootstrap. The swift-nio-http2
dependency is utilized here. It also provides a function to send a notification to a specific device token string.
Example APNSConnection
let apnsConfig = ...
let apns = try APNSConnection.connect(configuration: apnsConfig, on: group.next()).wait()
Alert
Alert
is the actual meta data of the push notification alert someone wishes to send. More details on the specifcs of each property are provided here. They follow a 1-1 naming scheme listed in Apple's documentation
Example Alert
let alert = Alert(title: "Hey There", subtitle: "Full moon sighting", body: "There was a full moon last night did you see it")
APSPayload
APSPayload
is the meta data of the push notification. Things like the alert, badge count. More details on the specifcs of each property are provided here. They follow a 1-1 naming scheme listed in Apple's documentation
Example APSPayload
let alert = ...
let aps = APSPayload(alert: alert, badge: 1, sound: "cow.wav")
Putting it all together
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
let apnsConfig = try APNSConfiguration(keyIdentifier: "9UC9ZLQ8YW",
teamIdentifier: "ABBM6U9RM5",
signingMode: .file(path: "/Users/kylebrowning/Downloads/AuthKey_9UC9ZLQ8YW.p8"),
topic: "com.grasscove.Fern",
environment: .sandbox)
let apns = try APNSConnection.connect(configuration: apnsConfig, on: group.next()).wait()
let alert = Alert(title: "Hey There", subtitle: "Full moon sighting", body: "There was a full moon last night did you see it")
let aps = APSPayload(alert: alert, badge: 1, sound: "cow.wav")
let notification = BasicNotification(aps: aps)
let res = try apns.send(notification, to: "de1d666223de85db0186f654852cc960551125ee841ca044fdf5ef6a4756a77e").wait()
try apns.close().wait()
try group.syncShutdownGracefully()
Custom Notification Data
Apple provides engineers with the ability to add custom payload data to each notification. In order to faciliate this we have the APNSNotification
.
Example
struct AcmeNotification: APNSNotification {
let acme2: [String]
let aps: APSPayload
init(acme2: [String], aps: APSPayload) {
self.acme2 = acme2
self.aps = aps
}
}
let apns: APNSConnection: = ...
let aps: APSPayload = ...
let notification = AcmeNotification(acme2: ["bang", "whiz"], aps: aps)
let res = try apns.send(notification, to: "de1d666223de85db0186f654852cc960551125ee841ca044fdf5ef6a4756a77e").wait()
Maturity
This package meets the following criteria according to the SSWG Incubation Process:
- Ecosystem (SwiftNIO)
- Code Style is up to date
- Errors implemented
- Apache 2 license
- Swift Code of Conduct
- Contributing Guide
- CircleCI builds
Vapor, and I, are in the processes of providing a (framework agnostic) higher-level library: APNSkit
. This is the initial work on connection pooling and things we thought would be hard to get right in the initial incubation periods before submitting..
Alternatives Considered
N/A
Special Thanks
Tanner | Everything really |
fumoboy007 | APNSSigner idea |
David Hart | Custom Notifications, best practices and Apples naming conventions |
IanPartridge | JWT, crypto usages, overall feedback |
Nathan Harris | General questions on Incubation process and templates |
Everyone who participated in the pitch | The feedback thread was so lively, thank you! |