Skip to content

Commit

Permalink
Merge pull request #19 from ably/feature/add-querytime
Browse files Browse the repository at this point in the history
Add Internet time/ Query time feature
  • Loading branch information
sacOO7 authored Nov 30, 2022
2 parents 3bf625d + 9ab257d commit 91c7a41
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 34 deletions.
45 changes: 41 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,49 @@ npm run dev
```

**2. Disable public channels. Default: false**
- Update `ABLY_DISABLE_PUBLIC_CHANNELS`, set as **true** in `.env` file.
- Update `ably` section under `config/broadcasting.php` with `'disable_public_channels' => env('ABLY_DISABLE_PUBLIC_CHANNELS', false)`
- Set `ABLY_DISABLE_PUBLIC_CHANNELS` as **true** in **.env** file.
```dotenv
ABLY_DISABLE_PUBLIC_CHANNELS=true
```
- Update ably section under `config/broadcasting.php` with
```php
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
'disable_public_channels' => env('ABLY_DISABLE_PUBLIC_CHANNELS', false)
],
```

**3. Update token expiry. Default: 3600 seconds (1 hr)**
- Update `ABLY_TOKEN_EXPIRY` in `.env` file.
- Update `ably` section under `config/broadcasting.php` with `'token_expiry' => env('ABLY_TOKEN_EXPIRY', 3600)`
- Set `ABLY_TOKEN_EXPIRY` in **.env** file.
```dotenv
ABLY_TOKEN_EXPIRY=21600
```
- Update ably section under `config/broadcasting.php` with
```php
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
'token_expiry' => env('ABLY_TOKEN_EXPIRY', 3600)
],
```

**4. Use internet time for issued token expiry. Default: false**
- If this option is enabled, internet time in UTC format is fetched from the Ably service and cached every 6 hrs.
- This option is useful when using laravel-broadcaster on a server where, for some reason, the server clock cannot be kept synchronized through normal means.
- Set `ABLY_SYNC_SERVER_TIME` as **true** in **.env** file.
```dotenv
ABLY_SYNC_SERVER_TIME=true
```
- Update ably section under `config/broadcasting.php` with
```php
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
'sync_server_time' => env('ABLY_SYNC_SERVER_TIME', false)
],
```


<a name="migrate-pusher-to-ably"></a>
## Migrating from pusher/pusher-compatible broadcasters
Expand Down
44 changes: 21 additions & 23 deletions src/AblyBroadcaster.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ class AblyBroadcaster extends Broadcaster
private $tokenExpiry = 3600;

/**
* Default channel capabilities, all public channels are by default given subscribe, history and channel-metadata access
* Public channel capabilities. By default, all public channels are given subscribe, history and channel-metadata access.
* Set as per https://ably.com/docs/core-features/authentication#capability-operations.
*
* @var array
*/
private $defaultChannelClaims = [
private $publicChannelsClaims = [
'public:*' => ['subscribe', 'history', 'channel-metadata'],
];

Expand All @@ -46,7 +46,7 @@ class AblyBroadcaster extends Broadcaster
*
* @var int
*/
private $serverTimeDiff;
private $serverTimeDiff = 0;

/**
* Create a new broadcaster instance.
Expand All @@ -59,13 +59,14 @@ public function __construct(AblyRest $ably, $config)
{
$this->ably = $ably;

// Local file cache is preferred to avoid sharing serverTimeDiff across different servers
$this->serverTimeDiff = Cache::store('file')->remember('ably_server_time_diff', 6 * 3600, function() {
return time() - round($this->ably->time() / 1000);
});

if (array_key_exists('sync_server_time', $config) && $config['sync_server_time']) {
// Local file cache is preferred to avoid sharing serverTimeDiff across different servers
$this->serverTimeDiff = Cache::store('file')->remember('ably_server_time_diff', 6 * 3600, function() {
return time() - round($this->ably->time() / 1000);
});
}
if (array_key_exists('disable_public_channels', $config) && $config['disable_public_channels']) {
$this->defaultChannelClaims = ['public:*' => ['channel-metadata']];
$this->publicChannelsClaims = ['public:*' => ['channel-metadata']];
}
if (array_key_exists('token_expiry', $config)) {
$this->tokenExpiry = $config['token_expiry'];
Expand All @@ -77,11 +78,7 @@ public function __construct(AblyRest $ably, $config)
*/
private function getServerTime()
{
if ($this->serverTimeDiff != null) {
return time() - $this->serverTimeDiff;
}

return time();
return time() - $this->serverTimeDiff;
}

/**
Expand Down Expand Up @@ -117,9 +114,10 @@ public function auth($request)
$channelName = $request->channel_name;
$token = $request->token;
$connectionId = $request->socket_id;
$normalizedChannelName = $this->normalizeChannelName($channelName);
$userId = null;
$channelCapability = ['*'];
$guardedChannelCapability = ['*']; // guardedChannel is either private or presence channel

$normalizedChannelName = $this->normalizeChannelName($channelName);
$user = $this->retrieveUser($request, $normalizedChannelName);
if ($user) {
$userId = method_exists($user, 'getAuthIdentifierForBroadcasting')
Expand All @@ -133,7 +131,7 @@ public function auth($request)
try {
$userChannelMetaData = parent::verifyUserCanAccessChannel($request, $normalizedChannelName);
if (is_array($userChannelMetaData) && array_key_exists('capability', $userChannelMetaData)) {
$channelCapability = $userChannelMetaData['capability'];
$guardedChannelCapability = $userChannelMetaData['capability'];
unset($userChannelMetaData['capability']);
}
} catch (\Exception $e) {
Expand All @@ -142,7 +140,7 @@ public function auth($request)
}

try {
$signedToken = $this->getSignedToken($channelName, $token, $userId, $channelCapability);
$signedToken = $this->getSignedToken($channelName, $token, $userId, $guardedChannelCapability);
} catch (\Exception $_) { // excluding exception to avoid exposing private key
throw new AccessDeniedHttpException('malformed token, '.$this->stringify($channelName, $connectionId, $userId));
}
Expand Down Expand Up @@ -196,18 +194,18 @@ public function broadcast($channels, $event, $payload = [])
* @param string $channelName
* @param string $token
* @param string $clientId
* @param string[] $channelCapability
* @param string[] $guardedChannelCapability
* @return string
*/
public function getSignedToken($channelName, $token, $clientId, $channelCapability = ['*'])
public function getSignedToken($channelName, $token, $clientId, $guardedChannelCapability)
{
$header = [
'typ' => 'JWT',
'alg' => 'HS256',
'kid' => $this->getPublicToken(),
];
// Set capabilities for public channel as per https://ably.com/docs/core-features/authentication#capability-operations
$channelClaims = $this->defaultChannelClaims;
$channelClaims = $this->publicChannelsClaims;
$serverTimeFn = function () {
return $this->getServerTime();
};
Expand All @@ -220,8 +218,8 @@ public function getSignedToken($channelName, $token, $clientId, $channelCapabili
$iat = $serverTimeFn();
$exp = $iat + $this->tokenExpiry;
}
if ($channelName) {
$channelClaims[$channelName] = $channelCapability;
if ($channelName && $this->isGuardedChannel($channelName)) {
$channelClaims[$channelName] = $guardedChannelCapability;
}
$claims = [
'iat' => $iat,
Expand Down
16 changes: 9 additions & 7 deletions tests/AblyBroadcasterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class AblyBroadcasterTest extends TestCase

public $ably;

public $guardedChannelCapability = ['*'];

protected function setUp(): void
{
parent::setUp();
Expand Down Expand Up @@ -144,7 +146,7 @@ public function testGenerateAndValidateToken()

public function testShouldGetSignedToken()
{
$token = $this->broadcaster->getSignedToken(null, null, 'user123');
$token = $this->broadcaster->getSignedToken(null, null, 'user123', $this->guardedChannelCapability);
$parsedToken = Utils::parseJwt($token);
$header = $parsedToken['header'];
$payload = $parsedToken['payload'];
Expand All @@ -163,7 +165,7 @@ public function testShouldGetSignedToken()

public function testShouldGetSignedTokenForGivenChannel()
{
$token = $this->broadcaster->getSignedToken('private:channel', null, 'user123');
$token = $this->broadcaster->getSignedToken('private:channel', null, 'user123', $this->guardedChannelCapability);
$parsedToken = Utils::parseJwt($token);
$header = $parsedToken['header'];
$payload = $parsedToken['payload'];
Expand All @@ -182,7 +184,7 @@ public function testShouldGetSignedTokenForGivenChannel()

public function testShouldHaveUpgradedCapabilitiesForValidToken()
{
$token = $this->broadcaster->getSignedToken('private:channel', null, 'user123');
$token = $this->broadcaster->getSignedToken('private:channel', null, 'user123', $this->guardedChannelCapability);

$parsedToken = Utils::parseJwt($token);
$payload = $parsedToken['payload'];
Expand All @@ -191,7 +193,7 @@ public function testShouldHaveUpgradedCapabilitiesForValidToken()
$iat = $payload['iat'];
$exp = $payload['exp'];

$token = $this->broadcaster->getSignedToken('private:channel2', $token, 'user123');
$token = $this->broadcaster->getSignedToken('private:channel2', $token, 'user123', $this->guardedChannelCapability);
$parsedToken = Utils::parseJwt($token);
$payload = $parsedToken['payload'];
$expectedCapability = '{"public:*":["subscribe","history","channel-metadata"],"private:channel":["*"],"private:channel2":["*"]}';
Expand All @@ -200,7 +202,7 @@ public function testShouldHaveUpgradedCapabilitiesForValidToken()
self::assertEquals($iat, $payload['iat']);
self::assertEquals($exp, $payload['exp']);

$token = $this->broadcaster->getSignedToken('private:channel3', $token, 'user98');
$token = $this->broadcaster->getSignedToken('private:channel3', $token, 'user98', $this->guardedChannelCapability);
$parsedToken = Utils::parseJwt($token);
$payload = $parsedToken['payload'];
$expectedCapability = '{"public:*":["subscribe","history","channel-metadata"],"private:channel":["*"],"private:channel2":["*"],"private:channel3":["*"]}';
Expand All @@ -226,7 +228,7 @@ public function testAuthSignedToken()
$this->getMockRequestWithUserForChannel('private:test1', null)
);
self::assertEquals('string', gettype($prevResponse['token']));
$expectedToken = $this->broadcaster->getSignedToken('private:test1', null, 42);
$expectedToken = $this->broadcaster->getSignedToken('private:test1', null, 42, $this->guardedChannelCapability);
self::assertEquals($expectedToken, $prevResponse['token']);
self::assertTrue(Utils::isJwtValid($expectedToken, function () {
return time();
Expand All @@ -237,7 +239,7 @@ public function testAuthSignedToken()
);

self::assertEquals('string', gettype($response['token']));
$expectedToken = $this->broadcaster->getSignedToken('presence:test2', $prevResponse['token'], 42);
$expectedToken = $this->broadcaster->getSignedToken('presence:test2', $prevResponse['token'], 42, $this->guardedChannelCapability);
self::assertEquals($expectedToken, $response['token']);
self::assertTrue(Utils::isJwtValid($expectedToken, function () {
return time();
Expand Down

0 comments on commit 91c7a41

Please sign in to comment.