Skip to content

Commit 319cf1a

Browse files
committed
First commit
0 parents  commit 319cf1a

File tree

8 files changed

+368
-0
lines changed

8 files changed

+368
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/vendor
2+
/.idea
3+
.DS_Store
4+
coverage.xml
5+
composer.lock

.scrutinizer.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
filter:
2+
paths: [src/*]
3+
excluded_paths: [tests/*]
4+
checks:
5+
php:
6+
code_rating: true
7+
remove_extra_empty_lines: true
8+
remove_php_closing_tag: true
9+
remove_trailing_whitespace: true
10+
fix_use_statements:
11+
remove_unused: true
12+
preserve_multiple: false
13+
preserve_blanklines: true
14+
order_alphabetically: true
15+
fix_php_opening_tag: true
16+
fix_linefeed: true
17+
fix_line_ending: true
18+
fix_identation_4spaces: true
19+
fix_doc_comments: true
20+
tools:
21+
external_code_coverage: true
22+
php_code_coverage: false
23+
php_code_sniffer:
24+
config:
25+
standard: PSR2
26+
filter:
27+
paths: ['src']
28+
php_loc:
29+
enabled: true
30+
excluded_dirs: [vendor]
31+
php_cpd:
32+
enabled: true
33+
excluded_dirs: [vendor]

.travis.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
language: php
2+
3+
php:
4+
- 7.0
5+
- 7.1
6+
7+
cache:
8+
directories:
9+
- $HOME/.composer/cache
10+
11+
before_install:
12+
- travis_retry composer self-update
13+
14+
install:
15+
- travis_retry composer install --no-interaction --prefer-dist --no-suggest
16+
17+
script:
18+
- vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
19+
20+
after_script:
21+
- wget https://scrutinizer-ci.com/ocular.phar
22+
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 Laravel Shield
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

composer.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "laravel-shield/mailgun",
3+
"description": "A mailgun service for Laravel Shield.",
4+
"type": "library",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Ashley Clarke",
9+
"email": "[email protected]"
10+
},
11+
{
12+
"name": "Paul Redmond",
13+
"email": "[email protected]"
14+
}
15+
],
16+
"require": {
17+
"laravel-shield/shield": "^1.0"
18+
},
19+
"require-dev": {
20+
"laravel-shield/testing": "^1.0"
21+
},
22+
"autoload": {
23+
"psr-4": {
24+
"Shield\\Mailgun\\": "src/"
25+
}
26+
},
27+
"autoload-dev": {
28+
"psr-4": {
29+
"Shield\\Mailgun\\Test\\": "tests/"
30+
}
31+
}
32+
}

phpunit.xml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit backupGlobals="false"
3+
backupStaticAttributes="false"
4+
bootstrap="vendor/autoload.php"
5+
colors="true"
6+
convertErrorsToExceptions="true"
7+
convertNoticesToExceptions="true"
8+
convertWarningsToExceptions="true"
9+
processIsolation="false"
10+
stopOnFailure="false">
11+
<testsuites>
12+
13+
<testsuite name="Unit Tests">
14+
<directory suffix="Test.php">./tests/Unit</directory>
15+
</testsuite>
16+
17+
</testsuites>
18+
<filter>
19+
<whitelist processUncoveredFilesFromWhitelist="true">
20+
<directory suffix=".php">./src</directory>
21+
</whitelist>
22+
</filter>
23+
<php>
24+
<env name="APP_ENV" value="testing"/>
25+
<env name="CACHE_DRIVER" value="array"/>
26+
<env name="SESSION_DRIVER" value="array"/>
27+
<env name="QUEUE_DRIVER" value="sync"/>
28+
</php>
29+
<logging>
30+
<log type="coverage-clover" target="coverage.xml" showUncoveredFiles="true"/>
31+
</logging>
32+
</phpunit>

src/Mailgun.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Shield\Mailgun;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Collection;
7+
use Shield\Shield\Contracts\Service;
8+
9+
/**
10+
* Class Skeleton
11+
*
12+
* @package \Shield\Skeleton
13+
*/
14+
class Mailgun implements Service
15+
{
16+
public function verify(Request $request, Collection $config): bool
17+
{
18+
$tolerance = $config->get('tolerance', 60 * 5);
19+
$timestamp = $request->input('timestamp');
20+
21+
if (
22+
! $request->isMethod('POST') ||
23+
abs(time() - $timestamp) > $tolerance
24+
) {
25+
return false;
26+
}
27+
28+
$signature = hash_hmac(
29+
'sha256',
30+
$request->input('timestamp') . $request->input('token'),
31+
$config->get('token')
32+
);
33+
34+
return $signature === $request->input('signature');
35+
}
36+
37+
public function headers(): array
38+
{
39+
return [];
40+
}
41+
}

tests/Unit/ServiceTest.php

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?php
2+
3+
namespace Shield\Mailgun\Test\Unit;
4+
5+
use Shield\Mailgun\Mailgun;
6+
use Shield\Testing\TestCase;
7+
use Illuminate\Http\Request;
8+
use PHPUnit\Framework\Assert;
9+
use Illuminate\Support\Carbon;
10+
use Shield\Shield\Contracts\Service;
11+
12+
/**
13+
* Class ServiceTest
14+
*
15+
* @package \Shield\Mailgun\Test\Unit
16+
*/
17+
class ServiceTest extends TestCase
18+
{
19+
/**
20+
* @var \Shield\Mailgun\Mailgun
21+
*/
22+
protected $service;
23+
24+
protected function setUp()
25+
{
26+
parent::setUp();
27+
28+
$this->service = new Mailgun;
29+
}
30+
31+
/** @test */
32+
public function it_is_a_service()
33+
{
34+
Assert::assertInstanceOf(Service::class, new Mailgun);
35+
}
36+
37+
/** @test */
38+
public function it_can_verify_a_valid_request()
39+
{
40+
$token = 'raNd0mk3y';
41+
$this->app['config']['shield.services.mailgun.options.token'] = $token;
42+
43+
// Build the signature for the request payload
44+
$timestamp = Carbon::now()->timestamp;
45+
$signature = $this->buildSignature($timestamp, $token);
46+
47+
$request = $this->request(json_encode([
48+
'timestamp' => $timestamp,
49+
'token' => $token,
50+
'signature' => $signature,
51+
]));
52+
53+
$headers = [
54+
'Content-Type' => 'application/json'
55+
];
56+
57+
$request->headers->add($headers);
58+
59+
Assert::assertTrue($this->service->verify($request, $this->getConfig()));
60+
}
61+
62+
/** @test */
63+
public function it_will_not_verify_an_old_request()
64+
{
65+
$token = 'raNd0mk3y';
66+
$tolerance = 60;
67+
68+
$this->app['config']['shield.services.mailgun.options.token'] = $token;
69+
$this->app['config']['shield.services.mailgun.options.tolerance'] = $tolerance;
70+
71+
// Build the signature for the request payload
72+
$timestamp = Carbon::now()->subSeconds($tolerance + 1)->timestamp;
73+
$signature = $this->buildSignature($timestamp, $token);
74+
75+
$request = $this->request(json_encode([
76+
'timestamp' => $timestamp,
77+
'token' => $token,
78+
'signature' => $signature,
79+
]));
80+
81+
$headers = [
82+
'Content-Type' => 'application/json'
83+
];
84+
85+
$request->headers->add($headers);
86+
87+
Assert::assertFalse($this->service->verify($request, $this->getConfig()));
88+
}
89+
90+
/** @test */
91+
public function it_will_not_verify_a_bad_request()
92+
{
93+
$token = 'good';
94+
$this->app['config']['shield.services.mailgun.options.token'] = $token;
95+
96+
// Build the signature for the request payload
97+
$timestamp = Carbon::now()->timestamp;
98+
$signature = $this->buildSignature($timestamp, $token);
99+
100+
$request = $this->request(json_encode([
101+
'timestamp' => $timestamp,
102+
'token' => 'bad',
103+
'signature' => $signature,
104+
]));
105+
106+
$headers = [
107+
'Content-Type' => 'application/json'
108+
];
109+
110+
$request->headers->add($headers);
111+
112+
Assert::assertFalse($this->service->verify($request, $this->getConfig()));
113+
}
114+
115+
/** @test */
116+
public function it_requires_a_post_request()
117+
{
118+
// Set up valid data
119+
$token = 'raNd0mk3y';
120+
$this->app['config']['shield.services.mailgun.options.token'] = $token;
121+
122+
// Build the signature for the request payload
123+
$timestamp = Carbon::now()->timestamp;
124+
$signature = $this->buildSignature($timestamp, $token);
125+
126+
$requestBody = json_encode([
127+
'timestamp' => $timestamp,
128+
'token' => $token,
129+
'signature' => $signature,
130+
]);
131+
132+
$examples = ['GET', 'PUT', 'PATCH', 'DELETE', 'POST'];
133+
134+
foreach ($examples as $example) {
135+
136+
$request = Request::create('http://example.com', $example, [], [], [], [], $requestBody);
137+
$request->headers->add([
138+
'Content-Type' => 'application/json'
139+
]);
140+
141+
$assertion = $example === 'POST' ? 'assertTrue' : 'assertFalse';
142+
143+
Assert::$assertion(
144+
$this->service->verify($request, $this->getConfig()),
145+
"Expected $example to $assertion, but it did not."
146+
);
147+
}
148+
}
149+
150+
/** @test */
151+
public function it_has_correct_headers_required()
152+
{
153+
Assert::assertArraySubset([], $this->service->headers());
154+
}
155+
156+
/**
157+
* Get a configuration value for mailgun
158+
* @param $value An optional value
159+
* @param $default When a value is requested, a default value
160+
*
161+
* @return Collection|string
162+
*/
163+
protected function getConfig($value = null, $default = null)
164+
{
165+
$config = collect($this->app['config']['shield.services.mailgun.options']);
166+
167+
if ($value === null) {
168+
return $config;
169+
}
170+
171+
return $config->get($value, $default);
172+
}
173+
174+
protected function buildSignature($timestamp, $token)
175+
{
176+
return hash_hmac(
177+
'sha256',
178+
sprintf('%s%s', $timestamp, $token),
179+
$this->getConfig('token')
180+
);
181+
}
182+
}

0 commit comments

Comments
 (0)