Skip to content

Commit

Permalink
Added new errors, generate new token only after hour, tests
Browse files Browse the repository at this point in the history
  • Loading branch information
polyitan committed Jun 8, 2022
1 parent 20941fd commit 143a84e
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 276 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Before sending any pushes need to instantiate the APNs provider. For provider wi
let keyP8 = <#Apple Auth Key (.p8) content#>
let keyId = <#Apple Auth Key ID#>
let teamId = <#Apple Developer Team ID#>
let provider = APNSProvider.init(p8: keyP8, keyId: keyId, teamId: teamId, issuedAt: Date())
let provider = APNSProvider.init(p8: keyP8, keyId: keyId, teamId: teamId)
```
### Payload examples
Plain payload:
Expand Down
37 changes: 0 additions & 37 deletions Sources/SwiftyAPNS/Extensions/URLRequest+Extensions.swift

This file was deleted.

23 changes: 0 additions & 23 deletions Sources/SwiftyAPNS/Jwt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,6 @@

import Foundation

internal enum APNSJwtProviderError {

case badUrl
case encodePayload
case parseResponce
case emptyData
}

extension APNSJwtProviderError: LocalizedError {
var errorDescription: String? {
switch self {
case .badUrl: return
"The url was invalid"
case .encodePayload: return
"Can't encode payload"
case .parseResponce: return
"Can't parse responce"
case .emptyData: return
"Empty data"
}
}
}

/// The token that you include with your notification requests uses the JSON Web Token (JWT) specification
internal struct APNSJwt: Codable {

Expand Down
4 changes: 3 additions & 1 deletion Sources/SwiftyAPNS/Notification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@
// Copyright © 2017 Sergii Tkachenko. All rights reserved.
//

public typealias APNSDeviceToken = String

public struct APNSNotification<Payload: Payloadable> {
/// The Remote Notification Payload.
public var payload: Payload

/// Specify the hexadecimal string of the device token for the target device.
public var token: String
public var token: APNSDeviceToken

/// The optional settings for the notification
public var options: APNSNotificationOptions
Expand Down
45 changes: 45 additions & 0 deletions Sources/SwiftyAPNS/Provider/BearerToken.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// APNSBearerToken.swift
// SwiftyAPNS
//
// Created by Sergii Tkachenko on 08.06.2022.
// Copyright © 2022 Sergii Tkachenko. All rights reserved.
//

import Foundation

internal class APNSBearerToken {

private let keyId: String
private let teamId: String
private var issuedAt: Date

private let p8: P8
private var jwtEx: APNSJwt

private var bearer: String?
private var expired: Bool {
return Date().timeIntervalSince(issuedAt) >= TimeInterval(60 * 59)
}

init(p8: P8, keyId: String, teamId: String, issuedAt: Date = Date()) {
self.p8 = p8; self.keyId = keyId; self.teamId = teamId; self.issuedAt = issuedAt
self.jwtEx = APNSJwt(keyId: keyId, teamId: teamId, issuedAt: issuedAt)
}

func generateIfExpired() throws -> String {
if bearer == nil || expired {
try generate()
}
guard let bearer = bearer else {
throw APNSProviderError.emptyData
}
return bearer
}

private func generate() throws {
issuedAt = Date()
jwtEx = APNSJwt(keyId: keyId, teamId: teamId, issuedAt: issuedAt)
bearer = try jwtEx.sign(with: p8)
}
}
98 changes: 27 additions & 71 deletions Sources/SwiftyAPNS/Provider/CertificateProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,12 @@

import Foundation

private enum APNSCertificateProviderError: LocalizedError {

case badUrl
case encodePayload
case parseResponce
case emptyData

public var errorDescription: String? {
switch self {
case .badUrl: return
"The url was invalid"
case .encodePayload: return
"Can't encode payload"
case .parseResponce: return
"Can't parse responce"
case .emptyData: return
"Empty data"
}
}
}

internal final class APNSCertificateProvider: NSObject, APNSSendMessageProtocol {

private var identity: SecIdentity
private var sesion: URLSession?
private var sesion: URLSession = URLSession.shared

private static let decoder = JSONDecoder()

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

public func push<P: Payloadable>(_ notification: APNSNotification<P>, completion: @escaping (Result<APNSResponse, Error>) -> Void) {

let options = notification.options
var components = URLComponents()
components.scheme = "https"
components.host = options.url
components.path = "/3/device/\(notification.token)"

guard let url = components.url else {
completion(.failure(APNSCertificateProviderError.badUrl))
return
}

var request = URLRequest.init(url: url)
request.httpMethod = "POST"
if let port = options.port {
components.port = port.rawValue
}
request.applyOptions(options)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let payload = try encoder.encode(notification.payload)
request.httpBody = payload
} catch {
completion(.failure(APNSCertificateProviderError.encodePayload))
return
}

let task = self.sesion?.dataTask(with: request) { (data, responce, error) in
if let error = error {
completion(.failure(error))
} else if let responce = responce as? HTTPURLResponse, let data = data {
if let apnsStatus = APNSStatus(code: responce.statusCode),
let apnsId = responce.allHeaderFields["apns-id"] as? String
{
let decoder = JSONDecoder()
let reason = try? decoder.decode(APNSError.self, from: data)
let apnsResponce = APNSResponse(status: apnsStatus, apnsId: apnsId, reason: reason)
completion(.success(apnsResponce))
let request = try APNSRequestFactory.makeRequest(notification)
let task = self.sesion.dataTask(with: request) { (data, responce, error) in
if let error = error {
completion(.failure(error))
} else if let responce = responce as? HTTPURLResponse, let data = data {
if let apnsStatus = APNSStatus(code: responce.statusCode),
let apnsId = responce.allHeaderFields["apns-id"] as? String
{
let reason = try? Self.decoder.decode(APNSError.self, from: data)
let apnsResponce = APNSResponse(status: apnsStatus, apnsId: apnsId, reason: reason)
completion(.success(apnsResponce))
} else {
completion(.failure(APNSProviderError.parseResponce))
}
} else {
completion(.failure(APNSCertificateProviderError.parseResponce))
completion(.failure(APNSProviderError.emptyData))
}
} else {
completion(.failure(APNSCertificateProviderError.emptyData))
}
task.resume()
} catch {
completion(.failure(error))
}
task?.resume()
}
}

extension APNSCertificateProvider: URLSessionDelegate {
func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) {
if let error = error {
print("Error: \(error)")
print("APNS session error: \(error)")
}
}

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

var certificates = [SecCertificate]()
if let cert = certificate {
certificates.append(cert)
if let certificate = certificate {
certificates.append(certificate)
}

let cred = URLCredential.init(identity: self.identity, certificates: certificates, persistence: .forSession)
completionHandler(.useCredential, cred)
let credential = URLCredential.init(identity: self.identity, certificates: certificates, persistence: .forSession)
completionHandler(.useCredential, credential)
}
}
85 changes: 27 additions & 58 deletions Sources/SwiftyAPNS/Provider/KeyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,77 +10,46 @@ import Foundation

internal final class APNSKeyProvider: APNSSendMessageProtocol {

private let p8: P8
private let jwtEx: APNSJwt
private let token: APNSBearerToken
private let sesion: URLSession

public init(p8: P8, keyId: String, teamId: String, issuedAt: Date, sandbox: Bool = true,
private static let encoder = JSONEncoder()
private static let decoder = JSONDecoder()

public init(p8: P8, keyId: String, teamId: String, sandbox: Bool = true,
configuration: URLSessionConfiguration = URLSessionConfiguration.default,
qeue: OperationQueue = OperationQueue.main)
{
self.p8 = p8
self.jwtEx = APNSJwt(keyId: keyId, teamId: teamId, issuedAt: issuedAt)
self.token = APNSBearerToken(p8: p8, keyId: keyId, teamId: teamId)
self.sesion = URLSession.init(configuration: configuration, delegate: nil, delegateQueue: qeue)
}

public func push<P: Payloadable>(_ notification: APNSNotification<P>, completion: @escaping (Result<APNSResponse, Error>) -> Void) {

let options = notification.options
var components = URLComponents()
components.scheme = "https"
components.host = options.url
components.path = "/3/device/\(notification.token)"

guard let url = components.url else {
completion(.failure(APNSJwtProviderError.badUrl))
return
}

var dataToken: String
do {
dataToken = try jwtEx.sign(with: self.p8)
} catch {
completion(.failure(error))
return
}

var request = URLRequest.init(url: url)
request.httpMethod = "POST"
request.setValue("application/json;", forHTTPHeaderField: "Content-Type")
request.setValue("bearer \(dataToken)", forHTTPHeaderField: "Authorization")
if let port = options.port {
components.port = port.rawValue
}
request.applyOptions(options)

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
do {
let payload = try encoder.encode(notification.payload)
request.httpBody = payload
} catch {
completion(.failure(APNSJwtProviderError.encodePayload))
return
}

let task = self.sesion.dataTask(with: request) { (data, responce, error) in
if let error = error {
completion(.failure(error))
} else if let responce = responce as? HTTPURLResponse, let data = data {
if let apnsStatus = APNSStatus(code: responce.statusCode),
let apnsId = responce.allHeaderFields["apns-id"] as? String
{
let decoder = JSONDecoder()
let reason = try? decoder.decode(APNSError.self, from: data)
let apnsResponce = APNSResponse(status: apnsStatus, apnsId: apnsId, reason: reason)
completion(.success(apnsResponce))
var request = try APNSRequestFactory.makeRequest(notification)
let dataToken = try token.generateIfExpired()
request.setValue("application/json;", forHTTPHeaderField: "Content-Type")
request.setValue("bearer \(dataToken)", forHTTPHeaderField: "Authorization")
let task = self.sesion.dataTask(with: request) { (data, responce, error) in
if let error = error {
completion(.failure(error))
} else if let responce = responce as? HTTPURLResponse, let data = data {
if let apnsStatus = APNSStatus(code: responce.statusCode),
let apnsId = responce.allHeaderFields["apns-id"] as? String
{
let reason = try? Self.decoder.decode(APNSError.self, from: data)
let apnsResponce = APNSResponse(status: apnsStatus, apnsId: apnsId, reason: reason)
completion(.success(apnsResponce))
} else {
completion(.failure(APNSProviderError.parseResponce))
}
} else {
completion(.failure(APNSJwtProviderError.parseResponce))
completion(.failure(APNSProviderError.emptyData))
}
} else {
completion(.failure(APNSJwtProviderError.emptyData))
}
task.resume()
} catch {
completion(.failure(error))
}
task.resume()
}
}
4 changes: 2 additions & 2 deletions Sources/SwiftyAPNS/Provider/Provider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ extension APNSProvider {
self.provider = APNSCertificateProvider(identity: identity, sandbox: sandbox, configuration: configuration, qeue: qeue)
}

public init(p8: P8, keyId: String, teamId: String, issuedAt: Date, sandbox: Bool = true,
public init(p8: P8, keyId: String, teamId: String, sandbox: Bool = true,
configuration: URLSessionConfiguration = URLSessionConfiguration.default,
qeue: OperationQueue = OperationQueue.main)
{
self.provider = APNSKeyProvider(p8: p8, keyId: keyId, teamId: teamId, issuedAt: issuedAt, sandbox: sandbox, configuration: configuration, qeue: qeue)
self.provider = APNSKeyProvider(p8: p8, keyId: keyId, teamId: teamId, sandbox: sandbox, configuration: configuration, qeue: qeue)
}
}
Loading

0 comments on commit 143a84e

Please sign in to comment.