diff --git a/.gitignore b/.gitignore
index ff332dc..739c2e3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
/vendor/
-/bin/
/composer.phar
/phpunit.xml
/composer.lock
diff --git a/.travis.yml b/.travis.yml
index 7971e57..32e4c9d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,19 +1,56 @@
language: php
+sudo: false
+
+cache:
+ directories:
+ - $HOME/.composer/cache
+
php:
- - 5.3.3
- 5.4
- 5.5
- 5.6
+ - 7.0
+ - 7.1
- hhvm
+ - nightly
-before_script:
- - composer self-update
- - composer install --no-interaction --prefer-source
-
-script: phpunit --coverage-text
+env:
+ global:
+ - SYMFONY_VERSION=""
matrix:
+ fast_finish: true
+ include:
+ - php: 7.0
+ env: SYMFONY_VERSION="2.3.*"
+ - php: 7.0
+ env: SYMFONY_VERSION="2.4.*"
+ - php: 7.0
+ env: SYMFONY_VERSION="2.5.*"
+ - php: 7.0
+ env: SYMFONY_VERSION="2.6.*"
+ - php: 7.0
+ env: SYMFONY_VERSION="2.7.*"
+ - php: 7.0
+ env: SYMFONY_VERSION="2.8.*"
+ - php: 7.0
+ env: SYMFONY_VERSION="3.0.*"
+ - php: 7.0
+ env: SYMFONY_VERSION="3.1.*"
+ - php: 7.0
+ env: SYMFONY_VERSION="dev-master"
allow_failures:
- php: hhvm
- fast_finish: true
+ - php: nightly
+ - env: SYMFONY_VERSION="dev-master"
+
+before_install:
+ - composer selfupdate
+ - .travis/update_symfony_deps_version.sh
+
+install:
+ - composer install --no-interaction --prefer-source
+
+script:
+ - vendor/bin/phpunit --coverage-text
diff --git a/.travis/update_symfony_deps_version.sh b/.travis/update_symfony_deps_version.sh
new file mode 100755
index 0000000..054957d
--- /dev/null
+++ b/.travis/update_symfony_deps_version.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+if [ "$SYMFONY_VERSION" != "" ]; then
+ composer require --dev --no-update \
+ symfony/http-kernel:$SYMFONY_VERSION \
+ symfony/http-foundation:$SYMFONY_VERSION
+fi
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..5671fde
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,45 @@
+Contributing
+============
+
+First of all, **thank you** for contributing, **you are awesome**!
+
+Here are a few rules to follow in order to ease code reviews, and discussions
+before maintainers accept and merge your work:
+
+ * You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and
+ [PSR-2](http://www.php-fig.org/psr/2/) recommendations. Use the [PHP-CS-Fixer
+ tool](http://cs.sensiolabs.org/) to fix the syntax of your code automatically.
+ * You MUST run the test suite.
+ * You MUST write (or update) unit tests.
+ * You SHOULD write documentation.
+
+Please, write [commit messages that make
+sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
+and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing)
+before submitting your Pull Request.
+
+One may ask you to [squash your
+commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html)
+too. This is used to "clean" your Pull Request before merging it (we don't want
+commits such as `fix tests`, `fix 2`, `fix 3`, etc.).
+
+Also, while creating your Pull Request on GitHub, you MUST write a description
+which gives the context and/or explains why you are creating it.
+
+Thank you!
+
+Running tests
+-------------
+
+Before running the test suite, execute the following Composer command to install
+the dependencies used by the bundle:
+
+```bash
+$ composer install --dev
+```
+
+Then, execute the tests executing:
+
+```bash
+$ vendor/bin/phpunit
+```
diff --git a/LICENSE b/LICENSE
index c12101d..c544829 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2014 Sylvain Mauduit
+Copyright (c) 2016 Sylvain Mauduit
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index ee62cbb..98b5ba0 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,25 @@
-# Github WebHook Stack middleware
+Github WebHook Stack middleware
+==================
- 
+[](http://travis-ci.org/Swop/github-webhook-stackphp)
[Stack](http://stackphp.com) middleware to restrict application access to GitHub Event bot with signed payload.
-Every incoming request will have its X-Hub-Signature header checked in order to see if the request was originally performed by GitHub.
-Any requests which doesn't have correct signature will provoke a _401 Unauthorized_ response.
+Every incoming request will see its `X-Hub-Signature` header checked in order to validate that the request was originally performed by GitHub.
+Any requests which doesn't have correct signature will lead to a `401 Unauthorized` JSON response.
-## Usage
+Installation
+------------
+
+The recommended way to install this library is through [Composer](https://getcomposer.org/):
+
+```
+composer require "swop/github-webhook-stackphp"
+```
+
+Usage
+------------
### Silex example
```php
require __DIR__ . '/../vendor/autoload.php';
@@ -15,14 +27,14 @@ require __DIR__ . '/../vendor/autoload.php';
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
-$app = new Silex\Application();
+$app = new \Silex\Application();
$app->get('/', function(Request $request) {
return new Response('Hello world!', 200);
});
-$app = (new Stack\Builder())
- ->push('Swop\Stack\GitHubWebHook', 'MyGitHubWebhookSecret')
+$app = (new \Stack\Builder())
+ ->push('Swop\GitHubWebHookStackPHP\GitHubWebHook', 'my_secret')
->resolve($app)
;
@@ -48,7 +60,8 @@ $kernel = new AppKernel('dev', true);
$kernel->loadClassCache();
$stack = (new Stack\Builder())
- ->push('Swop\Stack\GitHubWebHook', 'MyGitHubWebhookSecret')
+ ->push('Swop\GitHubWebHookStackPHP\GitHubWebHook', 'my_secret')
+;
$kernel = $stack->resolve($kernel);
@@ -59,18 +72,18 @@ $response->send();
$kernel->terminate($request, $response);
```
-## Intallation
+Contributing
+------------
-The recommended way to install this library is through [Composer](http://getcomposer.org/):
+See [CONTRIBUTING](https://github.com/Swop/github-webhook-stackphp.png/blob/master/CONTRIBUTING.md) file.
+
+Original Credits
+------------
+
+* [Sylvain MAUDUIT](https://github.com/Swop) ([@Swop](https://twitter.com/Swop)) as main author.
-``` json
-{
- "require": {
- "swop/stack-github-webhook": "~1.0"
- }
-}
-```
-## License
+License
+------------
-This library is released under the MIT License. See the bundled LICENSE file for details.
+This library is released under the MIT license. See the complete license in the bundled [LICENSE](https://github.com/Swop/github-webhook-stackphp.png/blob/master/LICENSE) file.
diff --git a/composer.json b/composer.json
index 6d7de9b..2975ee6 100644
--- a/composer.json
+++ b/composer.json
@@ -1,40 +1,44 @@
{
- "name": "swop/stack-github-webhook",
+ "name": "swop/github-webhook-stackphp",
"license": "MIT",
"type": "library",
- "description": "Stack middleware to restrict application access to GitHub Event bot with signed payload",
+ "description": "Stack middleware which will verify if the incoming GitHub web hook request is correctly signed.",
"keywords": [
"stack",
"github",
"stack middleware",
- "webhook"
+ "webhook",
+ "middleware",
+ "security"
],
"authors": [
{
"name": "Sylvain Mauduit",
- "email": "swop@swop.io",
+ "email": "sylvain@mauduit.fr",
"homepage": "https://github.com/Swop"
}
],
"require": {
- "symfony/http-foundation": "~2.1",
- "symfony/http-kernel": "~2.1",
- "php": ">=5.3.3"
+ "php": ">=5.4",
+ "symfony/http-foundation": "^2.3|^3.0",
+ "symfony/http-kernel": "^2.3|^3.0",
+ "stack/builder": "^1.0",
+ "symfony/psr-http-message-bridge": "^1.0",
+ "zendframework/zend-diactoros": "^1.3",
+ "http-interop/http-middleware": "^0.2.1",
+ "swop/github-webhook-middleware": "^1.0"
},
"require-dev": {
- "phpunit/phpunit": "~3.7"
+ "phpunit/phpunit": "^4.5.0"
},
"autoload": {
"psr-4": {
- "Swop\\Stack\\": "src/"
+ "Swop\\GitHubWebHookStackPHP\\": "src/"
}
},
- "config": {
- "bin-dir": "bin"
- },
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0.x-dev"
}
}
}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 10a917b..2201c3d 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -6,7 +6,7 @@
bootstrap = "tests/bootstrap.php" >
-
+
./tests
diff --git a/src/GitHubWebHook.php b/src/GitHubWebHook.php
index 44d7fc3..40d3ea3 100644
--- a/src/GitHubWebHook.php
+++ b/src/GitHubWebHook.php
@@ -2,34 +2,39 @@
/*
* This file licensed under the MIT license.
*
- * (c) Sylvain Mauduit
+ * (c) Sylvain Mauduit
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
-namespace Swop\Stack;
+namespace Swop\GitHubWebHookStackPHP;
-use Symfony\Component\HttpFoundation\JsonResponse;
+use Swop\GitHubWebHook\Security\SignatureValidator;
+use Swop\GitHubWebHookMiddleware\GithubWebHook as GithubWebHookMiddleware;
+use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
+use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpFoundation\Request;
-use Symfony\Component\HttpFoundation\Response;
+/**
+ * This class offers a StackPHP middleware which could be used to verify that a request coming from GitHub
+ * in a web hook context contains proper signature based on the provided secret.
+ *
+ * @author Sylvain Mauduit
+ */
class GitHubWebHook implements HttpKernelInterface
{
/** @var HttpKernelInterface */
- private $kernel;
- /** @var string */
- private $gitHubWebHookSecret;
+ private $next;
/**
- * @param HttpKernelInterface $kernel Application kernel
- * @param string $gitHubWebHookSecret GitHub secret key configured in the WebHook
+ * @param HttpKernelInterface $next Next middleware
+ * @param string $secret GitHub web hook secret
*/
- public function __construct(HttpKernelInterface $kernel, $gitHubWebHookSecret)
+ public function __construct(HttpKernelInterface $next, $secret)
{
- $this->kernel = $kernel;
- $this->gitHubWebHookSecret = $gitHubWebHookSecret;
+ $this->next = $this->createBridge($next, $secret);
}
/**
@@ -37,30 +42,33 @@ public function __construct(HttpKernelInterface $kernel, $gitHubWebHookSecret)
*/
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
- if (!$this->isSignatureValid($request)) {
- return new JsonResponse(
- array('error' => Response::HTTP_UNAUTHORIZED, 'message' => 'Unauthorized'),
- Response::HTTP_UNAUTHORIZED
- );
- }
-
- return $this->kernel->handle($request, $type, $catch);
+ return $this->next->handle($request, $type, $catch);
}
+//
+// /**
+// * @param HttpKernelInterface $next Next middleware
+// * @param string $secret GitHub web hook secret
+// *
+// * @return $this
+// */
+// static public function create(HttpKernelInterface $next, $secret)
+// {
+// return new self($next, $secret);
+// }
- private function isSignatureValid(Request $request)
+ /**
+ * @param HttpKernelInterface $next
+ * @param string $secret
+ *
+ * @return HttpKernelInterface
+ */
+ private function createBridge(HttpKernelInterface $next, $secret)
{
- $hubSignature = $request->headers->get('X-Hub-Signature');
- $explodeResult = explode('=', $hubSignature, 2);
-
- if (2 !== count($explodeResult)) {
- return false;
- }
-
- list($algorithm, $hash) = $explodeResult;
- $payload = $request->getContent();
-
- $payloadHash = hash_hmac($algorithm, $payload, $this->gitHubWebHookSecret);
-
- return $hash === $payloadHash;
+ return new Psr7MiddlewareBridge(
+ $next,
+ new GithubWebHookMiddleware(new SignatureValidator(), $secret),
+ new DiactorosFactory(),
+ new HttpFoundationFactory()
+ );
}
}
diff --git a/src/Psr7MiddlewareBridge.php b/src/Psr7MiddlewareBridge.php
new file mode 100644
index 0000000..d4b89d5
--- /dev/null
+++ b/src/Psr7MiddlewareBridge.php
@@ -0,0 +1,78 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace Swop\GitHubWebHookStackPHP;
+
+use Interop\Http\Middleware\DelegateInterface;
+use Interop\Http\Middleware\ServerMiddlewareInterface;
+use Psr\Http\Message\RequestInterface;
+use Symfony\Bridge\PsrHttpMessage\HttpFoundationFactoryInterface;
+use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
+
+/**
+ * This Middleware implementation allow a compatibility between PSR-15 middlewares and StackPHP ones.
+ *
+ * @author Sylvain Mauduit
+ */
+class Psr7MiddlewareBridge implements HttpKernelInterface, DelegateInterface
+{
+ /** @var HttpKernelInterface */
+ private $next;
+ /** @var ServerMiddlewareInterface */
+ private $psrMiddleware;
+ /** @var HttpMessageFactoryInterface */
+ private $httpMessageFactory;
+ /** @var HttpFoundationFactoryInterface */
+ private $httpFoundationFactory;
+
+ /**
+ * @param HttpKernelInterface $next Next middleware
+ * @param ServerMiddlewareInterface $psrMiddleware Wrapped middleware
+ * @param HttpMessageFactoryInterface $httpMessageFactory PSR-7 message factory
+ * @param HttpFoundationFactoryInterface $httpFoundationFactory Symfony's Http Foundation request/response factory
+ */
+ public function __construct(
+ HttpKernelInterface $next,
+ ServerMiddlewareInterface $psrMiddleware,
+ HttpMessageFactoryInterface $httpMessageFactory,
+ HttpFoundationFactoryInterface $httpFoundationFactory
+ ) {
+ $this->next = $next;
+ $this->psrMiddleware = $psrMiddleware;
+ $this->httpMessageFactory = $httpMessageFactory;
+ $this->httpFoundationFactory = $httpFoundationFactory;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
+ {
+ $psr7Request = $this->httpMessageFactory->createRequest($request);
+
+ $psr7Response = $this->psrMiddleware->process($psr7Request, $this);
+
+ return $this->httpFoundationFactory->createResponse($psr7Response);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function process(RequestInterface $request)
+ {
+ $httpFoundationRequest = $this->httpFoundationFactory->createRequest($request);
+
+ $httpFoundationResponse = $this->next->handle($httpFoundationRequest);
+
+ return $this->httpMessageFactory->createResponse($httpFoundationResponse);
+ }
+}
+
diff --git a/tests/GitHubWebHookTest.php b/tests/GitHubWebHookTest.php
new file mode 100644
index 0000000..6c8fd68
--- /dev/null
+++ b/tests/GitHubWebHookTest.php
@@ -0,0 +1,63 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+namespace Swop\GitHubWebHookStackPHP\Tests;
+
+use Prophecy\Argument;
+use Swop\GitHubWebHookStackPHP\GitHubWebHook;
+use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
+use Symfony\Bridge\PsrHttpMessage\Factory\HttpFoundationFactory;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * @author Sylvain Mauduit
+ */
+class GitHubWebHookTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInvalidRequestShouldReturnA401Response()
+ {
+ $next = $this->prophesize('Symfony\Component\HttpKernel\HttpKernelInterface');
+ $next->handle(Argument::any())->shouldNotBeCalled();
+
+ $request = Request::create('http://localhost/');
+
+ $middleware = new GitHubWebHook($next->reveal(), 'my_secret');
+ $response = $middleware->handle($request);
+
+ $this->assertEquals(401, $response->getStatusCode());
+ $this->assertEquals(json_encode(array('error' => 401, 'message' => 'Unauthorized')), $response->getContent());
+ }
+
+ public function testValidRequestShouldBeHandledByTheNextMiddleware()
+ {
+ $content = '{"content": "This is the content"}';
+ $request = Request::create('http://localhost/', 'GET', [], [], [], [], $content);
+ $request->headers->set('X-Hub-Signature', sprintf('sha1=%s', hash_hmac('sha1', $content, 'my_secret')));
+
+ $psrFactory = new DiactorosFactory();
+ $foundationFactory = new HttpFoundationFactory();
+ $psrRequest = $psrFactory->createRequest($request);
+ $expectedRequest = $foundationFactory->createRequest($psrRequest);
+
+ $response = new Response('OK');
+ $expectedResponse = $foundationFactory->createResponse($psrFactory->createResponse($response));
+
+ $next = $this->prophesize('Symfony\Component\HttpKernel\HttpKernelInterface');
+ $next->handle($expectedRequest)
+ ->shouldBeCalledTimes(1)
+ ->willReturn($response)
+ ;
+
+ $middleware = new GitHubWebHook($next->reveal(), 'my_secret');
+ $response = $middleware->handle($request);
+
+ $this->assertEquals($expectedResponse, $response);
+ }
+}
diff --git a/tests/Stack/GitHubWebHookTest.php b/tests/Stack/GitHubWebHookTest.php
deleted file mode 100644
index c7b0dc0..0000000
--- a/tests/Stack/GitHubWebHookTest.php
+++ /dev/null
@@ -1,119 +0,0 @@
-requestStub = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->headerBagStub = $this->getMockBuilder('Symfony\Component\HttpFoundation\HeaderBag')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->requestStub->headers = $this->headerBagStub;
-// $this->requestStub->expects($this->at(0))
-// ->method('__get')
-// ->with($this->equalTo('headers'))
-// ->will($this->returnValue($this->headerBagStub));
-
- $this->responseStub = $this->getMockBuilder('Symfony\Component\HttpFoundation\Response')
- ->disableOriginalConstructor()
- ->getMock();
-
- $this->kernelStub = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
- }
-
- /**
- * @dataProvider correctSignatures
- */
- public function testCorrectSignature($requestContent, $requestSignature, $gitHubWebHookSecret)
- {
- $this->configureHeaderBagStub($requestSignature);
- $this->configureRequestStub($requestContent);
-
- $this->kernelStub->expects($this->once())
- ->method('handle')
- ->will($this->returnValue($this->responseStub));
-
- $stackGitHubWebHook = new GitHubWebHook($this->kernelStub, $gitHubWebHookSecret);
- $response = $stackGitHubWebHook->handle($this->requestStub);
-
- $this->assertEquals($response, $this->responseStub);
- }
-
- /**
- * @dataProvider incorrectSignatures
- */
- public function testIncorrectSignature($requestContent, $requestSignature, $gitHubWebHookSecret)
- {
- $this->configureHeaderBagStub($requestSignature);
- $this->configureRequestStub($requestContent);
-
- $this->kernelStub->expects($this->never())
- ->method('handle');
-
- $stackGitHubWebHook = new GitHubWebHook($this->kernelStub, $gitHubWebHookSecret);
- $response = $stackGitHubWebHook->handle($this->requestStub);
-
- $this->assertEquals(401, $response->getStatusCode());
- $this->assertEquals(array('error' => 401, 'message' => 'Unauthorized'), json_decode($response->getContent(), true));
- }
-
- public function correctSignatures()
- {
- // $requestContent, $requestSignature, $gitHubWebHookSecret
-
- return array(
- array('{"foo": "bar"}', "sha1=" . hash_hmac('sha1', '{"foo": "bar"}', 'ThisIsMySecret'), 'ThisIsMySecret'),
- array('{"foo": "bar"}', "md5=" . hash_hmac('md5', '{"foo": "bar"}', 'ThisIsMyOtherSecret'), 'ThisIsMyOtherSecret'),
- array('{"foo": "bar", "baz": true}', "sha256=" . hash_hmac('sha256', '{"foo": "bar", "baz": true}', 'ThisIsMySecret'), 'ThisIsMySecret'),
- );
- }
-
- public function incorrectSignatures()
- {
- // $requestContent, $requestSignature, $gitHubWebHookSecret
-
- return array(
- array('{"foo": "bar"}', "sha1=WrongHash", 'ThisIsMySecret'),
- array('{"foo": "bar"}', null, 'ThisIsMySecret'),
- array('{"foo": "bar"}', "NoAlgorithm", 'ThisIsMySecret'),
- );
- }
-
- private function configureHeaderBagStub($requestSignature)
- {
- $this->headerBagStub->expects($this->once())
- ->method('get')
- ->with($this->equalTo('X-Hub-Signature'))
- ->will($this->returnValue($requestSignature));
- }
-
- private function configureRequestStub($requestContent)
- {
- $this->requestStub->expects($this->any())
- ->method('getContent')
- ->will($this->returnValue($requestContent));
- }
-}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 9a6e7af..ab9fac5 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,3 +1,5 @@
setPsr4("Swop\\GitHubWebHookStackPHP\\Tests\\", "tests");