From e249e6e0efb1510cfe419757541c0d288eefb363 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Fri, 13 Dec 2024 01:42:57 -0800 Subject: [PATCH 1/4] Fixed the links on the README --- README.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index b365c04..7d4024f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# WebPush +# Swift WebPush

@@ -16,19 +16,19 @@ A server-side Swift implementation of the WebPush standard. ## Quick Links -- [Documentation](https://swiftpackageindex.com/mochidev/swift-WebPush/documentation) +- [Documentation](https://swiftpackageindex.com/mochidev/swift-webpush/documentation) - [Updates on Mastodon](https://mastodon.social/tags/SwiftWebPush) ## Installation Add `WebPush` as a dependency in your `Package.swift` file to start using it. Then, add `import WebPush` to any file you wish to use the library in. -Please check the [releases](https://github.com/mochidev/WebPush/releases) for recommended versions. +Please check the [releases](https://github.com/mochidev/swift-webpush/releases) for recommended versions. ```swift dependencies: [ .package( - url: "https://github.com/mochidev/WebPush.git", + url: "https://github.com/mochidev/swift-webPush.git", .upToNextMinor(from: "0.1.1") ), ], @@ -39,10 +39,13 @@ targets: [ dependencies: [ "WebPush", ] - ) - .testTarget(name: "MyPackageTests", dependencies: [ - "WebPushTesting", - ] + ), + .testTarget( + name: "MyPackageTests", + dependencies: [ + "WebPushTesting", + ] + ), ] ``` From f6e7d519de37481952424e55ee7f06ad7182f2b5 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Fri, 13 Dec 2024 02:20:31 -0800 Subject: [PATCH 2/4] Added VAPID Configuration Error type --- .../Errors/VAPIDConfigurationError.swift | 30 +++++++++++++++++++ .../WebPush/VAPID/VAPIDConfiguration.swift | 8 ++--- 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 Sources/WebPush/Errors/VAPIDConfigurationError.swift diff --git a/Sources/WebPush/Errors/VAPIDConfigurationError.swift b/Sources/WebPush/Errors/VAPIDConfigurationError.swift new file mode 100644 index 0000000..672d444 --- /dev/null +++ b/Sources/WebPush/Errors/VAPIDConfigurationError.swift @@ -0,0 +1,30 @@ +// +// VAPIDConfigurationError.swift +// swift-webpush +// +// Created by Dimitri Bouniol on 2024-12-13. +// Copyright © 2024 Mochi Development, Inc. All rights reserved. +// + +import Foundation + +extension VAPID { + /// An error encountered during ``VAPID/Configuration`` initialization or decoding. + public struct ConfigurationError: LocalizedError, Hashable { + enum Kind { + case keysNotProvided + } + + var kind: Kind + + /// VAPID keys not found during initialization. + public static let keysNotProvided = Self(kind: .keysNotProvided) + + public var errorDescription: String? { + switch kind { + case .keysNotProvided: + "VAPID keys not found during initialization." + } + } + } +} diff --git a/Sources/WebPush/VAPID/VAPIDConfiguration.swift b/Sources/WebPush/VAPID/VAPIDConfiguration.swift index 3e66cde..5d871ce 100644 --- a/Sources/WebPush/VAPID/VAPIDConfiguration.swift +++ b/Sources/WebPush/VAPID/VAPIDConfiguration.swift @@ -46,14 +46,14 @@ extension VoluntaryApplicationServerIdentification { contactInformation: ContactInformation, expirationDuration: Duration = .hours(22), validityDuration: Duration = .hours(20) - ) throws { + ) throws(ConfigurationError) { self.primaryKey = primaryKey var keys = keys if let primaryKey { keys.insert(primaryKey) } guard !keys.isEmpty - else { throw CancellationError() } // TODO: No keys error + else { throw .keysNotProvided } self.keys = keys var deprecatedKeys = deprecatedKeys ?? [] @@ -88,14 +88,14 @@ extension VoluntaryApplicationServerIdentification { primaryKey: Key?, keys: Set, deprecatedKeys: Set? = nil - ) throws { + ) throws(ConfigurationError) { self.primaryKey = primaryKey var keys = keys if let primaryKey { keys.insert(primaryKey) } guard !keys.isEmpty - else { throw CancellationError() } // TODO: No keys error + else { throw .keysNotProvided } self.keys = keys var deprecatedKeys = deprecatedKeys ?? [] From b120f65fcd4e16dbb842f9372cd74f5b8fcdd041 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Fri, 13 Dec 2024 02:33:44 -0800 Subject: [PATCH 3/4] Added UserAgentKeyMaterial Error type --- .../Errors/Base64URLDecodingError.swift | 18 +++++++++ .../Errors/UserAgentKeyMaterialError.swift | 39 +++++++++++++++++++ Sources/WebPush/Subscriber.swift | 9 +++-- 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 Sources/WebPush/Errors/Base64URLDecodingError.swift create mode 100644 Sources/WebPush/Errors/UserAgentKeyMaterialError.swift diff --git a/Sources/WebPush/Errors/Base64URLDecodingError.swift b/Sources/WebPush/Errors/Base64URLDecodingError.swift new file mode 100644 index 0000000..1d09a54 --- /dev/null +++ b/Sources/WebPush/Errors/Base64URLDecodingError.swift @@ -0,0 +1,18 @@ +// +// Base64URLDecodingError.swift +// swift-webpush +// +// Created by Dimitri Bouniol on 2024-12-13. +// Copyright © 2024 Mochi Development, Inc. All rights reserved. +// + +import Foundation + +/// An error encountered while decoding Base64 data. +public struct Base64URLDecodingError: LocalizedError, Hashable { + public init() {} + + public var errorDescription: String? { + "The Base64 data could not be decoded." + } +} diff --git a/Sources/WebPush/Errors/UserAgentKeyMaterialError.swift b/Sources/WebPush/Errors/UserAgentKeyMaterialError.swift new file mode 100644 index 0000000..80e00ec --- /dev/null +++ b/Sources/WebPush/Errors/UserAgentKeyMaterialError.swift @@ -0,0 +1,39 @@ +// +// UserAgentKeyMaterialError.swift +// swift-webpush +// +// Created by Dimitri Bouniol on 2024-12-13. +// Copyright © 2024 Mochi Development, Inc. All rights reserved. +// + +import Foundation + +/// An error encountered during ``VAPID/Configuration`` initialization or decoding. +public struct UserAgentKeyMaterialError: LocalizedError { + enum Kind { + case invalidPublicKey + case invalidAuthenticationSecret + } + + var kind: Kind + var underlyingError: any Error + + /// The public key was invalid. + public static func invalidPublicKey(underlyingError: Error) -> Self { + Self(kind: .invalidPublicKey, underlyingError: underlyingError) + } + + /// The authentication secret was invalid. + public static func invalidAuthenticationSecret(underlyingError: Error) -> Self { + Self(kind: .invalidAuthenticationSecret, underlyingError: underlyingError) + } + + public var errorDescription: String? { + switch kind { + case .invalidPublicKey: + "Subscriber Public Key (`\(UserAgentKeyMaterial.CodingKeys.publicKey)`) was invalid: \(underlyingError.localizedDescription)." + case .invalidAuthenticationSecret: + "Subscriber Authentication Secret (`\(UserAgentKeyMaterial.CodingKeys.authenticationSecret)`) was invalid: \(underlyingError.localizedDescription)." + } + } +} diff --git a/Sources/WebPush/Subscriber.swift b/Sources/WebPush/Subscriber.swift index 9df940e..5915bd2 100644 --- a/Sources/WebPush/Subscriber.swift +++ b/Sources/WebPush/Subscriber.swift @@ -67,15 +67,15 @@ public struct UserAgentKeyMaterial: Sendable { public init( publicKey: String, authenticationSecret: String - ) throws { + ) throws(UserAgentKeyMaterialError) { guard let publicKeyData = Data(base64URLEncoded: publicKey) - else { throw CancellationError() } // invalid public key error (underlying error = URLDecoding error) + else { throw .invalidPublicKey(underlyingError: Base64URLDecodingError()) } do { self.publicKey = try P256.KeyAgreement.PublicKey(x963Representation: publicKeyData) - } catch { throw CancellationError() } // invalid public key error (underlying error = error) + } catch { throw .invalidPublicKey(underlyingError: error) } guard let authenticationSecretData = Data(base64URLEncoded: authenticationSecret) - else { throw CancellationError() } // invalid authentication secret error (underlying error = URLDecoding error) + else { throw .invalidAuthenticationSecret(underlyingError: Base64URLDecodingError()) } self.authenticationSecret = authenticationSecretData } @@ -190,5 +190,6 @@ public struct Subscriber: SubscriberProtocol, Codable, Hashable, Sendable { } extension Subscriber: Identifiable { + /// A safe identifier to use for the subscriber without exposing key material. public var id: String { endpoint.absoluteString } } From 0818d11faddd328b412d46993384766791d258e3 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Fri, 13 Dec 2024 03:11:34 -0800 Subject: [PATCH 4/4] Added Bad Subscriber and HTTP Error types --- .../WebPush/Errors/BadSubscriberError.swift | 20 ++++++++++++++ Sources/WebPush/Errors/HTTPError.swift | 27 +++++++++++++++++++ .../Errors/VAPIDConfigurationError.swift | 6 +++++ Sources/WebPush/WebPushManager.swift | 15 ++++++++--- 4 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 Sources/WebPush/Errors/BadSubscriberError.swift create mode 100644 Sources/WebPush/Errors/HTTPError.swift diff --git a/Sources/WebPush/Errors/BadSubscriberError.swift b/Sources/WebPush/Errors/BadSubscriberError.swift new file mode 100644 index 0000000..8751e1a --- /dev/null +++ b/Sources/WebPush/Errors/BadSubscriberError.swift @@ -0,0 +1,20 @@ +// +// BadSubscriberError.swift +// swift-webpush +// +// Created by Dimitri Bouniol on 2024-12-13. +// Copyright © 2024 Mochi Development, Inc. All rights reserved. +// + +import Foundation + +/// The subscription is no longer valid and should be removed and re-registered. +/// +/// - Warning: Do not continue to send notifications to invalid subscriptions or you'll risk being rate limited by push services. +public struct BadSubscriberError: LocalizedError, Hashable { + public init() {} + + public var errorDescription: String? { + "The subscription is no longer valid." + } +} diff --git a/Sources/WebPush/Errors/HTTPError.swift b/Sources/WebPush/Errors/HTTPError.swift new file mode 100644 index 0000000..e520378 --- /dev/null +++ b/Sources/WebPush/Errors/HTTPError.swift @@ -0,0 +1,27 @@ +// +// HTTPError.swift +// swift-webpush +// +// Created by Dimitri Bouniol on 2024-12-13. +// Copyright © 2024 Mochi Development, Inc. All rights reserved. +// + +import AsyncHTTPClient +import Foundation + +/// An unknown HTTP error was encountered. +/// +/// - SeeAlso: [RFC8030 Generic Event Delivery Using HTTP Push](https://datatracker.ietf.org/doc/html/rfc8030) +/// - SeeAlso: [RFC8292 Voluntary Application Server Identification (VAPID) for Web Push](https://datatracker.ietf.org/doc/html/rfc8292) +/// - SeeAlso: [Sending web push notifications in web apps and browsers — Review responses for push notification errors](https://developer.apple.com/documentation/usernotifications/sending-web-push-notifications-in-web-apps-and-browsers#Review-responses-for-push-notification-errors) +public struct HTTPError: LocalizedError { + let response: HTTPClientResponse + + init(response: HTTPClientResponse) { + self.response = response + } + + public var errorDescription: String? { + "A \(response.status) HTTP error was encountered: \(response)." + } +} diff --git a/Sources/WebPush/Errors/VAPIDConfigurationError.swift b/Sources/WebPush/Errors/VAPIDConfigurationError.swift index 672d444..f8cc251 100644 --- a/Sources/WebPush/Errors/VAPIDConfigurationError.swift +++ b/Sources/WebPush/Errors/VAPIDConfigurationError.swift @@ -13,6 +13,7 @@ extension VAPID { public struct ConfigurationError: LocalizedError, Hashable { enum Kind { case keysNotProvided + case matchingKeyNotFound } var kind: Kind @@ -20,10 +21,15 @@ extension VAPID { /// VAPID keys not found during initialization. public static let keysNotProvided = Self(kind: .keysNotProvided) + /// A VAPID key for the subscriber was not found. + public static let matchingKeyNotFound = Self(kind: .matchingKeyNotFound) + public var errorDescription: String? { switch kind { case .keysNotProvided: "VAPID keys not found during initialization." + case .matchingKeyNotFound: + "A VAPID key for the subscriber was not found." } } } diff --git a/Sources/WebPush/WebPushManager.swift b/Sources/WebPush/WebPushManager.swift index 8e8969b..51accce 100644 --- a/Sources/WebPush/WebPushManager.swift +++ b/Sources/WebPush/WebPushManager.swift @@ -296,7 +296,12 @@ public actor WebPushManager: Sendable { urgency: Urgency ) async throws { guard let signingKey = vapidKeyLookup[subscriber.vapidKeyID] - else { throw CancellationError() } // throw key not found error + else { + logger.warning("A key was not found for this subscriber.", metadata: [ + "vapidKeyID": "\(subscriber.vapidKeyID)" + ]) + throw VAPID.ConfigurationError.matchingKeyNotFound + } /// Prepare authorization, private keys, and payload ahead of time to bail early if they can't be created. let authorization = try loadCurrentVAPIDAuthorizationHeader(endpoint: subscriber.endpoint, signingKey: signingKey) @@ -305,7 +310,7 @@ public actor WebPushManager: Sendable { /// Perform key exchange between the user agent's public key and our private key, deriving a shared secret. let userAgent = subscriber.userAgentKeyMaterial guard let sharedSecret = try? applicationServerECDHPrivateKey.sharedSecretFromKeyAgreement(with: userAgent.publicKey) - else { throw CancellationError() } // throw bad subscription + else { throw BadSubscriberError() } /// Generate a 16-byte salt. var salt: [UInt8] = Array(repeating: 0, count: 16) @@ -376,8 +381,10 @@ public actor WebPushManager: Sendable { /// Check the response and determine if the subscription should be removed from our records, or if the notification should just be skipped. switch response.status { case .created: break - case .notFound, .gone: throw CancellationError() // throw bad subscription - default: throw CancellationError() //Abort(response.status, headers: response.headers, reason: response.description) + case .notFound, .gone: throw BadSubscriberError() + // TODO: 413 payload too large - warn and throw error + // TODO: 429 too many requests, 500 internal server error, 503 server shutting down - check config and perform a retry after a delay? + default: throw HTTPError(response: response) } logger.trace("Sent \(message) notification to \(subscriber): \(response)") }