Skip to content

Errors #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# WebPush
# Swift WebPush

<p align="center">
<a href="https://swiftpackageindex.com/mochidev/swift-webpush">
Expand All @@ -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")
),
],
Expand All @@ -39,10 +39,13 @@ targets: [
dependencies: [
"WebPush",
]
)
.testTarget(name: "MyPackageTests", dependencies: [
"WebPushTesting",
]
),
.testTarget(
name: "MyPackageTests",
dependencies: [
"WebPushTesting",
]
),
]
```

Expand Down
20 changes: 20 additions & 0 deletions Sources/WebPush/Errors/BadSubscriberError.swift
Original file line number Diff line number Diff line change
@@ -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."
}
}
18 changes: 18 additions & 0 deletions Sources/WebPush/Errors/Base64URLDecodingError.swift
Original file line number Diff line number Diff line change
@@ -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."
}
}
27 changes: 27 additions & 0 deletions Sources/WebPush/Errors/HTTPError.swift
Original file line number Diff line number Diff line change
@@ -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)."
}
}
39 changes: 39 additions & 0 deletions Sources/WebPush/Errors/UserAgentKeyMaterialError.swift
Original file line number Diff line number Diff line change
@@ -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)."
}
}
}
36 changes: 36 additions & 0 deletions Sources/WebPush/Errors/VAPIDConfigurationError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// 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
case matchingKeyNotFound
}

var kind: Kind

/// 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."
}
}
}
}
9 changes: 5 additions & 4 deletions Sources/WebPush/Subscriber.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 }
}
8 changes: 4 additions & 4 deletions Sources/WebPush/VAPID/VAPIDConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? []
Expand Down Expand Up @@ -88,14 +88,14 @@ extension VoluntaryApplicationServerIdentification {
primaryKey: Key?,
keys: Set<Key>,
deprecatedKeys: Set<Key>? = 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 ?? []
Expand Down
15 changes: 11 additions & 4 deletions Sources/WebPush/WebPushManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)")
}
Expand Down
Loading