Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
alcalyn committed Jun 20, 2016
2 parents 8696cfc + 5350a46 commit c1ad796
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 50 deletions.
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
# Sandstone

[![Latest Stable Version](https://poser.pugx.org/eole/sandstone/v/stable)](https://packagist.org/packages/eole/sandstone)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/eole-io/sandstone/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/eole-io/sandstone/?branch=master)
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/914c7d8f-a51a-4146-b211-44bcf81f5b48/mini.png)](https://insight.sensiolabs.com/projects/914c7d8f-a51a-4146-b211-44bcf81f5b48)
[![License](https://poser.pugx.org/eole/sandstone/license)](https://packagist.org/packages/eole/sandstone)


Sandstone extends Silex to easily mount a RestApi working together with a **Websocket server**.
Also integrates a **Push server** to send messages from RestApi to websocket server.

Declare a websocket topic is like:

``` php
$app->topic('chat/{channel}', function ($topicPattern, $arguments) {
$channelName = $arguments['channel'];

return new ChatTopic($topicPattern, $channelName);
})
->assert('channel', '^[a-zA-Z0-9]+$');
```

Forward all events from RestApi to websocket server:

``` php
$app->forwardEventToPushServer('article.created');
```


## Installation

Expand Down Expand Up @@ -256,6 +274,23 @@ which becomes highly recommended for scaling bigger applications.*
Now you have a Rest Api working with a websocket server,
an interessant part of Sandstone can be aborded.

> **Note**:
> Using `$app->topic()` is the same as for [Silex routing](http://silex.sensiolabs.org/doc/master/usage.html#routing) (`$app->post()`),
> so the following example is also relevant:
``` php
$app
->topic('chat/{channel}', function ($topicPattern) {
return new ChatTopic($topicPattern);
})
->value('channel', 'general')
->assert('channel', '^[a-zA-Z0-9]+$')
->convert('channel', function () { /* ... */ })
->before(function () { /* ... */ })
->when('chatEnabled()')
;
```


### Push server

Expand Down Expand Up @@ -395,6 +430,45 @@ $app['serializer.builder']->addMetadataDir(
> as it will be used in both rest api stack and websocket server stack.

### Websocket authentication

You can **optionally** use Sandstone's OAuth2 for Websocket authentication.

Sandstone provides a simple way to handle OAuth tokens (storing as files),
and a basic JSON controller to get new access tokens (from password or refresh tokens).

It needs the firewall name and the user provider you use to secure your api,
an array of OAuth client, an OAuth scope, and a folder to store tokens.

Configuration reference:

``` php
$app->register(new Eole\Sandstone\OAuth2\Silex\OAuth2ServiceProvider(), [
'oauth.firewall_name' => 'api',
'oauth.security.user_provider' => 'my_app.user_provider',
'oauth.tokens_dir' => '/var/oauth-tokens',
'oauth.scope' => [
'id' => 'your-scope-id',
'description' => 'Your scope.',
],
'oauth.clients' => [
'client-id' => [
'id' => 'client-id',
'name' => 'client-name',
'secret' => 'client-secret',
],
],
]);

$app->mount('oauth', new Eole\Sandstone\OAuth2\Silex\OAuth2JsonControllerProvider());
```

Then a client can get an access token through the Rest Api (OAuth2 controller), and can use it
for both Api calls and websocket server connections, by passing it as a GET parameter:

`wss://domain.tld:8080/?access_token=accesstoken`


## References

Sandstone is built on a few other cool PHP libraries you may want to check documentation:
Expand Down
34 changes: 32 additions & 2 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
use Symfony\Component\HttpKernel\KernelEvents;
use Silex\Controller;
use Silex\Application as BaseApplication;
use Eole\Sandstone\Websocket\ServiceProvider as WebsocketServiceProvider;
use Eole\Sandstone\PushServer\ServiceProvider as PushServerServiceProvider;

class Application extends BaseApplication
{
Expand Down Expand Up @@ -48,6 +50,14 @@ private function fixAuthorizationHeader()
*/
public function topic($pattern, callable $factory)
{
if (!$this->offsetExists('sandstone.websocket.topics')) {
throw new \LogicException(sprintf(
'You must register Websocket server service provider (%s) in order to use %s method.',
WebsocketServiceProvider::class,
__METHOD__
));
}

return $this['sandstone.websocket.topics']->match($pattern, $factory);
}

Expand All @@ -70,7 +80,17 @@ public function isPushServerEnabled()
*/
public function forwardEventToPushServer($eventName)
{
$this['sandstone.push.event_forwarder']->forwardAllEvents($eventName);
if (!$this->offsetExists('sandstone.push')) {
throw new \LogicException(sprintf(
'You must register Push server service provider (%s) in order to use %s method.',
PushServerServiceProvider::class,
__METHOD__
));
}

$this->before(function () use ($eventName) {
$this['sandstone.push.event_forwarder']->forwardAllEvents($eventName);
});

return $this;
}
Expand All @@ -84,7 +104,17 @@ public function forwardEventToPushServer($eventName)
*/
public function forwardEventsToPushServer(array $eventsNames)
{
$this['sandstone.push.event_forwarder']->forwardAllEvents($eventsNames);
if (!$this->offsetExists('sandstone.push')) {
throw new \LogicException(sprintf(
'You must register Push server service provider (%s) in order to use %s method.',
PushServerServiceProvider::class,
__METHOD__
));
}

$this->before(function () use ($eventsNames) {
$this['sandstone.push.event_forwarder']->forwardAllEvents($eventsNames);
});

return $this;
}
Expand Down
20 changes: 8 additions & 12 deletions src/OAuth2/Controller/OAuth2Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Symfony\Component\HttpFoundation\Request;
use League\OAuth2\Server\Exception\OAuthException;
use League\OAuth2\Server\AuthorizationServer;
use Alcalyn\SerializableApiResponse\ApiResponse;

class OAuth2Controller
{
Expand All @@ -25,21 +24,18 @@ public function __construct(AuthorizationServer $authorizationServer)
/**
* @param Request $request
*
* @return ApiResponse
* @return array like
* access_token:"...",
* token_type:"Bearer"
* expires_in:3600
* refresh_token:"..."
*
* @throws OAuthException
*/
public function postAccessToken(Request $request)
{
$this->authorizationServer->setRequest($request);

try {
$token = $this->authorizationServer->issueAccessToken();

return new ApiResponse($token);
} catch (OAuthException $e) {
return new ApiResponse(array(
'type' => $e->errorType,
'message' => $e->getMessage(),
), $e->httpStatusCode);
}
return $this->authorizationServer->issueAccessToken();
}
}
21 changes: 0 additions & 21 deletions src/OAuth2/Silex/OAuth2ControllerProvider.php

This file was deleted.

32 changes: 32 additions & 0 deletions src/OAuth2/Silex/OAuth2JsonControllerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Eole\Sandstone\OAuth2\Silex;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Silex\Api\ControllerProviderInterface;
use Silex\Application;

class OAuth2JsonControllerProvider implements ControllerProviderInterface
{
/**
* {@inheritDoc}
*/
public function connect(Application $app)
{
$controllers = $app['controllers_factory'];

$controllers->post('/access-token', function (Request $request) use ($app) {
try {
$token = $app['sandstone.oauth.controller']->postAccessToken($request);

return new JsonResponse($token);
} catch (OAuthException $e) {
throw new HttpException($e->httpStatusCode, $e->errorType.': '.$e->getMessage(), $e);
}
});

return $controllers;
}
}
3 changes: 2 additions & 1 deletion src/Websocket/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
use Symfony\Component\Security\Core\User\UserInterface;
use Ratchet\ConnectionInterface;
use Ratchet\Wamp\WampServerInterface;
use Eole\Sandstone\OAuth2\Security\Authentication\Token\OAuth2Token;
use Eole\Sandstone\Application as SandstoneApplication;

class Application implements WampServerInterface
final class Application implements WampServerInterface
{
/**
* @var SandstoneApplication
Expand Down
7 changes: 3 additions & 4 deletions src/Websocket/Routing/TopicRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
namespace Eole\Sandstone\Websocket\Routing;

use Silex\Route;
use Eole\Sandstone\Websocket\Topic;

class TopicRoute extends Route
{
/**
* @param string $topicPath
* @param string|Topic $topic topic or topic class name.
* @param callable $topicFactory
* @param array $defaults
* @param array $requirements
*/
public function __construct($topicPath, $topic, array $defaults = array(), array $requirements = array())
public function __construct($topicPath, callable $topicFactory, array $defaults = array(), array $requirements = array())
{
$defaults['_topic'] = $topic;
$defaults['_topic_factory'] = $topicFactory;

parent::__construct($topicPath, $defaults, $requirements);
}
Expand Down
17 changes: 8 additions & 9 deletions src/Websocket/Routing/TopicRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@
namespace Eole\Sandstone\Websocket\Routing;

use LogicException;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Eole\Sandstone\Websocket\Topic;
use Eole\Sandstone\Application;

class TopicRouter
{
/**
* @var Application
* @var UrlMatcher
*/
private $sandstoneApplication;
private $urlMatcher;

/**
* @param Application $sandstoneApplication
* @param UrlMatcher $urlMatcher
*/
public function __construct(Application $sandstoneApplication)
public function __construct(UrlMatcher $urlMatcher)
{
$this->sandstoneApplication = $sandstoneApplication;
$this->urlMatcher = $urlMatcher;
}

/**
Expand All @@ -30,9 +30,8 @@ public function __construct(Application $sandstoneApplication)
*/
public function loadTopic($topicPath)
{
$urlMatcher = $this->sandstoneApplication['sandstone.websocket.url_matcher'];
$arguments = $urlMatcher->match('/'.$topicPath);
$topicFactory = $arguments['_topic'];
$arguments = $this->urlMatcher->match('/'.$topicPath);
$topicFactory = $arguments['_topic_factory'];

if (!is_callable($topicFactory)) {
throw new LogicException("Topic $topicPath is not a callback.");
Expand Down
2 changes: 1 addition & 1 deletion src/Websocket/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function register(Container $app)
};

$app['sandstone.websocket.router'] = function () use ($app) {
return new TopicRouter($app);
return new TopicRouter($app['sandstone.websocket.url_matcher']);
};
}
}

0 comments on commit c1ad796

Please sign in to comment.