Skip to content

Commit 01577e2

Browse files
authored
Merge pull request #2 from renoki-co/feature/validator
[feature] SNS Message Validator
2 parents b834d96 + 356e3dc commit 01577e2

File tree

10 files changed

+204
-128
lines changed

10 files changed

+204
-128
lines changed

README.md

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -82,29 +82,6 @@ protected $except = [
8282

8383
If you have registered the route and created a SNS Topic, you should register the URL and click the confirmation button from the AWS Dashboard. In a short while, if you implemented the route well, you'll be seeing that your endpoint is registered.
8484

85-
## Whitelisting Topic ARNs
86-
87-
When receiving the SNS messages to your endpoint, it's good to filter them out. To do it, specify an `$allowedTopicArns` static attribute on your extended controller.
88-
89-
The package will filter out any messages coming from other topic ARNs.
90-
91-
```php
92-
use RenokiCo\AwsWebhooks\Http\Controllers\SesWebhook;
93-
94-
class MySesController extends SesWebhook
95-
{
96-
/**
97-
* List the allowed SNS Topic ARNs
98-
* that are allowed to run the business logic.
99-
*
100-
* @var array
101-
*/
102-
protected static $allowedTopicArns = [
103-
'arn:aws:sns:us-west-2:123456789012:MyTopic',
104-
];
105-
}
106-
```
107-
10885
## Simple Email Service (SES)
10986

11087
Simple Email Service integrates with SNS to send notifications regarding mails. For example, you can catch bouncers or click/opens for various addresses.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
],
1414
"require": {
1515
"laravel/framework": "^6.18.28|^7.22.1",
16-
"rennokki/laravel-sns-events": "^5.1"
16+
"rennokki/laravel-sns-events": "^6.0"
1717
},
1818
"autoload": {
1919
"psr-4": {

src/Http/Controllers/CloudwatchWebhook.php

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,6 @@
77

88
class CloudwatchWebhook extends SnsController
99
{
10-
/**
11-
* List the allowed SNS Topic ARNs
12-
* that are allowed to run the business logic.
13-
*
14-
* @var array
15-
*/
16-
protected static $allowedTopicArns = [
17-
//
18-
];
19-
2010
/**
2111
* Handle logic at the controller level on notification.
2212
*
@@ -26,10 +16,6 @@ class CloudwatchWebhook extends SnsController
2616
*/
2717
protected function onNotification(array $snsMessage, Request $request): void
2818
{
29-
if (! $this->shouldAllow($snsMessage)) {
30-
return;
31-
}
32-
3319
$decodedMessage = json_decode($snsMessage['Message'], true);
3420

3521
$state = $decodedMessage['NewStateValue'] ?? null;
@@ -42,20 +28,6 @@ protected function onNotification(array $snsMessage, Request $request): void
4228
}
4329
}
4430

45-
/**
46-
* Check if the topic ARN from
47-
* the SNS message is whitelisted.
48-
*
49-
* @param array $snsMessage
50-
* @return bool
51-
*/
52-
protected function shouldAllow(array $snsMessage): bool
53-
{
54-
return in_array(
55-
$snsMessage['TopicArn'] ?? null, static::$allowedTopicArns
56-
);
57-
}
58-
5931
/**
6032
* Handle the event when an alarm transitioned to OK.
6133
*

src/Http/Controllers/SesWebhook.php

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,6 @@
77

88
class SesWebhook extends SnsController
99
{
10-
/**
11-
* List the allowed SNS Topic ARNs
12-
* that are allowed to run the business logic.
13-
*
14-
* @var array
15-
*/
16-
protected static $allowedTopicArns = [
17-
//
18-
];
19-
2010
/**
2111
* Associate each SNS `eventType` value
2212
* with a callable method from this class.
@@ -44,10 +34,6 @@ class SesWebhook extends SnsController
4434
*/
4535
protected function onNotification(array $snsMessage, Request $request): void
4636
{
47-
if (! $this->shouldAllow($snsMessage)) {
48-
return;
49-
}
50-
5137
$decodedMessage = json_decode($snsMessage['Message'], true);
5238

5339
$eventType = $decodedMessage['eventType'] ?? null;
@@ -62,20 +48,6 @@ protected function onNotification(array $snsMessage, Request $request): void
6248
}
6349
}
6450

65-
/**
66-
* Check if the topic ARN from
67-
* the SNS message is whitelisted.
68-
*
69-
* @param array $snsMessage
70-
* @return bool
71-
*/
72-
protected function shouldAllow(array $snsMessage): bool
73-
{
74-
return in_array(
75-
$snsMessage['TopicArn'] ?? null, static::$allowedTopicArns
76-
);
77-
}
78-
7951
/**
8052
* Handle the Bounce event.
8153
*

tests/CloudwatchTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ class CloudwatchTest extends TestCase
77
public function test_callable_methods()
88
{
99
$payloads = [
10-
$this->getCloudwatchNotificationPayload('ALARM', 'OK'),
11-
$this->getCloudwatchNotificationPayload('OK', 'ALARM'),
12-
$this->getCloudwatchNotificationPayload('INSUFFICIENT_DATA', 'OK'),
10+
$this->getCloudwatchMessage('ALARM', 'OK'),
11+
$this->getCloudwatchMessage('OK', 'ALARM'),
12+
$this->getCloudwatchMessage('INSUFFICIENT_DATA', 'OK'),
1313
];
1414

1515
foreach ($payloads as $payload) {
16-
$this
17-
->json('GET', route('cloudwatch'), $payload)
16+
$this->withHeaders($this->getHeadersForMessage($payload))
17+
->json('GET', route('cloudwatch', ['certificate' => static::$certificate]), $payload)
1818
->assertSee('OK');
1919
}
2020
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
3+
namespace RenokiCo\AwsWebhooks\Test\Concerns;
4+
5+
use Aws\Sns\Message;
6+
use Aws\Sns\MessageValidator;
7+
8+
trait GeneratesSnsMessages
9+
{
10+
/**
11+
* Get the private key to sign the request.
12+
*
13+
* @var string
14+
*/
15+
protected static $privateKey;
16+
17+
/**
18+
* The certificate to sign the request.
19+
*
20+
* @var string
21+
*/
22+
protected static $certificate;
23+
24+
/**
25+
* An valid certificate URL for test.
26+
*
27+
* @var string
28+
*/
29+
public static $validCertUrl = 'https://sns.us-west-2.amazonaws.com/bar.pem';
30+
31+
/**
32+
* Initialize the SSL keys and private keys.
33+
*
34+
* @return void
35+
*/
36+
protected static function initializeSsl(): void
37+
{
38+
self::$privateKey = openssl_pkey_new();
39+
40+
$csr = openssl_csr_new([], self::$privateKey);
41+
42+
$x509 = openssl_csr_sign($csr, null, self::$privateKey, 1);
43+
44+
openssl_x509_export($x509, self::$certificate);
45+
46+
openssl_x509_free($x509);
47+
}
48+
49+
/**
50+
* Deinitialize the SSL keys.
51+
*
52+
* @return void
53+
*/
54+
protected static function tearDownSsl(): void
55+
{
56+
openssl_pkey_free(self::$privateKey);
57+
}
58+
59+
/**
60+
* Get the signature for the message.
61+
*
62+
* @param string $stringToSign
63+
* @return string
64+
*/
65+
protected function getSignature($stringToSign)
66+
{
67+
openssl_sign($stringToSign, $signature, self::$privateKey);
68+
69+
return base64_encode($signature);
70+
}
71+
72+
/**
73+
* Get an example subscription payload for testing.
74+
*
75+
* @param array $custom
76+
* @return array
77+
*/
78+
protected function getSubscriptionConfirmationPayload(array $custom = []): array
79+
{
80+
$validator = new MessageValidator;
81+
82+
$message = array_merge([
83+
'Type' => 'SubscriptionConfirmation',
84+
'MessageId' => '165545c9-2a5c-472c-8df2-7ff2be2b3b1b',
85+
'Token' => '2336412f37...',
86+
'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:MyTopic',
87+
'Message' => 'You have chosen to subscribe to the topic arn:aws:sns:us-west-2:123456789012:MyTopic.\nTo confirm the subscription, visit the SubscribeURL included in this message.',
88+
'SubscribeURL' => 'https://example.com',
89+
'Timestamp' => now()->toDateTimeString(),
90+
'SignatureVersion' => '1',
91+
'Signature' => true,
92+
'SigningCertURL' => static::$validCertUrl,
93+
], $custom);
94+
95+
$message['Signature'] = $this->getSignature(
96+
$validator->getStringToSign(new Message($message))
97+
);
98+
99+
return $message;
100+
}
101+
102+
/**
103+
* Get an example notification payload for testing.
104+
*
105+
* @param array $payload
106+
* @param array $custom
107+
* @return array
108+
*/
109+
protected function getNotificationPayload(array $payload = [], array $custom = []): array
110+
{
111+
$validator = new MessageValidator;
112+
113+
$payload = json_encode($payload);
114+
115+
$message = array_merge([
116+
'Type' => 'Notification',
117+
'MessageId' => '22b80b92-fdea-4c2c-8f9d-bdfb0c7bf324',
118+
'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:MyTopic',
119+
'Subject' => 'My First Message',
120+
'Message' => "{$payload}",
121+
'Timestamp' => now()->toDateTimeString(),
122+
'SignatureVersion' => '1',
123+
'Token' => '2336412f37...',
124+
'Signature' => true,
125+
'SigningCertURL' => static::$validCertUrl,
126+
'UnsubscribeURL' => 'https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-west-2:123456789012:MyTopic:c9135db0-26c4-47ec-8998-413945fb5a96',
127+
], $custom);
128+
129+
$message['Signature'] = $this->getSignature(
130+
$validator->getStringToSign(new Message($message))
131+
);
132+
133+
return $message;
134+
}
135+
136+
/**
137+
* Get the right headers for a SNS message.
138+
*
139+
* @param array $message
140+
* @return array
141+
*/
142+
protected function getHeadersForMessage(array $message): array
143+
{
144+
return [
145+
'X-AMZ-SNS-MESSAGE-TYPE' => $message['Type'],
146+
'X-AMZ-SNS-MESSAGE-ID' => $message['MessageId'],
147+
'X-AMZ-SNS-TOPIC-ARN' => $message['TopicArn'],
148+
'X-AMZ-SNS-SUBSCRIPTION-ARN' => "{$message['TopicArn']}:c9135db0-26c4-47ec-8998-413945fb5a96",
149+
];
150+
}
151+
}

