Skip to content

Commit d3c2c5f

Browse files
committed
Create apns
1 parent 0e9a1c1 commit d3c2c5f

17 files changed

+502
-25
lines changed

Config.plist

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>KeyPath</key>
6+
<string>YOUR_p8_KEY_PATH</string>
7+
<key>KeyId</key>
8+
<string>YOUR_KEY_ID</string>
9+
<key>TeamId</key>
10+
<string>YOUR_TEAM_ID</string>
11+
<key>Environment</key>
12+
<string>sandbox, production or all</string>
13+
</dict>
14+
</plist>

Package.resolved

Lines changed: 24 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,26 @@
44
import PackageDescription
55

66
let package = Package(
7-
name: "apns",
7+
name: "APNS",
88
products: [
99
// Products define the executables and libraries produced by a package, and make them visible to other packages.
1010
.library(
11-
name: "apns",
12-
targets: ["apns"]),
11+
name: "APNS",
12+
targets: ["APNS"]),
1313
],
1414
dependencies: [
1515
// Dependencies declare other packages that this package depends on.
16-
.package(url: "https://github.com/vapor/jwt.git", from: "2.2.0")
16+
.package(url: "https://github.com/vapor/jwt.git", from: "2.2.0"),
17+
.package(url: "https://github.com/vapor/clibressl.git", from: "1.0.0")
1718
],
1819
targets: [
1920
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2021
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
2122
.target(
22-
name: "apns",
23-
dependencies: ["JWT"]),
23+
name: "APNS",
24+
dependencies: ["JWT", "CLibreSSL"]),
2425
.testTarget(
25-
name: "apnsTests",
26-
dependencies: ["apns"]),
26+
name: "APNSTests",
27+
dependencies: ["APNS"]),
2728
]
2829
)

Sources/APNS/APNSError.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Foundation
2+
3+
// TODO: Error handling
4+
public enum APNSError: Error {
5+
case unknown
6+
}

Sources/APNS/APNSHeader.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW1
2+
import Foundation
3+
4+
struct APNSHeader: Encodable {
5+
let topic: String
6+
let identifier: String?
7+
let expiration: Int
8+
let priority: Int
9+
let collapseIdentifier: String?
10+
11+
private enum CodingKeys: String, CodingKey {
12+
case
13+
topic = "apns-topic",
14+
identifier = "apns-id",
15+
expiration = "apns-expiration",
16+
priority = "apns-priority",
17+
collapseIdentifier = "apns-collapse-id"
18+
}
19+
}

Sources/APNS/APNSRequest.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Foundation
2+
3+
public struct APNSRequest {
4+
let header: APNSHeader
5+
let payload: Payload
6+
7+
public init(topic: String,
8+
payload: Payload,
9+
apnsIdentifier: UUID? = nil,
10+
priority: Priority = .immediately,
11+
expiration: Date? = nil,
12+
collapseIdentifier: String? = nil) {
13+
self.payload = payload
14+
self.header = APNSHeader(topic: topic,
15+
identifier: apnsIdentifier?.uuidString,
16+
expiration: Int(expiration?.timeIntervalSince1970.rounded() ?? 0),
17+
priority: priority.rawValue,
18+
collapseIdentifier: collapseIdentifier)
19+
}
20+
}

Sources/APNS/APNSSession.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Foundation
2+
import Dispatch
3+
4+
class APNSSession {
5+
private let session = URLSession(configuration: .default)
6+
func sendSync(request: URLRequest) -> (Data?, URLResponse?, Error?) {
7+
var result: (Data?, URLResponse?, Error?)! = nil
8+
let semaphor = DispatchSemaphore(value: 0)
9+
session.dataTask(with: request) { data, response, error in
10+
result = (data, response, error)
11+
semaphor.signal()
12+
}
13+
.resume()
14+
semaphor.wait()
15+
return result
16+
}
17+
}

Sources/APNS/Alert.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Foundation
2+
/// https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html#//apple_ref/doc/uid/TP40008194-CH17-SW1
3+
public struct Alert: Codable {
4+
let title: String?
5+
let subtitle: String?
6+
let body: String?
7+
let titleLocalizationKey: String?
8+
let titleLocalizationArguments: [String]?
9+
let actionLocalizationKey: String?
10+
let bodyLocalizationKey: String?
11+
let bodyLocalizationArguments: [String]?
12+
let launchImage: String?
13+
14+
public init(title: String? = nil,
15+
subtitle: String? = nil,
16+
body: String? = nil,
17+
titleLocalizationKey: String? = nil,
18+
titleLocalizationArguments: [String]? = nil,
19+
actionLocalizationKey: String? = nil,
20+
bodyLocalizationKey: String? = nil,
21+
bodyLocalizationArguments: [String]? = nil,
22+
launchImage: String? = nil) {
23+
self.title = title
24+
self.subtitle = subtitle
25+
self.body = body
26+
self.titleLocalizationKey = titleLocalizationKey
27+
self.titleLocalizationArguments = titleLocalizationArguments
28+
self.actionLocalizationKey = actionLocalizationKey
29+
self.bodyLocalizationKey = bodyLocalizationKey
30+
self.bodyLocalizationArguments = bodyLocalizationArguments
31+
self.launchImage = launchImage
32+
}
33+
34+
35+
private enum CodingKeys: String, CodingKey {
36+
case
37+
title,
38+
subtitle,
39+
body,
40+
titleLocalizationKey = "title-loc-key",
41+
titleLocalizationArguments = "title-loc-args",
42+
actionLocalizationKey = "action-loc-key",
43+
bodyLocalizationKey = "loc-key",
44+
bodyLocalizationArguments = "loc-args",
45+
launchImage = "launch-image"
46+
}
47+
}

