diff --git a/src/Symfony/Component/Mailer/Bridge/Sweego/README.md b/src/Symfony/Component/Mailer/Bridge/Sweego/README.md index 221dce1a662dc..0845037fb7cca 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sweego/README.md +++ b/src/Symfony/Component/Mailer/Bridge/Sweego/README.md @@ -24,6 +24,33 @@ MAILER_DSN=sweego+api://API_KEY@default where: - `API_KEY` is your Sweego API Key +Webhook +------- + +Configure the webhook routing: + +```yaml +framework: + webhook: + routing: + sweego_mailer: + service: mailer.webhook.request_parser.sweego + secret: '%env(SWEEGO_WEBHOOK_SECRET)%' +``` + +And a consumer: + +```php +#[AsRemoteEventConsumer(name: 'sweego_mailer')] +class SweegoMailEventConsumer implements ConsumerInterface +{ + public function consume(RemoteEvent|AbstractMailerEvent $event): void + { + // your code + } +} +``` + Sponsor ------- diff --git a/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/Fixtures/sent.json b/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/Fixtures/sent.json deleted file mode 100644 index de6504c1d867c..0000000000000 --- a/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/Fixtures/sent.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "event_type": "email_sent", - "timestamp": "2024-08-15T16:05:59+00:00", - "swg_uid": "02-0d4affd0-1183-43b1-a980-ab30b3374dd3", - "event_id": "97cf3afe-f63a-4d92-abac-bde9c7e6523e", - "channel": "email", - "headers": { - "x-transaction-id": "d4fbec9d-eed9-44d5-af47-c1126467a5ca" - }, - "campaign_tags": null, - "campaign_type": "transac", - "campaign_id": "transac", - "recipient": "recipient@example.com", - "domain_from": "example.org" -} diff --git a/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/Fixtures/sent.php b/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/Fixtures/sent.php deleted file mode 100644 index b771b2e791954..0000000000000 --- a/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/Fixtures/sent.php +++ /dev/null @@ -1,12 +0,0 @@ -setRecipientEmail('recipient@example.com'); -$wh->setMetadata([ - 'x-transaction-id' => 'd4fbec9d-eed9-44d5-af47-c1126467a5ca', -]); -$wh->setDate(\DateTimeImmutable::createFromFormat(\DATE_ATOM, '2024-08-15T16:05:59+00:00')); - -return $wh; diff --git a/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php index e60f2ebb3f882..329354c29ab06 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php +++ b/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php @@ -28,6 +28,9 @@ protected function createRequest(string $payload): Request { return Request::create('/', 'POST', [], [], [], [ 'Content-Type' => 'application/json', + 'HTTP_webhook-id' => '9f26b9d0-13d7-410c-ba04-5019cd30e6d0', + 'HTTP_webhook-timestamp' => '1723737959', + 'HTTP_webhook-signature' => 'W+fm4VPshCGjuT0HxyV00QEbFitZd2Rdvx82bWM7VXc=', ], $payload); } } diff --git a/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/SweegoWrongSignatureRequestParserTest.php b/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/SweegoWrongSignatureRequestParserTest.php new file mode 100644 index 0000000000000..e797a3b542f31 --- /dev/null +++ b/src/Symfony/Component/Mailer/Bridge/Sweego/Tests/Webhook/SweegoWrongSignatureRequestParserTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Bridge\Sweego\Tests\Webhook; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Mailer\Bridge\Sweego\RemoteEvent\SweegoPayloadConverter; +use Symfony\Component\Mailer\Bridge\Sweego\Webhook\SweegoRequestParser; +use Symfony\Component\Webhook\Client\RequestParserInterface; +use Symfony\Component\Webhook\Exception\RejectWebhookException; +use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; + +class SweegoWrongSignatureRequestParserTest extends AbstractRequestParserTestCase +{ + protected function createRequestParser(): RequestParserInterface + { + $this->expectException(RejectWebhookException::class); + $this->expectExceptionMessage('Invalid signature.'); + + return new SweegoRequestParser(new SweegoPayloadConverter()); + } + + protected function createRequest(string $payload): Request + { + return Request::create('/', 'POST', [], [], [], [ + 'Content-Type' => 'application/json', + 'HTTP_webhook-id' => '9f26b9d0-13d7-410c-ba04-5019cd30e6d0', + 'HTTP_webhook-timestamp' => '1723737959', + 'HTTP_webhook-signature' => 'wrong_signature', + ], $payload); + } +} diff --git a/src/Symfony/Component/Mailer/Bridge/Sweego/Webhook/SweegoRequestParser.php b/src/Symfony/Component/Mailer/Bridge/Sweego/Webhook/SweegoRequestParser.php index 775b755c3f26d..ec81bbdec9b68 100644 --- a/src/Symfony/Component/Mailer/Bridge/Sweego/Webhook/SweegoRequestParser.php +++ b/src/Symfony/Component/Mailer/Bridge/Sweego/Webhook/SweegoRequestParser.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpFoundation\ChainRequestMatcher; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcher\HeaderRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcherInterface; @@ -34,6 +35,7 @@ protected function getRequestMatcher(): RequestMatcherInterface return new ChainRequestMatcher([ new MethodRequestMatcher('POST'), new IsJsonRequestMatcher(), + new HeaderRequestMatcher(['webhook-id', 'webhook-timestamp', 'webhook-signature']), ]); } @@ -51,10 +53,28 @@ protected function doParse(Request $request, #[\SensitiveParameter] string $secr throw new RejectWebhookException(406, 'Payload is malformed.'); } + $this->validateSignature($request, $secret); + try { return $this->converter->convert($content); } catch (ParseException $e) { throw new RejectWebhookException(406, $e->getMessage(), $e); } } + + private function validateSignature(Request $request, string $secret): void + { + $contentToSign = \sprintf( + '%s.%s.%s', + $request->headers->get('webhook-id'), + $request->headers->get('webhook-timestamp'), + $request->getContent(), + ); + + $computedSignature = base64_encode(hash_hmac('sha256', $contentToSign, base64_decode($secret), true)); + + if (!hash_equals($computedSignature, $request->headers->get('webhook-signature'))) { + throw new RejectWebhookException(403, 'Invalid signature.'); + } + } } diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/README.md b/src/Symfony/Component/Notifier/Bridge/Sweego/README.md index 807d14000ced5..283c3b398c70c 100644 --- a/src/Symfony/Component/Notifier/Bridge/Sweego/README.md +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/README.md @@ -44,6 +44,33 @@ $sms->options($options); $texter->send($sms); ``` +Webhook +------- + +Configure the webhook routing: + +```yaml +framework: + webhook: + routing: + sweego_sms: + service: notifier.webhook.request_parser.sweego + secret: '%env(SWEEGO_WEBHOOK_SECRET)%' +``` + +And a consumer: + +```php +#[AsRemoteEventConsumer(name: 'sweego_sms')] +class SweegoSmsEventConsumer implements ConsumerInterface +{ + public function consume(RemoteEvent|SmsEvent $event): void + { + // your code + } +} +``` + Sponsor ------- diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php index 50d74d158246c..8357a7748433d 100644 --- a/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoRequestParserTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Notifier\Bridge\Sweego\Tests\Webhook; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Notifier\Bridge\Sweego\Webhook\SweegoRequestParser; use Symfony\Component\Webhook\Client\RequestParserInterface; use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; @@ -21,4 +22,14 @@ protected function createRequestParser(): RequestParserInterface { return new SweegoRequestParser(); } + + protected function createRequest(string $payload): Request + { + return Request::create('/', 'POST', [], [], [], [ + 'Content-Type' => 'application/json', + 'HTTP_webhook-id' => 'a5ccc627-6e43-4012-bb29-f1bfe3a3d13e', + 'HTTP_webhook-timestamp' => '1725290740', + 'HTTP_webhook-signature' => 'k7SwzHXZqVKNvCpp6HwGS/5aDZ6NraYnKmVkBdx7MHE=', + ], $payload); + } } diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoWrongSignatureRequestParserTest.php b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoWrongSignatureRequestParserTest.php new file mode 100644 index 0000000000000..69689d4195553 --- /dev/null +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/Tests/Webhook/SweegoWrongSignatureRequestParserTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Notifier\Bridge\Sweego\Tests\Webhook; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Notifier\Bridge\Sweego\Webhook\SweegoRequestParser; +use Symfony\Component\Webhook\Client\RequestParserInterface; +use Symfony\Component\Webhook\Exception\RejectWebhookException; +use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase; + +class SweegoWrongSignatureRequestParserTest extends AbstractRequestParserTestCase +{ + protected function createRequestParser(): RequestParserInterface + { + $this->expectException(RejectWebhookException::class); + $this->expectExceptionMessage('Invalid signature.'); + + return new SweegoRequestParser(); + } + + protected function createRequest(string $payload): Request + { + return Request::create('/', 'POST', [], [], [], [ + 'Content-Type' => 'application/json', + 'HTTP_webhook-id' => 'a5ccc627-6e43-4012-bb29-f1bfe3a3d13e', + 'HTTP_webhook-timestamp' => '1725290740', + 'HTTP_webhook-signature' => 'wrong_signature', + ], $payload); + } +} diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/Webhook/SweegoRequestParser.php b/src/Symfony/Component/Notifier/Bridge/Sweego/Webhook/SweegoRequestParser.php index e35620e956d28..68256d002d00e 100644 --- a/src/Symfony/Component/Notifier/Bridge/Sweego/Webhook/SweegoRequestParser.php +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/Webhook/SweegoRequestParser.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpFoundation\ChainRequestMatcher; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcher\HeaderRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher; use Symfony\Component\HttpFoundation\RequestMatcherInterface; @@ -32,6 +33,7 @@ protected function getRequestMatcher(): RequestMatcherInterface return new ChainRequestMatcher([ new MethodRequestMatcher('POST'), new IsJsonRequestMatcher(), + new HeaderRequestMatcher(['webhook-id', 'webhook-timestamp', 'webhook-signature']), ]); } @@ -43,6 +45,8 @@ protected function doParse(Request $request, #[\SensitiveParameter] string $secr throw new RejectWebhookException(406, 'Payload is malformed.'); } + $this->validateSignature($request, $secret); + $name = match ($payload['event_type']) { 'sms_sent' => SmsEvent::DELIVERED, default => throw new RejectWebhookException(406, \sprintf('Unsupported event "%s".', $payload['event'])), @@ -53,4 +57,20 @@ protected function doParse(Request $request, #[\SensitiveParameter] string $secr return $event; } + + private function validateSignature(Request $request, string $secret): void + { + $contentToSign = \sprintf( + '%s.%s.%s', + $request->headers->get('webhook-id'), + $request->headers->get('webhook-timestamp'), + $request->getContent(), + ); + + $computedSignature = base64_encode(hash_hmac('sha256', $contentToSign, base64_decode($secret), true)); + + if (!hash_equals($computedSignature, $request->headers->get('webhook-signature'))) { + throw new RejectWebhookException(403, 'Invalid signature.'); + } + } } diff --git a/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json b/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json index 81cbdd8cd9897..006d739b86151 100644 --- a/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json +++ b/src/Symfony/Component/Notifier/Bridge/Sweego/composer.json @@ -23,6 +23,9 @@ "require-dev": { "symfony/webhook": "^6.4|^7.0" }, + "conflict": { + "symfony/http-foundation": "<7.1" + }, "autoload": { "psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Sweego\\": "" }, "exclude-from-classmap": [ pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy