├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── composer.json
├── phpunit.xml.dist
├── src
├── Event
│ ├── GitHubEvent.php
│ ├── GitHubEventFactory.php
│ └── GitHubEventFactoryInterface.php
├── Exception
│ ├── GitHubWebHookException.php
│ ├── InvalidGitHubRequestPayloadException.php
│ ├── InvalidGitHubRequestSignatureException.php
│ └── MissingGitHubEventTypeException.php
└── Security
│ ├── SignatureValidator.php
│ └── SignatureValidatorInterface.php
└── tests
├── Event
└── GitHubEventFactoryTest.php
├── Security
└── SignatureValidatorTest.php
└── bootstrap.php
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 | on: [push, pull_request]
3 | jobs:
4 | tests:
5 | name: PHP ${{ matrix.php }} Tests
6 | runs-on: ubuntu-latest
7 | strategy:
8 | fail-fast: true
9 | matrix:
10 | php: ['7.2', '7.3', '7.4']
11 | steps:
12 | - name: Checkout code
13 | uses: actions/checkout@v2
14 | - name: Setup PHP
15 | uses: shivammathur/setup-php@v2
16 | with:
17 | php-version: ${{ matrix.php }}
18 | tools: composer:v2
19 | coverage: none
20 | - name: Install dependencies
21 | run: composer update --prefer-dist --no-interaction --no-progress
22 | - name: Execute tests
23 | run: vendor/bin/phpunit --coverage-text --verbose
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /phpunit.xml
3 | /composer.lock
4 | !.gitkeep
5 | .phpunit.result.cache
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Contributing
2 | ============
3 |
4 | First of all, **thank you** for contributing, **you are awesome**!
5 |
6 | Here are a few rules to follow in order to ease code reviews, and discussions
7 | before maintainers accept and merge your work:
8 |
9 | * You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and
10 | [PSR-2](http://www.php-fig.org/psr/2/) recommendations. Use the [PHP-CS-Fixer
11 | tool](http://cs.sensiolabs.org/) to fix the syntax of your code automatically.
12 | * You MUST run the test suite.
13 | * You MUST write (or update) unit tests.
14 | * You SHOULD write documentation.
15 |
16 | Please, write [commit messages that make
17 | sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
18 | and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
19 | before submitting your Pull Request.
20 |
21 | One may ask you to [squash your
22 | commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
23 | too. This is used to "clean" your Pull Request before merging it (we don't want
24 | commits such as `fix tests`, `fix 2`, `fix 3`, etc.).
25 |
26 | Also, while creating your Pull Request on GitHub, you MUST write a description
27 | which gives the context and/or explains why you are creating it.
28 |
29 | Thank you!
30 |
31 | Running tests
32 | -------------
33 |
34 | Before running the test suite, execute the following Composer command to install
35 | the dependencies used by the bundle:
36 |
37 | ```bash
38 | $ composer install --dev
39 | ```
40 |
41 | Then, execute the tests executing:
42 |
43 | ```bash
44 | $ vendor/bin/phpunit
45 | ```
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Sylvain Mauduit
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 furnished
10 | 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Github WebHook
2 | ==================
3 |
4 | [](https://github.com/Swop/github-webhook/actions)
6 |
7 | This library offers a set of tools which could become handy when dealing with GitHub web hook requests.
8 |
9 | Installation
10 | ------------
11 |
12 | The recommended way to install this library is through [Composer](https://getcomposer.org/):
13 |
14 | ```
15 | composer require "swop/github-webhook"
16 | ```
17 |
18 | Usage
19 | ------------
20 |
21 | ### Payload signature checking
22 |
23 | The `SignatureValidator` will verify if the incoming GitHub web hook request is correctly signed.
24 |
25 | ```php
26 | use Swop\GitHubWebHook\Security\SignatureValidator;
27 |
28 | $validator = new SignatureValidator();
29 |
30 | /** @var \Psr\Http\Message\ServerRequestInterface $request */
31 | if ($validator->validate($request, 'secret')) {
32 | // Request is correctly signed
33 | }
34 | ````
35 |
36 | ### GitHub event object creation
37 | The `GitHubEventFactory` can build GitHubEvent objects representing the GitHub event.
38 |
39 | ```php
40 | use Swop\GitHubWebHook\Event\GitHubEventFactory;
41 |
42 | $factory = new GitHubEventFactory();
43 |
44 | /** @var \Psr\Http\Message\ServerRequestInterface $request */
45 | $gitHubEvent = $factory->buildFromRequest(RequestInterface $request);
46 |
47 | $gitHubEvent->getType(); // Event type
48 | $gitHubEvent->getPayload(); // Event deserialized payload
49 | ````
50 |
51 | Contributing
52 | ------------
53 |
54 | See [CONTRIBUTING](https://github.com/Swop/github-webhook/blob/master/CONTRIBUTING.md) file.
55 |
56 | Original Credits
57 | ------------
58 |
59 | * [Sylvain MAUDUIT](https://github.com/Swop) ([@Swop](https://twitter.com/Swop)) as main author.
60 |
61 |
62 | License
63 | ------------
64 |
65 | This library is released under the MIT license. See the complete license in the bundled [LICENSE](https://github.com/Swop/github-webhook/blob/master/LICENSE) file.
66 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "swop/github-webhook",
3 | "license": "MIT",
4 | "type": "library",
5 | "description": "Library which deals with incoming GitHub web hooks requests (signature validation & payload parsing)",
6 | "keywords": [
7 | "github",
8 | "webhook",
9 | "security"
10 | ],
11 | "authors": [
12 | {
13 | "name": "Sylvain Mauduit",
14 | "email": "sylvain@mauduit.fr",
15 | "homepage": "https://github.com/Swop"
16 | }
17 | ],
18 | "require": {
19 | "php": "^7.2|^8.0",
20 | "psr/http-message": "^1.0",
21 | "ext-json": "*"
22 | },
23 | "require-dev": {
24 | "phpunit/phpunit": "^8.0",
25 | "laminas/laminas-diactoros": "^2.4"
26 | },
27 | "autoload": {
28 | "psr-4": {
29 | "Swop\\GitHubWebHook\\": "src/"
30 | }
31 | },
32 | "autoload-dev": {
33 | "psr-4": {
34 | "Swop\\GitHubWebHook\\Tests\\": "tests/"
35 | }
36 | },
37 | "extra": {
38 | "branch-alias": {
39 | "dev-master": "3.0.x-dev"
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ./tests
15 |
16 |
17 |
18 |
19 |
20 | ./src
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/Event/GitHubEvent.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Swop\GitHubWebHook\Event;
11 |
12 | /**
13 | * Represents a GitHub Event
14 | *
15 | * @author Sylvain Mauduit
16 | */
17 | class GitHubEvent
18 | {
19 | /** @var string */
20 | private $type;
21 | /** @var array */
22 | private $payload;
23 |
24 | public function __construct(string $type, array $payload)
25 | {
26 | $this->type = $type;
27 | $this->payload = $payload;
28 | }
29 |
30 | public function getType(): string
31 | {
32 | return $this->type;
33 | }
34 |
35 | public function getPayload(): array
36 | {
37 | return $this->payload;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Event/GitHubEventFactory.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Swop\GitHubWebHook\Event;
11 |
12 | use Psr\Http\Message\RequestInterface;
13 | use Swop\GitHubWebHook\Exception\InvalidGitHubRequestPayloadException;
14 | use Swop\GitHubWebHook\Exception\MissingGitHubEventTypeException;
15 |
16 | /**
17 | * Factory implementation which handle the GitHub event creation
18 | *
19 | * @author Sylvain Mauduit
20 | */
21 | class GitHubEventFactory implements GitHubEventFactoryInterface
22 | {
23 | /**
24 | * {@inheritdoc}
25 | */
26 | public function build($type, array $payload): GitHubEvent
27 | {
28 | return new GitHubEvent($type, $payload);
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function buildFromRequest(RequestInterface $request): GitHubEvent
35 | {
36 | $eventType = $request->getHeader('X-GitHub-Event');
37 |
38 | if (count($eventType) > 0) {
39 | $eventType = current($eventType);
40 | } else {
41 | throw new MissingGitHubEventTypeException($request);
42 | }
43 |
44 | $body = $request->getBody();
45 | $body->rewind();
46 | $body = $body->getContents();
47 |
48 | $payload = json_decode($body, true);
49 |
50 | if (json_last_error() !== JSON_ERROR_NONE) {
51 | throw new InvalidGitHubRequestPayloadException($request, $body);
52 | }
53 |
54 | return $this->build($eventType, $payload);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Event/GitHubEventFactoryInterface.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Swop\GitHubWebHook\Event;
11 |
12 | use Psr\Http\Message\RequestInterface;
13 | use Swop\GitHubWebHook\Exception\InvalidGitHubRequestPayloadException;
14 | use Swop\GitHubWebHook\Exception\MissingGitHubEventTypeException;
15 |
16 | /**
17 | * Factory which handle the GitHub event creation
18 | *
19 | * @author Sylvain Mauduit
20 | */
21 | interface GitHubEventFactoryInterface
22 | {
23 | /**
24 | * Build a GitHub web hook event object depending on the given type/payload
25 | */
26 | public function build(string $type, array $payload): GitHubEvent;
27 |
28 | /**
29 | * Build a GitHub web hook event object based on the incoming request
30 | *
31 | * @throws InvalidGitHubRequestPayloadException If the request payload is not a valid JSON content
32 | * @throws MissingGitHubEventTypeException If the GitHub event type could not be found in the request headers
33 | */
34 | public function buildFromRequest(RequestInterface $request): GitHubEvent;
35 | }
36 |
--------------------------------------------------------------------------------
/src/Exception/GitHubWebHookException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Swop\GitHubWebHook\Exception;
11 |
12 | use Psr\Http\Message\RequestInterface;
13 |
14 | /**
15 | * Generic exception which can be thrown during GitHub web hook handling
16 | *
17 | * @author Sylvain Mauduit
18 | */
19 | abstract class GitHubWebHookException extends \Exception
20 | {
21 | /** @var RequestInterface */
22 | private $request;
23 |
24 | public function __construct(RequestInterface $request, string $message, ?\Throwable $previous = null)
25 | {
26 | $this->request = $request;
27 |
28 | parent::__construct($message, 0, $previous);
29 | }
30 |
31 | public function getRequest(): RequestInterface
32 | {
33 | return $this->request;
34 | }
35 |
36 | /**
37 | * Get the message to display in non-debug environments
38 | */
39 | public function getPublicMessage(): string
40 | {
41 | return $this->getMessage();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Exception/InvalidGitHubRequestPayloadException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Swop\GitHubWebHook\Exception;
11 |
12 | use Psr\Http\Message\RequestInterface;
13 |
14 | /**
15 | * Thrown when the incoming GitHub web hook request contains invalid payload body
16 | *
17 | * @author Sylvain Mauduit
18 | */
19 | class InvalidGitHubRequestPayloadException extends GitHubWebHookException
20 | {
21 | /** @var string */
22 | private $requestBody;
23 |
24 | public function __construct(RequestInterface $request, string $requestBody, ?\Throwable $previous = null)
25 | {
26 | $this->requestBody = $requestBody;
27 |
28 | parent::__construct($request, 'Invalid GitHub request payload.', $previous);
29 | }
30 |
31 | public function getRequestBody(): string
32 | {
33 | return $this->requestBody;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Exception/InvalidGitHubRequestSignatureException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Swop\GitHubWebHook\Exception;
11 |
12 | use Psr\Http\Message\RequestInterface;
13 |
14 | /**
15 | * Thrown when the incoming GitHub web hook request doesn't have proper signature
16 | *
17 | * @author Sylvain Mauduit
18 | */
19 | class InvalidGitHubRequestSignatureException extends GitHubWebHookException
20 | {
21 | /** @var ?string */
22 | private $signature;
23 |
24 | public function __construct(RequestInterface $request, ?string $signature, ?\Throwable $previous = null)
25 | {
26 | $this->signature = $signature;
27 |
28 | parent::__construct($request, 'Invalid GitHub request signature.', $previous);
29 | }
30 |
31 | public function getSignature(): string
32 | {
33 | return $this->signature;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Exception/MissingGitHubEventTypeException.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Swop\GitHubWebHook\Exception;
11 |
12 | use Psr\Http\Message\RequestInterface;
13 |
14 | /**
15 | * Thrown when the incoming GitHub web hook request doesn't have event type header
16 | *
17 | * @author Sylvain Mauduit
18 | */
19 | class MissingGitHubEventTypeException extends GitHubWebHookException
20 | {
21 | public function __construct(RequestInterface $request, ?\Throwable $previous = null)
22 | {
23 | parent::__construct($request, 'A GitHub event type should be provided as a X-GitHub-Event header.', $previous);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Security/SignatureValidator.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Swop\GitHubWebHook\Security;
11 |
12 | use Psr\Http\Message\RequestInterface;
13 | use Swop\GitHubWebHook\Exception\InvalidGitHubRequestSignatureException;
14 |
15 | /**
16 | * Signature validator implementation
17 | *
18 | * @see \Swop\GitHubWebHook\Security\SignatureValidatorInterface
19 | *
20 | * @author Sylvain Mauduit
21 | */
22 | class SignatureValidator implements SignatureValidatorInterface
23 | {
24 | /**
25 | * {@inheritdoc}
26 | */
27 | public function validate(RequestInterface $request, string $secret): void
28 | {
29 | $signature = $this->getSignatureFromHeader($request);
30 | $requestBody = $request->getBody();
31 | $requestBody->rewind();
32 |
33 | $payload = $requestBody->getContents();
34 |
35 | if (!$this->validateSignature($signature, $payload, $secret)) {
36 | throw new InvalidGitHubRequestSignatureException($request, $signature);
37 | }
38 | }
39 |
40 | private function getSignatureFromHeader(RequestInterface $request): ?string
41 | {
42 | $headerValues = $request->getHeader('X-Hub-Signature-256');
43 |
44 | return empty($headerValues) ? null : current($headerValues);
45 | }
46 |
47 | private function validateSignature(?string $signature, string $payload, string $secret): bool
48 | {
49 | if (null === $signature) {
50 | return false;
51 | }
52 |
53 | $explodeResult = explode('=', $signature, 2);
54 |
55 | if (2 !== count($explodeResult)) {
56 | return false;
57 | }
58 |
59 | list($algorithm, $hash) = $explodeResult;
60 |
61 | if (empty($algorithm) || empty($hash)) {
62 | return false;
63 | }
64 |
65 | if (!in_array($algorithm, hash_algos())) {
66 | return false;
67 | }
68 |
69 | $payloadHash = hash_hmac($algorithm, $payload, $secret);
70 |
71 | return $hash === $payloadHash;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Security/SignatureValidatorInterface.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 | namespace Swop\GitHubWebHook\Security;
11 |
12 | use Psr\Http\Message\RequestInterface;
13 | use Swop\GitHubWebHook\Exception\InvalidGitHubRequestSignatureException;
14 |
15 | /**
16 | * Class which implements this interface will check if a given PSR-7 request coming from GitHub in a web hook context
17 | * contains proper signature based on the provided secret.
18 | *
19 | * @author Sylvain Mauduit
20 | */
21 | interface SignatureValidatorInterface
22 | {
23 | /**
24 | * Checks is the request contains valid signature
25 | *
26 | * @param RequestInterface $request Incoming request
27 | * @param string $secret GitHub web hook secret
28 | *
29 | * @throws InvalidGitHubRequestSignatureException
30 | */
31 | public function validate(RequestInterface $request, string $secret): void;
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Event/GitHubEventFactoryTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Swop\GitHubWebHook\Tests\Event;
12 |
13 | use Laminas\Diactoros\Request;
14 | use Laminas\Diactoros\Stream;
15 | use PHPUnit\Framework\TestCase;
16 | use Psr\Http\Message\RequestInterface;
17 | use Swop\GitHubWebHook\Event\GitHubEvent;
18 | use Swop\GitHubWebHook\Event\GitHubEventFactory;
19 | use Swop\GitHubWebHook\Exception\InvalidGitHubRequestPayloadException;
20 | use Swop\GitHubWebHook\Exception\MissingGitHubEventTypeException;
21 |
22 | /**
23 | * @author Sylvain Mauduit
24 | */
25 | class GitHubEventFactoryTest extends TestCase
26 | {
27 | /**
28 | * @dataProvider buildDataProvider
29 | */
30 | public function testBuild(string $type, array $payload, GitHubEvent $expectedEvent)
31 | {
32 | $factory = new GitHubEventFactory();
33 |
34 | $this->assertEquals($expectedEvent, $factory->build($type, $payload));
35 | }
36 |
37 | public function testBuildFromRequestShouldFailIfPayloadIsNotJSON()
38 | {
39 | $factory = new GitHubEventFactory();
40 |
41 | $request = $this->buildRequest('this_is_invalid_json', ['X-GitHub-Event' => ['push']]);
42 |
43 | $this->expectException(InvalidGitHubRequestPayloadException::class);
44 |
45 | $factory->buildFromRequest($request);
46 | }
47 |
48 | public function testBuildFromRequestShouldFailIfEventTypeIsNotPresentInHeaders()
49 | {
50 | $factory = new GitHubEventFactory();
51 |
52 | $request = $this->buildRequest('{}', []);
53 |
54 | $this->expectException(MissingGitHubEventTypeException::class);
55 |
56 | $factory->buildFromRequest($request);
57 | }
58 |
59 | public function testBuildFromRequest()
60 | {
61 | $factory = new GitHubEventFactory();
62 |
63 | $request = $this->buildRequest('{"key": "value"}', ['X-GitHub-Event' => ['push']]);
64 |
65 | $event = $factory->buildFromRequest($request);
66 |
67 | $this->assertEquals(new GitHubEvent('push', ['key' => 'value']), $event);
68 | }
69 |
70 | public function buildDataProvider(): array
71 | {
72 | return [
73 | ['event_type', ['event_payload'], new GitHubEvent('event_type', ['event_payload'])],
74 | ];
75 | }
76 |
77 | private function buildRequest(string $body, array $headers): RequestInterface
78 | {
79 | $stream = new Stream('php://memory', 'wb+');
80 | $stream->write($body);
81 |
82 | $request = (new Request())
83 | ->withBody($stream)
84 | ;
85 |
86 | foreach ($headers as $headerName => $headerValue) {
87 | $request = $request->withHeader($headerName, $headerValue);
88 | }
89 |
90 | return $request;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/tests/Security/SignatureValidatorTest.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * For the full copyright and license information, please view the LICENSE
8 | * file that was distributed with this source code.
9 | */
10 |
11 | namespace Swop\GitHubWebHook\Tests\Security;
12 |
13 | use PHPUnit\Framework\TestCase;
14 | use Psr\Http\Message\RequestInterface;
15 | use Swop\GitHubWebHook\Exception\InvalidGitHubRequestSignatureException;
16 | use Swop\GitHubWebHook\Security\SignatureValidator;
17 | use Laminas\Diactoros\Request;
18 | use Laminas\Diactoros\Stream;
19 |
20 | /**
21 | * @author Sylvain Mauduit
22 | */
23 | class SignatureValidatorTest extends TestCase
24 | {
25 | const SECRET = 'MyDirtySecret';
26 |
27 | /**
28 | * @dataProvider correctSignatures
29 | */
30 | public function testCorrectSignature(string $requestBody, string $signature)
31 | {
32 | $valid = true;
33 |
34 | try {
35 | (new SignatureValidator())->validate($this->createRequest($requestBody, $signature), self::SECRET);
36 | } catch (InvalidGitHubRequestSignatureException $e) {
37 | $valid = false;
38 | }
39 |
40 | $this->assertTrue($valid);
41 | }
42 |
43 | /**
44 | * @dataProvider incorrectSignatures
45 | */
46 | public function testIncorrectSignature(string $requestBody, ?string $signature)
47 | {
48 | $this->expectException(InvalidGitHubRequestSignatureException::class);
49 |
50 | (new SignatureValidator())->validate($this->createRequest($requestBody, $signature), self::SECRET);
51 | }
52 |
53 | public function correctSignatures(): array
54 | {
55 | return [
56 | [
57 | '{"foo": "bar"}',
58 | $this->createSignature('{"foo": "bar"}')
59 | ],
60 | [
61 | '{"foo": "bar"}',
62 | $this->createSignature('{"foo": "bar"}', SignatureValidatorTest::SECRET, 'md5')
63 | ],
64 | [
65 | '{"foo": "bar", "baz": true}',
66 | $this->createSignature('{"foo": "bar", "baz": true}', SignatureValidatorTest::SECRET, 'sha1')
67 | ],
68 | [
69 | '{"foo": "bar", "baz": true}',
70 | $this->createSignature('{"foo": "bar", "baz": true}', SignatureValidatorTest::SECRET, 'sha256')
71 | ],
72 | ];
73 | }
74 |
75 | public function incorrectSignatures(): array
76 | {
77 | return [
78 | [
79 | '{"foo": "bar"}',
80 | 'sha1=WrongHashOrInvalidSecret'
81 | ],
82 | [
83 | '{"foo": "bar"}',
84 | null // No HTTP_X_Hub_Signature header
85 | ],
86 | [
87 | '{"foo": "bar"}',
88 | 'Invalid Signature Header'
89 | ],
90 | [
91 | '{"foo": "bar"}',
92 | 'sha1=' // No hash value
93 | ],
94 | [
95 | '{"foo": "bar"}',
96 | '=hash' // No algorithm
97 | ],
98 | [
99 | '{"foo": "bar"}',
100 | '=' // No algo nor hash
101 | ],
102 | ];
103 | }
104 |
105 | private function createSignature(string $signedContent, string $secret = SignatureValidatorTest::SECRET, string $algo = 'sha1'): string
106 | {
107 | return sprintf('%s=%s', $algo, hash_hmac($algo, $signedContent, $secret));
108 | }
109 |
110 | private function createRequest(string $requestContent, ?string $requestSignature): RequestInterface
111 | {
112 | if (null === $requestSignature) {
113 | $requestSignatureHeader = [];
114 | } else {
115 | $requestSignatureHeader = [$requestSignature];
116 | }
117 |
118 | $stream = new Stream('php://temp', 'wb+');
119 | $stream->write($requestContent);
120 |
121 | $request = (new Request())
122 | ->withBody($stream);
123 |
124 | if (!empty($requestSignatureHeader)) {
125 | $request = $request->withAddedHeader('X-Hub-Signature-256', $requestSignatureHeader);
126 | }
127 |
128 | return $request;
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/tests/bootstrap.php:
--------------------------------------------------------------------------------
1 | setPsr4("Swop\\GitHubWebHook\\Tests\\", "tests");
6 |
--------------------------------------------------------------------------------