Skip to content
This repository was archived by the owner on Aug 3, 2026. It is now read-only.

Commit 4ebab17

Browse files
committed
http option changes
1 parent 78b7255 commit 4ebab17

4 files changed

Lines changed: 185 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
55
## [Unreleased]
66

77
### Fixed
8+
- Web Push HTTP transport now forwards certificate-bundle / global-CA /
9+
common-name TLS options into `esp_http_client`, improving mixed Arduino +
10+
ESP-IDF compatibility for provider HTTPS endpoints.
11+
- Transport failure logs now include endpoint host, path, origin class,
12+
`esp_err_t`, and HTTP status for field diagnostics.
813
- Fixed ESP32 VAPID key-pair validation and runtime public-key derivation to use the library DRBG-backed mbedTLS path instead of relying on null RNG callbacks.
914
- CI now pins PIOArduino Core to `v6.1.19` and installs the ESP32 platform via `pio pkg install`, restoring PlatformIO compatibility with the current `platform-espressif32` package.
1015

@@ -13,6 +18,8 @@ All notable changes to this project will be documented in this file.
1318
- Breaking: renamed `PushMessage.sub` to `PushMessage.subscription` for API consistency.
1419
- Breaking: removed app-level metadata fields `deviceId`, `disabledTags`, and `deleted` from the transport struct.
1520
- `validateSubscription()` now validates only the required Web Push transport fields: `endpoint`, `p256dh`, and `auth`.
21+
- `WebPushConfig` gained transport defaults for `useTlsCertBundle`,
22+
`useGlobalCaStore`, and `skipTlsCommonNameCheck`.
1623

1724
## [2.0.0] - 2026-03-28
1825

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ ArduinoJson v7+ is required for the structured payload API.
2020
- Payload-size guard with the RFC-safe default limit of 3993 bytes.
2121
- Small per-origin JWT cache to avoid re-signing every message.
2222
- Configurable queue length, memory caps, retries, timeouts, and worker task settings.
23+
- Optional TLS transport controls for certificate bundle, global CA store, and
24+
common-name verification behavior.
2325

2426
## Quick Start
2527

