我的 Web-Push 实现在 Chrome 和 Firefox 上运行良好,但在 MS Edge 上运行不佳(即使在 % 解码之后)。
问题:如何使其发挥作用?
仅供参考,他们的 URL 以不同的格式显示:
Chrome 网址: “https://fcm.googleapis.com/fcm/send/eBt-ioGXt4A:APA91bGkIsaIeVWAG-cU-UysXKl5pevjnZzIRT0GS69gx0 ZKbnbrOD4FB7m8Ou8ZpS0X0hcEeSLjxYdsYKi896YibgFrocO7L8qDU2SeZfr6L7Pqc2DZ7A_82Qik7PINAF_S2rskKofe"
边缘网址: “https://wns2-pn1p.notify.windows.com/w/?token=BQYAAACbMWA0QtGcCaM00KJntpBBrdmzSvWUnBmgRwUFnM6fJEy SP1Jr0Fi3OU2rgTICDfl2Bsj6jS4eNZLo0FQK1diyqN1v6zi7k9yBijIksEvKegd2q2Z%2bGAIoIt0QfnQyluOGSNgXCrF0jr4 h3Ka5aJUVdl7aBSkkULq5PI7wvGl3mGMD9I3xk71jG%2bjBAwoF2ThvYfefEEpd5xMAJ%2bWLBd3FD56kD1zTplOhwS4Leysw3 SFBXh393%2b8MfDFJCAi%2f0mfKLBy%2fTCuha50GJT7oBJHemhFCi5E2CliZ9dFB2IPCpEa%2bEfgVWHC6GkpRMjUSCs0%3d"
客户端打印错误:
400:“传输错误 - 服务器回复:”
ChatGPT 表示服务器没有义务提供到底出了什么问题的实际文本,以阻止其内部工作。
对于 Chrome,它只是返回 201,并在仪表板中看到一条通知。
请注意,相同的 VAPID 密钥、Endpoint、P256DH、AUTH 等组合适用于已知的 C# 推送通知 API (github)。
源代码:从pusha(github)库中,我导出了相关部分并使用Qt框架实现了一个最小的工作示例。可能需要添加 ecec(github) 库的一些文件。完成后,仅使用以下单个源文件就可以正常工作!
web-push.pro
TEMPLATE = app
CONFIG += console c++17
CONFIG -= app_bundle
QT += network
LIBS += -lcrypto -lssl #ssl is optional
SOURCES += \
ecec/encrypt.c \
ecec/keys.c \
ecec/trailer.c \
main.cpp
HEADERS += \
ecec/ece.h \
ecec/keys.h \
ecec/trailer.h
main.cpp
#include<openssl/pem.h>
#include<QByteArray>
#include<QDateTime>
#include<QDebug>
#include<QCoreApplication>
#include<QFile>
#include<QJsonDocument>
#include<QJsonObject>
#include<QNetworkAccessManager>
#include<QNetworkReply>
#include<ecec/ece.h>
#define ENDPOINT "https://fcm.googleapis.com/fcm/send/eBt-ioGXt4A:APA91bGkIsaIeVWAG-cU-UysXKl5pevjnZzIRT0GS69gx0ZKbnbrOD4FB7m8Ou8ZpS0X0hcEeSLjxYdsYKi896YibgFrocO7L8qDU2SeZfr6L7Pqc2DZ7A_82Qik7PINAF_S2rskKofe"
#define P256DH "BM8Md9QY7egq1UqZytneixPkITMK5556EHBQD0yTZY6eW6eeK89TsdpymT79rIc9xfouL1FLH-ACb9kEuf6iea0"
#define AUTH "w7o5Sip9yBm6ME1C88pebg"
#define VAPID_PRIVATE_KEY "T2blCzCxRzFl2yjI-Q6WLxW1PoiTxSLPExtP9yOxkgM"
#define VAPID_PUBLIC_KEY "BMRAeeyEY6imWuCktv6NX3o5prv4UWndTUWTEO4dCgmzX8YDgjuIbPslpSM2fdfTbOjmnLIBkJKch2wGnTm8_sY"
#define WEBPUSH_PAYLOAD_KEY_AUD "aud"
#define WEBPUSH_PAYLOAD_KEY_EXP "exp"
#define WEBPUSH_PAYLOAD_KEY_SUB "sub"
#define WEBPUSH_VAPID_KEY_ALG "alg"
#define WEBPUSH_VAPID_KEY_TYP "typ"
#define HTTP_DH ";dh="
#define HTTP_AUTHORIZATION "authorization"
#define HTTP_CONTENT_ENCODING "content-encoding"
#define HTTP_CONTENT_TYPE "content-type"
#define HTTP_CONTENT_LENGTH "content-length"
#define HTTP_CRYPTO_KEY "crypto-key"
#define HTTP_ENCRYPTION "encryption"
#define HTTP_P256 "p256ecdsa="
#define HTTP_RS_SALT "rs=4096;salt="
#define HTTP_TTL "ttl"
#define QT_URL_ENCODING QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals
#define MAKE_UNIQUE(MALLOC, FREE) std::unique_ptr<std::remove_pointer<decltype(MALLOC)>::type, \
decltype(&FREE)>{MALLOC, &FREE}
static const QByteArray HTTP_AUTHORIZATION_v = "webPush ",
HTTP_ENCODING_v = "aesgcm",
HTTP_CONTENT_TYPE_v = "application/octet-stream",
HTTP_TTL_v = "3600",
WEBPUSH_VAPID_ALG_v = "ES256", // must be in capital
WEBPUSH_VAPID_TYP_v = "jwt";
#define SUBSCRIBER "mailto:[email protected]"
#define MY_PAYLOAD "{ \
\"body\": \"Shree Vallabh!\", \
\"tag\": \"AAHLAAD\", \
\"data\": {\"tag\": \"test\"}, \
\"title\": \"Saarathy\" \
}"
#define SYMBOL_DOT "."
struct Subscription
{
uint8_t m_P256dh[ECE_WEBPUSH_PUBLIC_KEY_LENGTH];
uint8_t m_Auth[ECE_WEBPUSH_AUTH_SECRET_LENGTH];
};
struct Payload
{
uint8_t m_Salt[ECE_SALT_LENGTH], m_PublicKeySender[ECE_WEBPUSH_PUBLIC_KEY_LENGTH];
QByteArray m_Cipher;
size_t m_CipherLength;
};
auto
GetJson (QVariantMap map)
{
auto object = QJsonObject::fromVariantMap(map);
return QJsonDocument(object).toJson(QJsonDocument::Compact);
}
auto
CreateECKey (const QByteArray& rawKey)
{
auto pECKey = MAKE_UNIQUE(::EC_KEY_new_by_curve_name(NID_X9_62_prime256v1), ::EC_KEY_free);
::EC_KEY_oct2priv(pECKey.get(),
reinterpret_cast<const uint8_t*>(rawKey.data()), rawKey.size());
const ::EC_GROUP* const pGroup = ::EC_KEY_get0_group(pECKey.get());
auto point = MAKE_UNIQUE(::EC_POINT_new(pGroup), ::EC_POINT_free);
::EC_POINT_mul(pGroup, point.get(),
::EC_KEY_get0_private_key(pECKey.get()), nullptr, nullptr, nullptr);
::EC_KEY_set_public_key(pECKey.get(), point.get());
return pECKey;
}
QByteArray
VapidSign (EC_KEY& ecKey,
const QByteArray& sign)
{
unsigned char digest[SHA256_DIGEST_LENGTH];
::SHA256(reinterpret_cast<const unsigned char*>(sign.data()), sign.size(), digest);
const auto pSign = MAKE_UNIQUE(::ECDSA_do_sign(digest, SHA256_DIGEST_LENGTH, &ecKey),
::ECDSA_SIG_free);
const ::BIGNUM *pR = nullptr, *pS = nullptr;
::ECDSA_SIG_get0(pSign.get(), &pR, &pS);
const auto r_Size = BN_num_bytes(pR), s_Size = BN_num_bytes(pS);
QByteArray signValue(r_Size + s_Size, 0);
::BN_bn2bin(pR, reinterpret_cast<unsigned char*>(signValue.data()));
::BN_bn2bin(pS, reinterpret_cast<unsigned char*>(&signValue[r_Size]));
return signValue;
}
QByteArray
VapidAuthorize (const QString& endpoint,
const QString& subscriber,
const int expiration,
EC_KEY& ecKey)
{
const auto audience = endpoint.section('/', 0, 2);
const auto params = GetJson({{WEBPUSH_PAYLOAD_KEY_AUD, audience},
{WEBPUSH_PAYLOAD_KEY_EXP, expiration},
{WEBPUSH_PAYLOAD_KEY_SUB, subscriber}}),
header = GetJson({{WEBPUSH_VAPID_KEY_ALG, WEBPUSH_VAPID_ALG_v},
{WEBPUSH_VAPID_KEY_TYP, WEBPUSH_VAPID_TYP_v}}),
sign = header.toBase64(QT_URL_ENCODING) + SYMBOL_DOT + params.toBase64(QT_URL_ENCODING);
return sign + SYMBOL_DOT + VapidSign(ecKey, sign).toBase64(QT_URL_ENCODING);
}
size_t
GetCipherLength (const uint32_t rs,
const size_t padSize,
const size_t padLen,
const size_t plaintextLen)
{
const size_t overhead = padSize + ECE_TAG_LENGTH, dataLen = plaintextLen + padLen,
maxBlockLen = rs - overhead, numRecords = (dataLen / maxBlockLen) + 1;
if(rs <= overhead or padLen > SIZE_MAX - plaintextLen or
numRecords > (SIZE_MAX - dataLen) / overhead)
return 0;
return dataLen + (overhead * numRecords);
}
void
WebPush (const QString& endpoint,
const QByteArray& authorization,
const QByteArray& vapidPublicKey,
const Payload& payload)
{
int argc;
QCoreApplication app{argc, nullptr};
const auto dh = QByteArray(reinterpret_cast<const char*>(payload.m_PublicKeySender),
ECE_WEBPUSH_PUBLIC_KEY_LENGTH).toBase64(QT_URL_ENCODING),
salt = QByteArray(reinterpret_cast<const char*>(payload.m_Salt),
ECE_SALT_LENGTH).toBase64(QT_URL_ENCODING);
QNetworkAccessManager networkManager;
QNetworkRequest request(QUrl{endpoint});
request.setRawHeader(HTTP_AUTHORIZATION, HTTP_AUTHORIZATION_v + authorization);
request.setRawHeader(HTTP_CONTENT_ENCODING, HTTP_ENCODING_v);
request.setRawHeader(HTTP_CONTENT_TYPE, HTTP_CONTENT_TYPE_v);
request.setRawHeader(HTTP_CONTENT_LENGTH, QByteArray::number(payload.m_Cipher.size()));
request.setRawHeader(HTTP_CRYPTO_KEY, HTTP_P256 + vapidPublicKey + HTTP_DH + dh);
request.setRawHeader(HTTP_ENCRYPTION, HTTP_RS_SALT + salt);
request.setRawHeader(HTTP_TTL, HTTP_TTL_v);
QNetworkReply* pReply = networkManager.post(request, payload.m_Cipher);
QObject::connect(pReply, &QNetworkReply::finished, pReply, &QNetworkReply::deleteLater);
QObject::connect(pReply, &QNetworkReply::finished, pReply,
[=] ()
{
qDebug() << pReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()
<< "====>" << pReply->errorString();
::exit(0);
});
app.exec();
}
QByteArray
CreateVapidPrivateKey ()
{
auto pECKey = MAKE_UNIQUE(::EC_KEY_new_by_curve_name(NID_X9_62_prime256v1), ::EC_KEY_free);
::EC_KEY_generate_key(pECKey.get());
QByteArray privKeyRaw(32, 0);
::BN_bn2binpad(::EC_KEY_get0_private_key(pECKey.get()),
reinterpret_cast<unsigned char*>(privKeyRaw.data()), 32);
return privKeyRaw.toBase64(QT_URL_ENCODING);
}
QByteArray
CreateVapidPublicKey (const QByteArray& privateKeyBase64)
{
const auto privateKey = QByteArray::fromBase64(privateKeyBase64, QT_URL_ENCODING);
auto pECKey = MAKE_UNIQUE(::EC_KEY_new_by_curve_name(NID_X9_62_prime256v1), ::EC_KEY_free);
auto pKeyBn = MAKE_UNIQUE(::BN_bin2bn(reinterpret_cast<const unsigned char*>(privateKey.data()),
privateKey.size(), nullptr),
::BN_free);
::EC_KEY_set_private_key(pECKey.get(), pKeyBn.get());
const auto* const pGroup = ::EC_KEY_get0_group(pECKey.get());
auto pPoint = MAKE_UNIQUE(::EC_POINT_new(pGroup), ::EC_POINT_free);
::EC_POINT_mul(pGroup, pPoint.get(), pKeyBn.get(), nullptr, nullptr, nullptr);
QByteArray publicKey(65, 0);
publicKey[0] = 0x04;
::EC_POINT_point2oct(pGroup, pPoint.get(), POINT_CONVERSION_UNCOMPRESSED,
reinterpret_cast<unsigned char*>(publicKey.data()), publicKey.size(), nullptr);
return publicKey.toBase64(QT_URL_ENCODING);
}
int main ()
{
// auto privKey = CreateVapidPrivateKey(), pubKey = CreateVapidPublicKey(privKey);
// qDebug() << privKey << pubKey << "\n=======";
qDebug() << "OpenSSL version:" << OpenSSL_version(OPENSSL_VERSION);
QByteArray content = MY_PAYLOAD, p256dh = P256DH, auth = AUTH,
vapidPrivateKey = VAPID_PRIVATE_KEY, vapidPublicKey = VAPID_PUBLIC_KEY, contentJson;
QString endpoint = ENDPOINT;
qDebug() << endpoint << p256dh << auth << vapidPrivateKey << vapidPublicKey;
endpoint = QUrl::fromPercentEncoding(endpoint.toUtf8());
p256dh = QByteArray::fromBase64(p256dh, QT_URL_ENCODING);
auth = QByteArray::fromBase64(auth, QT_URL_ENCODING);
qDebug() << endpoint << "-----";
Subscription subscription = {};
::memcpy(subscription.m_P256dh, p256dh.data(), sizeof(subscription.m_P256dh));
::memcpy(subscription.m_Auth, auth.data(), sizeof(subscription.m_Auth));
auto pECKey = CreateECKey(QByteArray::fromBase64(vapidPrivateKey, QT_URL_ENCODING));
const auto authorization = VapidAuthorize(endpoint, SUBSCRIBER,
QDateTime::currentSecsSinceEpoch() + (12 * 3600), *pECKey);
Payload payload{};
payload.m_Cipher.resize(payload.m_CipherLength = GetCipherLength(
ECE_WEBPUSH_DEFAULT_RS + ECE_TAG_LENGTH, ECE_AESGCM_PAD_SIZE, 0, content.size()));
::ece_webpush_aesgcm_encrypt(subscription.m_P256dh, ECE_WEBPUSH_PUBLIC_KEY_LENGTH,
subscription.m_Auth, ECE_WEBPUSH_AUTH_SECRET_LENGTH,
ECE_WEBPUSH_DEFAULT_RS, 0,
reinterpret_cast<const uint8_t*>(content.data()), content.size(),
payload.m_Salt, ECE_SALT_LENGTH,
payload.m_PublicKeySender, ECE_WEBPUSH_PUBLIC_KEY_LENGTH,
reinterpret_cast<uint8_t*>(payload.m_Cipher.data()),
&payload.m_CipherLength);
WebPush(endpoint, authorization, vapidPublicKey, payload);
}
请注意,您的 PAYLOAD 可能会有所不同,即接受 JSON。您可能必须以这样的方式设计worker.js,使其接受与您的框架相关的格式。
可以通过在发送
encryption
标头时进行细微更改来解决此问题。您必须删除 rs=4096;
值之前的字符串 salt
。
#define HTTP_RS_SALT "salt="
此外,对于
application/octet-stream
标题,更喜欢使用 content-type
,而不是 application/json
。