Skip to content

Commit

Permalink
Breakout APNSwift into packages (#170)
Browse files Browse the repository at this point in the history
  • Loading branch information
kylebrowning authored Mar 3, 2023
1 parent 12b9017 commit ecf0f44
Show file tree
Hide file tree
Showing 58 changed files with 1,094 additions and 721 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ on:
jobs:
focal:
container:
image: swift:5.6-focal
image: swift:5.7-focal
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: swift test --enable-test-discovery
thread:
container:
image: swift:5.6-focal
image: swift:5.7-focal
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- run: swift test --enable-test-discovery --sanitize=thread
address:
container:
image: swift:5.6-focal
image: swift:5.7-focal
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ Package.resolved
DerivedData
.swiftpm
xcuserdata/
.vscode
59 changes: 47 additions & 12 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,39 +1,74 @@
// swift-tools-version:5.6
// swift-tools-version:5.7
import PackageDescription

let package = Package(
name: "apnswift",
platforms: [
.macOS(.v12),
.iOS(.v15),
.macOS(.v13),
.iOS(.v16),
.watchOS(.v9),
.tvOS(.v16),
],
products: [
.executable(name: "APNSwiftExample", targets: ["APNSwiftExample"]),
.library(name: "APNSwift", targets: ["APNSwift"]),
.executable(name: "APNSExample", targets: ["APNSExample"]),
.library(name: "APNS", targets: ["APNS"]),
.library(name: "APNSCore", targets: ["APNSCore"]),
.library(name: "APNSURLSession", targets: ["APNSURLSession"]),
.library(name: "APNSTestServer", targets: ["APNSTestServer"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"3.0.0"),
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.10.0"),
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.42.0"),
.package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.6.0"),
.package(url: "https://github.com/apple/swift-nio-http2.git", from: "1.9.0"),
],
targets: [
.executableTarget(
name: "APNSwiftExample",
name: "APNSExample",
dependencies: [
.target(name: "APNSwift")
.target(name: "APNSCore"),
.target(name: "APNS"),
]),
.testTarget(
name: "APNSwiftTests",
name: "APNSTests",
dependencies: [
.target(name: "APNSwift")
.target(name: "APNSCore"),
.target(name: "APNS"),
]),
.target(
name: "APNSwift",
name: "APNSCore",
dependencies: [
.product(name: "Crypto", package: "swift-crypto"),
]
),
.target(
name: "APNS",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "AsyncHTTPClient", package: "async-http-client"),
]),
.target(name: "APNSCore"),
]
),
.target(
name: "APNSTestServer",
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "Crypto", package: "swift-crypto"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
.product(name: "NIOSSL", package: "swift-nio-ssl"),
.product(name: "NIOHTTP1", package: "swift-nio"),
.product(name: "NIOHTTP2", package: "swift-nio-http2"),
]
),
.target(
name: "APNSURLSession",
dependencies: [
.target(name: "APNSCore"),
]
),
]
)
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ try await client.sendAlertNotification(
payload: Payload()
),
deviceToken: "device-token",
deadline: .distantFuture,
deadline: .nanoseconds(Int64.max),
logger: myLogger
)
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ try await client.sendAlertNotification(
payload: Payload()
),
deviceToken: "device-token",
deadline: .distantFuture,
deadline: .nanoseconds(Int64.max),
logger: myLogger
)
```
Expand Down
212 changes: 212 additions & 0 deletions Sources/APNS/APNSClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the APNSwift open source project
//
// Copyright (c) 2022 the APNSwift project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of APNSwift project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import APNSCore
import AsyncHTTPClient
import Dispatch
import struct Foundation.Date
import struct Foundation.UUID
import NIOConcurrencyHelpers
import NIOCore
import NIOHTTP1
import NIOSSL
import NIOTLS

/// A client to talk with the Apple Push Notification services.
public final class APNSClient<Decoder: APNSJSONDecoder, Encoder: APNSJSONEncoder>: APNSClientProtocol {

/// The configuration used by the ``APNSClient``.
private let configuration: APNSClientConfiguration

/// The ``HTTPClient`` used by the APNS.
private let httpClient: HTTPClient

/// The decoder for the responses from APNs.
private let responseDecoder: Decoder

/// The encoder for the requests to APNs.
@usableFromInline
/* private */ internal let requestEncoder: Encoder

/// The authentication token manager.
private let authenticationTokenManager: APNSAuthenticationTokenManager<ContinuousClock>?

/// The ByteBufferAllocator
@usableFromInline
/* private */ internal let byteBufferAllocator: ByteBufferAllocator

/// Default ``HTTPHeaders`` which will be adapted for each request. This saves some allocations.
private let defaultRequestHeaders: HTTPHeaders = {
var headers = HTTPHeaders()
headers.reserveCapacity(10)
headers.add(name: "content-type", value: "application/json")
headers.add(name: "user-agent", value: "APNS/swift-nio")
return headers
}()

/// Initializes a new APNS.
///
/// The client will create an internal ``HTTPClient`` which is used to make requests to APNs.
/// This ``HTTPClient`` is intentionally internal since both authentication mechanisms are bound to a
/// single connection and these connections cannot be shared.
///
///
/// - Parameters:
/// - configuration: The configuration used by the APNS.
/// - eventLoopGroupProvider: Specify how EventLoopGroup will be created.
/// - responseDecoder: The decoder for the responses from APNs.
/// - requestEncoder: The encoder for the requests to APNs.
/// - backgroundActivityLogger: The logger used by the APNS.
public init(
configuration: APNSClientConfiguration,
eventLoopGroupProvider: NIOEventLoopGroupProvider,
responseDecoder: Decoder,
requestEncoder: Encoder,
byteBufferAllocator: ByteBufferAllocator = .init()
) {
self.configuration = configuration
self.byteBufferAllocator = byteBufferAllocator
self.responseDecoder = responseDecoder
self.requestEncoder = requestEncoder

var tlsConfiguration = TLSConfiguration.makeClientConfiguration()
switch configuration.authenticationMethod.method {
case .jwt(let privateKey, let teamIdentifier, let keyIdentifier):
self.authenticationTokenManager = APNSAuthenticationTokenManager(
privateKey: privateKey,
teamIdentifier: teamIdentifier,
keyIdentifier: keyIdentifier,
clock: ContinuousClock()
)
case .tls(let privateKey, let certificateChain):
self.authenticationTokenManager = nil
tlsConfiguration.privateKey = privateKey
tlsConfiguration.certificateChain = certificateChain
}

var httpClientConfiguration = HTTPClient.Configuration()
httpClientConfiguration.tlsConfiguration = tlsConfiguration
httpClientConfiguration.httpVersion = .automatic
httpClientConfiguration.proxy = configuration.proxy

let httpClientEventLoopGroupProvider: HTTPClient.EventLoopGroupProvider

switch eventLoopGroupProvider {
case .shared(let eventLoopGroup):
httpClientEventLoopGroupProvider = .shared(eventLoopGroup)
case .createNew:
httpClientEventLoopGroupProvider = .createNew
}

self.httpClient = HTTPClient(
eventLoopGroupProvider: httpClientEventLoopGroupProvider,
configuration: httpClientConfiguration
)
}

/// Shuts down the client and event loop gracefully. This function is clearly an outlier in that it uses a completion
/// callback instead of an EventLoopFuture. The reason for that is that NIO's EventLoopFutures will call back on an event loop.
/// The virtue of this function is to shut the event loop down. To work around that we call back on a DispatchQueue
/// instead.
///
/// - Important: This will only shutdown the event loop if the provider passed to the client was ``createNew``.
/// For shared event loops the owner of the event loop is responsible for handling the lifecycle.
///
/// - Parameters:
/// - queue: The queue on which the callback is invoked on.
/// - callback: The callback that is invoked when everything is shutdown.
public func shutdown(queue: DispatchQueue = .global(), callback: @escaping (Error?) -> Void) {
self.httpClient.shutdown(callback)
}

/// Shuts down the client and `EventLoopGroup` if it was created by the client.
public func syncShutdown() throws {
try self.httpClient.syncShutdown()
}
}


// MARK: - Raw sending

extension APNSClient {

public func send(_ request: APNSCore.APNSRequest<some APNSCore.APNSMessage>) async throws -> APNSCore.APNSResponse {
var headers = self.defaultRequestHeaders

// Push type
headers.add(name: "apns-push-type", value: request.pushType.configuration.rawValue)

// APNS ID
if let apnsID = request.apnsID {
headers.add(name: "apns-id", value: apnsID.uuidString.lowercased())
}

// Expiration
if let expiration = request.expiration?.expiration {
headers.add(name: "apns-expiration", value: String(expiration))
}

// Priority
if let priority = request.priority?.rawValue {
headers.add(name: "apns-priority", value: String(priority))
}

// Topic
if let topic = request.topic {
headers.add(name: "apns-topic", value: topic)
}

// Collapse ID
if let collapseID = request.collapseID {
headers.add(name: "apns-collapse-id", value: collapseID)
}

// Authorization token
if let authenticationTokenManager = self.authenticationTokenManager {
let token = try await authenticationTokenManager.nextValidToken
headers.add(name: "authorization", value: token)
}

// Device token
let requestURL = "\(self.configuration.environment.absoluteURL)/\(request.deviceToken)"
var byteBuffer = self.byteBufferAllocator.buffer(capacity: 0)

try self.requestEncoder.encode(request.message, into: &byteBuffer)

var httpClientRequest = HTTPClientRequest(url: requestURL)
httpClientRequest.method = .POST
httpClientRequest.headers = headers
httpClientRequest.body = .bytes(byteBuffer)

let response = try await self.httpClient.execute(httpClientRequest, deadline: .distantFuture)

let apnsID = response.headers.first(name: "apns-id").flatMap { UUID(uuidString: $0) }

if response.status == .ok {
return APNSResponse(apnsID: apnsID)
}

let body = try await response.body.collect(upTo: 1024)
let errorResponse = try responseDecoder.decode(APNSErrorResponse.self, from: body)

let error = APNSError(
responseStatus: Int(response.status.code),
apnsID: apnsID,
apnsResponse: errorResponse,
timestamp: errorResponse.timestampInSeconds.flatMap { Date(timeIntervalSince1970: $0) }
)

throw error
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
//===----------------------------------------------------------------------===//

import APNSCore
import Crypto
import NIOSSL
import NIOTLS
Expand Down Expand Up @@ -57,30 +58,11 @@ public struct APNSClientConfiguration {
internal var method: Method
}

/// The APNs environment.
public struct Environment {
/// The production APNs environment.
public static let production = Self(url: "https://api.push.apple.com")

/// The sandbox APNs environment.
public static let sandbox = Self(url: "https://api.development.push.apple.com")

/// Creates an APNs environment with a custom URL.
///
/// - Note: This is mostly used for testing purposes.
public static func custom(url: String) -> Self {
Self(url: url)
}

/// The environment's URL.
let url: String
}

/// The authentication method used by the ``APNSClient``.
public var authenticationMethod: AuthenticationMethod

/// The environment used by the ``APNSClient``.
public var environment: Environment
public var environment: APNSEnvironment

/// Upstream proxy, defaults to no proxy.
public var proxy: HTTPClient.Configuration.Proxy?
Expand All @@ -92,10 +74,9 @@ public struct APNSClientConfiguration {
/// - environment: The environment used by the ``APNSClient``.
public init(
authenticationMethod: AuthenticationMethod,
environment: Environment
environment: APNSEnvironment
) {
self.authenticationMethod = authenticationMethod
self.environment = environment
}
}

File renamed without changes.
File renamed without changes.
Loading

0 comments on commit ecf0f44

Please sign in to comment.