diff options
Diffstat (limited to '')
-rw-r--r-- | vendor/minishlink/web-push/src/Encryption.php | 642 | ||||
-rw-r--r-- | vendor/minishlink/web-push/src/MessageSentReport.php | 362 | ||||
-rw-r--r-- | vendor/minishlink/web-push/src/Notification.php | 172 | ||||
-rw-r--r-- | vendor/minishlink/web-push/src/Subscription.php | 244 | ||||
-rw-r--r-- | vendor/minishlink/web-push/src/SubscriptionInterface.php | 80 | ||||
-rw-r--r-- | vendor/minishlink/web-push/src/Utils.php | 126 | ||||
-rw-r--r-- | vendor/minishlink/web-push/src/VAPID.php | 394 | ||||
-rw-r--r-- | vendor/minishlink/web-push/src/WebPush.php | 824 |
8 files changed, 1422 insertions, 1422 deletions
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 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the WebPush library. - * - * (c) Louis Lagrange <lagrange.louis@gmail.com> - * - * 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; - } -} +<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * 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 @@ -<?php -/** - * @author Igor Timoshenkov [it@campoint.net] - * @started: 03.09.2018 9:21 - */ - -namespace Minishlink\WebPush; - -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; - -/** - * Standardized response from sending a message - */ -class MessageSentReport implements \JsonSerializable -{ - - /** - * @var boolean - */ - protected $success; - - /** - * @var RequestInterface - */ - protected $request; - - /** - * @var ResponseInterface | null - */ - protected $response; - - /** - * @var string - */ - protected $reason; - - /** - * @param RequestInterface $request - * @param ResponseInterface $response - * @param bool $success - * @param string $reason - */ - public function __construct(RequestInterface $request, ?ResponseInterface $response = null, bool $success = true, $reason = 'OK') - { - $this->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(), - ]; - } -} +<?php
+/**
+ * @author Igor Timoshenkov [it@campoint.net]
+ * @started: 03.09.2018 9:21
+ */
+
+namespace Minishlink\WebPush;
+
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+
+/**
+ * Standardized response from sending a message
+ */
+class MessageSentReport implements \JsonSerializable
+{
+
+ /**
+ * @var boolean
+ */
+ protected $success;
+
+ /**
+ * @var RequestInterface
+ */
+ protected $request;
+
+ /**
+ * @var ResponseInterface | null
+ */
+ protected $response;
+
+ /**
+ * @var string
+ */
+ protected $reason;
+
+ /**
+ * @param RequestInterface $request
+ * @param ResponseInterface $response
+ * @param bool $success
+ * @param string $reason
+ */
+ public function __construct(RequestInterface $request, ?ResponseInterface $response = null, bool $success = true, $reason = 'OK')
+ {
+ $this->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 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the WebPush library. - * - * (c) Louis Lagrange <lagrange.louis@gmail.com> - * - * 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; - } -} +<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * 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 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the WebPush library. - * - * (c) Louis Lagrange <lagrange.louis@gmail.com> - * - * 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; - } -} +<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * 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 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the WebPush library. - * - * (c) Louis Lagrange <lagrange.louis@gmail.com> - * - * 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 <sb@firstvector.org> - */ -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; -} +<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * 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 <sb@firstvector.org>
+ */
+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 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the WebPush library. - * - * (c) Louis Lagrange <lagrange.louis@gmail.com> - * - * 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')), - ]; - } -} +<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * 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 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the WebPush library. - * - * (c) Louis Lagrange <lagrange.louis@gmail.com> - * - * 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) - ]; - } -} +<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * 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 @@ -<?php - -declare(strict_types=1); - -/* - * This file is part of the WebPush library. - * - * (c) Louis Lagrange <lagrange.louis@gmail.com> - * - * 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; - } -} +<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the WebPush library.
+ *
+ * (c) Louis Lagrange <lagrange.louis@gmail.com>
+ *
+ * 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;
+ }
+}
|