Skip to content
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
56 changes: 52 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [ main, master ]
branches: [ main, master, 'feature/**' ]
tags: ['v*']
pull_request:
workflow_dispatch:
Expand Down Expand Up @@ -49,13 +49,62 @@ jobs:
--project-option "platform=https://github.com/pioarduino/platform-espressif32.git" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++17" \
--project-option "lib_deps=ArduinoJson@>=7.0.0, https://github.com/ESPToolKit/esp-worker.git"
--project-option "lib_deps=ArduinoJson@>=7.0.0"
fi
done

arduino-cli:
espidf-smoke:
runs-on: ubuntu-latest
needs: build-examples
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Cache PlatformIO
uses: actions/cache@v4
with:
path: ~/.platformio
key: ${{ runner.os }}-platformio-${{ hashFiles('**/library.json') }}
restore-keys: |
${{ runner.os }}-platformio-

- name: Install PIOArduino Core
run: python -m pip install --upgrade https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip

- name: Install PIOArduino ESP32 Platform
run: pio platform install https://github.com/pioarduino/platform-espressif32.git

- name: Build ESP-IDF smoke target
run: |
set -euo pipefail
tmpdir="$(mktemp -d)"
cat > "${tmpdir}/main.cpp" <<'EOF'
#include <ESPWebPush.h>

extern "C" void app_main(void) {
ESPWebPush webPush;
WebPushVapidConfig vapid{};
vapid.subject = "mailto:test@example.com";
}
EOF

pio ci "${tmpdir}/main.cpp" \
--board esp32dev \
--lib="." \
--project-option "platform=https://github.com/pioarduino/platform-espressif32.git" \
--project-option "framework=espidf" \
--project-option "build_unflags=-std=gnu++11" \
--project-option "build_flags=-std=gnu++17" \
--project-option "lib_deps=ArduinoJson@>=7.0.0"

arduino-cli:
runs-on: ubuntu-latest
needs: [build-examples, espidf-smoke]
env:
ESP32_CORE_VERSION: 3.3.3
ESP32_PACKAGE_URL: https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
Expand Down Expand Up @@ -104,7 +153,6 @@ jobs:
run: |
arduino-cli lib update-index
arduino-cli lib install "ArduinoJson"
arduino-cli lib install --git-url "https://github.com/ESPToolKit/esp-worker.git"

- name: Add local library to sketchbook
run: |
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.venv
.pio/
.venv
1 change: 0 additions & 1 deletion .pio/build/project.checksum

This file was deleted.

42 changes: 27 additions & 15 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,35 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changed
- Breaking: renamed the public transport struct from `Subscription` to `WebPushSubscription` everywhere with no compatibility alias.
- Breaking: renamed `PushMessage.sub` to `PushMessage.subscription` for API consistency.
- Breaking: removed app-level metadata fields `deviceId`, `disabledTags`, and `deleted` from the transport struct.
- `validateSubscription()` now validates only the required Web Push transport fields: `endpoint`, `p256dh`, and `auth`.

## [2.0.0] - 2026-03-28

### Added
- Core ESPWebPush implementation: VAPID JWT signing, AES-GCM payload encryption, and HTTP delivery.
- Async queue + worker task with configurable stack, priority, queue length, and memory caps.
- Sync `send()` API returning structured `WebPushResult`.
- Retry/backoff handling for network/transport failures.
- Basic example sketch and CI workflows.
- Teardown lifecycle tests for pre-init `deinit()`, idempotent `deinit()`, re-init, and destructor teardown.
- Strict `PushPayload` API with typed notification fields and ArduinoJson v7+ overloads.
- User-provided network validator callback support.
- `WebPushVapidConfig` with standards-based `subject`, public key, and private key inputs.
- `WebPushEnqueueResult` for async preflight / queue outcomes.
- `WebPushJoinStatus` plus `requestStop()` / `join(timeoutMs)` for bounded worker shutdown.
- RFC 8291 Appendix A key-derivation and encrypted-body test coverage.
- Payload-size guard with the RFC-safe default limit of 3993 bytes.
- Small per-origin JWT cache for VAPID header reuse.