tests/Controllers/CloudwatchController.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
namespace RenokiCo\AwsWebhooks\Test\Controllers;
44

5+
use Aws\Sns\MessageValidator;
6+
use Illuminate\Http\Request;
57
use RenokiCo\AwsWebhooks\Http\Controllers\CloudwatchWebhook;
68

79
class CloudwatchController extends CloudwatchWebhook
810
{
911
/**
10-
* List the allowed SNS Topic ARNs
11-
* that are allowed to run the business logic.
12+
* Get the message validator instance.
1213
*
13-
* @var array
14+
* @param \Illuminate\Http\Request $request
15+
* @return \Aws\Sns\MessageValidator
1416
*/
15-
protected static $allowedTopicArns = [
16-
'arn:aws:sns:us-west-2:123456789012:MyTopic',
17-
];
17+
protected function getMessageValidator(Request $request)
18+
{
19+
return new MessageValidator(function ($url) use ($request) {
20+
return $request->certificate ?: $url;
21+
});
22+
}
1823
}

tests/Controllers/SesController.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,22 @@
22

33
namespace RenokiCo\AwsWebhooks\Test\Controllers;
44

5+
use Aws\Sns\MessageValidator;
6+
use Illuminate\Http\Request;
57
use RenokiCo\AwsWebhooks\Http\Controllers\SesWebhook;
68