Sources/APNS/Aps.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
/// https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/PayloadKeyReference.html#//apple_ref/doc/uid/TP40008194-CH17-SW1
3+
public struct Aps: Codable, JsonInitializable {
4+
let alert: Alert?
5+
let badge: Int?
6+
let sound: String?
7+
let contentAvailable: Int?
8+
let category: String?
9+
let threadId: String?
10+
11+
public init(alert: Alert?,
12+
badge: Int? = nil,
13+
sound: String? = nil,
14+
contentAvailable: Int? = nil,
15+
category: String? = nil,
16+
threadId: String? = nil) {
17+
self.alert = alert
18+
self.badge = badge
19+
self.sound = sound
20+
self.contentAvailable = contentAvailable
21+
self.category = category
22+
self.threadId = threadId
23+
}
24+
25+
private enum CodingKeys: String, CodingKey {
26+
case
27+
alert,
28+
badge,
29+
sound,
30+
contentAvailable = "content-available",
31+
category,
32+
threadId = "thread-id"
33+
}
34+
}

Sources/APNS/AuthenticationKey.swift

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import Foundation
2+
import JWT
3+
import CLibreSSL
4+
5+
extension JWT {
6+
private func time(with key: String) -> Date? {
7+
guard let interval = payload[key]?.double else { return nil }
8+
return Date(timeIntervalSince1970: interval)
9+
}
10+
var issuedAtTime: Date? { return time(with: "iat") }
11+
}
12+
13+
class AuthenticationKey {
14+
let privateKey: String
15+
let publicKey: String
16+
let keyId: String
17+
let teamId: String
18+
private var cache: (jwt: JWT, token: String)?
19+
func getToken() throws -> String {
20+
let now = Date()
21+
if let cache = cache, let issuedAtTime = cache.jwt.issuedAtTime, issuedAtTime > now.addingTimeInterval(-60*55) {
22+
return cache.token
23+
}
24+
let jwt = try JWT(additionalHeaders: [KeyID(keyId)],
25+
payload: JSON(.object(["iss":.string(teamId), "iat": .number(.int(Int(now.timeIntervalSince1970.rounded())))])),
26+
signer: ES256(key: privateKey.bytes.base64Decoded))
27+
let token = try jwt.createToken()
28+
let jwtToVerify = try JWT(token: token)
29+
try jwtToVerify.verifySignature(using: ES256(key: publicKey.bytes.base64Decoded))
30+
self.cache = (jwt, token)
31+
return token
32+
}
33+
34+
init(path: String, keyId: String, teamId: String) throws {
35+
(self.privateKey, self.publicKey) = try AuthenticationKey.extractKeys(from: path)
36+
self.keyId = keyId
37+
self.teamId = teamId
38+
}
39+
40+
private static func extractKeys(from path: String) throws -> (privateKey: String, publicKey: String) {
41+
guard FileManager.default.fileExists(atPath: path) else {
42+
throw APNSError.unknown
43+
}
44+
45+
var pKey = EVP_PKEY_new()
46+
let fp = fopen(path, "r")
47+
PEM_read_PrivateKey(fp, &pKey, nil, nil)
48+
fclose(fp)
49+
50+
guard let ecKey = EVP_PKEY_get1_EC_KEY(pKey) else {
51+
throw APNSError.unknown
52+
}
53+
54+
EC_KEY_set_conv_form(ecKey, POINT_CONVERSION_UNCOMPRESSED)
55+
56+
var _pub: UnsafeMutablePointer<UInt8>? = nil
57+
let pubLen = i2o_ECPublicKey(ecKey, &_pub)
58+
guard let pub = _pub else {
59+
throw APNSError.unknown
60+
}
61+
62+
guard let priKeyHex = BN_bn2hex(EC_KEY_get0_private_key(ecKey)), let priKeyHexString = String(validatingUTF8:priKeyHex) else {
63+
throw APNSError.unknown
64+
}
65+
let pubKeyHexString = Data(bytes: Bytes((0..<Int(pubLen)).map { Byte(pub[$0]) })).hexString
66+
67+
let priBase64String = String(bytes: try dataFromHexadecimalString(text: "00\(priKeyHexString)").base64Encoded)
68+
let pubBase64String = String(bytes: try dataFromHexadecimalString(text: pubKeyHexString).base64Encoded)
69+
70+
return (priBase64String, pubBase64String)
71+
}
72+
73+
private static func dataFromHexadecimalString(text: String) throws -> Data {
74+
var data = Data(capacity: text.characters.count / 2)
75+
let regex = try! NSRegularExpression(pattern: "[0-9a-f]{1,2}", options: .caseInsensitive)
76+
regex.enumerateMatches(in: text, options: [], range: NSRange(location: 0, length: text.characters.count)) { match, flags, stop in
77+
let range = Range<String.Index>(match!.range, in: text)!
78+
let byteString = text[range]
79+
var num = UInt8(byteString, radix: 16)
80+
data.append(&num!, count: 1)
81+
}
82+
return data
83+
}
84+
}
85+
86+
struct KeyID: Header {
87+
static let name = "kid"
88+
var node: Node
89+
init(_ keyID: String) {
90+
node = Node(keyID)
91+
}
92+
}

0 commit comments

Comments
 (0)