From 75160b12821f7f4299cce7f0b69c83c1502ae071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=20Luka=20=C5=A0ijanec?= Date: Mon, 27 May 2024 13:08:29 +0200 Subject: 2024-02-19 upstream --- vendor/minishlink/web-push/composer.json | 78 +- vendor/minishlink/web-push/src/Encryption.php | 642 ++++++++-------- .../minishlink/web-push/src/MessageSentReport.php | 362 ++++----- vendor/minishlink/web-push/src/Notification.php | 172 ++--- vendor/minishlink/web-push/src/Subscription.php | 244 +++--- .../web-push/src/SubscriptionInterface.php | 80 +- vendor/minishlink/web-push/src/Utils.php | 126 ++-- vendor/minishlink/web-push/src/VAPID.php | 394 +++++----- vendor/minishlink/web-push/src/WebPush.php | 824 ++++++++++----------- 9 files changed, 1461 insertions(+), 1461 deletions(-) (limited to 'vendor/minishlink/web-push') diff --git a/vendor/minishlink/web-push/composer.json b/vendor/minishlink/web-push/composer.json index c03a696..8645a53 100644 --- a/vendor/minishlink/web-push/composer.json +++ b/vendor/minishlink/web-push/composer.json @@ -1,39 +1,39 @@ -{ - "name": "minishlink/web-push", - "type": "library", - "description": "Web Push library for PHP", - "keywords": ["push", "notifications", "web", "WebPush", "Push API"], - "homepage": "https://github.com/web-push-libs/web-push-php", - "license": "MIT", - "authors": [ - { - "name": "Louis Lagrange", - "email": "lagrange.louis@gmail.com", - "homepage": "https://github.com/Minishlink" - } - ], - "scripts": { - "test:unit": "./vendor/bin/phpunit --color", - "test:typing": "./vendor/bin/phpstan analyse --level max src", - "test:syntax": "./vendor/bin/php-cs-fixer fix ./src --dry-run --stop-on-violation --using-cache=no" - }, - "require": { - "php": "^7.1", - "ext-json": "*", - "ext-gmp": "*", - "lib-openssl": "*", - "guzzlehttp/guzzle": "^6.2", - "web-token/jwt-signature": "^1.0", - "web-token/jwt-key-mgmt": "^1.0" - }, - "require-dev": { - "phpunit/phpunit": "^7.0", - "phpstan/phpstan": "0.11.2", - "friendsofphp/php-cs-fixer": "^2.14" - }, - "autoload": { - "psr-4" : { - "Minishlink\\WebPush\\" : "src" - } - } -} +{ + "name": "minishlink/web-push", + "type": "library", + "description": "Web Push library for PHP", + "keywords": ["push", "notifications", "web", "WebPush", "Push API"], + "homepage": "https://github.com/web-push-libs/web-push-php", + "license": "MIT", + "authors": [ + { + "name": "Louis Lagrange", + "email": "lagrange.louis@gmail.com", + "homepage": "https://github.com/Minishlink" + } + ], + "scripts": { + "test:unit": "./vendor/bin/phpunit --color", + "test:typing": "./vendor/bin/phpstan analyse --level max src", + "test:syntax": "./vendor/bin/php-cs-fixer fix ./src --dry-run --stop-on-violation --using-cache=no" + }, + "require": { + "php": "^7.1", + "ext-json": "*", + "ext-gmp": "*", + "lib-openssl": "*", + "guzzlehttp/guzzle": "^6.2", + "web-token/jwt-signature": "^1.0", + "web-token/jwt-key-mgmt": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0", + "phpstan/phpstan": "0.11.2", + "friendsofphp/php-cs-fixer": "^2.14" + }, + "autoload": { + "psr-4" : { + "Minishlink\\WebPush\\" : "src" + } + } +} diff --git a/vendor/minishlink/web-push/src/Encryption.php b/vendor/minishlink/web-push/src/Encryption.php index e9fe1ac..c867265 100644 --- a/vendor/minishlink/web-push/src/Encryption.php +++ b/vendor/minishlink/web-push/src/Encryption.php @@ -1,321 +1,321 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Minishlink\WebPush; - -use Base64Url\Base64Url; -use Jose\Component\Core\Util\Ecc\NistCurve; -use Jose\Component\Core\Util\Ecc\Point; -use Jose\Component\Core\Util\Ecc\PrivateKey; -use Jose\Component\Core\Util\Ecc\PublicKey; - -class Encryption -{ - public const MAX_PAYLOAD_LENGTH = 4078; - public const MAX_COMPATIBILITY_PAYLOAD_LENGTH = 3052; - - /** - * @param string $payload - * @param int $maxLengthToPad - * @param string $contentEncoding - * @return string padded payload (plaintext) - * @throws \ErrorException - */ - public static function padPayload(string $payload, int $maxLengthToPad, string $contentEncoding): string - { - $payloadLen = Utils::safeStrlen($payload); - $padLen = $maxLengthToPad ? $maxLengthToPad - $payloadLen : 0; - - if ($contentEncoding === "aesgcm") { - return pack('n*', $padLen).str_pad($payload, $padLen + $payloadLen, chr(0), STR_PAD_LEFT); - } elseif ($contentEncoding === "aes128gcm") { - return str_pad($payload.chr(2), $padLen + $payloadLen, chr(0), STR_PAD_RIGHT); - } else { - throw new \ErrorException("This content encoding is not supported"); - } - } - - /** - * @param string $payload With padding - * @param string $userPublicKey Base 64 encoded (MIME or URL-safe) - * @param string $userAuthToken Base 64 encoded (MIME or URL-safe) - * @param string $contentEncoding - * @return array - * - * @throws \ErrorException - */ - public static function encrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding): array - { - return self::deterministicEncrypt( - $payload, - $userPublicKey, - $userAuthToken, - $contentEncoding, - self::createLocalKeyObject(), - random_bytes(16) - ); - } - - /** - * @param string $payload - * @param string $userPublicKey - * @param string $userAuthToken - * @param string $contentEncoding - * @param array $localKeyObject - * @param string $salt - * @return array - * - * @throws \ErrorException - */ - public static function deterministicEncrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding, array $localKeyObject, string $salt): array - { - $userPublicKey = Base64Url::decode($userPublicKey); - $userAuthToken = Base64Url::decode($userAuthToken); - - $curve = NistCurve::curve256(); - - // get local key pair - list($localPublicKeyObject, $localPrivateKeyObject) = $localKeyObject; - $localPublicKey = hex2bin(Utils::serializePublicKey($localPublicKeyObject)); - if (!$localPublicKey) { - throw new \ErrorException('Failed to convert local public key from hexadecimal to binary'); - } - - // get user public key object - [$userPublicKeyObjectX, $userPublicKeyObjectY] = Utils::unserializePublicKey($userPublicKey); - $userPublicKeyObject = $curve->getPublicKeyFrom( - gmp_init(bin2hex($userPublicKeyObjectX), 16), - gmp_init(bin2hex($userPublicKeyObjectY), 16) - ); - - // get shared secret from user public key and local private key - $sharedSecret = $curve->mul($userPublicKeyObject->getPoint(), $localPrivateKeyObject->getSecret())->getX(); - $sharedSecret = hex2bin(str_pad(gmp_strval($sharedSecret, 16), 64, '0', STR_PAD_LEFT)); - if (!$sharedSecret) { - throw new \ErrorException('Failed to convert shared secret from hexadecimal to binary'); - } - - // section 4.3 - $ikm = self::getIKM($userAuthToken, $userPublicKey, $localPublicKey, $sharedSecret, $contentEncoding); - - // section 4.2 - $context = self::createContext($userPublicKey, $localPublicKey, $contentEncoding); - - // derive the Content Encryption Key - $contentEncryptionKeyInfo = self::createInfo($contentEncoding, $context, $contentEncoding); - $contentEncryptionKey = self::hkdf($salt, $ikm, $contentEncryptionKeyInfo, 16); - - // section 3.3, derive the nonce - $nonceInfo = self::createInfo('nonce', $context, $contentEncoding); - $nonce = self::hkdf($salt, $ikm, $nonceInfo, 12); - - // encrypt - // "The additional data passed to each invocation of AEAD_AES_128_GCM is a zero-length octet sequence." - $tag = ''; - $encryptedText = openssl_encrypt($payload, 'aes-128-gcm', $contentEncryptionKey, OPENSSL_RAW_DATA, $nonce, $tag); - - // return values in url safe base64 - return [ - 'localPublicKey' => $localPublicKey, - 'salt' => $salt, - 'cipherText' => $encryptedText.$tag, - ]; - } - - public static function getContentCodingHeader($salt, $localPublicKey, $contentEncoding): string - { - if ($contentEncoding === "aes128gcm") { - return $salt - .pack('N*', 4096) - .pack('C*', Utils::safeStrlen($localPublicKey)) - .$localPublicKey; - } - - return ""; - } - - /** - * HMAC-based Extract-and-Expand Key Derivation Function (HKDF). - * - * This is used to derive a secure encryption key from a mostly-secure shared - * secret. - * - * This is a partial implementation of HKDF tailored to our specific purposes. - * In particular, for us the value of N will always be 1, and thus T always - * equals HMAC-Hash(PRK, info | 0x01). - * - * See {@link https://www.rfc-editor.org/rfc/rfc5869.txt} - * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js} - * - * @param string $salt A non-secret random value - * @param string $ikm Input keying material - * @param string $info Application-specific context - * @param int $length The length (in bytes) of the required output key - * - * @return string - */ - private static function hkdf(string $salt, string $ikm, string $info, int $length): string - { - // extract - $prk = hash_hmac('sha256', $ikm, $salt, true); - - // expand - return mb_substr(hash_hmac('sha256', $info.chr(1), $prk, true), 0, $length, '8bit'); - } - - /** - * Creates a context for deriving encryption parameters. - * See section 4.2 of - * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00} - * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}. - * - * @param string $clientPublicKey The client's public key - * @param string $serverPublicKey Our public key - * - * @return null|string - * - * @throws \ErrorException - */ - private static function createContext(string $clientPublicKey, string $serverPublicKey, $contentEncoding): ?string - { - if ($contentEncoding === "aes128gcm") { - return null; - } - - if (Utils::safeStrlen($clientPublicKey) !== 65) { - throw new \ErrorException('Invalid client public key length'); - } - - // This one should never happen, because it's our code that generates the key - if (Utils::safeStrlen($serverPublicKey) !== 65) { - throw new \ErrorException('Invalid server public key length'); - } - - $len = chr(0).'A'; // 65 as Uint16BE - - return chr(0).$len.$clientPublicKey.$len.$serverPublicKey; - } - - /** - * Returns an info record. See sections 3.2 and 3.3 of - * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00} - * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}. - * - * @param string $type The type of the info record - * @param string|null $context The context for the record - * @param string $contentEncoding - * @return string - * - * @throws \ErrorException - */ - private static function createInfo(string $type, ?string $context, string $contentEncoding): string - { - if ($contentEncoding === "aesgcm") { - if (!$context) { - throw new \ErrorException('Context must exist'); - } - - if (Utils::safeStrlen($context) !== 135) { - throw new \ErrorException('Context argument has invalid size'); - } - - return 'Content-Encoding: '.$type.chr(0).'P-256'.$context; - } elseif ($contentEncoding === "aes128gcm") { - return 'Content-Encoding: '.$type.chr(0); - } - - throw new \ErrorException('This content encoding is not supported.'); - } - - /** - * @return array - */ - private static function createLocalKeyObject(): array - { - try { - return self::createLocalKeyObjectUsingOpenSSL(); - } catch (\Exception $e) { - return self::createLocalKeyObjectUsingPurePhpMethod(); - } - } - - /** - * @return array - */ - private static function createLocalKeyObjectUsingPurePhpMethod(): array - { - $curve = NistCurve::curve256(); - $privateKey = $curve->createPrivateKey(); - - return [ - $curve->createPublicKey($privateKey), - $privateKey, - ]; - } - - /** - * @return array - */ - private static function createLocalKeyObjectUsingOpenSSL(): array - { - $keyResource = openssl_pkey_new([ - 'curve_name' => 'prime256v1', - 'private_key_type' => OPENSSL_KEYTYPE_EC, - ]); - - if (!$keyResource) { - throw new \RuntimeException('Unable to create the key'); - } - - $details = openssl_pkey_get_details($keyResource); - openssl_pkey_free($keyResource); - - if (!$details) { - throw new \RuntimeException('Unable to get the key details'); - } - - return [ - PublicKey::create(Point::create( - gmp_init(bin2hex($details['ec']['x']), 16), - gmp_init(bin2hex($details['ec']['y']), 16) - )), - PrivateKey::create(gmp_init(bin2hex($details['ec']['d']), 16)) - ]; - } - - /** - * @param string $userAuthToken - * @param string $userPublicKey - * @param string $localPublicKey - * @param string $sharedSecret - * @param string $contentEncoding - * @return string - * @throws \ErrorException - */ - private static function getIKM(string $userAuthToken, string $userPublicKey, string $localPublicKey, string $sharedSecret, string $contentEncoding): string - { - if (!empty($userAuthToken)) { - if ($contentEncoding === "aesgcm") { - $info = 'Content-Encoding: auth'.chr(0); - } elseif ($contentEncoding === "aes128gcm") { - $info = "WebPush: info".chr(0).$userPublicKey.$localPublicKey; - } else { - throw new \ErrorException("This content encoding is not supported"); - } - - return self::hkdf($userAuthToken, $sharedSecret, $info, 32); - } - - return $sharedSecret; - } -} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Minishlink\WebPush; + +use Base64Url\Base64Url; +use Jose\Component\Core\Util\Ecc\NistCurve; +use Jose\Component\Core\Util\Ecc\Point; +use Jose\Component\Core\Util\Ecc\PrivateKey; +use Jose\Component\Core\Util\Ecc\PublicKey; + +class Encryption +{ + public const MAX_PAYLOAD_LENGTH = 4078; + public const MAX_COMPATIBILITY_PAYLOAD_LENGTH = 3052; + + /** + * @param string $payload + * @param int $maxLengthToPad + * @param string $contentEncoding + * @return string padded payload (plaintext) + * @throws \ErrorException + */ + public static function padPayload(string $payload, int $maxLengthToPad, string $contentEncoding): string + { + $payloadLen = Utils::safeStrlen($payload); + $padLen = $maxLengthToPad ? $maxLengthToPad - $payloadLen : 0; + + if ($contentEncoding === "aesgcm") { + return pack('n*', $padLen).str_pad($payload, $padLen + $payloadLen, chr(0), STR_PAD_LEFT); + } elseif ($contentEncoding === "aes128gcm") { + return str_pad($payload.chr(2), $padLen + $payloadLen, chr(0), STR_PAD_RIGHT); + } else { + throw new \ErrorException("This content encoding is not supported"); + } + } + + /** + * @param string $payload With padding + * @param string $userPublicKey Base 64 encoded (MIME or URL-safe) + * @param string $userAuthToken Base 64 encoded (MIME or URL-safe) + * @param string $contentEncoding + * @return array + * + * @throws \ErrorException + */ + public static function encrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding): array + { + return self::deterministicEncrypt( + $payload, + $userPublicKey, + $userAuthToken, + $contentEncoding, + self::createLocalKeyObject(), + random_bytes(16) + ); + } + + /** + * @param string $payload + * @param string $userPublicKey + * @param string $userAuthToken + * @param string $contentEncoding + * @param array $localKeyObject + * @param string $salt + * @return array + * + * @throws \ErrorException + */ + public static function deterministicEncrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding, array $localKeyObject, string $salt): array + { + $userPublicKey = Base64Url::decode($userPublicKey); + $userAuthToken = Base64Url::decode($userAuthToken); + + $curve = NistCurve::curve256(); + + // get local key pair + list($localPublicKeyObject, $localPrivateKeyObject) = $localKeyObject; + $localPublicKey = hex2bin(Utils::serializePublicKey($localPublicKeyObject)); + if (!$localPublicKey) { + throw new \ErrorException('Failed to convert local public key from hexadecimal to binary'); + } + + // get user public key object + [$userPublicKeyObjectX, $userPublicKeyObjectY] = Utils::unserializePublicKey($userPublicKey); + $userPublicKeyObject = $curve->getPublicKeyFrom( + gmp_init(bin2hex($userPublicKeyObjectX), 16), + gmp_init(bin2hex($userPublicKeyObjectY), 16) + ); + + // get shared secret from user public key and local private key + $sharedSecret = $curve->mul($userPublicKeyObject->getPoint(), $localPrivateKeyObject->getSecret())->getX(); + $sharedSecret = hex2bin(str_pad(gmp_strval($sharedSecret, 16), 64, '0', STR_PAD_LEFT)); + if (!$sharedSecret) { + throw new \ErrorException('Failed to convert shared secret from hexadecimal to binary'); + } + + // section 4.3 + $ikm = self::getIKM($userAuthToken, $userPublicKey, $localPublicKey, $sharedSecret, $contentEncoding); + + // section 4.2 + $context = self::createContext($userPublicKey, $localPublicKey, $contentEncoding); + + // derive the Content Encryption Key + $contentEncryptionKeyInfo = self::createInfo($contentEncoding, $context, $contentEncoding); + $contentEncryptionKey = self::hkdf($salt, $ikm, $contentEncryptionKeyInfo, 16); + + // section 3.3, derive the nonce + $nonceInfo = self::createInfo('nonce', $context, $contentEncoding); + $nonce = self::hkdf($salt, $ikm, $nonceInfo, 12); + + // encrypt + // "The additional data passed to each invocation of AEAD_AES_128_GCM is a zero-length octet sequence." + $tag = ''; + $encryptedText = openssl_encrypt($payload, 'aes-128-gcm', $contentEncryptionKey, OPENSSL_RAW_DATA, $nonce, $tag); + + // return values in url safe base64 + return [ + 'localPublicKey' => $localPublicKey, + 'salt' => $salt, + 'cipherText' => $encryptedText.$tag, + ]; + } + + public static function getContentCodingHeader($salt, $localPublicKey, $contentEncoding): string + { + if ($contentEncoding === "aes128gcm") { + return $salt + .pack('N*', 4096) + .pack('C*', Utils::safeStrlen($localPublicKey)) + .$localPublicKey; + } + + return ""; + } + + /** + * HMAC-based Extract-and-Expand Key Derivation Function (HKDF). + * + * This is used to derive a secure encryption key from a mostly-secure shared + * secret. + * + * This is a partial implementation of HKDF tailored to our specific purposes. + * In particular, for us the value of N will always be 1, and thus T always + * equals HMAC-Hash(PRK, info | 0x01). + * + * See {@link https://www.rfc-editor.org/rfc/rfc5869.txt} + * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js} + * + * @param string $salt A non-secret random value + * @param string $ikm Input keying material + * @param string $info Application-specific context + * @param int $length The length (in bytes) of the required output key + * + * @return string + */ + private static function hkdf(string $salt, string $ikm, string $info, int $length): string + { + // extract + $prk = hash_hmac('sha256', $ikm, $salt, true); + + // expand + return mb_substr(hash_hmac('sha256', $info.chr(1), $prk, true), 0, $length, '8bit'); + } + + /** + * Creates a context for deriving encryption parameters. + * See section 4.2 of + * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00} + * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}. + * + * @param string $clientPublicKey The client's public key + * @param string $serverPublicKey Our public key + * + * @return null|string + * + * @throws \ErrorException + */ + private static function createContext(string $clientPublicKey, string $serverPublicKey, $contentEncoding): ?string + { + if ($contentEncoding === "aes128gcm") { + return null; + } + + if (Utils::safeStrlen($clientPublicKey) !== 65) { + throw new \ErrorException('Invalid client public key length'); + } + + // This one should never happen, because it's our code that generates the key + if (Utils::safeStrlen($serverPublicKey) !== 65) { + throw new \ErrorException('Invalid server public key length'); + } + + $len = chr(0).'A'; // 65 as Uint16BE + + return chr(0).$len.$clientPublicKey.$len.$serverPublicKey; + } + + /** + * Returns an info record. See sections 3.2 and 3.3 of + * {@link https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-00} + * From {@link https://github.com/GoogleChrome/push-encryption-node/blob/master/src/encrypt.js}. + * + * @param string $type The type of the info record + * @param string|null $context The context for the record + * @param string $contentEncoding + * @return string + * + * @throws \ErrorException + */ + private static function createInfo(string $type, ?string $context, string $contentEncoding): string + { + if ($contentEncoding === "aesgcm") { + if (!$context) { + throw new \ErrorException('Context must exist'); + } + + if (Utils::safeStrlen($context) !== 135) { + throw new \ErrorException('Context argument has invalid size'); + } + + return 'Content-Encoding: '.$type.chr(0).'P-256'.$context; + } elseif ($contentEncoding === "aes128gcm") { + return 'Content-Encoding: '.$type.chr(0); + } + + throw new \ErrorException('This content encoding is not supported.'); + } + + /** + * @return array + */ + private static function createLocalKeyObject(): array + { + try { + return self::createLocalKeyObjectUsingOpenSSL(); + } catch (\Exception $e) { + return self::createLocalKeyObjectUsingPurePhpMethod(); + } + } + + /** + * @return array + */ + private static function createLocalKeyObjectUsingPurePhpMethod(): array + { + $curve = NistCurve::curve256(); + $privateKey = $curve->createPrivateKey(); + + return [ + $curve->createPublicKey($privateKey), + $privateKey, + ]; + } + + /** + * @return array + */ + private static function createLocalKeyObjectUsingOpenSSL(): array + { + $keyResource = openssl_pkey_new([ + 'curve_name' => 'prime256v1', + 'private_key_type' => OPENSSL_KEYTYPE_EC, + ]); + + if (!$keyResource) { + throw new \RuntimeException('Unable to create the key'); + } + + $details = openssl_pkey_get_details($keyResource); + openssl_pkey_free($keyResource); + + if (!$details) { + throw new \RuntimeException('Unable to get the key details'); + } + + return [ + PublicKey::create(Point::create( + gmp_init(bin2hex($details['ec']['x']), 16), + gmp_init(bin2hex($details['ec']['y']), 16) + )), + PrivateKey::create(gmp_init(bin2hex($details['ec']['d']), 16)) + ]; + } + + /** + * @param string $userAuthToken + * @param string $userPublicKey + * @param string $localPublicKey + * @param string $sharedSecret + * @param string $contentEncoding + * @return string + * @throws \ErrorException + */ + private static function getIKM(string $userAuthToken, string $userPublicKey, string $localPublicKey, string $sharedSecret, string $contentEncoding): string + { + if (!empty($userAuthToken)) { + if ($contentEncoding === "aesgcm") { + $info = 'Content-Encoding: auth'.chr(0); + } elseif ($contentEncoding === "aes128gcm") { + $info = "WebPush: info".chr(0).$userPublicKey.$localPublicKey; + } else { + throw new \ErrorException("This content encoding is not supported"); + } + + return self::hkdf($userAuthToken, $sharedSecret, $info, 32); + } + + return $sharedSecret; + } +} diff --git a/vendor/minishlink/web-push/src/MessageSentReport.php b/vendor/minishlink/web-push/src/MessageSentReport.php index c569952..a6945e6 100644 --- a/vendor/minishlink/web-push/src/MessageSentReport.php +++ b/vendor/minishlink/web-push/src/MessageSentReport.php @@ -1,181 +1,181 @@ -request = $request; - $this->response = $response; - $this->success = $success; - $this->reason = $reason; - } - - /** - * @return bool - */ - public function isSuccess(): bool - { - return $this->success; - } - - /** - * @param bool $success - * - * @return MessageSentReport - */ - public function setSuccess(bool $success): MessageSentReport - { - $this->success = $success; - return $this; - } - - /** - * @return RequestInterface - */ - public function getRequest(): RequestInterface - { - return $this->request; - } - - /** - * @param RequestInterface $request - * - * @return MessageSentReport - */ - public function setRequest(RequestInterface $request): MessageSentReport - { - $this->request = $request; - return $this; - } - - /** - * @return ResponseInterface | null - */ - public function getResponse(): ?ResponseInterface - { - return $this->response; - } - - /** - * @param ResponseInterface $response - * - * @return MessageSentReport - */ - public function setResponse(ResponseInterface $response): MessageSentReport - { - $this->response = $response; - return $this; - } - - /** - * @return string - */ - public function getEndpoint(): string - { - return $this->request->getUri()->__toString(); - } - - /** - * @return bool - */ - public function isSubscriptionExpired(): bool - { - if (!$this->response) { - return false; - } - - return \in_array($this->response->getStatusCode(), [404, 410], true); - } - - /** - * @return string - */ - public function getReason(): string - { - return $this->reason; - } - - /** - * @param string $reason - * - * @return MessageSentReport - */ - public function setReason(string $reason): MessageSentReport - { - $this->reason = $reason; - return $this; - } - - /** - * @return string - */ - public function getRequestPayload(): string - { - return $this->request->getBody()->getContents(); - } - - /** - * @return string | null - */ - public function getResponseContent(): ?string - { - if (!$this->response) { - return null; - } - - return $this->response->getBody()->getContents(); - } - - /** - * @return array|mixed - */ - public function jsonSerialize() - { - return [ - 'success' => $this->isSuccess(), - 'expired' => $this->isSubscriptionExpired(), - 'reason' => $this->reason, - 'endpoint' => $this->getEndpoint(), - 'payload' => $this->request->getBody()->getContents(), - ]; - } -} +request = $request; + $this->response = $response; + $this->success = $success; + $this->reason = $reason; + } + + /** + * @return bool + */ + public function isSuccess(): bool + { + return $this->success; + } + + /** + * @param bool $success + * + * @return MessageSentReport + */ + public function setSuccess(bool $success): MessageSentReport + { + $this->success = $success; + return $this; + } + + /** + * @return RequestInterface + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * @param RequestInterface $request + * + * @return MessageSentReport + */ + public function setRequest(RequestInterface $request): MessageSentReport + { + $this->request = $request; + return $this; + } + + /** + * @return ResponseInterface | null + */ + public function getResponse(): ?ResponseInterface + { + return $this->response; + } + + /** + * @param ResponseInterface $response + * + * @return MessageSentReport + */ + public function setResponse(ResponseInterface $response): MessageSentReport + { + $this->response = $response; + return $this; + } + + /** + * @return string + */ + public function getEndpoint(): string + { + return $this->request->getUri()->__toString(); + } + + /** + * @return bool + */ + public function isSubscriptionExpired(): bool + { + if (!$this->response) { + return false; + } + + return \in_array($this->response->getStatusCode(), [404, 410], true); + } + + /** + * @return string + */ + public function getReason(): string + { + return $this->reason; + } + + /** + * @param string $reason + * + * @return MessageSentReport + */ + public function setReason(string $reason): MessageSentReport + { + $this->reason = $reason; + return $this; + } + + /** + * @return string + */ + public function getRequestPayload(): string + { + return $this->request->getBody()->getContents(); + } + + /** + * @return string | null + */ + public function getResponseContent(): ?string + { + if (!$this->response) { + return null; + } + + return $this->response->getBody()->getContents(); + } + + /** + * @return array|mixed + */ + public function jsonSerialize() + { + return [ + 'success' => $this->isSuccess(), + 'expired' => $this->isSubscriptionExpired(), + 'reason' => $this->reason, + 'endpoint' => $this->getEndpoint(), + 'payload' => $this->request->getBody()->getContents(), + ]; + } +} diff --git a/vendor/minishlink/web-push/src/Notification.php b/vendor/minishlink/web-push/src/Notification.php index 1107404..15d634f 100644 --- a/vendor/minishlink/web-push/src/Notification.php +++ b/vendor/minishlink/web-push/src/Notification.php @@ -1,86 +1,86 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Minishlink\WebPush; - -class Notification -{ - /** @var SubscriptionInterface */ - private $subscription; - - /** @var null|string */ - private $payload; - - /** @var array Options : TTL, urgency, topic */ - private $options; - - /** @var array Auth details : GCM, VAPID */ - private $auth; - - /** - * Notification constructor. - * - * @param SubscriptionInterface $subscription - * @param null|string $payload - * @param array $options - * @param array $auth - */ - public function __construct(SubscriptionInterface $subscription, ?string $payload, array $options, array $auth) - { - $this->subscription = $subscription; - $this->payload = $payload; - $this->options = $options; - $this->auth = $auth; - } - - /** - * @return SubscriptionInterface - */ - public function getSubscription(): SubscriptionInterface - { - return $this->subscription; - } - - /** - * @return null|string - */ - public function getPayload(): ?string - { - return $this->payload; - } - - /** - * @param array $defaultOptions - * - * @return array - */ - public function getOptions(array $defaultOptions = []): array - { - $options = $this->options; - $options['TTL'] = array_key_exists('TTL', $options) ? $options['TTL'] : $defaultOptions['TTL']; - $options['urgency'] = array_key_exists('urgency', $options) ? $options['urgency'] : $defaultOptions['urgency']; - $options['topic'] = array_key_exists('topic', $options) ? $options['topic'] : $defaultOptions['topic']; - - return $options; - } - - /** - * @param array $defaultAuth - * - * @return array - */ - public function getAuth(array $defaultAuth): array - { - return count($this->auth) > 0 ? $this->auth : $defaultAuth; - } -} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Minishlink\WebPush; + +class Notification +{ + /** @var SubscriptionInterface */ + private $subscription; + + /** @var null|string */ + private $payload; + + /** @var array Options : TTL, urgency, topic */ + private $options; + + /** @var array Auth details : GCM, VAPID */ + private $auth; + + /** + * Notification constructor. + * + * @param SubscriptionInterface $subscription + * @param null|string $payload + * @param array $options + * @param array $auth + */ + public function __construct(SubscriptionInterface $subscription, ?string $payload, array $options, array $auth) + { + $this->subscription = $subscription; + $this->payload = $payload; + $this->options = $options; + $this->auth = $auth; + } + + /** + * @return SubscriptionInterface + */ + public function getSubscription(): SubscriptionInterface + { + return $this->subscription; + } + + /** + * @return null|string + */ + public function getPayload(): ?string + { + return $this->payload; + } + + /** + * @param array $defaultOptions + * + * @return array + */ + public function getOptions(array $defaultOptions = []): array + { + $options = $this->options; + $options['TTL'] = array_key_exists('TTL', $options) ? $options['TTL'] : $defaultOptions['TTL']; + $options['urgency'] = array_key_exists('urgency', $options) ? $options['urgency'] : $defaultOptions['urgency']; + $options['topic'] = array_key_exists('topic', $options) ? $options['topic'] : $defaultOptions['topic']; + + return $options; + } + + /** + * @param array $defaultAuth + * + * @return array + */ + public function getAuth(array $defaultAuth): array + { + return count($this->auth) > 0 ? $this->auth : $defaultAuth; + } +} diff --git a/vendor/minishlink/web-push/src/Subscription.php b/vendor/minishlink/web-push/src/Subscription.php index 1232893..f570c30 100644 --- a/vendor/minishlink/web-push/src/Subscription.php +++ b/vendor/minishlink/web-push/src/Subscription.php @@ -1,122 +1,122 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Minishlink\WebPush; - -class Subscription implements SubscriptionInterface -{ - /** @var string */ - private $endpoint; - - /** @var null|string */ - private $publicKey; - - /** @var null|string */ - private $authToken; - - /** @var null|string */ - private $contentEncoding; - - /** - * Subscription constructor. - * - * @param string $endpoint - * @param null|string $publicKey - * @param null|string $authToken - * @param string $contentEncoding (Optional) Must be "aesgcm" - * @throws \ErrorException - */ - public function __construct( - string $endpoint, - ?string $publicKey = null, - ?string $authToken = null, - ?string $contentEncoding = null - ) { - $this->endpoint = $endpoint; - - if ($publicKey || $authToken || $contentEncoding) { - $supportedContentEncodings = ['aesgcm', 'aes128gcm']; - if ($contentEncoding && !in_array($contentEncoding, $supportedContentEncodings)) { - throw new \ErrorException('This content encoding ('.$contentEncoding.') is not supported.'); - } - - $this->publicKey = $publicKey; - $this->authToken = $authToken; - $this->contentEncoding = $contentEncoding ?: "aesgcm"; - } - } - - /** - * Subscription factory. - * - * @param array $associativeArray (with keys endpoint, publicKey, authToken, contentEncoding) - * @return self - * @throws \ErrorException - */ - public static function create(array $associativeArray): self - { - if (array_key_exists('keys', $associativeArray) && is_array($associativeArray['keys'])) { - return new self( - $associativeArray['endpoint'], - $associativeArray['keys']['p256dh'] ?? null, - $associativeArray['keys']['auth'] ?? null, - $associativeArray['contentEncoding'] ?? "aesgcm" - ); - } - - if (array_key_exists('publicKey', $associativeArray) || array_key_exists('authToken', $associativeArray) || array_key_exists('contentEncoding', $associativeArray)) { - return new self( - $associativeArray['endpoint'], - $associativeArray['publicKey'] ?? null, - $associativeArray['authToken'] ?? null, - $associativeArray['contentEncoding'] ?? "aesgcm" - ); - } - - return new self( - $associativeArray['endpoint'] - ); - } - - /** - * {@inheritDoc} - */ - public function getEndpoint(): string - { - return $this->endpoint; - } - - /** - * {@inheritDoc} - */ - public function getPublicKey(): ?string - { - return $this->publicKey; - } - - /** - * {@inheritDoc} - */ - public function getAuthToken(): ?string - { - return $this->authToken; - } - - /** - * {@inheritDoc} - */ - public function getContentEncoding(): ?string - { - return $this->contentEncoding; - } -} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Minishlink\WebPush; + +class Subscription implements SubscriptionInterface +{ + /** @var string */ + private $endpoint; + + /** @var null|string */ + private $publicKey; + + /** @var null|string */ + private $authToken; + + /** @var null|string */ + private $contentEncoding; + + /** + * Subscription constructor. + * + * @param string $endpoint + * @param null|string $publicKey + * @param null|string $authToken + * @param string $contentEncoding (Optional) Must be "aesgcm" + * @throws \ErrorException + */ + public function __construct( + string $endpoint, + ?string $publicKey = null, + ?string $authToken = null, + ?string $contentEncoding = null + ) { + $this->endpoint = $endpoint; + + if ($publicKey || $authToken || $contentEncoding) { + $supportedContentEncodings = ['aesgcm', 'aes128gcm']; + if ($contentEncoding && !in_array($contentEncoding, $supportedContentEncodings)) { + throw new \ErrorException('This content encoding ('.$contentEncoding.') is not supported.'); + } + + $this->publicKey = $publicKey; + $this->authToken = $authToken; + $this->contentEncoding = $contentEncoding ?: "aesgcm"; + } + } + + /** + * Subscription factory. + * + * @param array $associativeArray (with keys endpoint, publicKey, authToken, contentEncoding) + * @return self + * @throws \ErrorException + */ + public static function create(array $associativeArray): self + { + if (array_key_exists('keys', $associativeArray) && is_array($associativeArray['keys'])) { + return new self( + $associativeArray['endpoint'], + $associativeArray['keys']['p256dh'] ?? null, + $associativeArray['keys']['auth'] ?? null, + $associativeArray['contentEncoding'] ?? "aesgcm" + ); + } + + if (array_key_exists('publicKey', $associativeArray) || array_key_exists('authToken', $associativeArray) || array_key_exists('contentEncoding', $associativeArray)) { + return new self( + $associativeArray['endpoint'], + $associativeArray['publicKey'] ?? null, + $associativeArray['authToken'] ?? null, + $associativeArray['contentEncoding'] ?? "aesgcm" + ); + } + + return new self( + $associativeArray['endpoint'] + ); + } + + /** + * {@inheritDoc} + */ + public function getEndpoint(): string + { + return $this->endpoint; + } + + /** + * {@inheritDoc} + */ + public function getPublicKey(): ?string + { + return $this->publicKey; + } + + /** + * {@inheritDoc} + */ + public function getAuthToken(): ?string + { + return $this->authToken; + } + + /** + * {@inheritDoc} + */ + public function getContentEncoding(): ?string + { + return $this->contentEncoding; + } +} diff --git a/vendor/minishlink/web-push/src/SubscriptionInterface.php b/vendor/minishlink/web-push/src/SubscriptionInterface.php index e3f18d6..9e09bed 100644 --- a/vendor/minishlink/web-push/src/SubscriptionInterface.php +++ b/vendor/minishlink/web-push/src/SubscriptionInterface.php @@ -1,40 +1,40 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Minishlink\WebPush; - -/** - * @author Sergii Bondarenko - */ -interface SubscriptionInterface -{ - /** - * @return string - */ - public function getEndpoint(): string; - - /** - * @return null|string - */ - public function getPublicKey(): ?string; - - /** - * @return null|string - */ - public function getAuthToken(): ?string; - - /** - * @return null|string - */ - public function getContentEncoding(): ?string; -} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Minishlink\WebPush; + +/** + * @author Sergii Bondarenko + */ +interface SubscriptionInterface +{ + /** + * @return string + */ + public function getEndpoint(): string; + + /** + * @return null|string + */ + public function getPublicKey(): ?string; + + /** + * @return null|string + */ + public function getAuthToken(): ?string; + + /** + * @return null|string + */ + public function getContentEncoding(): ?string; +} diff --git a/vendor/minishlink/web-push/src/Utils.php b/vendor/minishlink/web-push/src/Utils.php index 30c2018..bd7f6c4 100644 --- a/vendor/minishlink/web-push/src/Utils.php +++ b/vendor/minishlink/web-push/src/Utils.php @@ -1,63 +1,63 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Minishlink\WebPush; - -use Jose\Component\Core\Util\Ecc\PublicKey; - -class Utils -{ - /** - * @param string $value - * - * @return int - */ - public static function safeStrlen(string $value): int - { - return mb_strlen($value, '8bit'); - } - - /** - * @param PublicKey $publicKey - * - * @return string - */ - public static function serializePublicKey(PublicKey $publicKey): string - { - $hexString = '04'; - $hexString .= str_pad(gmp_strval($publicKey->getPoint()->getX(), 16), 64, '0', STR_PAD_LEFT); - $hexString .= str_pad(gmp_strval($publicKey->getPoint()->getY(), 16), 64, '0', STR_PAD_LEFT); - - return $hexString; - } - - /** - * @param string $data - * - * @return array - */ - public static function unserializePublicKey(string $data): array - { - $data = bin2hex($data); - if (mb_substr($data, 0, 2, '8bit') !== '04') { - throw new \InvalidArgumentException('Invalid data: only uncompressed keys are supported.'); - } - $data = mb_substr($data, 2, null, '8bit'); - $dataLength = self::safeStrlen($data); - - return [ - hex2bin(mb_substr($data, 0, $dataLength / 2, '8bit')), - hex2bin(mb_substr($data, $dataLength / 2, null, '8bit')), - ]; - } -} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Minishlink\WebPush; + +use Jose\Component\Core\Util\Ecc\PublicKey; + +class Utils +{ + /** + * @param string $value + * + * @return int + */ + public static function safeStrlen(string $value): int + { + return mb_strlen($value, '8bit'); + } + + /** + * @param PublicKey $publicKey + * + * @return string + */ + public static function serializePublicKey(PublicKey $publicKey): string + { + $hexString = '04'; + $hexString .= str_pad(gmp_strval($publicKey->getPoint()->getX(), 16), 64, '0', STR_PAD_LEFT); + $hexString .= str_pad(gmp_strval($publicKey->getPoint()->getY(), 16), 64, '0', STR_PAD_LEFT); + + return $hexString; + } + + /** + * @param string $data + * + * @return array + */ + public static function unserializePublicKey(string $data): array + { + $data = bin2hex($data); + if (mb_substr($data, 0, 2, '8bit') !== '04') { + throw new \InvalidArgumentException('Invalid data: only uncompressed keys are supported.'); + } + $data = mb_substr($data, 2, null, '8bit'); + $dataLength = self::safeStrlen($data); + + return [ + hex2bin(mb_substr($data, 0, $dataLength / 2, '8bit')), + hex2bin(mb_substr($data, $dataLength / 2, null, '8bit')), + ]; + } +} diff --git a/vendor/minishlink/web-push/src/VAPID.php b/vendor/minishlink/web-push/src/VAPID.php index c741ec9..e1f555f 100644 --- a/vendor/minishlink/web-push/src/VAPID.php +++ b/vendor/minishlink/web-push/src/VAPID.php @@ -1,197 +1,197 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Minishlink\WebPush; - -use Base64Url\Base64Url; -use Jose\Component\Core\AlgorithmManager; -use Jose\Component\Core\Converter\StandardConverter; -use Jose\Component\Core\JWK; -use Jose\Component\Core\Util\Ecc\NistCurve; -use Jose\Component\Core\Util\Ecc\Point; -use Jose\Component\Core\Util\Ecc\PublicKey; -use Jose\Component\KeyManagement\JWKFactory; -use Jose\Component\Signature\Algorithm\ES256; -use Jose\Component\Signature\JWSBuilder; -use Jose\Component\Signature\Serializer\CompactSerializer; - -class VAPID -{ - private const PUBLIC_KEY_LENGTH = 65; - private const PRIVATE_KEY_LENGTH = 32; - - /** - * @param array $vapid - * - * @return array - * - * @throws \ErrorException - */ - public static function validate(array $vapid): array - { - if (!isset($vapid['subject'])) { - throw new \ErrorException('[VAPID] You must provide a subject that is either a mailto: or a URL.'); - } - - if (isset($vapid['pemFile'])) { - $vapid['pem'] = file_get_contents($vapid['pemFile']); - - if (!$vapid['pem']) { - throw new \ErrorException('Error loading PEM file.'); - } - } - - if (isset($vapid['pem'])) { - $jwk = JWKFactory::createFromKey($vapid['pem']); - if ($jwk->get('kty') !== 'EC' || !$jwk->has('d') || !$jwk->has('x') || !$jwk->has('y')) { - throw new \ErrorException('Invalid PEM data.'); - } - $publicKey = PublicKey::create(Point::create( - gmp_init(bin2hex(Base64Url::decode($jwk->get('x'))), 16), - gmp_init(bin2hex(Base64Url::decode($jwk->get('y'))), 16) - )); - - $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey)); - if (!$binaryPublicKey) { - throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary'); - } - $vapid['publicKey'] = base64_encode($binaryPublicKey); - $vapid['privateKey'] = base64_encode(str_pad(Base64Url::decode($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); - } - - if (!isset($vapid['publicKey'])) { - throw new \ErrorException('[VAPID] You must provide a public key.'); - } - - $publicKey = Base64Url::decode($vapid['publicKey']); - - if (Utils::safeStrlen($publicKey) !== self::PUBLIC_KEY_LENGTH) { - throw new \ErrorException('[VAPID] Public key should be 65 bytes long when decoded.'); - } - - if (!isset($vapid['privateKey'])) { - throw new \ErrorException('[VAPID] You must provide a private key.'); - } - - $privateKey = Base64Url::decode($vapid['privateKey']); - - if (Utils::safeStrlen($privateKey) !== self::PRIVATE_KEY_LENGTH) { - throw new \ErrorException('[VAPID] Private key should be 32 bytes long when decoded.'); - } - - return [ - 'subject' => $vapid['subject'], - 'publicKey' => $publicKey, - 'privateKey' => $privateKey, - ]; - } - - /** - * This method takes the required VAPID parameters and returns the required - * header to be added to a Web Push Protocol Request. - * - * @param string $audience This must be the origin of the push service - * @param string $subject This should be a URL or a 'mailto:' email address - * @param string $publicKey The decoded VAPID public key - * @param string $privateKey The decoded VAPID private key - * @param string $contentEncoding - * @param null|int $expiration The expiration of the VAPID JWT. (UNIX timestamp) - * - * @return array Returns an array with the 'Authorization' and 'Crypto-Key' values to be used as headers - * @throws \ErrorException - */ - public static function getVapidHeaders(string $audience, string $subject, string $publicKey, string $privateKey, string $contentEncoding, ?int $expiration = null) - { - $expirationLimit = time() + 43200; // equal margin of error between 0 and 24h - if (null === $expiration || $expiration > $expirationLimit) { - $expiration = $expirationLimit; - } - - $header = [ - 'typ' => 'JWT', - 'alg' => 'ES256', - ]; - - $jwtPayload = json_encode([ - 'aud' => $audience, - 'exp' => $expiration, - 'sub' => $subject, - ], JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK); - if (!$jwtPayload) { - throw new \ErrorException('Failed to encode JWT payload in JSON'); - } - - list($x, $y) = Utils::unserializePublicKey($publicKey); - $jwk = JWK::create([ - 'kty' => 'EC', - 'crv' => 'P-256', - 'x' => Base64Url::encode($x), - 'y' => Base64Url::encode($y), - 'd' => Base64Url::encode($privateKey), - ]); - - $jsonConverter = new StandardConverter(); - $jwsCompactSerializer = new CompactSerializer($jsonConverter); - $jwsBuilder = new JWSBuilder($jsonConverter, AlgorithmManager::create([new ES256()])); - $jws = $jwsBuilder - ->create() - ->withPayload($jwtPayload) - ->addSignature($jwk, $header) - ->build(); - - $jwt = $jwsCompactSerializer->serialize($jws, 0); - $encodedPublicKey = Base64Url::encode($publicKey); - - if ($contentEncoding === "aesgcm") { - return [ - 'Authorization' => 'WebPush '.$jwt, - 'Crypto-Key' => 'p256ecdsa='.$encodedPublicKey, - ]; - } elseif ($contentEncoding === 'aes128gcm') { - return [ - 'Authorization' => 'vapid t='.$jwt.', k='.$encodedPublicKey, - ]; - } - - throw new \ErrorException('This content encoding is not supported'); - } - - /** - * This method creates VAPID keys in case you would not be able to have a Linux bash. - * DO NOT create keys at each initialization! Save those keys and reuse them. - * - * @return array - * @throws \ErrorException - */ - public static function createVapidKeys(): array - { - $curve = NistCurve::curve256(); - $privateKey = $curve->createPrivateKey(); - $publicKey = $curve->createPublicKey($privateKey); - - $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey)); - if (!$binaryPublicKey) { - throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary'); - } - - $binaryPrivateKey = hex2bin(str_pad(gmp_strval($privateKey->getSecret(), 16), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); - if (!$binaryPrivateKey) { - throw new \ErrorException('Failed to convert VAPID private key from hexadecimal to binary'); - } - - return [ - 'publicKey' => Base64Url::encode($binaryPublicKey), - 'privateKey' => Base64Url::encode($binaryPrivateKey) - ]; - } -} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Minishlink\WebPush; + +use Base64Url\Base64Url; +use Jose\Component\Core\AlgorithmManager; +use Jose\Component\Core\Converter\StandardConverter; +use Jose\Component\Core\JWK; +use Jose\Component\Core\Util\Ecc\NistCurve; +use Jose\Component\Core\Util\Ecc\Point; +use Jose\Component\Core\Util\Ecc\PublicKey; +use Jose\Component\KeyManagement\JWKFactory; +use Jose\Component\Signature\Algorithm\ES256; +use Jose\Component\Signature\JWSBuilder; +use Jose\Component\Signature\Serializer\CompactSerializer; + +class VAPID +{ + private const PUBLIC_KEY_LENGTH = 65; + private const PRIVATE_KEY_LENGTH = 32; + + /** + * @param array $vapid + * + * @return array + * + * @throws \ErrorException + */ + public static function validate(array $vapid): array + { + if (!isset($vapid['subject'])) { + throw new \ErrorException('[VAPID] You must provide a subject that is either a mailto: or a URL.'); + } + + if (isset($vapid['pemFile'])) { + $vapid['pem'] = file_get_contents($vapid['pemFile']); + + if (!$vapid['pem']) { + throw new \ErrorException('Error loading PEM file.'); + } + } + + if (isset($vapid['pem'])) { + $jwk = JWKFactory::createFromKey($vapid['pem']); + if ($jwk->get('kty') !== 'EC' || !$jwk->has('d') || !$jwk->has('x') || !$jwk->has('y')) { + throw new \ErrorException('Invalid PEM data.'); + } + $publicKey = PublicKey::create(Point::create( + gmp_init(bin2hex(Base64Url::decode($jwk->get('x'))), 16), + gmp_init(bin2hex(Base64Url::decode($jwk->get('y'))), 16) + )); + + $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey)); + if (!$binaryPublicKey) { + throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary'); + } + $vapid['publicKey'] = base64_encode($binaryPublicKey); + $vapid['privateKey'] = base64_encode(str_pad(Base64Url::decode($jwk->get('d')), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); + } + + if (!isset($vapid['publicKey'])) { + throw new \ErrorException('[VAPID] You must provide a public key.'); + } + + $publicKey = Base64Url::decode($vapid['publicKey']); + + if (Utils::safeStrlen($publicKey) !== self::PUBLIC_KEY_LENGTH) { + throw new \ErrorException('[VAPID] Public key should be 65 bytes long when decoded.'); + } + + if (!isset($vapid['privateKey'])) { + throw new \ErrorException('[VAPID] You must provide a private key.'); + } + + $privateKey = Base64Url::decode($vapid['privateKey']); + + if (Utils::safeStrlen($privateKey) !== self::PRIVATE_KEY_LENGTH) { + throw new \ErrorException('[VAPID] Private key should be 32 bytes long when decoded.'); + } + + return [ + 'subject' => $vapid['subject'], + 'publicKey' => $publicKey, + 'privateKey' => $privateKey, + ]; + } + + /** + * This method takes the required VAPID parameters and returns the required + * header to be added to a Web Push Protocol Request. + * + * @param string $audience This must be the origin of the push service + * @param string $subject This should be a URL or a 'mailto:' email address + * @param string $publicKey The decoded VAPID public key + * @param string $privateKey The decoded VAPID private key + * @param string $contentEncoding + * @param null|int $expiration The expiration of the VAPID JWT. (UNIX timestamp) + * + * @return array Returns an array with the 'Authorization' and 'Crypto-Key' values to be used as headers + * @throws \ErrorException + */ + public static function getVapidHeaders(string $audience, string $subject, string $publicKey, string $privateKey, string $contentEncoding, ?int $expiration = null) + { + $expirationLimit = time() + 43200; // equal margin of error between 0 and 24h + if (null === $expiration || $expiration > $expirationLimit) { + $expiration = $expirationLimit; + } + + $header = [ + 'typ' => 'JWT', + 'alg' => 'ES256', + ]; + + $jwtPayload = json_encode([ + 'aud' => $audience, + 'exp' => $expiration, + 'sub' => $subject, + ], JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK); + if (!$jwtPayload) { + throw new \ErrorException('Failed to encode JWT payload in JSON'); + } + + list($x, $y) = Utils::unserializePublicKey($publicKey); + $jwk = JWK::create([ + 'kty' => 'EC', + 'crv' => 'P-256', + 'x' => Base64Url::encode($x), + 'y' => Base64Url::encode($y), + 'd' => Base64Url::encode($privateKey), + ]); + + $jsonConverter = new StandardConverter(); + $jwsCompactSerializer = new CompactSerializer($jsonConverter); + $jwsBuilder = new JWSBuilder($jsonConverter, AlgorithmManager::create([new ES256()])); + $jws = $jwsBuilder + ->create() + ->withPayload($jwtPayload) + ->addSignature($jwk, $header) + ->build(); + + $jwt = $jwsCompactSerializer->serialize($jws, 0); + $encodedPublicKey = Base64Url::encode($publicKey); + + if ($contentEncoding === "aesgcm") { + return [ + 'Authorization' => 'WebPush '.$jwt, + 'Crypto-Key' => 'p256ecdsa='.$encodedPublicKey, + ]; + } elseif ($contentEncoding === 'aes128gcm') { + return [ + 'Authorization' => 'vapid t='.$jwt.', k='.$encodedPublicKey, + ]; + } + + throw new \ErrorException('This content encoding is not supported'); + } + + /** + * This method creates VAPID keys in case you would not be able to have a Linux bash. + * DO NOT create keys at each initialization! Save those keys and reuse them. + * + * @return array + * @throws \ErrorException + */ + public static function createVapidKeys(): array + { + $curve = NistCurve::curve256(); + $privateKey = $curve->createPrivateKey(); + $publicKey = $curve->createPublicKey($privateKey); + + $binaryPublicKey = hex2bin(Utils::serializePublicKey($publicKey)); + if (!$binaryPublicKey) { + throw new \ErrorException('Failed to convert VAPID public key from hexadecimal to binary'); + } + + $binaryPrivateKey = hex2bin(str_pad(gmp_strval($privateKey->getSecret(), 16), 2 * self::PRIVATE_KEY_LENGTH, '0', STR_PAD_LEFT)); + if (!$binaryPrivateKey) { + throw new \ErrorException('Failed to convert VAPID private key from hexadecimal to binary'); + } + + return [ + 'publicKey' => Base64Url::encode($binaryPublicKey), + 'privateKey' => Base64Url::encode($binaryPrivateKey) + ]; + } +} diff --git a/vendor/minishlink/web-push/src/WebPush.php b/vendor/minishlink/web-push/src/WebPush.php index aaa9b4b..1f83812 100644 --- a/vendor/minishlink/web-push/src/WebPush.php +++ b/vendor/minishlink/web-push/src/WebPush.php @@ -1,412 +1,412 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Minishlink\WebPush; - -use Base64Url\Base64Url; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\RequestException; -use GuzzleHttp\Psr7\Request; -use Psr\Http\Message\ResponseInterface; - -class WebPush -{ - public const GCM_URL = 'https://android.googleapis.com/gcm/send'; - public const FCM_BASE_URL = 'https://fcm.googleapis.com'; - - /** - * @var Client - */ - private $client; - - /** - * @var array - */ - private $auth; - - /** - * @var null|array Array of array of Notifications - */ - private $notifications; - - /** - * @var array Default options : TTL, urgency, topic, batchSize - */ - private $defaultOptions; - - /** - * @var int Automatic padding of payloads, if disabled, trade security for bandwidth - */ - private $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH; - - /** - * @var bool Reuse VAPID headers in the same flush session to improve performance - */ - private $reuseVAPIDHeaders = false; - - /** - * @var array Dictionary for VAPID headers cache - */ - private $vapidHeaders = []; - - /** - * WebPush constructor. - * - * @param array $auth Some servers needs authentication - * @param array $defaultOptions TTL, urgency, topic, batchSize - * @param int|null $timeout Timeout of POST request - * @param array $clientOptions - * - * @throws \ErrorException - */ - public function __construct(array $auth = [], array $defaultOptions = [], ?int $timeout = 30, array $clientOptions = []) - { - $extensions = [ - 'curl' => '[WebPush] curl extension is not loaded but is required. You can fix this in your php.ini.', - 'gmp' => '[WebPush] gmp extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.', - 'mbstring' => '[WebPush] mbstring extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.', - 'openssl' => '[WebPush] openssl extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.', - ]; - foreach ($extensions as $extension => $message) { - if (!extension_loaded($extension)) { - trigger_error($message, E_USER_WARNING); - } - } - - if (ini_get('mbstring.func_overload') >= 2) { - trigger_error("[WebPush] mbstring.func_overload is enabled for str* functions. You must disable it if you want to send push notifications with payload or use VAPID. You can fix this in your php.ini.", E_USER_NOTICE); - } - - if (isset($auth['VAPID'])) { - $auth['VAPID'] = VAPID::validate($auth['VAPID']); - } - - $this->auth = $auth; - - $this->setDefaultOptions($defaultOptions); - - if (!array_key_exists('timeout', $clientOptions) && isset($timeout)) { - $clientOptions['timeout'] = $timeout; - } - $this->client = new Client($clientOptions); - } - - /** - * Send a notification. - * - * @param SubscriptionInterface $subscription - * @param string|null $payload If you want to send an array, json_encode it - * @param bool $flush If you want to flush directly (usually when you send only one notification) - * @param array $options Array with several options tied to this notification. If not set, will use the default options that you can set in the WebPush object - * @param array $auth Use this auth details instead of what you provided when creating WebPush - * - * @return \Generator|MessageSentReport[]|true Return an array of information if $flush is set to true and the queued requests has failed. - * Else return true - * - * @throws \ErrorException - */ - public function sendNotification(SubscriptionInterface $subscription, ?string $payload = null, bool $flush = false, array $options = [], array $auth = []) - { - if (isset($payload)) { - if (Utils::safeStrlen($payload) > Encryption::MAX_PAYLOAD_LENGTH) { - throw new \ErrorException('Size of payload must not be greater than '.Encryption::MAX_PAYLOAD_LENGTH.' octets.'); - } - - $contentEncoding = $subscription->getContentEncoding(); - if (!$contentEncoding) { - throw new \ErrorException('Subscription should have a content encoding'); - } - - $payload = Encryption::padPayload($payload, $this->automaticPadding, $contentEncoding); - } - - if (array_key_exists('VAPID', $auth)) { - $auth['VAPID'] = VAPID::validate($auth['VAPID']); - } - - $this->notifications[] = new Notification($subscription, $payload, $options, $auth); - - return false !== $flush ? $this->flush() : true; - } - - /** - * Flush notifications. Triggers the requests. - * - * @param null|int $batchSize Defaults the value defined in defaultOptions during instantiation (which defaults to 1000). - * - * @return \Generator|MessageSentReport[] - * @throws \ErrorException - */ - public function flush(?int $batchSize = null): \Generator - { - if (null === $this->notifications || empty($this->notifications)) { - yield from []; - return; - } - - if (null === $batchSize) { - $batchSize = $this->defaultOptions['batchSize']; - } - - $batches = array_chunk($this->notifications, $batchSize); - - // reset queue - $this->notifications = []; - - foreach ($batches as $batch) { - // for each endpoint server type - $requests = $this->prepare($batch); - - $promises = []; - - foreach ($requests as $request) { - $promises[] = $this->client->sendAsync($request) - ->then(function ($response) use ($request) { - /** @var ResponseInterface $response * */ - return new MessageSentReport($request, $response); - }) - ->otherwise(function ($reason) { - /** @var RequestException $reason **/ - return new MessageSentReport($reason->getRequest(), $reason->getResponse(), false, $reason->getMessage()); - }); - } - - foreach ($promises as $promise) { - yield $promise->wait(); - } - } - - if ($this->reuseVAPIDHeaders) { - $this->vapidHeaders = []; - } - } - - /** - * @param array $notifications - * - * @return array - * - * @throws \ErrorException - */ - private function prepare(array $notifications): array - { - $requests = []; - /** @var Notification $notification */ - foreach ($notifications as $notification) { - $subscription = $notification->getSubscription(); - $endpoint = $subscription->getEndpoint(); - $userPublicKey = $subscription->getPublicKey(); - $userAuthToken = $subscription->getAuthToken(); - $contentEncoding = $subscription->getContentEncoding(); - $payload = $notification->getPayload(); - $options = $notification->getOptions($this->getDefaultOptions()); - $auth = $notification->getAuth($this->auth); - - if (!empty($payload) && !empty($userPublicKey) && !empty($userAuthToken)) { - if (!$contentEncoding) { - throw new \ErrorException('Subscription should have a content encoding'); - } - - $encrypted = Encryption::encrypt($payload, $userPublicKey, $userAuthToken, $contentEncoding); - $cipherText = $encrypted['cipherText']; - $salt = $encrypted['salt']; - $localPublicKey = $encrypted['localPublicKey']; - - $headers = [ - 'Content-Type' => 'application/octet-stream', - 'Content-Encoding' => $contentEncoding, - ]; - - if ($contentEncoding === "aesgcm") { - $headers['Encryption'] = 'salt='.Base64Url::encode($salt); - $headers['Crypto-Key'] = 'dh='.Base64Url::encode($localPublicKey); - } - - $encryptionContentCodingHeader = Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding); - $content = $encryptionContentCodingHeader.$cipherText; - - $headers['Content-Length'] = Utils::safeStrlen($content); - } else { - $headers = [ - 'Content-Length' => 0, - ]; - - $content = ''; - } - - $headers['TTL'] = $options['TTL']; - - if (isset($options['urgency'])) { - $headers['Urgency'] = $options['urgency']; - } - - if (isset($options['topic'])) { - $headers['Topic'] = $options['topic']; - } - - // if GCM - if (substr($endpoint, 0, strlen(self::GCM_URL)) === self::GCM_URL) { - if (array_key_exists('GCM', $auth)) { - $headers['Authorization'] = 'key='.$auth['GCM']; - } else { - throw new \ErrorException('No GCM API Key specified.'); - } - } - // if VAPID (GCM doesn't support it but FCM does) - elseif (array_key_exists('VAPID', $auth) && $contentEncoding) { - $audience = parse_url($endpoint, PHP_URL_SCHEME).'://'.parse_url($endpoint, PHP_URL_HOST); - if (!parse_url($audience)) { - throw new \ErrorException('Audience "'.$audience.'"" could not be generated.'); - } - - $vapidHeaders = $this->getVAPIDHeaders($audience, $contentEncoding, $auth['VAPID']); - - $headers['Authorization'] = $vapidHeaders['Authorization']; - - if ($contentEncoding === 'aesgcm') { - if (array_key_exists('Crypto-Key', $headers)) { - $headers['Crypto-Key'] .= ';'.$vapidHeaders['Crypto-Key']; - } else { - $headers['Crypto-Key'] = $vapidHeaders['Crypto-Key']; - } - } - } - - $requests[] = new Request('POST', $endpoint, $headers, $content); - } - - return $requests; - } - - /** - * @return bool - */ - public function isAutomaticPadding(): bool - { - return $this->automaticPadding !== 0; - } - - /** - * @return int - */ - public function getAutomaticPadding() - { - return $this->automaticPadding; - } - - /** - * @param int|bool $automaticPadding Max padding length - * - * @return WebPush - * - * @throws \Exception - */ - public function setAutomaticPadding($automaticPadding): WebPush - { - if ($automaticPadding > Encryption::MAX_PAYLOAD_LENGTH) { - throw new \Exception('Automatic padding is too large. Max is '.Encryption::MAX_PAYLOAD_LENGTH.'. Recommended max is '.Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH.' for compatibility reasons (see README).'); - } elseif ($automaticPadding < 0) { - throw new \Exception('Padding length should be positive or zero.'); - } elseif ($automaticPadding === true) { - $this->automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH; - } elseif ($automaticPadding === false) { - $this->automaticPadding = 0; - } else { - $this->automaticPadding = $automaticPadding; - } - - return $this; - } - - /** - * @return bool - */ - public function getReuseVAPIDHeaders() - { - return $this->reuseVAPIDHeaders; - } - - /** - * Reuse VAPID headers in the same flush session to improve performance - * @param bool $enabled - * - * @return WebPush - */ - public function setReuseVAPIDHeaders(bool $enabled) - { - $this->reuseVAPIDHeaders = $enabled; - - return $this; - } - - /** - * @return array - */ - public function getDefaultOptions(): array - { - return $this->defaultOptions; - } - - /** - * @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize' - * - * @return WebPush - */ - public function setDefaultOptions(array $defaultOptions) - { - $this->defaultOptions['TTL'] = isset($defaultOptions['TTL']) ? $defaultOptions['TTL'] : 2419200; - $this->defaultOptions['urgency'] = isset($defaultOptions['urgency']) ? $defaultOptions['urgency'] : null; - $this->defaultOptions['topic'] = isset($defaultOptions['topic']) ? $defaultOptions['topic'] : null; - $this->defaultOptions['batchSize'] = isset($defaultOptions['batchSize']) ? $defaultOptions['batchSize'] : 1000; - - return $this; - } - - /** - * @return int - */ - public function countPendingNotifications(): int - { - return null !== $this->notifications ? count($this->notifications) : 0; - } - - /** - * @param string $audience - * @param string $contentEncoding - * @param array $vapid - * @return array - * @throws \ErrorException - */ - private function getVAPIDHeaders(string $audience, string $contentEncoding, array $vapid) - { - $vapidHeaders = null; - - $cache_key = null; - if ($this->reuseVAPIDHeaders) { - $cache_key = implode('#', [$audience, $contentEncoding, crc32(serialize($vapid))]); - if (array_key_exists($cache_key, $this->vapidHeaders)) { - $vapidHeaders = $this->vapidHeaders[$cache_key]; - } - } - - if (!$vapidHeaders) { - $vapidHeaders = VAPID::getVapidHeaders($audience, $vapid['subject'], $vapid['publicKey'], $vapid['privateKey'], $contentEncoding); - } - - if ($this->reuseVAPIDHeaders) { - $this->vapidHeaders[$cache_key] = $vapidHeaders; - } - - return $vapidHeaders; - } -} + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Minishlink\WebPush; + +use Base64Url\Base64Url; +use GuzzleHttp\Client; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Request; +use Psr\Http\Message\ResponseInterface; + +class WebPush +{ + public const GCM_URL = 'https://android.googleapis.com/gcm/send'; + public const FCM_BASE_URL = 'https://fcm.googleapis.com'; + + /** + * @var Client + */ + private $client; + + /** + * @var array + */ + private $auth; + + /** + * @var null|array Array of array of Notifications + */ + private $notifications; + + /** + * @var array Default options : TTL, urgency, topic, batchSize + */ + private $defaultOptions; + + /** + * @var int Automatic padding of payloads, if disabled, trade security for bandwidth + */ + private $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH; + + /** + * @var bool Reuse VAPID headers in the same flush session to improve performance + */ + private $reuseVAPIDHeaders = false; + + /** + * @var array Dictionary for VAPID headers cache + */ + private $vapidHeaders = []; + + /** + * WebPush constructor. + * + * @param array $auth Some servers needs authentication + * @param array $defaultOptions TTL, urgency, topic, batchSize + * @param int|null $timeout Timeout of POST request + * @param array $clientOptions + * + * @throws \ErrorException + */ + public function __construct(array $auth = [], array $defaultOptions = [], ?int $timeout = 30, array $clientOptions = []) + { + $extensions = [ + 'curl' => '[WebPush] curl extension is not loaded but is required. You can fix this in your php.ini.', + 'gmp' => '[WebPush] gmp extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.', + 'mbstring' => '[WebPush] mbstring extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.', + 'openssl' => '[WebPush] openssl extension is not loaded but is required for sending push notifications with payload or for VAPID authentication. You can fix this in your php.ini.', + ]; + foreach ($extensions as $extension => $message) { + if (!extension_loaded($extension)) { + trigger_error($message, E_USER_WARNING); + } + } + + if (ini_get('mbstring.func_overload') >= 2) { + trigger_error("[WebPush] mbstring.func_overload is enabled for str* functions. You must disable it if you want to send push notifications with payload or use VAPID. You can fix this in your php.ini.", E_USER_NOTICE); + } + + if (isset($auth['VAPID'])) { + $auth['VAPID'] = VAPID::validate($auth['VAPID']); + } + + $this->auth = $auth; + + $this->setDefaultOptions($defaultOptions); + + if (!array_key_exists('timeout', $clientOptions) && isset($timeout)) { + $clientOptions['timeout'] = $timeout; + } + $this->client = new Client($clientOptions); + } + + /** + * Send a notification. + * + * @param SubscriptionInterface $subscription + * @param string|null $payload If you want to send an array, json_encode it + * @param bool $flush If you want to flush directly (usually when you send only one notification) + * @param array $options Array with several options tied to this notification. If not set, will use the default options that you can set in the WebPush object + * @param array $auth Use this auth details instead of what you provided when creating WebPush + * + * @return \Generator|MessageSentReport[]|true Return an array of information if $flush is set to true and the queued requests has failed. + * Else return true + * + * @throws \ErrorException + */ + public function sendNotification(SubscriptionInterface $subscription, ?string $payload = null, bool $flush = false, array $options = [], array $auth = []) + { + if (isset($payload)) { + if (Utils::safeStrlen($payload) > Encryption::MAX_PAYLOAD_LENGTH) { + throw new \ErrorException('Size of payload must not be greater than '.Encryption::MAX_PAYLOAD_LENGTH.' octets.'); + } + + $contentEncoding = $subscription->getContentEncoding(); + if (!$contentEncoding) { + throw new \ErrorException('Subscription should have a content encoding'); + } + + $payload = Encryption::padPayload($payload, $this->automaticPadding, $contentEncoding); + } + + if (array_key_exists('VAPID', $auth)) { + $auth['VAPID'] = VAPID::validate($auth['VAPID']); + } + + $this->notifications[] = new Notification($subscription, $payload, $options, $auth); + + return false !== $flush ? $this->flush() : true; + } + + /** + * Flush notifications. Triggers the requests. + * + * @param null|int $batchSize Defaults the value defined in defaultOptions during instantiation (which defaults to 1000). + * + * @return \Generator|MessageSentReport[] + * @throws \ErrorException + */ + public function flush(?int $batchSize = null): \Generator + { + if (null === $this->notifications || empty($this->notifications)) { + yield from []; + return; + } + + if (null === $batchSize) { + $batchSize = $this->defaultOptions['batchSize']; + } + + $batches = array_chunk($this->notifications, $batchSize); + + // reset queue + $this->notifications = []; + + foreach ($batches as $batch) { + // for each endpoint server type + $requests = $this->prepare($batch); + + $promises = []; + + foreach ($requests as $request) { + $promises[] = $this->client->sendAsync($request) + ->then(function ($response) use ($request) { + /** @var ResponseInterface $response * */ + return new MessageSentReport($request, $response); + }) + ->otherwise(function ($reason) { + /** @var RequestException $reason **/ + return new MessageSentReport($reason->getRequest(), $reason->getResponse(), false, $reason->getMessage()); + }); + } + + foreach ($promises as $promise) { + yield $promise->wait(); + } + } + + if ($this->reuseVAPIDHeaders) { + $this->vapidHeaders = []; + } + } + + /** + * @param array $notifications + * + * @return array + * + * @throws \ErrorException + */ + private function prepare(array $notifications): array + { + $requests = []; + /** @var Notification $notification */ + foreach ($notifications as $notification) { + $subscription = $notification->getSubscription(); + $endpoint = $subscription->getEndpoint(); + $userPublicKey = $subscription->getPublicKey(); + $userAuthToken = $subscription->getAuthToken(); + $contentEncoding = $subscription->getContentEncoding(); + $payload = $notification->getPayload(); + $options = $notification->getOptions($this->getDefaultOptions()); + $auth = $notification->getAuth($this->auth); + + if (!empty($payload) && !empty($userPublicKey) && !empty($userAuthToken)) { + if (!$contentEncoding) { + throw new \ErrorException('Subscription should have a content encoding'); + } + + $encrypted = Encryption::encrypt($payload, $userPublicKey, $userAuthToken, $contentEncoding); + $cipherText = $encrypted['cipherText']; + $salt = $encrypted['salt']; + $localPublicKey = $encrypted['localPublicKey']; + + $headers = [ + 'Content-Type' => 'application/octet-stream', + 'Content-Encoding' => $contentEncoding, + ]; + + if ($contentEncoding === "aesgcm") { + $headers['Encryption'] = 'salt='.Base64Url::encode($salt); + $headers['Crypto-Key'] = 'dh='.Base64Url::encode($localPublicKey); + } + + $encryptionContentCodingHeader = Encryption::getContentCodingHeader($salt, $localPublicKey, $contentEncoding); + $content = $encryptionContentCodingHeader.$cipherText; + + $headers['Content-Length'] = Utils::safeStrlen($content); + } else { + $headers = [ + 'Content-Length' => 0, + ]; + + $content = ''; + } + + $headers['TTL'] = $options['TTL']; + + if (isset($options['urgency'])) { + $headers['Urgency'] = $options['urgency']; + } + + if (isset($options['topic'])) { + $headers['Topic'] = $options['topic']; + } + + // if GCM + if (substr($endpoint, 0, strlen(self::GCM_URL)) === self::GCM_URL) { + if (array_key_exists('GCM', $auth)) { + $headers['Authorization'] = 'key='.$auth['GCM']; + } else { + throw new \ErrorException('No GCM API Key specified.'); + } + } + // if VAPID (GCM doesn't support it but FCM does) + elseif (array_key_exists('VAPID', $auth) && $contentEncoding) { + $audience = parse_url($endpoint, PHP_URL_SCHEME).'://'.parse_url($endpoint, PHP_URL_HOST); + if (!parse_url($audience)) { + throw new \ErrorException('Audience "'.$audience.'"" could not be generated.'); + } + + $vapidHeaders = $this->getVAPIDHeaders($audience, $contentEncoding, $auth['VAPID']); + + $headers['Authorization'] = $vapidHeaders['Authorization']; + + if ($contentEncoding === 'aesgcm') { + if (array_key_exists('Crypto-Key', $headers)) { + $headers['Crypto-Key'] .= ';'.$vapidHeaders['Crypto-Key']; + } else { + $headers['Crypto-Key'] = $vapidHeaders['Crypto-Key']; + } + } + } + + $requests[] = new Request('POST', $endpoint, $headers, $content); + } + + return $requests; + } + + /** + * @return bool + */ + public function isAutomaticPadding(): bool + { + return $this->automaticPadding !== 0; + } + + /** + * @return int + */ + public function getAutomaticPadding() + { + return $this->automaticPadding; + } + + /** + * @param int|bool $automaticPadding Max padding length + * + * @return WebPush + * + * @throws \Exception + */ + public function setAutomaticPadding($automaticPadding): WebPush + { + if ($automaticPadding > Encryption::MAX_PAYLOAD_LENGTH) { + throw new \Exception('Automatic padding is too large. Max is '.Encryption::MAX_PAYLOAD_LENGTH.'. Recommended max is '.Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH.' for compatibility reasons (see README).'); + } elseif ($automaticPadding < 0) { + throw new \Exception('Padding length should be positive or zero.'); + } elseif ($automaticPadding === true) { + $this->automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH; + } elseif ($automaticPadding === false) { + $this->automaticPadding = 0; + } else { + $this->automaticPadding = $automaticPadding; + } + + return $this; + } + + /** + * @return bool + */ + public function getReuseVAPIDHeaders() + { + return $this->reuseVAPIDHeaders; + } + + /** + * Reuse VAPID headers in the same flush session to improve performance + * @param bool $enabled + * + * @return WebPush + */ + public function setReuseVAPIDHeaders(bool $enabled) + { + $this->reuseVAPIDHeaders = $enabled; + + return $this; + } + + /** + * @return array + */ + public function getDefaultOptions(): array + { + return $this->defaultOptions; + } + + /** + * @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize' + * + * @return WebPush + */ + public function setDefaultOptions(array $defaultOptions) + { + $this->defaultOptions['TTL'] = isset($defaultOptions['TTL']) ? $defaultOptions['TTL'] : 2419200; + $this->defaultOptions['urgency'] = isset($defaultOptions['urgency']) ? $defaultOptions['urgency'] : null; + $this->defaultOptions['topic'] = isset($defaultOptions['topic']) ? $defaultOptions['topic'] : null; + $this->defaultOptions['batchSize'] = isset($defaultOptions['batchSize']) ? $defaultOptions['batchSize'] : 1000; + + return $this; + } + + /** + * @return int + */ + public function countPendingNotifications(): int + { + return null !== $this->notifications ? count($this->notifications) : 0; + } + + /** + * @param string $audience + * @param string $contentEncoding + * @param array $vapid + * @return array + * @throws \ErrorException + */ + private function getVAPIDHeaders(string $audience, string $contentEncoding, array $vapid) + { + $vapidHeaders = null; + + $cache_key = null; + if ($this->reuseVAPIDHeaders) { + $cache_key = implode('#', [$audience, $contentEncoding, crc32(serialize($vapid))]); + if (array_key_exists($cache_key, $this->vapidHeaders)) { + $vapidHeaders = $this->vapidHeaders[$cache_key]; + } + } + + if (!$vapidHeaders) { + $vapidHeaders = VAPID::getVapidHeaders($audience, $vapid['subject'], $vapid['publicKey'], $vapid['privateKey'], $contentEncoding); + } + + if ($this->reuseVAPIDHeaders) { + $this->vapidHeaders[$cache_key] = $vapidHeaders; + } + + return $vapidHeaders; + } +} -- cgit v1.2.3