@@ -143,8 +145,13 @@ if (webPush.isInitialized()) {
143145
- `ttlSeconds` - Web Push TTL header.
144146
- `maxRetries`, `retryBaseDelayMs`, `retryMaxDelayMs` - retry/backoff controls.
145147
- `maxPayloadBytes` - plaintext payload size guard. The default is 3993 bytes; use `0` to disable.
148+
- `useTlsCertBundle` - attach `esp_crt_bundle_attach` when the build provides the ESP x509 bundle.
149+
- `useGlobalCaStore` - forward `use_global_ca_store` to `esp_http_client`.
150+
- `skipTlsCommonNameCheck` - forward `skip_cert_common_name_check` to `esp_http_client`.
146151
- `networkValidator` - optional callback for application-defined network readiness checks.
147152

153+
The default transport behavior is certificate-bundle-backed (`useTlsCertBundle = true`), which matches common browser push providers such as FCM without extra app wiring.
154+
148155
## Gotchas
149156
- System time is required for VAPID JWT expiration.
150157
- Web Push endpoints require TLS-capable `esp_http_client`.

src/esp_webPush/webPush.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ struct WebPushConfig {
138138
uint32_t retryBaseDelayMs = 1500;
139139
uint32_t retryMaxDelayMs = 15000;
140140
size_t maxPayloadBytes = 3993;
141+
bool useTlsCertBundle = true;
142+
bool useGlobalCaStore = false;
143+
bool skipTlsCommonNameCheck = false;
141144
WebPushNetworkValidator networkValidator;
142145
};
143146

src/esp_webPush/webPush_http.cpp

Lines changed: 168 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,134 @@
11
#include "webPush.h"
22

3+
#include <cctype>
4+
#include <cstring>
5+
36
extern "C" {
47
#include "esp_http_client.h"
58
#include "esp_log.h"
69
}
710

11+
#if __has_include(<esp_crt_bundle.h>)
12+
extern "C" {
13+
#include <esp_crt_bundle.h>
14+
}
15+
#define ESPWEBPUSH_HAVE_CRT_BUNDLE 1
16+
#else
17+
#define ESPWEBPUSH_HAVE_CRT_BUNDLE 0
18+
#endif
19+
820
namespace {
921
constexpr const char *kTag = "ESPWebPush";
22+
23+
struct EndpointInfo {
24+
std::string host;
25+
std::string path = "/";
26+
const char *originClass = "custom";
27+
};
28+
29+
bool equalsIgnoreCase(const std::string &lhs, const char *rhs) {
30+
if (rhs == nullptr) {
31+
return false;
32+
}
33+
34+
const size_t rhsLength = std::strlen(rhs);
35+
if (lhs.size() != rhsLength) {
36+
return false;
37+
}
38+
39+
for (size_t i = 0; i < lhs.size(); ++i) {
40+
if (std::tolower(static_cast<unsigned char>(lhs[i])) !=
41+
std::tolower(static_cast<unsigned char>(rhs[i]))) {
42+
return false;
43+
}
44+
}
45+
46+
return true;
47+
}
48+
49+
bool endsWithIgnoreCase(const std::string &value, const char *suffix) {
50+
if (suffix == nullptr) {
51+
return false;
52+
}
53+
54+
const size_t suffixLength = std::strlen(suffix);
55+
if (value.size() < suffixLength) {
56+
return false;
57+
}
58+
59+
const size_t start = value.size() - suffixLength;
60+
for (size_t i = 0; i < suffixLength; ++i) {
61+
if (std::tolower(static_cast<unsigned char>(value[start + i])) !=
62+
std::tolower(static_cast<unsigned char>(suffix[i]))) {
63+
return false;
64+
}
65+
}
66+
67+
return true;
68+
}
69+
70+
bool parseEndpointInfo(const std::string &endpoint, EndpointInfo &infoOut) {
71+
const size_t schemeSeparator = endpoint.find("://");
72+
const size_t authorityStart = schemeSeparator == std::string::npos ? 0 : schemeSeparator + 3;
73+
if (authorityStart >= endpoint.size()) {
74+
return false;
75+
}
76+
77+
size_t authorityEnd = endpoint.find_first_of("/?#", authorityStart);
78+
if (authorityEnd == std::string::npos) {
79+
authorityEnd = endpoint.size();
80+
} else {
81+
infoOut.path = endpoint.substr(authorityEnd);
82+
}
83+
if (authorityEnd <= authorityStart) {
84+
return false;
85+
}
86+
87+
const std::string authority = endpoint.substr(authorityStart, authorityEnd - authorityStart);
88+
const size_t userInfoSeparator = authority.rfind('@');
89+
const std::string hostAndPort =
90+
userInfoSeparator == std::string::npos ? authority : authority.substr(userInfoSeparator + 1);
91+
if (hostAndPort.empty()) {
92+
return false;
93+
}
94+
95+
if (hostAndPort.front() == '[') {
96+
const size_t closingBracket = hostAndPort.find(']');
97+
if (closingBracket == std::string::npos || closingBracket <= 1) {
98+
return false;
99+
}
100+
101+
infoOut.host = hostAndPort.substr(1, closingBracket - 1);
102+
} else {
103+
const size_t portSeparator = hostAndPort.rfind(':');
104+
if (portSeparator != std::string::npos &&
105+
hostAndPort.find(':', portSeparator + 1) == std::string::npos) {
106+
infoOut.host = hostAndPort.substr(0, portSeparator);
107+
} else {
108+
infoOut.host = hostAndPort;
109+
}
110+
}
111+
112+
if (infoOut.host.empty()) {
113+
return false;
114+
}
115+
116+
if (equalsIgnoreCase(infoOut.host, "fcm.googleapis.com") ||
117+
endsWithIgnoreCase(infoOut.host, ".fcm.googleapis.com")) {
118+
infoOut.originClass = "fcm";
119+
} else if (equalsIgnoreCase(infoOut.host, "updates.push.services.mozilla.com") ||
120+
endsWithIgnoreCase(infoOut.host, ".push.services.mozilla.com")) {
121+
infoOut.originClass = "mozilla";
122+
} else if (equalsIgnoreCase(infoOut.host, "web.push.apple.com") ||
123+
endsWithIgnoreCase(infoOut.host, ".push.apple.com")) {
124+
infoOut.originClass = "apple";
125+
} else if (equalsIgnoreCase(infoOut.host, "wns.windows.com") ||
126+
endsWithIgnoreCase(infoOut.host, ".notify.windows.com")) {
127+
infoOut.originClass = "windows";
128+
}
129+
130+
return true;
131+
}
10132
} // namespace
11133

12134
void ESPWebPush::printHeaderErr(esp_err_t headErr, const char *headKey) const {
@@ -19,17 +141,28 @@ WebPushResult ESPWebPush::sendPushRequest(
19141
const std::string &endpoint, const std::string &jwt, const std::vector<uint8_t> &body
20142
) {
21143
WebPushResult result{};
144+
EndpointInfo endpointInfo{};
22145
if (endpoint.empty()) {
23146
result.error = WebPushError::InvalidSubscription;
24147
result.message = errorToString(result.error);
25148
return result;
26149
}
150+
if (!parseEndpointInfo(endpoint, endpointInfo)) {
151+
endpointInfo.host = endpointOrigin(endpoint);
152+
}
27153

28154
esp_http_client_config_t config = {};
29155
config.url = endpoint.c_str();
30156
config.method = HTTP_METHOD_POST;
31157
config.timeout_ms = static_cast<int>(_config.requestTimeoutMs);
32158
config.buffer_size_tx = 6144;
159+
config.skip_cert_common_name_check = _config.skipTlsCommonNameCheck;
160+
config.use_global_ca_store = _config.useGlobalCaStore;
161+
#if ESPWEBPUSH_HAVE_CRT_BUNDLE
162+
if (_config.useTlsCertBundle) {
163+
config.crt_bundle_attach = esp_crt_bundle_attach;
164+
}
165+
#endif
33166

34167
esp_http_client_handle_t client = esp_http_client_init(&config);
35168
if (!client) {
@@ -71,14 +204,47 @@ WebPushResult ESPWebPush::sendPushRequest(
71204
if (err != ESP_OK) {
72205
result.error = WebPushError::TransportError;
73206
result.message = errorToString(result.error);
74-
ESP_LOGE(kTag, "HTTP POST failed: %s (status %d)", esp_err_to_name(err), statusCode);
207+
ESP_LOGE(
208+
kTag,
209+
"HTTP POST failed: host=%s path=%s origin=%s err=%s(%d) status=%d",
210+
endpointInfo.host.c_str(),
211+
endpointInfo.path.c_str(),
212+
endpointInfo.originClass,
213+
esp_err_to_name(err),
214+
static_cast<int>(err),
215+
statusCode
216+
);
217+
if (err == ESP_ERR_HTTP_CONNECT) {
218+
ESP_LOGE(
219+
kTag,
220+
"TLS/connect setup failed before HTTP response: host=%s path=%s origin=%s",
221+
endpointInfo.host.c_str(),
222+
endpointInfo.path.c_str(),
223+
endpointInfo.originClass
224+
);
225+
}
75226
} else if (statusCode < 200 || statusCode >= 300) {
76227
result.error = WebPushError::HttpError;
77228
result.message = errorToString(result.error);
78-
ESP_LOGE(kTag, "HTTP POST failed: status %d", statusCode);
229+
ESP_LOGE(
230+
kTag,
231+
"HTTP POST failed: host=%s path=%s origin=%s status=%d",
232+
endpointInfo.host.c_str(),
233+
endpointInfo.path.c_str(),
234+
endpointInfo.originClass,
235+
statusCode
236+
);
79237
} else {
80238
result.error = WebPushError::None;
81239
result.message = errorToString(result.error);
240+
ESP_LOGI(
241+
kTag,
242+
"HTTP POST succeeded: host=%s path=%s origin=%s status=%d",
243+
endpointInfo.host.c_str(),
244+
endpointInfo.path.c_str(),
245+
endpointInfo.originClass,
246+
statusCode
247+
);
82248
}
83249

84250
esp_http_client_cleanup(client);

0 commit comments

Comments
 (0)