From 313072db071682a87242c88a497609439de2d91c Mon Sep 17 00:00:00 2001 From: Cyperghost Date: Mon, 24 Jun 2024 10:30:47 +0200 Subject: [PATCH] Update `minishlink/web-push` to `v9.0.0-rc2` --- .../files/lib/system/api/composer.json | 2 +- .../files/lib/system/api/composer.lock | 33 ++-- .../system/api/composer/autoload_files.php | 2 +- .../system/api/composer/autoload_static.php | 2 +- .../lib/system/api/composer/installed.json | 32 ++-- .../lib/system/api/composer/installed.php | 10 +- .../api/minishlink/web-push/composer.json | 40 +++-- .../api/minishlink/web-push/phpstan.neon | 8 - .../minishlink/web-push/src/Encryption.php | 157 ++++-------------- .../web-push/src/MessageSentReport.php | 43 +---- .../minishlink/web-push/src/Notification.php | 28 ++-- .../minishlink/web-push/src/Subscription.php | 29 +--- .../api/minishlink/web-push/src/Utils.php | 80 ++++++++- .../api/minishlink/web-push/src/VAPID.php | 4 +- .../api/minishlink/web-push/src/WebPush.php | 148 ++++++++++------- 15 files changed, 280 insertions(+), 338 deletions(-) delete mode 100644 wcfsetup/install/files/lib/system/api/minishlink/web-push/phpstan.neon diff --git a/wcfsetup/install/files/lib/system/api/composer.json b/wcfsetup/install/files/lib/system/api/composer.json index 4d60e69206..f6a5917673 100644 --- a/wcfsetup/install/files/lib/system/api/composer.json +++ b/wcfsetup/install/files/lib/system/api/composer.json @@ -19,7 +19,7 @@ "laminas/laminas-diactoros": "^3.3.0", "laminas/laminas-httphandlerrunner": "^2.10.0", "laminas/laminas-progressbar": "^2.13", - "minishlink/web-push": "^8.0", + "minishlink/web-push": "^v9.0.0-rc2", "nikic/fast-route": "2.0.0-beta1", "paragonie/constant_time_encoding": "^2.6.3", "pelago/emogrifier": "^7.2.0", diff --git a/wcfsetup/install/files/lib/system/api/composer.lock b/wcfsetup/install/files/lib/system/api/composer.lock index 05cb04fa03..5be39f1737 100644 --- a/wcfsetup/install/files/lib/system/api/composer.lock +++ b/wcfsetup/install/files/lib/system/api/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f2c08143de462de937858295f4bd8660", + "content-hash": "57415f3e44f525b3bdcdb2e419ea09e4", "packages": [ { "name": "brick/math", @@ -910,16 +910,16 @@ }, { "name": "minishlink/web-push", - "version": "v8.0.0", + "version": "v9.0.0-rc2", "source": { "type": "git", "url": "https://github.com/web-push-libs/web-push-php.git", - "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d" + "reference": "9d36211c435baecded11d7a227f5caa098f52f80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/ec034f1e287cd1e74235e349bd017d71a61e9d8d", - "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d", + "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/9d36211c435baecded11d7a227f5caa098f52f80", + "reference": "9d36211c435baecded11d7a227f5caa098f52f80", "shasum": "" }, "require": { @@ -927,20 +927,18 @@ "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", - "guzzlehttp/guzzle": "^7.0.1|^6.2", - "php": ">=8.0", - "spomky-labs/base64url": "^2.0", - "web-token/jwt-key-mgmt": "^2.0|^3.0.2", - "web-token/jwt-signature": "^2.0|^3.0.2", - "web-token/jwt-signature-algorithm-ecdsa": "^2.0|^3.0.2", - "web-token/jwt-util-ecc": "^2.0|^3.0.2" + "guzzlehttp/guzzle": "^7.4.5", + "php": ">=8.1", + "spomky-labs/base64url": "^2.0.4", + "web-token/jwt-library": "^3.3.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.13.2", - "phpstan/phpstan": "^1.9.8", - "phpunit/phpunit": "^9.5.27" + "friendsofphp/php-cs-fixer": "^v3.48.0", + "phpstan/phpstan": "^1.10.57", + "phpunit/phpunit": "^10.5.9" }, "suggest": { + "ext-bcmath": "Optional for performance.", "ext-gmp": "Optional for performance." }, "type": "library", @@ -971,9 +969,9 @@ ], "support": { "issues": "https://github.com/web-push-libs/web-push-php/issues", - "source": "https://github.com/web-push-libs/web-push-php/tree/v8.0.0" + "source": "https://github.com/web-push-libs/web-push-php/tree/v9.0.0-rc2" }, - "time": "2023-01-10T17:14:44+00:00" + "time": "2024-06-18T16:26:43+00:00" }, { "name": "nikic/fast-route", @@ -2668,6 +2666,7 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { + "minishlink/web-push": 5, "nikic/fast-route": 10 }, "prefer-stable": false, diff --git a/wcfsetup/install/files/lib/system/api/composer/autoload_files.php b/wcfsetup/install/files/lib/system/api/composer/autoload_files.php index 3ee8ebc190..1712380505 100644 --- a/wcfsetup/install/files/lib/system/api/composer/autoload_files.php +++ b/wcfsetup/install/files/lib/system/api/composer/autoload_files.php @@ -7,9 +7,9 @@ $baseDir = $vendorDir; return array( '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', + '3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php', '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', - '3109cb1a231dcd04bee1f9f620d46975' => $vendorDir . '/paragonie/sodium_compat/autoload.php', '2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', '07d7f1a47144818725fd8d91a907ac57' => $vendorDir . '/laminas/laminas-diactoros/src/functions/create_uploaded_file.php', 'da94ac5d3ca7d2dbab84ce561ce72bfd' => $vendorDir . '/laminas/laminas-diactoros/src/functions/marshal_headers_from_sapi.php', diff --git a/wcfsetup/install/files/lib/system/api/composer/autoload_static.php b/wcfsetup/install/files/lib/system/api/composer/autoload_static.php index 3e3e5de976..25590b047b 100644 --- a/wcfsetup/install/files/lib/system/api/composer/autoload_static.php +++ b/wcfsetup/install/files/lib/system/api/composer/autoload_static.php @@ -8,9 +8,9 @@ class ComposerStaticInita1f5f7c74275d47a45049a2936db1d0d { public static $files = array ( '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', + '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php', '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', - '3109cb1a231dcd04bee1f9f620d46975' => __DIR__ . '/..' . '/paragonie/sodium_compat/autoload.php', '2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', '07d7f1a47144818725fd8d91a907ac57' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/create_uploaded_file.php', 'da94ac5d3ca7d2dbab84ce561ce72bfd' => __DIR__ . '/..' . '/laminas/laminas-diactoros/src/functions/marshal_headers_from_sapi.php', diff --git a/wcfsetup/install/files/lib/system/api/composer/installed.json b/wcfsetup/install/files/lib/system/api/composer/installed.json index 891cfe36de..7d4948859a 100644 --- a/wcfsetup/install/files/lib/system/api/composer/installed.json +++ b/wcfsetup/install/files/lib/system/api/composer/installed.json @@ -940,17 +940,17 @@ }, { "name": "minishlink/web-push", - "version": "v8.0.0", - "version_normalized": "8.0.0.0", + "version": "v9.0.0-rc2", + "version_normalized": "9.0.0.0-RC2", "source": { "type": "git", "url": "https://github.com/web-push-libs/web-push-php.git", - "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d" + "reference": "9d36211c435baecded11d7a227f5caa098f52f80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/ec034f1e287cd1e74235e349bd017d71a61e9d8d", - "reference": "ec034f1e287cd1e74235e349bd017d71a61e9d8d", + "url": "https://api.github.com/repos/web-push-libs/web-push-php/zipball/9d36211c435baecded11d7a227f5caa098f52f80", + "reference": "9d36211c435baecded11d7a227f5caa098f52f80", "shasum": "" }, "require": { @@ -958,23 +958,21 @@ "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", - "guzzlehttp/guzzle": "^7.0.1|^6.2", - "php": ">=8.0", - "spomky-labs/base64url": "^2.0", - "web-token/jwt-key-mgmt": "^2.0|^3.0.2", - "web-token/jwt-signature": "^2.0|^3.0.2", - "web-token/jwt-signature-algorithm-ecdsa": "^2.0|^3.0.2", - "web-token/jwt-util-ecc": "^2.0|^3.0.2" + "guzzlehttp/guzzle": "^7.4.5", + "php": ">=8.1", + "spomky-labs/base64url": "^2.0.4", + "web-token/jwt-library": "^3.3.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^v3.13.2", - "phpstan/phpstan": "^1.9.8", - "phpunit/phpunit": "^9.5.27" + "friendsofphp/php-cs-fixer": "^v3.48.0", + "phpstan/phpstan": "^1.10.57", + "phpunit/phpunit": "^10.5.9" }, "suggest": { + "ext-bcmath": "Optional for performance.", "ext-gmp": "Optional for performance." }, - "time": "2023-01-10T17:14:44+00:00", + "time": "2024-06-18T16:26:43+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1004,7 +1002,7 @@ ], "support": { "issues": "https://github.com/web-push-libs/web-push-php/issues", - "source": "https://github.com/web-push-libs/web-push-php/tree/v8.0.0" + "source": "https://github.com/web-push-libs/web-push-php/tree/v9.0.0-rc2" }, "install-path": "../minishlink/web-push" }, diff --git a/wcfsetup/install/files/lib/system/api/composer/installed.php b/wcfsetup/install/files/lib/system/api/composer/installed.php index ef500128a8..ebe9ddb09e 100644 --- a/wcfsetup/install/files/lib/system/api/composer/installed.php +++ b/wcfsetup/install/files/lib/system/api/composer/installed.php @@ -3,7 +3,7 @@ 'name' => '__root__', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '6f6461b4a24316d70856362b6136992572e05b32', + 'reference' => 'dafe4e4d98ef3d45aa1c838703676812016db738', 'type' => 'project', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -13,7 +13,7 @@ '__root__' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => '6f6461b4a24316d70856362b6136992572e05b32', + 'reference' => 'dafe4e4d98ef3d45aa1c838703676812016db738', 'type' => 'project', 'install_path' => __DIR__ . '/../', 'aliases' => array(), @@ -128,9 +128,9 @@ 'dev_requirement' => false, ), 'minishlink/web-push' => array( - 'pretty_version' => 'v8.0.0', - 'version' => '8.0.0.0', - 'reference' => 'ec034f1e287cd1e74235e349bd017d71a61e9d8d', + 'pretty_version' => 'v9.0.0-rc2', + 'version' => '9.0.0.0-RC2', + 'reference' => '9d36211c435baecded11d7a227f5caa098f52f80', 'type' => 'library', 'install_path' => __DIR__ . '/../minishlink/web-push', 'aliases' => array(), diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/composer.json b/wcfsetup/install/files/lib/system/api/minishlink/web-push/composer.json index 2b164ac26e..b9b34b97d4 100644 --- a/wcfsetup/install/files/lib/system/api/minishlink/web-push/composer.json +++ b/wcfsetup/install/files/lib/system/api/minishlink/web-push/composer.json @@ -2,7 +2,13 @@ "name": "minishlink/web-push", "type": "library", "description": "Web Push library for PHP", - "keywords": ["push", "notifications", "web", "WebPush", "Push API"], + "keywords": [ + "push", + "notifications", + "web", + "WebPush", + "Push API" + ], "homepage": "https://github.com/web-push-libs/web-push-php", "license": "MIT", "authors": [ @@ -13,34 +19,36 @@ } ], "scripts": { - "test:unit": "./vendor/bin/phpunit --color", + "fix:syntax": "./vendor/bin/php-cs-fixer fix ./src --using-cache=no", + "fix:syntax_tests": "./vendor/bin/php-cs-fixer fix ./tests --using-cache=no", + "test:unit": "./vendor/bin/phpunit", + "test:unit_offline": "./vendor/bin/phpunit --exclude-group=online", "test:typing": "./vendor/bin/phpstan analyse", - "test:syntax": "./vendor/bin/php-cs-fixer fix ./src --dry-run --stop-on-violation --using-cache=no" + "test:syntax": "./vendor/bin/php-cs-fixer fix ./src --dry-run --stop-on-violation --using-cache=no", + "test:syntax_tests": "./vendor/bin/php-cs-fixer fix ./tests --dry-run --stop-on-violation --using-cache=no" }, "require": { - "php": ">=8.0", + "php": ">=8.1", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", - "guzzlehttp/guzzle": "^7.0.1|^6.2", - "web-token/jwt-signature": "^2.0|^3.0.2", - "web-token/jwt-key-mgmt": "^2.0|^3.0.2", - "web-token/jwt-signature-algorithm-ecdsa": "^2.0|^3.0.2", - "web-token/jwt-util-ecc": "^2.0|^3.0.2", - "spomky-labs/base64url": "^2.0" + "guzzlehttp/guzzle": "^7.4.5", + "web-token/jwt-library": "^3.3.0", + "spomky-labs/base64url": "^2.0.4" }, "suggest": { + "ext-bcmath": "Optional for performance.", "ext-gmp": "Optional for performance." }, "require-dev": { - "phpunit/phpunit": "^9.5.27", - "phpstan/phpstan": "^1.9.8", - "friendsofphp/php-cs-fixer": "^v3.13.2" + "phpunit/phpunit": "^10.5.9", + "phpstan/phpstan": "^1.10.57", + "friendsofphp/php-cs-fixer": "^v3.48.0" }, "autoload": { - "psr-4" : { - "Minishlink\\WebPush\\" : "src" + "psr-4": { + "Minishlink\\WebPush\\": "src" } } -} +} \ No newline at end of file diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/phpstan.neon b/wcfsetup/install/files/lib/system/api/minishlink/web-push/phpstan.neon deleted file mode 100644 index f031d802f5..0000000000 --- a/wcfsetup/install/files/lib/system/api/minishlink/web-push/phpstan.neon +++ /dev/null @@ -1,8 +0,0 @@ -parameters: - level: 7 - paths: - - src - checkMissingIterableValueType: false - reportUnmatchedIgnoredErrors: false - ignoreErrors: - - '#Unreachable statement \- code above always terminates\.#' diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Encryption.php b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Encryption.php index 404c0336dd..89fc8e8857 100644 --- a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Encryption.php +++ b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Encryption.php @@ -14,16 +14,14 @@ declare(strict_types=1); namespace Minishlink\WebPush; use Base64Url\Base64Url; -use Brick\Math\BigInteger; use Jose\Component\Core\JWK; -use Jose\Component\Core\Util\Ecc\NistCurve; use Jose\Component\Core\Util\Ecc\PrivateKey; use Jose\Component\Core\Util\ECKey; class Encryption { public const MAX_PAYLOAD_LENGTH = 4078; - public const MAX_COMPATIBILITY_PAYLOAD_LENGTH = 3052; + public const MAX_COMPATIBILITY_PAYLOAD_LENGTH = 2820; /** * @return string padded payload (plaintext) @@ -36,11 +34,12 @@ class Encryption if ($contentEncoding === "aesgcm") { return pack('n*', $padLen).str_pad($payload, $padLen + $payloadLen, chr(0), STR_PAD_LEFT); - } elseif ($contentEncoding === "aes128gcm") { + } + if ($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"); } + + throw new \ErrorException("This content encoding is not supported"); } /** @@ -63,7 +62,7 @@ class Encryption } /** - * @throws \ErrorException + * @throws \RuntimeException */ public static function deterministicEncrypt(string $payload, string $userPublicKey, string $userAuthToken, string $contentEncoding, array $localKeyObject, string $salt): array { @@ -88,7 +87,7 @@ class Encryption ]); } if (!$localPublicKey) { - throw new \ErrorException('Failed to convert local public key from hexadecimal to binary'); + throw new \RuntimeException('Failed to convert local public key from hexadecimal to binary.'); } // get user public key object @@ -225,7 +224,9 @@ class Encryption } return 'Content-Encoding: '.$type.chr(0).'P-256'.$context; - } elseif ($contentEncoding === "aes128gcm") { + } + + if ($contentEncoding === "aes128gcm") { return 'Content-Encoding: '.$type.chr(0); } @@ -233,61 +234,18 @@ class Encryption } private static function createLocalKeyObject(): array - { - try { - return self::createLocalKeyObjectUsingOpenSSL(); - } catch (\Exception $e) { - return self::createLocalKeyObjectUsingPurePhpMethod(); - } - } - - private static function createLocalKeyObjectUsingPurePhpMethod(): array - { - $curve = NistCurve::curve256(); - $privateKey = $curve->createPrivateKey(); - $publicKey = $curve->createPublicKey($privateKey); - - if ($publicKey->getPoint()->getX() instanceof BigInteger) { - return [ - new JWK([ - 'kty' => 'EC', - 'crv' => 'P-256', - 'x' => Base64Url::encode(self::addNullPadding($publicKey->getPoint()->getX()->toBytes(false))), - 'y' => Base64Url::encode(self::addNullPadding($publicKey->getPoint()->getY()->toBytes(false))), - 'd' => Base64Url::encode(self::addNullPadding($privateKey->getSecret()->toBytes(false))), - ]) - ]; - } - - return [ - new JWK([ - 'kty' => 'EC', - 'crv' => 'P-256', - 'x' => Base64Url::encode(self::addNullPadding(hex2bin(gmp_strval($publicKey->getPoint()->getX(), 16)))), - 'y' => Base64Url::encode(self::addNullPadding(hex2bin(gmp_strval($publicKey->getPoint()->getY(), 16)))), - 'd' => Base64Url::encode(self::addNullPadding(hex2bin(gmp_strval($privateKey->getSecret(), 16)))), - ]) - ]; - } - - 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'); + throw new \RuntimeException('Unable to create the local key.'); } $details = openssl_pkey_get_details($keyResource); - if (PHP_MAJOR_VERSION < 8) { - openssl_pkey_free($keyResource); - } - if (!$details) { - throw new \RuntimeException('Unable to get the key details'); + throw new \RuntimeException('Unable to get the local key details.'); } return [ @@ -297,94 +255,39 @@ class Encryption 'x' => Base64Url::encode(self::addNullPadding($details['ec']['x'])), 'y' => Base64Url::encode(self::addNullPadding($details['ec']['y'])), 'd' => Base64Url::encode(self::addNullPadding($details['ec']['d'])), - ]) + ]), ]; } /** - * @throws \ErrorException + * @throws \ValueError */ 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; - } - - private static function calculateAgreementKey(JWK $private_key, JWK $public_key): string - { - if (function_exists('openssl_pkey_derive')) { - try { - $publicPem = ECKey::convertPublicKeyToPEM($public_key); - $privatePem = ECKey::convertPrivateKeyToPEM($private_key); - - $result = openssl_pkey_derive($publicPem, $privatePem, 256); // @phpstan-ignore-line - if ($result === false) { - throw new \Exception('Unable to compute the agreement key'); - } - return $result; - } catch (\Throwable $throwable) { - //Does nothing. Will fallback to the pure PHP function - } - } - - - $curve = NistCurve::curve256(); - try { - $rec_x = self::convertBase64ToBigInteger($public_key->get('x')); - $rec_y = self::convertBase64ToBigInteger($public_key->get('y')); - $sen_d = self::convertBase64ToBigInteger($private_key->get('d')); - $priv_key = PrivateKey::create($sen_d); - $pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y); - - return hex2bin(str_pad($curve->mul($pub_key->getPoint(), $priv_key->getSecret())->getX()->toBase(16), 64, '0', STR_PAD_LEFT)); // @phpstan-ignore-line - } catch (\Throwable $e) { - $rec_x = self::convertBase64ToGMP($public_key->get('x')); - $rec_y = self::convertBase64ToGMP($public_key->get('y')); - $sen_d = self::convertBase64ToGMP($private_key->get('d')); - $priv_key = PrivateKey::create($sen_d); // @phpstan-ignore-line - $pub_key = $curve->getPublicKeyFrom($rec_x, $rec_y); // @phpstan-ignore-line - - return hex2bin(gmp_strval($curve->mul($pub_key->getPoint(), $priv_key->getSecret())->getX(), 16)); // @phpstan-ignore-line + if (empty($userAuthToken)) { + return $sharedSecret; } - } - - /** - * @throws \ErrorException - */ - private static function convertBase64ToBigInteger(string $value): BigInteger - { - $value = unpack('H*', Base64Url::decode($value)); - - if ($value === false) { - throw new \ErrorException('Unable to unpack hex value from string'); + if($contentEncoding === "aesgcm") { + $info = 'Content-Encoding: auth'.chr(0); + } elseif($contentEncoding === "aes128gcm") { + $info = "WebPush: info".chr(0).$userPublicKey.$localPublicKey; + } else { + throw new \ValueError("This content encoding is not supported."); } - return BigInteger::fromBase($value[1], 16); + return self::hkdf($userAuthToken, $sharedSecret, $info, 32); } - /** - * @throws \ErrorException - */ - private static function convertBase64ToGMP(string $value): \GMP + private static function calculateAgreementKey(JWK $private_key, JWK $public_key): string { - $value = unpack('H*', Base64Url::decode($value)); + $publicPem = ECKey::convertPublicKeyToPEM($public_key); + $privatePem = ECKey::convertPrivateKeyToPEM($private_key); - if ($value === false) { - throw new \ErrorException('Unable to unpack hex value from string'); + $result = openssl_pkey_derive($publicPem, $privatePem, 256); + if ($result === false) { + throw new \RuntimeException('Unable to compute the agreement key.'); } - - return gmp_init($value[1], 16); + return $result; } private static function addNullPadding(string $data): string diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/MessageSentReport.php b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/MessageSentReport.php index c1b04c38a2..59ad5fd1c3 100644 --- a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/MessageSentReport.php +++ b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/MessageSentReport.php @@ -1,4 +1,4 @@ -request = $request; - $this->response = $response; - $this->success = $success; - $this->reason = $reason; + public function __construct( + protected RequestInterface $request, + protected ?ResponseInterface $response = null, + protected bool $success = true, + protected string $reason = 'OK' + ) { } public function isSuccess(): bool @@ -110,11 +87,7 @@ class MessageSentReport implements \JsonSerializable public function getResponseContent(): ?string { - if (!$this->response) { - return null; - } - - return $this->response->getBody()->getContents(); + return $this->response?->getBody()->getContents(); } public function jsonSerialize(): array diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Notification.php b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Notification.php index d9414d4adf..a924fbb3c8 100644 --- a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Notification.php +++ b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Notification.php @@ -15,24 +15,16 @@ 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 : VAPID */ - private $auth; - - public function __construct(SubscriptionInterface $subscription, ?string $payload, array $options, array $auth) - { - $this->subscription = $subscription; - $this->payload = $payload; - $this->options = $options; - $this->auth = $auth; + /** + * @param array $options Options: TTL, urgency, topic + * @param array $auth Auth details: VAPID + */ + public function __construct( + private SubscriptionInterface $subscription, + private ?string $payload, + private array $options, + private array $auth + ) { } public function getSubscription(): SubscriptionInterface diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Subscription.php b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Subscription.php index 1f3a022ad8..bad30612d5 100644 --- a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Subscription.php +++ b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Subscription.php @@ -15,38 +15,21 @@ 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; - /** * @param string|null $contentEncoding (Optional) Must be "aesgcm" * @throws \ErrorException */ public function __construct( - string $endpoint, - ?string $publicKey = null, - ?string $authToken = null, - ?string $contentEncoding = null + private string $endpoint, + private ?string $publicKey = null, + private ?string $authToken = null, + private ?string $contentEncoding = null ) { - $this->endpoint = $endpoint; - - if ($publicKey || $authToken || $contentEncoding) { + if($publicKey || $authToken || $contentEncoding) { $supportedContentEncodings = ['aesgcm', 'aes128gcm']; - if ($contentEncoding && !in_array($contentEncoding, $supportedContentEncodings)) { + if ($contentEncoding && !in_array($contentEncoding, $supportedContentEncodings, true)) { throw new \ErrorException('This content encoding ('.$contentEncoding.') is not supported.'); } - - $this->publicKey = $publicKey; - $this->authToken = $authToken; $this->contentEncoding = $contentEncoding ?: "aesgcm"; } } diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Utils.php b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Utils.php index a23200aa14..887acb0abb 100644 --- a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Utils.php +++ b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/Utils.php @@ -14,7 +14,6 @@ declare(strict_types=1); namespace Minishlink\WebPush; use Base64Url\Base64Url; -use Brick\Math\BigInteger; use Jose\Component\Core\JWK; use Jose\Component\Core\Util\Ecc\PublicKey; @@ -29,13 +28,8 @@ class Utils { $hexString = '04'; $point = $publicKey->getPoint(); - if ($point->getX() instanceof BigInteger) { - $hexString .= str_pad($point->getX()->toBase(16), 64, '0', STR_PAD_LEFT); - $hexString .= str_pad($point->getY()->toBase(16), 64, '0', STR_PAD_LEFT); - } else { // @phpstan-ignore-line - $hexString .= str_pad(gmp_strval($point->getX(), 16), 64, '0', STR_PAD_LEFT); - $hexString .= str_pad(gmp_strval($point->getY(), 16), 64, '0', STR_PAD_LEFT); // @phpstan-ignore-line - } + $hexString .= str_pad($point->getX()->toBase(16), 64, '0', STR_PAD_LEFT); + $hexString .= str_pad($point->getY()->toBase(16), 64, '0', STR_PAD_LEFT); return $hexString; } @@ -63,4 +57,74 @@ class Utils hex2bin(mb_substr($data, $dataLength / 2, null, '8bit')), ]; } + + /** + * Generates user warning/notice if some requirements are not met. + * Does not throw exception to allow unusual or polyfill environments. + */ + public static function checkRequirement(): void + { + self::checkRequirementExtension(); + self::checkRequirementKeyCipherHash(); + } + + public static function checkRequirementExtension(): void + { + $requiredExtensions = [ + 'curl' => '[WebPush] curl extension is not loaded but is required. 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($requiredExtensions as $extension => $message) { + if(!extension_loaded($extension)) { + trigger_error($message, E_USER_WARNING); + } + } + + // Check optional extensions. + if(!extension_loaded("bcmath") && !extension_loaded("gmp")) { + trigger_error("It is highly recommended to install the GMP or BCMath extension to speed up calculations. The fastest available calculator implementation will be automatically selected at runtime.", E_USER_NOTICE); + } + } + + public static function checkRequirementKeyCipherHash(): void + { + // Print your current openssl version with: OPENSSL_VERSION_TEXT + // Check for outdated openssl without EC support. + $requiredCurves = [ + 'prime256v1' => '[WebPush] Openssl does not support required curve prime256v1.', + ]; + $availableCurves = openssl_get_curve_names(); + if($availableCurves === false) { + trigger_error('[WebPush] Openssl does not support curves.', E_USER_WARNING); + } else { + foreach($requiredCurves as $curve => $message) { + if(!in_array($curve, $availableCurves, true)) { + trigger_error($message, E_USER_WARNING); + } + } + } + + // Check for unusual openssl without cipher support. + $requiredCiphers = [ + 'aes-128-gcm' => '[WebPush] Openssl does not support required cipher aes-128-gcm.', + ]; + $availableCiphers = openssl_get_cipher_methods(); + foreach($requiredCiphers as $cipher => $message) { + if(!in_array($cipher, $availableCiphers, true)) { + trigger_error($message, E_USER_WARNING); + } + } + + // Check for unusual php without hash algo support. + $requiredHash = [ + 'sha256' => '[WebPush] Php does not support required hmac hash sha256.', + ]; + $availableHash = hash_hmac_algos(); + foreach($requiredHash as $hash => $message) { + if(!in_array($hash, $availableHash, true)) { + trigger_error($message, E_USER_WARNING); + } + } + } } diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/VAPID.php b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/VAPID.php index edfaa23d8d..5a40cd999a 100644 --- a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/VAPID.php +++ b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/VAPID.php @@ -97,7 +97,7 @@ class VAPID * @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) + public static function getVapidHeaders(string $audience, string $subject, string $publicKey, string $privateKey, string $contentEncoding, ?int $expiration = null): array { $expirationLimit = time() + 43200; // equal margin of error between 0 and 24h if (null === $expiration || $expiration > $expirationLimit) { @@ -176,7 +176,7 @@ class VAPID return [ 'publicKey' => Base64Url::encode($binaryPublicKey), - 'privateKey' => Base64Url::encode($binaryPrivateKey) + 'privateKey' => Base64Url::encode($binaryPrivateKey), ]; } } diff --git a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/WebPush.php b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/WebPush.php index 97784f1bbc..8739a98d5e 100644 --- a/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/WebPush.php +++ b/wcfsetup/install/files/lib/system/api/minishlink/web-push/src/WebPush.php @@ -15,69 +15,53 @@ namespace Minishlink\WebPush; use Base64Url\Base64Url; use GuzzleHttp\Client; +use GuzzleHttp\Pool; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request; use Psr\Http\Message\ResponseInterface; class WebPush { - /** - * @var Client - */ - protected $client; - - /** - * @var array - */ - protected $auth; + protected Client $client; + protected array $auth; /** * @var null|array Array of array of Notifications */ - protected $notifications; + protected ?array $notifications = null; /** - * @var array Default options : TTL, urgency, topic, batchSize + * @var array Default options: TTL, urgency, topic, batchSize, requestConcurrency */ - protected $defaultOptions; + protected array $defaultOptions; /** * @var int Automatic padding of payloads, if disabled, trade security for bandwidth */ - protected $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH; + protected int $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH; /** * @var bool Reuse VAPID headers in the same flush session to improve performance */ - protected $reuseVAPIDHeaders = false; + protected bool $reuseVAPIDHeaders = false; /** * @var array Dictionary for VAPID headers cache */ - protected $vapidHeaders = []; + protected array $vapidHeaders = []; /** * WebPush constructor. * - * @param array $auth Some servers needs authentication - * @param array $defaultOptions TTL, urgency, topic, batchSize + * @param array $auth Some servers need authentication + * @param array $defaultOptions TTL, urgency, topic, batchSize, requestConcurrency * @param int|null $timeout Timeout of POST request * * @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.', - '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); - } - } + Utils::checkRequirement(); if (isset($auth['VAPID'])) { $auth['VAPID'] = VAPID::validate($auth['VAPID']); @@ -140,7 +124,7 @@ class WebPush * * @param null|int $batchSize Defaults the value defined in defaultOptions during instantiation (which defaults to 1000). * - * @return \Generator|MessageSentReport[] + * @return \Generator * @throws \ErrorException */ public function flush(?int $batchSize = null): \Generator @@ -193,7 +177,59 @@ class WebPush } /** - * @throws \ErrorException + * Flush notifications. Triggers concurrent requests. + * + * @param callable(MessageSentReport): void $callback Callback for each notification + * @param null|int $batchSize Defaults the value defined in defaultOptions during instantiation (which defaults to 1000). + * @param null|int $requestConcurrency Defaults the value defined in defaultOptions during instantiation (which defaults to 100). + */ + public function flushPooled($callback, ?int $batchSize = null, ?int $requestConcurrency = null): void + { + if (empty($this->notifications)) { + return; + } + + if (null === $batchSize) { + $batchSize = $this->defaultOptions['batchSize']; + } + + if (null === $requestConcurrency) { + $requestConcurrency = $this->defaultOptions['requestConcurrency']; + } + + $batches = array_chunk($this->notifications, $batchSize); + $this->notifications = []; + + foreach ($batches as $batch) { + $batch = $this->prepare($batch); + $pool = new Pool($this->client, $batch, [ + 'requestConcurrency' => $requestConcurrency, + 'fulfilled' => function (ResponseInterface $response, int $index) use ($callback, $batch) { + /** @var \Psr\Http\Message\RequestInterface $request **/ + $request = $batch[$index]; + $callback(new MessageSentReport($request, $response)); + }, + 'rejected' => function (RequestException $reason) use ($callback) { + if (method_exists($reason, 'getResponse')) { + $response = $reason->getResponse(); + } else { + $response = null; + } + $callback(new MessageSentReport($reason->getRequest(), $response, false, $reason->getMessage())); + }, + ]); + + $promise = $pool->promise(); + $promise->wait(); + } + + if ($this->reuseVAPIDHeaders) { + $this->vapidHeaders = []; + } + } + + /** + * @throws \ErrorException|\Random\RandomException */ protected function prepare(array $notifications): array { @@ -281,50 +317,45 @@ class WebPush return $this->automaticPadding !== 0; } - /** - * @return int - */ - public function getAutomaticPadding() + public function getAutomaticPadding(): int { return $this->automaticPadding; } /** - * @param int|bool $automaticPadding Max padding length + * @param bool|int $automaticPadding Max padding length * - * @throws \Exception + * @throws \ValueError */ - public function setAutomaticPadding($automaticPadding): WebPush + public function setAutomaticPadding(bool|int $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; + if ($automaticPadding === true) { + $automaticPadding = Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH; } elseif ($automaticPadding === false) { - $this->automaticPadding = 0; - } else { - $this->automaticPadding = $automaticPadding; + $automaticPadding = 0; + } + + if($automaticPadding > Encryption::MAX_PAYLOAD_LENGTH) { + throw new \ValueError('Automatic padding is too large. Max is '.Encryption::MAX_PAYLOAD_LENGTH.'. Recommended max is '.Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH.' for compatibility reasons (see README).'); + } + if($automaticPadding < 0) { + throw new \ValueError('Padding length should be positive or zero.'); } + $this->automaticPadding = $automaticPadding; + return $this; } - /** - * @return bool - */ - public function getReuseVAPIDHeaders() + public function getReuseVAPIDHeaders(): bool { return $this->reuseVAPIDHeaders; } /** * Reuse VAPID headers in the same flush session to improve performance - * - * @return WebPush */ - public function setReuseVAPIDHeaders(bool $enabled) + public function setReuseVAPIDHeaders(bool $enabled): WebPush { $this->reuseVAPIDHeaders = $enabled; @@ -337,16 +368,16 @@ class WebPush } /** - * @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize' - * - * @return WebPush + * @param array $defaultOptions Keys 'TTL' (Time To Live, defaults 4 weeks), 'urgency', 'topic', 'batchSize', 'requestConcurrency' */ - public function setDefaultOptions(array $defaultOptions) + public function setDefaultOptions(array $defaultOptions): WebPush { $this->defaultOptions['TTL'] = $defaultOptions['TTL'] ?? 2419200; $this->defaultOptions['urgency'] = $defaultOptions['urgency'] ?? null; $this->defaultOptions['topic'] = $defaultOptions['topic'] ?? null; $this->defaultOptions['batchSize'] = $defaultOptions['batchSize'] ?? 1000; + $this->defaultOptions['requestConcurrency'] = $defaultOptions['requestConcurrency'] ?? 100; + return $this; } @@ -357,10 +388,9 @@ class WebPush } /** - * @return array * @throws \ErrorException */ - protected function getVAPIDHeaders(string $audience, string $contentEncoding, array $vapid) + protected function getVAPIDHeaders(string $audience, string $contentEncoding, array $vapid): ?array { $vapidHeaders = null; -- 2.20.1