Skip to content

Commit

Permalink
[Notifier][Webhook] Add Vonage support
Browse files Browse the repository at this point in the history
  • Loading branch information
smnandre authored and fabpot committed Aug 12, 2023
1 parent b4d215c commit 490adc1
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/Symfony/Component/Notifier/Bridge/Vonage/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

6.4
---

* Add support for `RemoteEvent` and `Webhook`

6.2
---

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"message_uuid": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
"to": "447700900000",
"from": "447700900001",
"timestamp": {},
"status": "delivered",
"usage": {
"currency": "EUR",
"price": "0.0333"
},
"client_ref": "string",
"channel": "sms",
"destination": {
"network_code": "12345"
},
"sms": {
"count_total": "2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;

$wh = new SmsEvent(SmsEvent::DELIVERED, 'aaaaaaaa-bbbb-cccc-dddd-0123456789ab', json_decode(file_get_contents(str_replace('.php', '.json', __FILE__)), true, flags: \JSON_THROW_ON_ERROR));
$wh->setRecipientPhone('447700900000');

return $wh;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"message_uuid": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
"to": "447700900000",
"from": "447700900001",
"timestamp": {},
"status": "rejected",
"error": {
"type": "https://developer.nexmo.com/api-errors/messages-olympus#1000",
"title": 1000,
"detail": "Throttled - You have exceeded the submission capacity allowed on this account. Please wait and retry",
"instance": "bf0ca0bf927b3b52e3cb03217e1a1ddf"
},
"usage": {
"currency": "EUR",
"price": "0.0333"
},
"client_ref": "string",
"channel": "sms",
"destination": {
"network_code": "12345"
},
"sms": {
"count_total": "2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;

$wh = new SmsEvent(SmsEvent::FAILED, 'aaaaaaaa-bbbb-cccc-dddd-0123456789ab', json_decode(file_get_contents(str_replace('.php', '.json', __FILE__)), true, flags: \JSON_THROW_ON_ERROR));
$wh->setRecipientPhone('447700900000');

return $wh;
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"message_uuid": "aaaaaaaa-bbbb-cccc-dddd-0123456789ab",
"to": "447700900000",
"from": "447700900001",
"timestamp": {},
"status": "undeliverable",
"error": {
"type": "https://developer.nexmo.com/api-errors/messages-olympus#1260",
"title": 1260,
"detail": "Destination unreachable - The message could not be delivered to the phone number. If using Viber Business Messages your account might not be enabled for this country.",
"instance": "bf0ca0bf927b3b52e3cb03217e1a1ddf"
},
"usage": {
"currency": "EUR",
"price": "0.0333"
},
"client_ref": "string",
"channel": "sms",
"destination": {
"network_code": "12345"
},
"sms": {
"count_total": "2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;

$wh = new SmsEvent(SmsEvent::FAILED, 'aaaaaaaa-bbbb-cccc-dddd-0123456789ab', json_decode(file_get_contents(str_replace('.php', '.json', __FILE__)), true, flags: \JSON_THROW_ON_ERROR));
$wh->setRecipientPhone('447700900000');

return $wh;
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Notifier\Bridge\Vonage\Tests\Webhook;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Notifier\Bridge\Vonage\Webhook\VonageRequestParser;
use Symfony\Component\Webhook\Client\RequestParserInterface;
use Symfony\Component\Webhook\Exception\RejectWebhookException;
use Symfony\Component\Webhook\Test\AbstractRequestParserTestCase;

class VonageRequestParserTest extends AbstractRequestParserTestCase
{
public function testMissingAuthorizationTokenThrows()
{
$request = $this->createRequest('{}');
$request->headers->remove('Authorization');
$parser = $this->createRequestParser();

$this->expectException(RejectWebhookException::class);
$this->expectExceptionMessage('Missing "Authorization" header');

$parser->parse($request, $this->getSecret());
}

public function testInvalidAuthorizationTokenThrows()
{
$request = $this->createRequest('{}');
$request->headers->set('Authorization', 'Invalid Header');
$parser = $this->createRequestParser();

$this->expectException(RejectWebhookException::class);
$this->expectExceptionMessage('Signature is wrong');

$parser->parse($request, $this->getSecret());
}

protected function createRequestParser(): RequestParserInterface
{
return new VonageRequestParser();
}

protected function createRequest(string $payload): Request
{
// JWT Token signed with the secret key
$jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.kK9JnTXZwzNo3BYNXJT57PGLnQk-Xyu7IBhRWFmc4C0';

$request = parent::createRequest($payload);
$request->headers->set('Authorization', 'Bearer '.$jwt);

return $request;
}

protected function getSecret(): string
{
return 'secret-key';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Notifier\Bridge\Vonage\Webhook;

use Symfony\Component\HttpFoundation\ChainRequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\RemoteEvent\Event\Sms\SmsEvent;
use Symfony\Component\Webhook\Client\AbstractRequestParser;
use Symfony\Component\Webhook\Exception\RejectWebhookException;

final class VonageRequestParser extends AbstractRequestParser
{
protected function getRequestMatcher(): RequestMatcherInterface
{
return new ChainRequestMatcher([
new MethodRequestMatcher('POST'),
new IsJsonRequestMatcher(),
]);
}

protected function doParse(Request $request, string $secret): ?SmsEvent
{
// Signed webhooks: https://developer.vonage.com/en/getting-started/concepts/webhooks#validating-signed-webhooks
if (!$request->headers->has('Authorization')) {
throw new RejectWebhookException(406, 'Missing "Authorization" header.');
}
$this->validateSignature(substr($request->headers->get('Authorization'), \strlen('Bearer ')), $secret);

// Statuses: https://developer.vonage.com/en/api/messages-olympus#message-status
$payload = $request->toArray();
if (
!isset($payload['status'])
|| !isset($payload['message_uuid'])
|| !isset($payload['to'])
|| !isset($payload['channel'])
) {
throw new RejectWebhookException(406, 'Payload is malformed.');
}

if ('sms' !== $payload['channel']) {
throw new RejectWebhookException(406, sprintf('Unsupported channel "%s".', $payload['channel']));
}

$name = match ($payload['status']) {
'delivered' => SmsEvent::DELIVERED,
'rejected' => SmsEvent::FAILED,
'submitted' => null,
'undeliverable' => SmsEvent::FAILED,
default => throw new RejectWebhookException(406, sprintf('Unsupported event "%s".', $payload['status'])),
};
if (!$name) {
return null;
}

$event = new SmsEvent($name, $payload['message_uuid'], $payload);
$event->setRecipientPhone($payload['to']);

return $event;
}

private function validateSignature(string $jwt, string $secret): void
{
$tokenParts = explode('.', $jwt);
if (3 !== \count($tokenParts)) {
throw new RejectWebhookException(406, 'Signature is wrong.');
}

[$header, $payload, $signature] = $tokenParts;
if ($signature !== $this->base64EncodeUrl(hash_hmac('sha256', $header.'.'.$payload, $secret, true))) {
throw new RejectWebhookException(406, 'Signature is wrong.');
}
}

private function base64EncodeUrl(string $string): string
{
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($string));
}
}
3 changes: 3 additions & 0 deletions src/Symfony/Component/Notifier/Bridge/Vonage/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
"symfony/http-client": "^5.4|^6.0|^7.0",
"symfony/notifier": "^6.2.7|^7.0"
},
"require-dev": {
"symfony/webhook": "^6.4|^7.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Notifier\\Bridge\\Vonage\\": "" },
"exclude-from-classmap": [
Expand Down

0 comments on commit 490adc1

Please sign in to comment.