### Changed
- Teardown contract now uses `isInitialized()` and removes the old `initialized()` naming.
- `deinit()` now always converges teardown, including worker/queue/crypto cleanup and runtime config/key release.
- Structured payload inputs now reject unknown fields, missing required fields, and invalid types before enqueue/send.
- ArduinoJson v7+ is now an explicit dependency.
- Reworked encryption and transport to use RFC 8188 / RFC 8291 `aes128gcm` only.
- `init()` now validates `mailto:` / `https://` VAPID subjects and verifies that the configured public key matches the private key.
- Async `send()` overloads now return `WebPushEnqueueResult` and only invoke callbacks for queued work.
- `deinit()` now returns `WebPushJoinStatus` and uses a bounded stop/join flow instead of waiting forever.
- JWT payload assembly now uses dynamic `std::string` construction instead of a fixed stack buffer.
- Structured and raw payload sends now enforce the payload-size guard before transport.
- README, example sketch, package metadata, and CI now describe the v2 API and drop stale `esp-worker` references.
- CI push triggers now include `feature/**` branches so v2 work runs workflows before merge.
- `library.json` now advertises both Arduino and ESP-IDF compatibility.
- Package metadata now reports the breaking release as `2.0.0`.

### Notes
- JWT signing requires a valid system clock (SNTP).
- Content encoding uses `aesgcm` with VAPID headers (`Authorization`, `Crypto-Key`, `Encryption`).
- Worker configuration now uses `WebPushWorkerConfig` with native FreeRTOS task creation.
- JWT signing still requires a valid system clock (SNTP).
- Push sends use `Content-Encoding: aes128gcm` with VAPID `Authorization`.
- Breaking changes in v2 include the new `init()` signature, async enqueue return type, bounded shutdown API, and RFC 8291-only protocol behavior.
127 changes: 72 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
# ESPWebPush

ESPWebPush is an **async-first** Web Push sender for ESP32 firmware. It handles VAPID JWT signing, Web Push AES-GCM payload encryption, and HTTP delivery so your devices can notify browsers without extra glue code.
ESPWebPush is an async-first Web Push sender for ESP32 firmware. It handles VAPID JWT signing, RFC 8291 `aes128gcm` payload encryption, and HTTP delivery so devices can notify browsers without custom glue code.

ArduinoJson v7+ is a required dependency for the structured payload API.
ArduinoJson v7+ is required for the structured payload API.

## CI / Release / License
[![CI](https://github.com/ESPToolKit/esp-webPush/actions/workflows/ci.yml/badge.svg)](https://github.com/ESPToolKit/esp-webPush/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/ESPToolKit/esp-webPush?sort=semver)](https://github.com/ESPToolKit/esp-webPush/releases)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)
[![License: MIT](https://img.shields.io/github/license/ESPToolKit/esp-webPush)](LICENSE.md)

## Features
- VAPID JWT signing (ES256) from base64url private key.
- Web Push AES-GCM payload encryption.
- RFC 8292 VAPID JWT signing with `mailto:` or `https://` subjects.
- RFC 8291 / RFC 8188 `aes128gcm` Web Push encryption.
- Async queue + worker task via native FreeRTOS APIs.
- Optional synchronous `send()` API.
- Bounded shutdown via `requestStop()`, `join(timeoutMs)`, and `deinit(timeoutMs)`.
- Sync `send()` API plus async `send()` overloads that return `WebPushEnqueueResult`.
- Strict `PushPayload` validation for browser notification fields.
- ArduinoJson v7+ overloads for validated `JsonDocument` / `JsonVariantConst` payloads.
- Configurable queue length, memory caps (internal vs PSRAM), stack, priority, retries, and timeouts.
- Optional application-provided network validator callback.
- Uses the standard Web Push headers (`Authorization`, `Crypto-Key`, `Encryption`, `TTL`).
- Payload-size guard with the RFC-safe default limit of 3993 bytes.
- Small per-origin JWT cache to avoid re-signing every message.
- Configurable queue length, memory caps, retries, timeouts, and worker task settings.

## Quick Start

Expand All @@ -31,33 +31,35 @@ ESPWebPush webPush;
void setup() {
Serial.begin(115200);

WebPushVapidConfig vapid;
vapid.subject = "mailto:notify@example.com";
vapid.publicKeyBase64 = "BAvapidPublicKeyBase64Url...";
vapid.privateKeyBase64 = "vapidPrivateKeyBase64Url...";

WebPushConfig cfg;
cfg.queueLength = 16;
cfg.queueMemory = WebPushQueueMemory::Psram;
cfg.worker.stackSizeBytes = 16 * 1024;
cfg.worker.priority = 3;
cfg.worker.name = "webpush";
cfg.maxPayloadBytes = 3993;
cfg.networkValidator = []() { return true; };

webPush.init(
"notify@example.com",
"BAvapidPublicKeyBase64Url...",
"vapidPrivateKeyBase64Url...",
cfg);
webPush.init(vapid, cfg);
}

void loop() {}
```

## Usage

### Subscription / Structured Payload
### WebPushSubscription / Structured Payload

```cpp
Subscription sub;
sub.endpoint = "https://fcm.googleapis.com/fcm/send/...";
sub.p256dh = "BME..."; // base64url from browser subscription
sub.auth = "nsa..."; // base64url from browser subscription
WebPushSubscription subscription;
subscription.endpoint = "https://fcm.googleapis.com/fcm/send/...";
subscription.p256dh = "BME...";
subscription.auth = "nsa...";

PushPayload payload;
payload.title = "Hello";
Expand All @@ -69,7 +71,7 @@ payload.icon = "https://example.com/icon.png";
### Async Send

```cpp
bool started = webPush.send(sub, payload, [](WebPushResult result) {
WebPushEnqueueResult enqueue = webPush.send(subscription, payload, [](WebPushResult result) {
if (!result.ok()) {
ESP_LOGE("WEBPUSH", "Push failed: %s (status %d)",
result.message, result.statusCode);
Expand All @@ -78,11 +80,13 @@ bool started = webPush.send(sub, payload, [](WebPushResult result) {
ESP_LOGI("WEBPUSH", "Push OK (status %d)", result.statusCode);
});

if (!started) {
ESP_LOGW("WEBPUSH", "Queue full or not initialized");
if (!enqueue.queued()) {
ESP_LOGW("WEBPUSH", "Enqueue failed: %s", enqueue.message);
}
```

Async preflight failures are returned through `WebPushEnqueueResult`. The callback only runs for messages that were actually queued.

### ArduinoJson v7+ Send

```cpp
Expand All @@ -91,13 +95,13 @@ doc["title"] = "Hello";
doc["body"] = "ESP32";
doc["tag"] = "demo";

WebPushResult result = webPush.send(sub, doc);
WebPushResult result = webPush.send(subscription, doc);
```

### Sync Send

```cpp
WebPushResult result = webPush.send(sub, payload);
WebPushResult result = webPush.send(subscription, payload);
if (!result.ok()) {
ESP_LOGW("WEBPUSH", "Sync push failed: %s", result.message);
}
Expand All @@ -107,7 +111,7 @@ if (!result.ok()) {

```cpp
PushMessage msg;
msg.sub = sub;
msg.subscription = subscription;
msg.payload = "{\"title\":\"Hello\",\"body\":\"ESP32\"}";

// Raw payload strings remain supported, but they are not schema-validated.
Expand All @@ -118,60 +122,73 @@ WebPushResult result = webPush.send(msg);

```cpp
if (webPush.isInitialized()) {
webPush.deinit();
WebPushJoinStatus stopStatus = webPush.deinit();
if (stopStatus == WebPushJoinStatus::Timeout) {
ESP_LOGW("WEBPUSH", "Worker did not stop within the timeout");
}
}
```

`requestStop()` marks shutdown and wakes the worker without blocking. `join(timeoutMs)` waits for the worker to exit and finalizes shutdown when the stop completes in time. `deinit(timeoutMs)` is the convenience wrapper that performs both in one call.

## Configuration

`WebPushConfig` lets you tune the worker and queue:

- `queueLength` – number of queued messages.
- `queueMemory` – `Internal`, `Psram`, or `Any`.
- `worker` – stack size, priority, core id, PSRAM stack usage.
- `requestTimeoutMs` – HTTP timeout.
- `ttlSeconds` – Web Push TTL header.
- `maxRetries`, `retryBaseDelayMs`, `retryMaxDelayMs` – retry/backoff controls.
- `networkValidator` – optional callback for application-defined network readiness checks.
- `queueLength` - number of queued messages.
- `queueMemory` - `Internal`, `Psram`, or `Any`.
- `worker` - stack size, priority, core id, and task name.
- `requestTimeoutMs` - HTTP timeout.
- `ttlSeconds` - Web Push TTL header.
- `maxRetries`, `retryBaseDelayMs`, `retryMaxDelayMs` - retry/backoff controls.
- `maxPayloadBytes` - plaintext payload size guard. The default is 3993 bytes; use `0` to disable.
- `networkValidator` - optional callback for application-defined network readiness checks.

## Gotchas
- **System time is required** for VAPID JWT expiration. Ensure SNTP is synced.
- Web Push endpoints require TLS; `esp_http_client` must be built with TLS support.
- `aesgcm` content encoding is used to match existing Web Push payloads.
- Structured payload inputs reject unknown top-level keys and invalid field types.

## API Reference (Core)

- `bool init(contactEmail, publicKeyBase64, privateKeyBase64, config)`
- `bool send(const PushMessage&, WebPushResultCB cb)` (async)
- `WebPushResult send(const PushMessage&)` (sync)
- `bool send(const Subscription&, const PushPayload&, WebPushResultCB cb)` / `WebPushResult send(const Subscription&, const PushPayload&)`
- `bool send(const Subscription&, const JsonDocument&, WebPushResultCB cb)` / `WebPushResult send(const Subscription&, const JsonDocument&)`
- `bool send(const Subscription&, JsonVariantConst, WebPushResultCB cb)` / `WebPushResult send(const Subscription&, JsonVariantConst)`
- System time is required for VAPID JWT expiration.
- Web Push endpoints require TLS-capable `esp_http_client`.
- Only `aes128gcm` is generated. Legacy `aesgcm` is intentionally not supported in v2.
- `subject` must start with `mailto:` or `https://`.
- The configured VAPID public key must match the private key.

## API Reference

- `bool init(const WebPushVapidConfig&, const WebPushConfig& = {})`
- `WebPushEnqueueResult send(const PushMessage&, WebPushResultCB cb)`
- `WebPushResult send(const PushMessage&)`
- `WebPushEnqueueResult send(const WebPushSubscription&, const PushPayload&, WebPushResultCB cb)`
- `WebPushResult send(const WebPushSubscription&, const PushPayload&)`
- `WebPushEnqueueResult send(const WebPushSubscription&, const JsonDocument&, WebPushResultCB cb)`
- `WebPushResult send(const WebPushSubscription&, const JsonDocument&)`
- `WebPushEnqueueResult send(const WebPushSubscription&, JsonVariantConst, WebPushResultCB cb)`
- `WebPushResult send(const WebPushSubscription&, JsonVariantConst)`
- `void requestStop()`
- `WebPushJoinStatus join(uint32_t timeoutMs)`
- `void setNetworkValidator(WebPushNetworkValidator)`
- `void deinit()` / `bool isInitialized() const`
- `const char* errorToString(WebPushError)`
- `WebPushJoinStatus deinit(uint32_t timeoutMs = 10000)` / `bool isInitialized() const`
- `const char *errorToString(WebPushError)`

## Restrictions
- ESP32-class targets only (Arduino + ESP-IDF).
## Compatibility
- ESP32-class targets only.
- Arduino and ESP-IDF frameworks are supported.
- Requires C++17, ArduinoJson v7+, and mbedTLS.
- Do not call from ISR context.

## Tests
Host-side tests are disabled. Use the `examples/` sketches with PlatformIO or Arduino CLI.
- On-device Unity tests live in `test/test_esp_webPush`.
- CI builds Arduino examples and includes an ESP-IDF compile smoke build.

## Formatting Baseline

This repository follows the firmware formatting baseline from `esptoolkit-template`:
- `.clang-format` is the source of truth for C/C++/INO layout.
- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline.
- Format all tracked firmware sources with `bash scripts/format_cpp.sh`.
- Format tracked firmware sources with `bash scripts/format_cpp.sh`.

## License
MIT see [LICENSE.md](LICENSE.md).
MIT - see [LICENSE.md](LICENSE.md).

## ESPToolKit
- Check out other libraries: <https://github.com/orgs/ESPToolKit/repositories>
- Hang out on Discord: <https://discord.gg/WG8sSqAy>
- Support the project: <https://ko-fi.com/esptoolkit>
- Visit the website: <https://www.esptoolkit.hu/>
Loading
Loading