Skip to content
This repository has been archived by the owner on May 27, 2020. It is now read-only.

Commit

Permalink
Merge pull request #6 from matthijs2704/auth_key
Browse files Browse the repository at this point in the history
Added support for token based authentication
  • Loading branch information
Matthijs Logemann authored Oct 6, 2016
2 parents 1e92a99 + 0205a09 commit 29cd52d
Show file tree
Hide file tree
Showing 12 changed files with 379 additions and 75 deletions.
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ let package = Package(
.Package(url: "https://github.com/vapor/json.git", majorVersion: 1, minor: 0),
.Package(url: "https://github.com/vapor/clibressl.git", majorVersion: 1),
.Package(url: "https://github.com/matthijs2704/SwiftString.git", majorVersion: 1, minor: 0),
.Package(url: "https://github.com/boostcode/CCurl.git", majorVersion: 0, minor: 2)
.Package(url: "https://github.com/boostcode/CCurl.git", majorVersion: 0, minor: 2),
.Package(url: "https://github.com/siemensikkema/vapor-jwt.git", majorVersion: 0, minor: 2)
]
)
41 changes: 24 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
![Vapor](https://img.shields.io/badge/Vapor-1.0-green.svg)
[![Crates.io](https://img.shields.io/crates/l/rustc-serialize.svg?maxAge=2592000)]()

VaporAPNS is a simple, yet elegant, Swift library that allows you to send Apple Push Notifications using HTTP/2 protocol in Linux & macOS.
VaporAPNS is a simple, yet elegant, Swift library that allows you to send Apple Push Notifications using HTTP/2 protocol in Linux & macOS. It has support for the brand-new [Token Based Authentication](https://developer.apple.com/videos/play/wwdc2016/724/) but if you need it, the traditional certificate authentication method is ready for you to use as well. Choose whatever you like!

## 🔧 Installation

Expand All @@ -21,16 +21,7 @@ brew reinstall curl --with-openssl --with-nghttp2
brew link curl --force
```

### 2- Prepare certificates

Create your APNS certificates, then export as `P12` file without a password. Then proceed in this way in your shell:

```shell
openssl pkcs12 -in Certificates.p12 -out push.crt.pem -clcerts -nokeys
openssl pkcs12 -in Certificates.p12 -out push.key.pem -nocerts -nodes
```

### 3- Add VaporAPNS to your project
### 2- Add VaporAPNS to your project

Add the following dependency to your `Package.swift` file:

Expand All @@ -46,12 +37,27 @@ It's really easy to get started with the VaporAPNS library! First you need to im
```swift
import VaporAPNS
```

### 🔒 Authentication methods
Then you need to get yourself an instance of the VaporAPNS class:
There are two ways you can initiate VaporAPNS. You can either use the new authentication key APNS authentication method or the 'old'/traditional certificates method.
#### 🔑 Authentication key authentication (preferred)
This is the easiest to setup authentication method. Also the token never expires so you won't have to renew the private key (unlike the certificates which expire at a certain date).
```swift
let vaporAPNS = try VaporAPNS(certPath: "/your/path/to/the/push.crt.pem", keyPath: "/your/path/to/the/push.key.pem")
let options = try! Options(topic: "<your bundle identifier>", teamId: "<your team identifier>", keyId: "<your key id>", keyPath: "/path/to/your/APNSAuthKey.p8")
let vaporAPNS = try VaporAPNS(options: options)
```

#### 🎫 Certificate authentication
If you decide to go with the more traditional authentication method, you need to convert your push certificate, using:
```shell
openssl pkcs12 -in Certificates.p12 -out push.crt.pem -clcerts -nokeys
openssl pkcs12 -in Certificates.p12 -out push.key.pem -nocerts -nodes
```
After you have those two files you can go ahead and create a VaporAPNS instance:
```swift
let options = try! Options(topic: "<your bundle identifier>", certPath: "/path/to/your/certificate.crt.pem", keyPath: "/path/to/your/certificatekey.key.pem")
let vaporAPNS = try VaporAPNS(options: options)
```
### 📦 Push notification payload
After you have the VaporAPNS instance, we can go ahead and create an Payload:
There are multiple quick ways to create a push notification payload. The most simple one only contains a body message:
```swift
Expand All @@ -75,14 +81,15 @@ payload.bodyLocArgs = [ "Jenna", "Frank" ]
```
The possibilities are endless!

### 🚀 Send it!

After we've created the payload it's time to actually send the push message. To do so, we have to create an ApplePushMessage object, by doing:
```swift
let pushMessage = ApplePushMessage(topic: "nl.logicbit.TestApp", priority: .immediately, payload: payload, deviceToken: "488681b8e30e6722012aeb88f485c823b9be15c42e6cc8db1550a8f1abb590d7", sandbox: true)
```
`topic` being the build identifier of your app.
Priority can either be `.energyEfficient` or `.immediately`. What does that mean? In short, immediately will `.immediately` deliver the push notification and `.energyEfficient` will take power considerations for the device into account. Use `.immediately` for normal message push notifications and `.energyEfficient` for content-available pushes.
`deviceToken` is the notification registration token of the device you want to send the push to.
`topic` being the build identifier of your app. This is an *optional* parameter. If left out or `nil` it'll use the topic from Options you've provided in the initializer.
Priority can either be `.energyEfficient` or `.immediately`. What does that mean? In short, immediately will `.immediately` deliver the push notification and `.energyEfficient` will take power considerations for the device into account. Use `.immediately` for normal message push notifications and `.energyEfficient` for content-available pushes.
`deviceToken` is the notification registration token of the device you want to send the push to.
`sandbox` determines to what APNS server to send the push to. Pass `true` for development and `false` for production.

Now you can send the notification using:
Expand Down
7 changes: 3 additions & 4 deletions Sources/VaporAPNS/ApplePushMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ public struct ApplePushMessage: NodeRepresentable {
/// Message ID
public let messageId: String = UUID().uuidString

/// Application BundleID
public let topic: String
public let topic: String?

public let collapseIdentifier: String?

Expand Down Expand Up @@ -56,7 +55,7 @@ public struct ApplePushMessage: NodeRepresentable {
/// Network error Clousure
public var networkError: ErrorCallback?

public init(topic: String, priority: Priority, expirationDate: Date? = nil, payload: Payload, deviceToken:String, sandbox:Bool = true, collapseIdentifier: String? = nil, threadIdentifier: String? = nil) {
public init(topic: String? = nil, priority: Priority, expirationDate: Date? = nil, payload: Payload, deviceToken:String, sandbox:Bool = true, collapseIdentifier: String? = nil, threadIdentifier: String? = nil) {
self.topic = topic
self.priority = priority
self.expirationDate = expirationDate
Expand All @@ -67,7 +66,7 @@ public struct ApplePushMessage: NodeRepresentable {
self.threadIdentifier = threadIdentifier
}

public func makeNode(context: Context) throws -> Node {
public func makeNode(context ntext: Context) throws -> Node {
return EmptyNode
}
}
19 changes: 19 additions & 0 deletions Sources/VaporAPNS/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public enum APNSError: CustomStringConvertible {
case internalServerError
case serviceUnavailable
case missingTopic
case invalidSignature
case unknownError(error: String)

init(errorReason: String) {
Expand Down Expand Up @@ -112,6 +113,7 @@ public enum APNSError: CustomStringConvertible {
case .internalServerError: return "An internal server error occurred."
case .serviceUnavailable: return "The service is unavailable."
case .missingTopic: return "The apns-topic header of the request was not specified and was required. The apns-topic header is mandatory when the client is connected using a certificate that supports multiple topics."
case .invalidSignature: return "The used signature may be wrong or something went wrong while signing. Double check the signing key and or try again."
case .unknownError(let error): return "This error has not been mapped yet in APNSError: \(error)"
}
}
Expand All @@ -122,3 +124,20 @@ public enum TokenError: Error {
case invalidTokenString
case wrongTokenLength
}

public enum InitializeError: Error, CustomStringConvertible {
case noAuthentication
case noTopic
case certificateFileDoesNotExist
case keyFileDoesNotExist

public var description: String {
switch self {
case .noAuthentication: return "APNS Authentication is required. You can either use APNS Auth Key authentication (easiest to setup and maintain) or the old fashioned certificates way"
case .noTopic: return "No APNS topic provided. This is required."
case .certificateFileDoesNotExist: return "Certificate file could not be found on your disk. Double check if the file exists and if the path is correct"
case .keyFileDoesNotExist: return "Key file could not be found on your disk. Double check if the file exists and if the path is correct"
}
}

}
107 changes: 90 additions & 17 deletions Sources/VaporAPNS/Options.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,108 @@ public struct Options: CustomStringConvertible, NodeInitializable {
case p443 = 443, p2197 = 2197
}

public var topic: String?
public var topic: String
public var port: Port = .p443
public var expiry: Date?
public var priority: Int?
public var apnsId: String?
public var development: Bool = true

public init() { }
// Authentication method: certificates
public var certPath: String?
public var keyPath: String?

// Authentication method: authentication key
public var teamId: String?
public var keyId: String?
public var privateKey: String?
public var publicKey: String?

public var debugLogging: Bool = false

public var usesCertificateAuthentication: Bool {
return certPath != nil && keyPath != nil
}

public init(topic: String, certPath: String, keyPath: String, port: Port = .p443, debugLogging: Bool = false) throws {
self.topic = topic
self.certPath = certPath
self.keyPath = keyPath

self.debugLogging = debugLogging

let fileManager = FileManager.default
guard fileManager.fileExists(atPath: certPath) else {
throw InitializeError.certificateFileDoesNotExist
}
guard fileManager.fileExists(atPath: keyPath) else {
throw InitializeError.keyFileDoesNotExist
}
}

public init(topic: String, teamId: String, keyId: String, keyPath: String, port: Port = .p443, debugLogging: Bool = false) throws {
self.teamId = teamId
self.topic = topic
self.keyId = keyId

self.debugLogging = debugLogging

let fileManager = FileManager.default
guard fileManager.fileExists(atPath: keyPath) else {
throw InitializeError.keyFileDoesNotExist
}

let (priv, pub) = try keyPath.tokenString()
self.privateKey = priv
self.publicKey = pub
}

public init(node: Node, in context: Context) throws {
topic = node["topic"]?.string
if let topic = node["topic"]?.string {
self.topic = topic
}else {
throw InitializeError.noTopic
}

if let portRaw = node["port"]?.int, let port = Port(rawValue: portRaw) {
self.port = port
}
if let expiryTimeSince1970 = node["date"]?.double {
expiry = Date(timeIntervalSince1970: expiryTimeSince1970)

var hasAnyAuthentication = false
var hasBothAuthentication = false

if let certPath = node["certificatePath"]?.string, let keyPath = node["keyPath"]?.string {
hasAnyAuthentication = true
self.certPath = certPath
self.keyPath = keyPath

}

if let privateKeyLocation = node["keyPath"]?.string, let keyId = node["keyId"]?.string {
if hasAnyAuthentication { hasBothAuthentication = true }
hasAnyAuthentication = true
let (priv, pub) = try privateKeyLocation.tokenString()
self.privateKey = priv
self.publicKey = pub
self.keyId = keyId
}

guard hasAnyAuthentication else {
throw InitializeError.noAuthentication
}

if hasBothAuthentication {
print ("You've seem to have specified both authentication methods, choosing preferred APNS Auth Key method...")
certPath = nil
keyPath = nil
}
priority = node["priority"]?.int
apnsId = node["apns-id"]?.string
development = node["development"]?.bool ?? development
}

public var description: String {
return
"Topic \(topic)" +
"\nPort \(port.rawValue)" +
"\nExpiry \(expiry) \(expiry?.timeIntervalSince1970.rounded())" +
"\nPriority \(priority)" +
"\nAPNSID \(apnsId)" +
"\nDevelopment \(development)"
"\nPort \(port.rawValue)" +
"\nPort \(port.rawValue)" +
"\nCER - Certificate path: \(certPath)" +
"\nCER - Key path: \(keyPath)" +
"\nTOK - Key ID: \(keyId)" +
"\nTOK - Private key: \(privateKey)" +
"\nTOK - Public key: \(publicKey)"
}
}
4 changes: 2 additions & 2 deletions Sources/VaporAPNS/Payload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import Foundation
import JSON

public class Payload: JSONRepresentable {
open class Payload: JSONRepresentable {
/// The number to display as the badge of the app icon.
var badge: Int?

Expand Down Expand Up @@ -49,7 +49,7 @@ public class Payload: JSONRepresentable {
// Any extra key-value pairs to add to the JSON
var extra: [String: NodeRepresentable] = [:]

public func makeJSON() throws -> JSON {
open func makeJSON() throws -> JSON {
var payloadData: [String: NodeRepresentable] = [:]
var apsPayloadData: [String: NodeRepresentable] = [:]

Expand Down
6 changes: 3 additions & 3 deletions Sources/VaporAPNS/Result.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import Foundation

public enum Result {
case success(apnsId:String?, serviceStatus: ServiceStatus)
case error(apnsId:String?, error: APNSError)
case networkError(apnsId:String?, error: Error)
case success(apnsId:String, serviceStatus: ServiceStatus)
case error(apnsId:String, error: APNSError)
case networkError(apnsId:String, error: Error)
}

public enum ServiceStatus: Int, Error {
Expand Down
Loading

0 comments on commit 29cd52d

Please sign in to comment.