79
class SesController extends SesWebhook
810
{
911
/**
10-
* List the allowed SNS Topic ARNs
11-
* that are allowed to run the business logic.
12+
* Get the message validator instance.
1213
*
13-
* @var array
14+
* @param \Illuminate\Http\Request $request
15+
* @return \Aws\Sns\MessageValidator
1416
*/
15-
protected static $allowedTopicArns = [
16-
'arn:aws:sns:us-west-2:123456789012:MyTopic',
17-
];
17+
protected function getMessageValidator(Request $request)
18+
{
19+
return new MessageValidator(function ($url) use ($request) {
20+
return $request->certificate ?: $url;
21+
});
22+
}
1823
}

tests/SesTest.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ class SesTest extends TestCase
99
public function test_callable_methods()
1010
{
1111
foreach (SesWebhook::$eventTypesWithCalledMethod as $callableEventType => $methodToCall) {
12-
$this
13-
->json('GET', route('ses'), $this->getSesMessage($callableEventType))
12+
$payload = $this->getSesMessage($callableEventType);
13+
14+
$this->withHeaders($this->getHeadersForMessage($payload))
15+
->json('GET', route('ses', ['certificate' => static::$certificate]), $payload)
1416
->assertSee('OK');
1517
}
1618
}

0 commit comments

Comments
 (0)