Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 10 additions & 2 deletions src/AblyRest.php
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,15 @@ public function request( $method, $path, $headers = array(), $params = array(),
if ( !empty( $this->options->fallbackHosts ) ) {
$res = $this->requestWithFallback( $method, $path, $mergedHeaders, $params );
} else {
$server = ($this->options->tls ? 'https://' : 'http://') . $this->options->host;
$server = ($this->options->tls ? 'https://' : 'http://') . $this->options->restHost;

if ( $this->options->tls && !empty( $this->options->tlsPort ) ) {
$server .= ':' . $this->options->tlsPort;
}
if ( !$this->options->tls && !empty( $this->options->port ) ) {
$server .= ':' . $this->options->port;
}

$res = $this->http->request( $method, $server . $path, $mergedHeaders, $params );
}
} catch (AblyRequestException $e) {
Expand Down Expand Up @@ -165,7 +173,7 @@ public function request( $method, $path, $headers = array(), $params = array(),
protected function requestWithFallback( $method, $path, $headers = array(), $params = array(), $attempt = 0 ) {
try {
if ( $attempt == 0 ) { // using default host
$server = ($this->options->tls ? 'https://' : 'http://') . $this->options->host;
$server = ($this->options->tls ? 'https://' : 'http://') . $this->options->restHost;
} else { // using a fallback host
Log::d( 'Connection failed, attempting with fallback server #' . $attempt );
// attempt 1 uses fallback host with index 0
Expand Down
41 changes: 30 additions & 11 deletions src/Auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class Auth {
private $basicAuth;
private $tokenDetails;
private $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);
Expand Down Expand Up @@ -57,20 +58,25 @@ public function isUsingBasicAuth() {
}

/**
* Ensures valid auth credentials are present for the library instance. This may rely on an already-known and valid token, and will obtain a new token if necessary.
* Ensures that a valid token is present for the library instance. This may rely on an already-known and valid token,
* 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 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 ) {
if ( !$force && !empty( $this->tokenDetails ) ) {
if ( empty( $this->tokenDetails->expires ) ) {
// using cached token
Log::d( 'Auth::authorise: using cached token, unknown expiration time' );
return $this;
} else if ( $this->tokenDetails->expires > $this->ably->systemTime() ) {
return $this->tokenDetails;
} else if ( $this->tokenDetails->expires - self::TOKEN_EXPIRY_MARGIN > $this->ably->systemTime() ) {
// using cached token
Log::d( 'Auth::authorise: using cached token, expires on ' . date( 'Y-m-d H:i:s', $this->tokenDetails->expires / 1000 ) );
return $this;
return $this->tokenDetails;
}
}
Log::d( 'Auth::authorise: requesting new token' );
Expand All @@ -84,17 +90,17 @@ public function authorise( $authOptions = array(), $tokenParams = array(), $forc
/**
* Get HTTP headers with authentication data
* Automatically attempts to authorise token requests
* @return Array Array of HTTP headers containing an `Authorization` header
*/
public function getAuthHeaders() {
$header = array();
if ( $this->isUsingBasicAuth() ) {
$header = array( 'authorization: Basic ' . base64_encode( $this->authOptions->key ) );
} else if ( !empty( $this->tokenDetails ) ) {
$this->authorise();
$header = array( 'authorization: Bearer '. base64_encode( $this->tokenDetails->token ) );
$header = array( 'Authorization: Basic ' . base64_encode( $this->authOptions->key ) );
} else {
throw new AblyException( 'Unable to provide auth headers. No auth parameters defined.', 40101, 401 );
$this->authorise();
$header = array( 'Authorization: Bearer '. base64_encode( $this->tokenDetails->token ) );
}

return $header;
}

Expand All @@ -106,11 +112,23 @@ public function getTokenDetails() {
}

/**
* Request a new Token
* @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 \Ably\Models\ClientOptions|array $options
* @throws \Ably\Exceptions\AblyException
* @return \Ably\Models\TokenDetails The new token
*/
public function requestToken( $authOptions = array(), $tokenParams = array() ) {

Expand Down Expand Up @@ -205,6 +223,7 @@ public function requestToken( $authOptions = array(), $tokenParams = array() ) {
* signed requests for submission by another client.
* @param \Ably\Models\AuthOptions $authOptions
* @param \Ably\Models\TokenParams $tokenParams
* @return \Ably\Models\TokenRequest A signed token request
*/
public function createTokenRequest( $authOptions = array(), $tokenParams = array() ) {
$authOptions = new AuthOptions( array_merge( $this->authOptions->toArray(), $authOptions ) );
Expand All @@ -227,7 +246,7 @@ public function createTokenRequest( $authOptions = array(), $tokenParams = array
$tokenRequest->keyName = $keyName;
}

if ( $tokenRequest->queryTime ) {
if ( $authOptions->queryTime ) {
$tokenRequest->timestamp = $this->ably->time();
} else if ( empty( $tokenRequest->timestamp ) ) {
$tokenRequest->timestamp = $this->ably->systemTime();
Expand Down
12 changes: 10 additions & 2 deletions src/Channel.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,20 +60,28 @@ 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
$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) {
$msg->setCipherParams( $this->options->cipherParams );
}

$json = $msg->toJSON();
} else if (count($args) == 1 && is_array( $args[0] )) { // array of Messages
$msg = $args[0];
$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) {
$msg->setCipherParams( $this->options->cipherParams );
}
Expand All @@ -93,7 +101,7 @@ public function publish() {

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

$this->ably->post( $this->channelPath . '/messages', $headers = array(), $json );
Expand Down
7 changes: 7 additions & 0 deletions src/Exceptions/AblyException.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ class AblyException extends Exception {
*/
public $errorInfo;

/**
* @param string $message Exception's error message text
* @param integer|null $code 5-digit Ably error code
* also used as a PHP exception code
* @see https://github.com/ably/ably-common/blob/master/protocol/errors.json
* @param integer|null $statusCode HTTP error code
*/
public function __construct( $message, $code = null, $statusCode = null ) {
parent::__construct( $message, $code );
$this->errorInfo = new ErrorInfo();
Expand Down
8 changes: 8 additions & 0 deletions src/Models/AuthOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ class AuthOptions extends BaseOptions {
*/
public $authMethod = 'GET';

/**
* @var boolean This may be set in instances that the library is to sign
* token requests based on a given key. If true, the library
* will query the Ably system for the current time instead of
* relying on a locally-available time of day.
*/
public $queryTime;

public function __construct( $options = array() ) {
parent::__construct( $options );

Expand Down
23 changes: 19 additions & 4 deletions src/Models/BaseMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
abstract class BaseMessage {

/**
* @var mixed The message payload.
* @var string Unique ID for this message. Populated by the system.
*/
public $id;
/**
* @var mixed|null The message payload.
*/
public $data;
/**
Expand Down Expand Up @@ -103,6 +107,10 @@ protected function encode() {

return $msg;
}

if ($this->clientId) {
$msg->clientId = $this->clientId;
}

$isBinary = false;
$encodings = array();
Expand All @@ -117,8 +125,10 @@ protected function encode() {
$msg->data = $this->data;
$isBinary = true;
}
} else if ( !isset( $this->data ) || $this->data === null ) {
return $msg;
} else {
throw new AblyException( 'Message data must be either, string, string with binary data, or JSON-encodable array or object.', 40003, 400 );
throw new AblyException( 'Message data must be either, string, string with binary data, JSON-encodable array or object, or null.', 40003, 400 );
}

if ( $this->cipherParams ) {
Expand All @@ -127,7 +137,7 @@ protected function encode() {
}

$msg->data = base64_encode( Crypto::encrypt( $msg->data, $this->cipherParams ) );
$encodings[] = 'cipher+' . $this->cipherParams->algorithm;
$encodings[] = 'cipher+' . $this->cipherParams->getAlgorithmString();
$encodings[] = 'base64';
} else {
if ( $isBinary ) {
Expand All @@ -136,7 +146,12 @@ protected function encode() {
}
}

$msg->encoding = implode( '/', $encodings );
if ( count( $encodings ) ) {
$msg->encoding = implode( '/', $encodings );
} else {
$msg->encoding = '';
}

return $msg;
}

Expand Down
31 changes: 23 additions & 8 deletions src/Models/CipherParams.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,35 @@
class CipherParams {
/** @var string Key used for encryption, may be a binary string. */
public $key;
/** @var string Algorithm to be used for encryption. Valid values are: 'aes-128-cbc' (default) and 'aes-256-cbc'. */
/** @var string Algorithm to be used for encryption. The only supported algorithm is currently 'aes'. */
public $algorithm;
/** @var string Key length of the algorithm. Valid values for 'aes' are 128 or 256. */
public $keyLength;
/** @var string Algorithm mode. The only supported mode is currenty 'cbc'. */
public $mode;
/** @var string Initialization vector for encryption, may be a binary string. */
public $iv;

/**
* Constructor
* @param string|null $key Encryption key, if not provided a random key is generated.
* @param string|null $algorithm Algorithm to be used for encryption. Valid values are: 'aes-128-cbc' (default) and 'aes-256-cbc'.
* @param string|null $iv Initialization vector for encryption, if not provided, random IV is generated.
* Constructor. The encryption algorithm defaults to the only supported algorithm - AES CBC with
* a default key length of 128. A random IV is generated.
* @param string|null $key Encryption key, if not provided a random key is generated.
* @param string|null $algorithm Encryption algorithm, defaults to 'aes'.
* @param Integer|null $keyLength Cipher key length, defaults to 128.
* @param string|null $mode Algorithm mode, defaults to 'cbc'.
*/
public function __construct( $key = null, $algorithm = null, $iv = null ) {
public function __construct( $key = null, $algorithm = 'aes', $keyLength = 128, $mode = 'cbc' ) {
$this->key = $key ? $key : openssl_random_pseudo_bytes( 16 );
$this->algorithm = $algorithm ? $algorithm : 'aes-128-cbc';
$this->iv = $iv ? $iv : openssl_random_pseudo_bytes( openssl_cipher_iv_length( $this->algorithm ) );
$this->algorithm = $algorithm;
$this->keyLength = $keyLength;
$this->mode = $mode;
$this->iv = openssl_random_pseudo_bytes( openssl_cipher_iv_length( $this->getAlgorithmString() ) );
}

/**
* @return string Algorithm string as required by openssl - for instance `aes-128-cbc`
*/
public function getAlgorithmString() {
return $this->algorithm . '-' . $this->keyLength . '-' . $this->mode;
}
}
28 changes: 18 additions & 10 deletions src/Models/ClientOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,24 @@ class ClientOptions extends AuthOptions {

/**
* @var string alternate server domain
* For use in development environments only.
* For development environments only.
*/
public $host;
public $restHost;

/**
* @var string optional prefix to be prepended to $host
* @var integer Allows a non-default Ably non-TLS port to be used.
* For development environments only.
*/
public $port;

/**
* @var integer Allows a non-default Ably TLS port to be used.
* For development environments only.
*/
public $tlsPort;

/**
* @var string optional prefix to be prepended to $restHost
* Example: 'sandbox' -> 'sandbox-rest.ably.io'
*/
public $environment;
Expand All @@ -70,12 +82,8 @@ class ClientOptions extends AuthOptions {
public function __construct( $options = array() ) {
parent::__construct( $options );

if ( empty( $this->environment ) && getenv( 'ABLY_ENV' ) ) {
$this->environment = getenv( 'ABLY_ENV' );
}

if ( empty( $this->host ) ) {
$this->host = 'rest.ably.io';
if ( empty( $this->restHost ) ) {
$this->restHost = 'rest.ably.io';

if ( empty( $this->environment ) ) {
$this->fallbackHosts = array(
Expand All @@ -91,7 +99,7 @@ public function __construct( $options = array() ) {
}

if ( !empty( $this->environment ) ) {
$this->host = $this->environment . '-' . $this->host;
$this->restHost = $this->environment . '-' . $this->restHost;
}
}
}
4 changes: 3 additions & 1 deletion src/Models/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ class Message extends BaseMessage {
protected function encode() {
$msg = parent::encode();

$msg->name = $this->name;
if ( isset( $this->name ) && $this->name ) {
$msg->name = $this->name;
}

return $msg;
}
Expand Down
20 changes: 1 addition & 19 deletions src/Models/PaginatedResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,7 @@ public function __construct( \Ably\AblyRest $ably, $model, $cipherParams, $path,
* @return PaginatedResult Returns self if the current page is the first
*/
public function first() {
if ($this->isFirst()) {
return this;
} else if (isset($this->paginationHeaders['first'])) {
if (isset($this->paginationHeaders['first'])) {
return new PaginatedResult( $this->ably, $this->model, $this->cipherParams, $this->paginationHeaders['first'] );
} else {
return null;
Expand Down Expand Up @@ -101,22 +99,6 @@ public function hasNext() {
return $this->isPaginated() && isset($this->paginationHeaders['next']);
}

/**
* @return boolean Whether the current page is the first, always true for single-page results
*/
public function isFirst() {
if (!$this->isPaginated() ) {
return true;
}

if ( isset($this->paginationHeaders['first']) && isset($this->paginationHeaders['current'])
&& $this->paginationHeaders['first'] == $this->paginationHeaders['current'] ) {
return true;
}

return false;
}

/**
* @return boolean Whether the current page is the last, always true for single-page results
*/
Expand Down
Loading