Skip to content

Instantly share code, notes, and snippets.

@kylebrowning
Created April 19, 2019 14:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kylebrowning/50e9c1baadca68f686b4c651ac3d4617 to your computer and use it in GitHub Desktop.
Save kylebrowning/50e9c1baadca68f686b4c651ac3d4617 to your computer and use it in GitHub Desktop.

NIO-based Apple Push Notification Service

Package Description

Apple push notification service implmentation build 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",
                      env: .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 = APNSNotification(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 APNSNotificationProtocol.

Example

struct AcmeNotification: APNSNotificationProtocol {
    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:

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!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment