Skip to content

Commit 143a84e

Browse files
committed
Added new errors, generate new token only after hour, tests
1 parent 20941fd commit 143a84e

File tree

12 files changed

+275
-276
lines changed

12 files changed

+275
-276
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Before sending any pushes need to instantiate the APNs provider. For provider wi
1010
let keyP8 = <#Apple Auth Key (.p8) content#>
1111
let keyId = <#Apple Auth Key ID#>
1212
let teamId = <#Apple Developer Team ID#>
13-
let provider = APNSProvider.init(p8: keyP8, keyId: keyId, teamId: teamId, issuedAt: Date())
13+
let provider = APNSProvider.init(p8: keyP8, keyId: keyId, teamId: teamId)
1414
```
1515
### Payload examples
1616
Plain payload:

Sources/SwiftyAPNS/Extensions/URLRequest+Extensions.swift

Lines changed: 0 additions & 37 deletions
This file was deleted.

Sources/SwiftyAPNS/Jwt.swift

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,6 @@
88

99
import Foundation
1010

11-
internal enum APNSJwtProviderError {
12-
13-
case badUrl
14-
case encodePayload
15-
case parseResponce
16-
case emptyData
17-
}
18-
19-
extension APNSJwtProviderError: LocalizedError {
20-
var errorDescription: String? {
21-
switch self {
22-
case .badUrl: return
23-
"The url was invalid"
24-
case .encodePayload: return
25-
"Can't encode payload"
26-
case .parseResponce: return
27-
"Can't parse responce"
28-
case .emptyData: return
29-
"Empty data"
30-
}
31-
}
32-
}
33-
3411
/// The token that you include with your notification requests uses the JSON Web Token (JWT) specification
3512
internal struct APNSJwt: Codable {
3613

Sources/SwiftyAPNS/Notification.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
// Copyright © 2017 Sergii Tkachenko. All rights reserved.
77
//
88

9+
public typealias APNSDeviceToken = String
10+
911
public struct APNSNotification<Payload: Payloadable> {
1012
/// The Remote Notification Payload.
1113
public var payload: Payload
1214

1315
/// Specify the hexadecimal string of the device token for the target device.
14-
public var token: String
16+
public var token: APNSDeviceToken
1517

1618
/// The optional settings for the notification
1719
public var options: APNSNotificationOptions
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// APNSBearerToken.swift
3+
// SwiftyAPNS
4+
//
5+
// Created by Sergii Tkachenko on 08.06.2022.
6+
// Copyright © 2022 Sergii Tkachenko. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
internal class APNSBearerToken {
12+
13+
private let keyId: String
14+
private let teamId: String
15+
private var issuedAt: Date
16+
17+
private let p8: P8
18+
private var jwtEx: APNSJwt
19+
20+
private var bearer: String?
21+
private var expired: Bool {
22+
return Date().timeIntervalSince(issuedAt) >= TimeInterval(60 * 59)
23+
}
24+
25+
init(p8: P8, keyId: String, teamId: String, issuedAt: Date = Date()) {
26+
self.p8 = p8; self.keyId = keyId; self.teamId = teamId; self.issuedAt = issuedAt
27+
self.jwtEx = APNSJwt(keyId: keyId, teamId: teamId, issuedAt: issuedAt)
28+
}
29+
30+
func generateIfExpired() throws -> String {
31+
if bearer == nil || expired {
32+
try generate()
33+
}
34+
guard let bearer = bearer else {
35+
throw APNSProviderError.emptyData
36+
}
37+
return bearer
38+
}
39+
40+
private func generate() throws {
41+
issuedAt = Date()
42+
jwtEx = APNSJwt(keyId: keyId, teamId: teamId, issuedAt: issuedAt)
43+
bearer = try jwtEx.sign(with: p8)
44+
}
45+
}

Sources/SwiftyAPNS/Provider/CertificateProvider.swift

Lines changed: 27 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,12 @@
88

99
import Foundation
1010

11-
private enum APNSCertificateProviderError: LocalizedError {
12-
13-
case badUrl
14-
case encodePayload
15-
case parseResponce
16-
case emptyData
17-
18-
public var errorDescription: String? {
19-
switch self {
20-
case .badUrl: return
21-
"The url was invalid"
22-
case .encodePayload: return
23-
"Can't encode payload"
24-
case .parseResponce: return
25-
"Can't parse responce"
26-
case .emptyData: return
27-
"Empty data"
28-
}
29-
}
30-
}
31-
3211
internal final class APNSCertificateProvider: NSObject, APNSSendMessageProtocol {
3312

3413
private var identity: SecIdentity
35-
private var sesion: URLSession?
14+
private var sesion: URLSession = URLSession.shared
15+
16+
private static let decoder = JSONDecoder()
3617

3718
public init(identity: SecIdentity, sandbox: Bool = true, configuration: URLSessionConfiguration = URLSessionConfiguration.default, qeue: OperationQueue = OperationQueue.main) {
3819
self.identity = identity
@@ -41,76 +22,51 @@ internal final class APNSCertificateProvider: NSObject, APNSSendMessageProtocol
4122
}
4223

4324
public func push<P: Payloadable>(_ notification: APNSNotification<P>, completion: @escaping (Result<APNSResponse, Error>) -> Void) {
44-
45-
let options = notification.options
46-
var components = URLComponents()
47-
components.scheme = "https"
48-
components.host = options.url
49-
components.path = "/3/device/\(notification.token)"
50-
51-
guard let url = components.url else {
52-
completion(.failure(APNSCertificateProviderError.badUrl))
53-
return
54-
}
55-
56-
var request = URLRequest.init(url: url)
57-
request.httpMethod = "POST"
58-
if let port = options.port {
59-
components.port = port.rawValue
60-
}
61-
request.applyOptions(options)
62-
63-
let encoder = JSONEncoder()
64-
encoder.outputFormatting = .prettyPrinted
6525
do {
66-
let payload = try encoder.encode(notification.payload)
67-
request.httpBody = payload
68-
} catch {
69-
completion(.failure(APNSCertificateProviderError.encodePayload))
70-
return
71-
}
72-
73-
let task = self.sesion?.dataTask(with: request) { (data, responce, error) in
74-
if let error = error {
75-
completion(.failure(error))
76-
} else if let responce = responce as? HTTPURLResponse, let data = data {
77-
if let apnsStatus = APNSStatus(code: responce.statusCode),
78-
let apnsId = responce.allHeaderFields["apns-id"] as? String
79-
{
80-
let decoder = JSONDecoder()
81-
let reason = try? decoder.decode(APNSError.self, from: data)
82-
let apnsResponce = APNSResponse(status: apnsStatus, apnsId: apnsId, reason: reason)
83-
completion(.success(apnsResponce))
26+
let request = try APNSRequestFactory.makeRequest(notification)
27+
let task = self.sesion.dataTask(with: request) { (data, responce, error) in
28+
if let error = error {
29+
completion(.failure(error))
30+
} else if let responce = responce as? HTTPURLResponse, let data = data {
31+
if let apnsStatus = APNSStatus(code: responce.statusCode),
32+
let apnsId = responce.allHeaderFields["apns-id"] as? String
33+
{
34+
let reason = try? Self.decoder.decode(APNSError.self, from: data)
35+
let apnsResponce = APNSResponse(status: apnsStatus, apnsId: apnsId, reason: reason)
36+
completion(.success(apnsResponce))
37+
} else {
38+
completion(.failure(APNSProviderError.parseResponce))
39+
}
8440
} else {
85-
completion(.failure(APNSCertificateProviderError.parseResponce))
41+
completion(.failure(APNSProviderError.emptyData))
8642
}
87-
} else {
88-
completion(.failure(APNSCertificateProviderError.emptyData))
8943
}
44+
task.resume()
45+
} catch {
46+
completion(.failure(error))
9047
}
91-
task?.resume()
9248
}
9349
}
9450

9551
extension APNSCertificateProvider: URLSessionDelegate {
9652
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
9753
if let error = error {
98-
print("Error: \(error)")
54+
print("APNS session error: \(error)")
9955
}
10056
}
10157

10258
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void) {
10359
var certificate: SecCertificate?
10460
_ = withUnsafeMutablePointer(to: &certificate) {
105-
SecIdentityCopyCertificate(self.identity, UnsafeMutablePointer($0))
61+
SecIdentityCopyCertificate(identity, UnsafeMutablePointer($0))
10662
}
10763

10864
var certificates = [SecCertificate]()
109-
if let cert = certificate {
110-
certificates.append(cert)
65+
if let certificate = certificate {
66+
certificates.append(certificate)
11167
}
11268

113-
let cred = URLCredential.init(identity: self.identity, certificates: certificates, persistence: .forSession)
114-
completionHandler(.useCredential, cred)
69+
let credential = URLCredential.init(identity: self.identity, certificates: certificates, persistence: .forSession)
70+
completionHandler(.useCredential, credential)
11571
}
11672
}

Sources/SwiftyAPNS/Provider/KeyProvider.swift

Lines changed: 27 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,77 +10,46 @@ import Foundation
1010

1111
internal final class APNSKeyProvider: APNSSendMessageProtocol {
1212

13-
private let p8: P8
14-
private let jwtEx: APNSJwt
13+
private let token: APNSBearerToken
1514
private let sesion: URLSession
1615

17-
public init(p8: P8, keyId: String, teamId: String, issuedAt: Date, sandbox: Bool = true,
16+
private static let encoder = JSONEncoder()
17+
private static let decoder = JSONDecoder()
18+
19+
public init(p8: P8, keyId: String, teamId: String, sandbox: Bool = true,
1820
configuration: URLSessionConfiguration = URLSessionConfiguration.default,
1921
qeue: OperationQueue = OperationQueue.main)
2022
{
21-
self.p8 = p8
22-
self.jwtEx = APNSJwt(keyId: keyId, teamId: teamId, issuedAt: issuedAt)
23+
self.token = APNSBearerToken(p8: p8, keyId: keyId, teamId: teamId)
2324
self.sesion = URLSession.init(configuration: configuration, delegate: nil, delegateQueue: qeue)
2425
}
2526

2627
public func push<P: Payloadable>(_ notification: APNSNotification<P>, completion: @escaping (Result<APNSResponse, Error>) -> Void) {
27-
28-
let options = notification.options
29-
var components = URLComponents()
30-
components.scheme = "https"
31-
components.host = options.url
32-
components.path = "/3/device/\(notification.token)"
33-
34-
guard let url = components.url else {
35-
completion(.failure(APNSJwtProviderError.badUrl))
36-
return
37-
}
38-
39-
var dataToken: String
4028
do {
41-
dataToken = try jwtEx.sign(with: self.p8)
42-
} catch {
43-
completion(.failure(error))
44-
return
45-
}
46-
47-
var request = URLRequest.init(url: url)
48-
request.httpMethod = "POST"
49-
request.setValue("application/json;", forHTTPHeaderField: "Content-Type")
50-
request.setValue("bearer \(dataToken)", forHTTPHeaderField: "Authorization")
51-
if let port = options.port {
52-
components.port = port.rawValue
53-
}
54-
request.applyOptions(options)
55-
56-
let encoder = JSONEncoder()
57-
encoder.outputFormatting = .prettyPrinted
58-
do {
59-
let payload = try encoder.encode(notification.payload)
60-
request.httpBody = payload
61-
} catch {
62-
completion(.failure(APNSJwtProviderError.encodePayload))
63-
return
64-
}
65-
66-
let task = self.sesion.dataTask(with: request) { (data, responce, error) in
67-
if let error = error {
68-
completion(.failure(error))
69-
} else if let responce = responce as? HTTPURLResponse, let data = data {
70-
if let apnsStatus = APNSStatus(code: responce.statusCode),
71-
let apnsId = responce.allHeaderFields["apns-id"] as? String
72-
{
73-
let decoder = JSONDecoder()
74-
let reason = try? decoder.decode(APNSError.self, from: data)
75-
let apnsResponce = APNSResponse(status: apnsStatus, apnsId: apnsId, reason: reason)
76-
completion(.success(apnsResponce))
29+
var request = try APNSRequestFactory.makeRequest(notification)
30+
let dataToken = try token.generateIfExpired()
31+
request.setValue("application/json;", forHTTPHeaderField: "Content-Type")
32+
request.setValue("bearer \(dataToken)", forHTTPHeaderField: "Authorization")
33+
let task = self.sesion.dataTask(with: request) { (data, responce, error) in
34+
if let error = error {
35+
completion(.failure(error))
36+
} else if let responce = responce as? HTTPURLResponse, let data = data {
37+
if let apnsStatus = APNSStatus(code: responce.statusCode),
38+
let apnsId = responce.allHeaderFields["apns-id"] as? String
39+
{
40+
let reason = try? Self.decoder.decode(APNSError.self, from: data)
41+
let apnsResponce = APNSResponse(status: apnsStatus, apnsId: apnsId, reason: reason)
42+
completion(.success(apnsResponce))
43+
} else {
44+
completion(.failure(APNSProviderError.parseResponce))
45+
}
7746
} else {
78-
completion(.failure(APNSJwtProviderError.parseResponce))
47+
completion(.failure(APNSProviderError.emptyData))
7948
}
80-
} else {
81-
completion(.failure(APNSJwtProviderError.emptyData))
8249
}
50+
task.resume()
51+
} catch {
52+
completion(.failure(error))
8353
}
84-
task.resume()
8554
}
8655
}

Sources/SwiftyAPNS/Provider/Provider.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ extension APNSProvider {
2424
self.provider = APNSCertificateProvider(identity: identity, sandbox: sandbox, configuration: configuration, qeue: qeue)
2525
}
2626

27-
public init(p8: P8, keyId: String, teamId: String, issuedAt: Date, sandbox: Bool = true,
27+
public init(p8: P8, keyId: String, teamId: String, sandbox: Bool = true,
2828
configuration: URLSessionConfiguration = URLSessionConfiguration.default,
2929
qeue: OperationQueue = OperationQueue.main)
3030
{
31-
self.provider = APNSKeyProvider(p8: p8, keyId: keyId, teamId: teamId, issuedAt: issuedAt, sandbox: sandbox, configuration: configuration, qeue: qeue)
31+
self.provider = APNSKeyProvider(p8: p8, keyId: keyId, teamId: teamId, sandbox: sandbox, configuration: configuration, qeue: qeue)
3232
}
3333
}

0 commit comments

Comments
 (0)