Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c71ef13
Added wildcard ('*') clientId support
bladeSk Nov 10, 2015
859d216
Merge remote-tracking branch 'upstream/master'
bladeSk Nov 10, 2015
3d4fd8a
Tests for Auth::getClientId behavior
bladeSk Nov 11, 2015
eb9c477
Added support for additional clientId parameter to Channel::publish()…
bladeSk Nov 11, 2015
87354ab
Improved requestToken() edge case ($tokenParams->clientId === null) h…
bladeSk Nov 11, 2015
f1d872a
Fixed parameter order for Auth::requestToken and Auth::authorise
bladeSk Dec 3, 2015
702fd65
Fixed parameter order in Auth::createTokenRequest
bladeSk Dec 3, 2015
3ed5803
A version header automatically added to every HTTP request + test
bladeSk Dec 3, 2015
949ae35
Updated models to reflect the latest spec changes in ClientOptions
bladeSk Dec 3, 2015
8dde0e0
Fixed all discrepancies related to handling of clientId
bladeSk Dec 9, 2015
08a68a7
PHP7 strict mode fixes and bug fixes in CurlWrapper.php
bladeSk Dec 10, 2015
34e1ab3
Updated testClientIdLib to verify automatic switching to token auth
bladeSk Dec 10, 2015
1cf1d74
Auth::authorise now stores provided parameters as defaults + test
bladeSk Dec 10, 2015
449cf10
Added support for connection timeout and fallback max retry count
bladeSk Dec 10, 2015
74316a4
Allow mocking of Auth, added test to verfiy (RSA10e)
bladeSk Dec 11, 2015
922363d
Fixed parameter overriding in Auth::createTokenRequest()
bladeSk Dec 14, 2015
ac97e4c
Added tests for authorise() parameters
bladeSk Dec 15, 2015
25cef0c
Changed Auth::getClientId() to magic property Auth::clientId
bladeSk Dec 15, 2015
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions src/AblyRest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@
*/
class AblyRest {

const API_VERSION = '0.8';

private $options;

/**
* @var \Ably\Http $http Class for making HTTP requests
* @var \Ably\Http $http object for making HTTP requests
*/
public $http;
/**
* @var \Ably\Auth $auth Class providing authorisation functionality
* @var \Ably\Auth $auth object providing authorisation functionality
*/
public $auth;
/**
* @var \Ably\Channels $channels Class for creating and releasing channels
* @var \Ably\Channels $channels object for creating and releasing channels
*/
public $channels;

Expand Down Expand Up @@ -51,8 +53,9 @@ public function __construct( $options = array() ) {
}

$httpClass = $this->options->httpClass;
$this->http = new $httpClass( $this->options->hostTimeout );
$this->auth = new Auth( $this, $this->options );
$this->http = new $httpClass( $this->options );
$authClass = $this->options->authClass;
$this->auth = new $authClass( $this, $this->options );
$this->channels = new Channels( $this );

return $this;
Expand Down Expand Up @@ -184,7 +187,7 @@ protected function requestWithFallback( $method, $path, $headers = array(), $par
}
catch (AblyRequestException $e) {
if ( $e->getCode() >= 50000 ) {
if ( $attempt < count( $this->options->fallbackHosts ) ) {
if ( $attempt < min( $this->options->httpMaxRetryCount, count( $this->options->fallbackHosts ) ) ) {
return $this->requestWithFallback( $method, $path, $headers, $params, $attempt + 1);
} else {
Log::e( 'Failed to connect to server and all of the fallback servers.' );
Expand Down
134 changes: 85 additions & 49 deletions src/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,22 @@

/**
* Provides authentification methods for AblyRest instances
* @property-read string|null $clientId ClientId currently in use. Null if not authenticated yet or when using anonymous auth.
*/
class Auth {
private $authOptions;
private $basicAuth;
private $tokenDetails;
private $ably;
protected $defaultAuthOptions;
protected $defaultTokenParams;
protected $basicAuth;
protected $tokenDetails;
protected $ably;
const TOKEN_EXPIRY_MARGIN = 15000; // a token is considered expired a bit earlier to prevent race conditions

public function __construct( AblyRest $ably, ClientOptions $options ) {
$this->authOptions = new AuthOptions($options);
$this->defaultAuthOptions = new AuthOptions($options);
$this->defaultTokenParams = $options->defaultTokenParams;
$this->ably = $ably;

if ( empty( $this->authOptions->useTokenAuth ) && $this->authOptions->key && empty( $this->authOptions->clientId ) ) {
if ( empty( $this->defaultAuthOptions->useTokenAuth ) && $this->defaultAuthOptions->key && empty( $this->defaultAuthOptions->clientId ) ) {
$this->basicAuth = true;
Log::d( 'Auth: anonymous, using basic auth' );

Expand All @@ -37,20 +40,43 @@ public function __construct( AblyRest $ably, ClientOptions $options ) {

$this->basicAuth = false;

if(!empty( $this->authOptions->authCallback )) {
if(!empty( $this->defaultAuthOptions->authCallback )) {
Log::d( 'Auth: using token auth with authCallback' );
} else if(!empty( $this->authOptions->authUrl )) {
} else if(!empty( $this->defaultAuthOptions->authUrl )) {
Log::d( 'Auth: using token auth with authUrl' );
} else if(!empty( $this->authOptions->key )) {
} else if(!empty( $this->defaultAuthOptions->key )) {
Log::d( 'Auth: using token auth with client-side signing' );
} else if(!empty( $this->authOptions->tokenDetails )) {
} else if(!empty( $this->defaultAuthOptions->tokenDetails )) {
Log::d( 'Auth: using token auth with supplied token only' );
} else {
Log::e( 'Auth: no authentication parameters supplied' );
throw new AblyException ( 'No authentication parameters supplied', 40103, 401 );
}

$this->tokenDetails = $this->authOptions->tokenDetails;
$this->tokenDetails = $this->defaultAuthOptions->tokenDetails;

if ( $this->defaultAuthOptions->clientId == '*' ) {
throw new AblyException ( 'Instantiating AblyRest with a wildcard clientId (`*`) not allowed.', 40003, 400 );
}
}

/**
* Magic getter for the $clientId property
*/
public function __get( $name ) {
if ($name == 'clientId') {
if ( empty( $this->tokenDetails ) ) {
if ( !empty( $this->defaultAuthOptions->clientId ) ) {
return $this->defaultAuthOptions->clientId;
}
} else {
return $this->tokenDetails->clientId;
}

return null;
}

throw new AblyException( 'Undefined property: '.__CLASS__.'::'.$name );
}

public function isUsingBasicAuth() {
Expand All @@ -62,12 +88,20 @@ public function isUsingBasicAuth() {
* and will obtain a new token if necessary.
* In the event that a new token request is made, the specified options are used.
* If not already using token based auth, this will enable it.
* @param array|null $authOptions Overridable auth options, if you don't wish to use the default ones
* @param array|null $tokenParams Requested token parameters
* @param array|null $authOptions Overridable auth options, if you don't wish to use the default ones
* @param boolean|null $force Forces generation of a fresh token
* @return \Ably\Models\TokenDetails The new token
*/
public function authorise( $authOptions = array(), $tokenParams = array(), $force = false ) {
public function authorise( $tokenParams = array(), $authOptions = array(), $force = false ) {

if ( !empty( $tokenParams ) ) {
$this->defaultTokenParams = new TokenParams( array_merge( $this->defaultTokenParams->toArray(), $tokenParams ) );
}
if ( !empty( $authOptions ) ) {
$this->defaultAuthOptions = new AuthOptions( array_merge( $this->defaultAuthOptions->toArray(), $authOptions ) );
}

if ( !$force && !empty( $this->tokenDetails ) ) {
if ( empty( $this->tokenDetails->expires ) ) {
// using cached token
Expand All @@ -79,9 +113,9 @@ public function authorise( $authOptions = array(), $tokenParams = array(), $forc
return $this->tokenDetails;
}
}

Log::d( 'Auth::authorise: requesting new token' );
$this->tokenDetails = $this->requestToken( $authOptions, $tokenParams );
$this->authOptions->tokenDetails = $this->tokenDetails;
$this->tokenDetails = $this->requestToken(); // parameters omitted as they get merged into defaults (see above)
$this->basicAuth = false;

return $this->tokenDetails;
Expand All @@ -95,7 +129,7 @@ public function authorise( $authOptions = array(), $tokenParams = array(), $forc
public function getAuthHeaders() {
$header = array();
if ( $this->isUsingBasicAuth() ) {
$header = array( 'Authorization: Basic ' . base64_encode( $this->authOptions->key ) );
$header = array( 'Authorization: Basic ' . base64_encode( $this->defaultAuthOptions->key ) );
} else {
$this->authorise();
$header = array( 'Authorization: Bearer '. base64_encode( $this->tokenDetails->token ) );
Expand All @@ -111,42 +145,37 @@ public function getTokenDetails() {
return $this->tokenDetails;
}

/**
* @return string|null Library instance's clientId, if instanced with a clientId
*/
public function getClientId() {
if ( !empty( $this->tokenDetails ) && !empty( $this->tokenDetails->clientId ) ) {
return $this->tokenDetails->clientId;
}

return null;
}

/**
* Request a new token.
* @param array|null $authOptions Overridable auth options, if you don't wish to use the default ones
* @param array|null $tokenParams Requested token parameters
* @param array|null $authOptions Overridable auth options, if you don't wish to use the default ones
* @param \Ably\Models\ClientOptions|array $options
* @throws \Ably\Exceptions\AblyException
* @return \Ably\Models\TokenDetails The new token
*/
public function requestToken( $authOptions = array(), $tokenParams = array() ) {
public function requestToken( $tokenParams = array(), $authOptions = array() ) {
// token clientId priority:
// $tokenParams->clientId overrides $authOptions->tokenId overrides $this->defaultTokenParams->clientId overrides $this->defaultAuthOptions->clientId
$tokenClientId = $this->defaultAuthOptions->clientId;
if ( !empty( $this->defaultTokenParams->clientId ) ) $tokenClientId = $this->defaultTokenParams->clientId;
// provided authOptions may override clientId, even with a null value
if ( array_key_exists( 'clientId', $authOptions ) ) $tokenClientId = $authOptions['clientId'];
// provided tokenParams may override clientId, even with a null value
if ( array_key_exists( 'clientId', $tokenParams ) ) $tokenClientId = $tokenParams['clientId'];

// merge provided auth options with defaults
$authOptions = new AuthOptions( array_merge( $this->authOptions->toArray(), $authOptions ) );
$tokenParams = new TokenParams( $tokenParams );

if ( empty( $tokenParams->clientId ) ) {
$tokenParams->clientId = $authOptions->clientId;
}
$authOptionsMerged = new AuthOptions( array_merge( $this->defaultAuthOptions->toArray(), $authOptions ) );
$tokenParamsMerged = new TokenParams( array_merge( $this->defaultTokenParams->toArray(), $tokenParams ) );

$tokenParamsMerged->clientId = $tokenClientId;

// get a signed token request
$signedTokenRequest = null;
if ( !empty( $authOptions->authCallback ) ) {
if ( !empty( $authOptionsMerged->authCallback ) ) {
Log::d( 'Auth::requestToken:', 'using token auth with auth_callback' );

$callback = $authOptions->authCallback;
$data = $callback($tokenParams);
$callback = $authOptionsMerged->authCallback;
$data = $callback($tokenParamsMerged);

// returned data can be either a signed TokenRequest or TokenDetails or just a token string
if ( is_a( $data, '\Ably\Models\TokenRequest' ) ) {
Expand All @@ -159,14 +188,14 @@ public function requestToken( $authOptions = array(), $tokenParams = array() ) {
Log::e( 'Auth::requestToken:', 'Invalid response from authCallback, expecting signed TokenRequest or TokenDetails or a token string' );
throw new AblyException( 'Invalid response from authCallback' );
}
} elseif ( !empty( $authOptions->authUrl ) ) {
} elseif ( !empty( $authOptionsMerged->authUrl ) ) {
Log::d( 'Auth::requestToken:', 'using token auth with auth_url' );

$data = $this->ably->http->request(
$authOptions->authMethod,
$authOptions->authUrl,
$authOptions->authHeaders ? : array(),
array_merge( $authOptions->authParams ? : array(), $tokenParams->toArray() )
$authOptionsMerged->authMethod,
$authOptionsMerged->authUrl,
$authOptionsMerged->authHeaders ? : array(),
array_merge( $authOptionsMerged->authParams ? : array(), $tokenParamsMerged->toArray() )
);

$data = $data['body'];
Expand All @@ -186,9 +215,9 @@ public function requestToken( $authOptions = array(), $tokenParams = array() ) {
Log::e( 'Auth::requestToken:', 'Invalid response from authURL, expecting token string or JSON representation of signed TokenRequest or TokenDetails' );
throw new AblyException( 'Invalid response from authURL' );
}
} elseif ( !empty( $authOptions->key ) ) {
} elseif ( !empty( $authOptionsMerged->key ) ) {
Log::d( 'Auth::requestToken:', 'using token auth with client-side signing' );
$signedTokenRequest = $this->createTokenRequest( $authOptions->toArray(), $tokenParams->toArray() );
$signedTokenRequest = $this->createTokenRequest( $tokenParams, $authOptions );
} else {
Log::e( 'Auth::requestToken:', 'Unable to request a Token, auth options don\'t provide means to do so' );
throw new AblyException( 'Unable to request a Token, auth options don\'t provide means to do so', 40101, 401 );
Expand Down Expand Up @@ -221,13 +250,20 @@ public function requestToken( $authOptions = array(), $tokenParams = array() ) {
* Create a signed token request based on known credentials
* and the given token params. This would typically be used if creating
* signed requests for submission by another client.
* @param \Ably\Models\AuthOptions $authOptions
* @param \Ably\Models\TokenParams $tokenParams
* @param \Ably\Models\AuthOptions $authOptions
* @return \Ably\Models\TokenRequest A signed token request
*/
public function createTokenRequest( $authOptions = array(), $tokenParams = array() ) {
$authOptions = new AuthOptions( array_merge( $this->authOptions->toArray(), $authOptions ) );
$tokenParams = new TokenParams( $tokenParams );
public function createTokenRequest( $tokenParams = array(), $authOptions = array() ) {
$tokenClientId = $this->defaultAuthOptions->clientId;
if ( !empty( $this->defaultTokenParams->clientId ) ) $tokenClientId = $this->defaultTokenParams->clientId;
if ( array_key_exists( 'clientId', $authOptions ) ) $tokenClientId = $authOptions['clientId'];
if ( array_key_exists( 'clientId', $tokenParams ) ) $tokenClientId = $tokenParams['clientId'];

$authOptions = new AuthOptions( array_merge( $this->defaultAuthOptions->toArray(), $authOptions ) );
$tokenParams = new TokenParams( array_merge( $this->defaultTokenParams->toArray(), $tokenParams ) );
$tokenParams->clientId = $tokenClientId;

$keyParts = explode( ':', $authOptions->key );

if ( count( $keyParts ) != 2 ) {
Expand Down
33 changes: 15 additions & 18 deletions src/Channel.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,57 +52,54 @@ public function __get( $name ) {

/**
* Posts a message to this channel
* @param mixed ... Either a Message, array of Messages, or name and data
* @param string $data Message data
* @param mixed ... Either a Message, array of Message-s, or (string eventName, string data, [string clientId])
* @throws \Ably\Exceptions\AblyException
*/
public function publish() {

$args = func_get_args();
$json = '';
$authClientId = $this->ably->auth->getClientId();

if (count($args) == 1 && is_a( $args[0], 'Ably\Models\Message' )) { // single Message
if ( count($args) == 1 && is_a( $args[0], 'Ably\Models\Message' ) ) { // single Message
$msg = $args[0];

if ( !empty( $msg->clientId ) && !empty( $authClientId ) && $msg->clientId != $authClientId) {
throw new AblyException( 'Message\'s clientId does not match the clientId of the authorisation token.', 40102, 401 );
}

if ($this->options->encrypted) {
if ( $this->options->encrypted ) {
$msg->setCipherParams( $this->options->cipherParams );
}

$json = $msg->toJSON();
} else if (count($args) == 1 && is_array( $args[0] )) { // array of Messages
} else if ( count($args) == 1 && is_array( $args[0] ) ) { // array of Messages
$jsonArray = array();

foreach ($args[0] as $msg) {
if ( !empty( $msg->clientId ) && !empty( $authClientId ) && $msg->clientId != $authClientId) {
throw new AblyException( 'Message\'s clientId does not match the clientId of the authorisation token.', 40102, 401 );
}

if ($this->options->encrypted) {
foreach ( $args[0] as $msg ) {
if ( $this->options->encrypted ) {
$msg->setCipherParams( $this->options->cipherParams );
}

$jsonArray[] = $msg->toJSON();
}

$json = '[' . implode( ',', $jsonArray ) . ']';
} else if (count($args) == 2) { // name and data
} else if ( count($args) >= 2 && count($args) <= 3 ) { // eventName, data[, clientId]
$msg = new Message();
$msg->name = $args[0];
$msg->data = $args[1];
if ( count($args) == 3 ) $msg->clientId = $args[2];

if ($this->options->encrypted) {
if ( $this->options->encrypted ) {
$msg->setCipherParams( $this->options->cipherParams );
}

$json = $msg->toJSON();
} else {
throw new AblyException( 'Wrong parameters provided, use either Message, array of Messages, or name and data', 40003, 400 );
}

$authClientId = $this->ably->auth->clientId;
// if the message has a clientId set and we're using token based auth, the clientIds must match unless we're a wildcard client
if ( !empty( $msg->clientId ) && !$this->ably->auth->isUsingBasicAuth() && $authClientId != '*' && $msg->clientId != $authClientId) {
throw new AblyException( 'Message\'s clientId does not match the clientId of the authorisation token.', 40102, 401 );
}

$this->ably->post( $this->channelPath . '/messages', $headers = array(), $json );
return true;
Expand Down
Loading