Skip to content

Commit

Permalink
First HTTP/2 Protocol implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
duccio committed Jun 1, 2016
1 parent f1e1930 commit a2d6a51
Show file tree
Hide file tree
Showing 7 changed files with 284 additions and 32 deletions.
76 changes: 70 additions & 6 deletions ApnsPHP/Abstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,20 @@ abstract class ApnsPHP_Abstract
const ENVIRONMENT_PRODUCTION = 0; /**< @type integer Production environment. */
const ENVIRONMENT_SANDBOX = 1; /**< @type integer Sandbox environment. */

const PROTOCOL_BINARY = 0; /**< @type integer Binary Provider API. */
const PROTOCOL_HTTP = 1; /**< @type integer APNs Provider API. */

const DEVICE_BINARY_SIZE = 32; /**< @type integer Device token length. */

const WRITE_INTERVAL = 10000; /**< @type integer Default write interval in micro seconds. */
const CONNECT_RETRY_INTERVAL = 1000000; /**< @type integer Default connect retry interval in micro seconds. */
const SOCKET_SELECT_TIMEOUT = 1000000; /**< @type integer Default socket select timeout in micro seconds. */

protected $_aServiceURLs = array(); /**< @type array Container for service URLs environments. */
protected $_aHTTPServiceURLs = array(); /**< @type array Container for HTTP/2 service URLs environments. */

protected $_nEnvironment; /**< @type integer Active environment. */
protected $_nProtocol; /**< @type integer Active protocol. */

protected $_nConnectTimeout; /**< @type integer Connect timeout in seconds. */
protected $_nConnectRetryTimes = 3; /**< @type integer Connect retry times. */
Expand All @@ -73,10 +78,11 @@ abstract class ApnsPHP_Abstract
* @param $nEnvironment @type integer Environment.
* @param $sProviderCertificateFile @type string Provider certificate file
* with key (Bundled PEM).
* @param $nProtocol @type integer Protocol.
* @throws ApnsPHP_Exception if the environment is not
* sandbox or production or the provider certificate file is not readable.
*/
public function __construct($nEnvironment, $sProviderCertificateFile)
public function __construct($nEnvironment, $sProviderCertificateFile, $nProtocol = self::PROTOCOL_BINARY)
{
if ($nEnvironment != self::ENVIRONMENT_PRODUCTION && $nEnvironment != self::ENVIRONMENT_SANDBOX) {
throw new ApnsPHP_Exception(
Expand All @@ -92,6 +98,13 @@ public function __construct($nEnvironment, $sProviderCertificateFile)
}
$this->_sProviderCertificateFile = $sProviderCertificateFile;

if ($nProtocol != self::PROTOCOL_BINARY && $nProtocol != self::PROTOCOL_HTTP) {
throw new ApnsPHP_Exception(
"Invalid protocol '{$nProtocol}'"
);
}
$this->_nProtocol = $nProtocol;

$this->_nConnectTimeout = ini_get("default_socket_timeout");
$this->_nWriteInterval = self::WRITE_INTERVAL;
$this->_nConnectRetryInterval = self::CONNECT_RETRY_INTERVAL;
Expand Down Expand Up @@ -357,22 +370,73 @@ public function disconnect()
{
if (is_resource($this->_hSocket)) {
$this->_log('INFO: Disconnected.');
return fclose($this->_hSocket);
if ($this->_nProtocol === self::PROTOCOL_HTTP) {
curl_close($this->_hSocket);
return true;
} else {
return fclose($this->_hSocket);
}
}
return false;
}

/**
* Connects to Apple Push Notification service server.
*
* @throws ApnsPHP_Exception if is unable to connect.
* @return @type boolean True if successful connected.
*/
protected function _connect()
{
$sURL = $this->_aServiceURLs[$this->_nEnvironment];
unset($aURLs);
return $this->_nProtocol === self::PROTOCOL_HTTP ? $this->_httpInit() : $this->_binaryConnect($this->_aServiceURLs[$this->_nEnvironment]);
}

/**
* Initializes cURL, the HTTP/2 backend used to connect to Apple Push Notification
* service server via HTTP/2 API protocol.
*
* @throws ApnsPHP_Exception if is unable to initialize.
* @return @type boolean True if successful initialized.
*/
protected function _httpInit()
{
$this->_log("INFO: Trying to initialize HTTP/2 backend...");

$this->_hSocket = curl_init();
if (!$this->_hSocket) {
throw new ApnsPHP_Exception(
"Unable to initialize HTTP/2 backend."
);
}

if (!curl_setopt_array($this->_hSocket, array(
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
CURLOPT_SSLCERT => $this->_sProviderCertificateFile,
CURLOPT_SSLCERTPASSWD => empty($this->_sProviderCertificatePassphrase) ? null : $this->_sProviderCertificatePassphrase,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_USERAGENT => 'ApnsPHP',
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_VERBOSE => false
))) {
throw new ApnsPHP_Exception(
"Unable to initialize HTTP/2 backend."
);
}

$this->_log("INFO: Initialized HTTP/2 backend.");

return true;
}

/**
* Connects to Apple Push Notification service server via binary protocol.
*
* @throws ApnsPHP_Exception if is unable to connect.
* @return @type boolean True if successful connected.
*/
protected function _binaryConnect($sURL)
{
$this->_log("INFO: Trying {$sURL}...");

/**
Expand Down Expand Up @@ -405,7 +469,7 @@ protected function _connect()

return true;
}

/**
* Logs a message through the Logger.
*
Expand Down
23 changes: 23 additions & 0 deletions ApnsPHP/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class ApnsPHP_Message

protected $_mCustomIdentifier; /**< @type mixed Custom message identifier. */

protected $_sTopic; /**< @type string The topic of the remote notification, which is typically the bundle ID for your app. */

/**
* Constructor.
*
Expand Down Expand Up @@ -486,4 +488,25 @@ public function getCustomIdentifier()
{
return $this->_mCustomIdentifier;
}

/**
* Set the topic of the remote notification, which is typically
* the bundle ID for your app.
*
* @param $sTopic @type string The topic of the remote notification.
*/
public function setTopic($sTopic)
{
$this->_sTopic = $sTopic;
}

/**
* Get the topic of the remote notification.
*
* @return @type string The topic of the remote notification.
*/
public function getTopic()
{
return $this->_sTopic;
}
}
110 changes: 87 additions & 23 deletions ApnsPHP/Push.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,31 @@ class ApnsPHP_Push extends ApnsPHP_Abstract
self::STATUS_CODE_INTERNAL_ERROR => 'Internal error'
); /**< @type array Error-response messages. */

protected $_aHTTPErrorResponseMessages = array(
200 => 'Success',
400 => 'Bad request',
403 => 'There was an error with the certificate',
405 => 'The request used a bad :method value. Only POST requests are supported',
410 => 'The device token is no longer active for the topic',
413 => 'The notification payload was too large',
429 => 'The server received too many requests for the same device token',
500 => 'Internal server error',
503 => 'The server is shutting down and unavailable',
self::STATUS_CODE_INTERNAL_ERROR => 'Internal error'
); /**< @type array HTTP/2 Error-response messages. */

protected $_nSendRetryTimes = 3; /**< @type integer Send retry times. */

protected $_aServiceURLs = array(
'tls://gateway.push.apple.com:2195', // Production environment
'tls://gateway.sandbox.push.apple.com:2195' // Sandbox environment
); /**< @type array Service URLs environments. */

protected $_aHTTPServiceURLs = array(
'https://api.push.apple.com:443', // Production environment
'https://api.development.push.apple.com:443' // Sandbox environment
); /**< @type array HTTP/2 Service URLs environments. */

protected $_aMessageQueue = array(); /**< @type array Message queue. */
protected $_aErrors = array(); /**< @type array Error container. */

Expand Down Expand Up @@ -98,16 +116,19 @@ public function add(ApnsPHP_Message $message)
$nMessageQueueLen = count($this->_aMessageQueue);
for ($i = 0; $i < $nRecipients; $i++) {
$nMessageID = $nMessageQueueLen + $i + 1;
$this->_aMessageQueue[$nMessageID] = array(
$aMessage = array(
'MESSAGE' => $message,
'BINARY_NOTIFICATION' => $this->_getBinaryNotification(
'ERRORS' => array()
);
if ($this->_nProtocol === self::PROTOCOL_BINARY) {
$aMessage['BINARY_NOTIFICATION'] = $this->_getBinaryNotification(
$message->getRecipient($i),
$sMessagePayload,
$nMessageID,
$message->getExpiry()
),
'ERRORS' => array()
);
);
}
$this->_aMessageQueue[$nMessageID] = $aMessage;
}
}

Expand Down Expand Up @@ -168,19 +189,31 @@ public function send()
}
}

$nLen = strlen($aMessage['BINARY_NOTIFICATION']);
$nLen = strlen($this->_nProtocol === self::PROTOCOL_HTTP ? $message->getPayload() : $aMessage['BINARY_NOTIFICATION']);
$this->_log("STATUS: Sending message ID {$k} {$sCustomIdentifier} (" . ($nErrors + 1) . "/{$this->_nSendRetryTimes}): {$nLen} bytes.");

$aErrorMessage = null;
if ($nLen !== ($nWritten = (int)@fwrite($this->_hSocket, $aMessage['BINARY_NOTIFICATION']))) {
$aErrorMessage = array(
'identifier' => $k,
'statusCode' => self::STATUS_CODE_INTERNAL_ERROR,
'statusMessage' => sprintf('%s (%d bytes written instead of %d bytes)',
$this->_aErrorResponseMessages[self::STATUS_CODE_INTERNAL_ERROR], $nWritten, $nLen
)
);

if ($this->_nProtocol === self::PROTOCOL_HTTP) {
if (!$this->_httpSend($message, $sReply)) {
$aErrorMessage = array(
'identifier' => $k,
'statusCode' => curl_getinfo($this->_hSocket, CURLINFO_HTTP_CODE),
'statusMessage' => $sReply
);
}
} else {
if ($nLen !== ($nWritten = (int)@fwrite($this->_hSocket, $aMessage['BINARY_NOTIFICATION']))) {
$aErrorMessage = array(
'identifier' => $k,
'statusCode' => self::STATUS_CODE_INTERNAL_ERROR,
'statusMessage' => sprintf('%s (%d bytes written instead of %d bytes)',
$this->_aErrorResponseMessages[self::STATUS_CODE_INTERNAL_ERROR], $nWritten, $nLen
)
);
}
}

usleep($this->_nWriteInterval);

$bError = $this->_updateQueue($aErrorMessage);
Expand All @@ -190,15 +223,19 @@ public function send()
}

if (!$bError) {
$read = array($this->_hSocket);
$null = NULL;
$nChangedStreams = @stream_select($read, $null, $null, 0, $this->_nSocketSelectTimeout);
if ($nChangedStreams === false) {
$this->_log('ERROR: Unable to wait for a stream availability.');
break;
} else if ($nChangedStreams > 0) {
$bError = $this->_updateQueue();
if (!$bError) {
if ($this->_nProtocol === self::PROTOCOL_BINARY) {
$read = array($this->_hSocket);
$null = NULL;
$nChangedStreams = @stream_select($read, $null, $null, 0, $this->_nSocketSelectTimeout);
if ($nChangedStreams === false) {
$this->_log('ERROR: Unable to wait for a stream availability.');
break;
} else if ($nChangedStreams > 0) {
$bError = $this->_updateQueue();
if (!$bError) {
$this->_aMessageQueue = array();
}
} else {
$this->_aMessageQueue = array();
}
} else {
Expand All @@ -210,6 +247,33 @@ public function send()
}
}

/**
* Send a message using the HTTP/2 API protocol.
*
* @param $message @type ApnsPHP_Message The message.
* @param $sReply @type string The reply message.
* @return @type xxx Xxxxx.
*/
private function _httpSend(ApnsPHP_Message $message, &$sReply)
{
$aHeaders = array('Content-Type: application/json');
$sTopic = $message->getTopic();
if (!empty($sTopic)) {
$aHeaders[] = sprintf('apns-topic: %s', $sTopic);
}

if (!(curl_setopt_array($this->_hSocket, array(
CURLOPT_POST => true,
CURLOPT_URL => sprintf('%s/3/device/%s', $this->_aHTTPServiceURLs[$this->_nEnvironment], $message->getRecipient()),
CURLOPT_HTTPHEADER => $aHeaders,
CURLOPT_POSTFIELDS => $message->getPayload()
)) && ($sReply = curl_exec($this->_hSocket)) !== false)) {
return false;
}

return curl_getinfo($this->_hSocket, CURLINFO_HTTP_CODE) === 200;
}

/**
* Returns messages in the message queue.
*
Expand Down
2 changes: 2 additions & 0 deletions Objective-C Demo/Classes/DemoAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(
bundle:nil];
self.window.rootViewController = self.viewController;

NSLog(@"Started.");

return YES;
}

Expand Down
15 changes: 13 additions & 2 deletions Objective-C Demo/Demo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0730;
TargetAttributes = {
1D6058900D05DD3D006BFB54 = {
DevelopmentTeam = AL6LXF5WS6;
};
};
};
buildConfigurationList = C01FCF4E08A954540054247B /* Build configuration list for PBXProject "Demo" */;
compatibilityVersion = "Xcode 3.2";
Expand Down Expand Up @@ -222,27 +227,33 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
GCC_DYNAMIC_NO_PIC = NO;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = Demo_Prefix.pch;
INFOPLIST_FILE = "Demo-Info.plist";
PRODUCT_BUNDLE_IDENTIFIER = it.immobiliare.labs.apnsphp;
PRODUCT_BUNDLE_IDENTIFIER = com.armiento.test;
PRODUCT_NAME = ApnsPHP;
PROVISIONING_PROFILE = "";
};
name = Debug;
};
1D6058950D05DD3E006BFB54 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = YES;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = Demo_Prefix.pch;
INFOPLIST_FILE = "Demo-Info.plist";
PRODUCT_BUNDLE_IDENTIFIER = it.immobiliare.labs.apnsphp;
PRODUCT_BUNDLE_IDENTIFIER = com.armiento.test;
PRODUCT_NAME = ApnsPHP;
PROVISIONING_PROFILE = "";
VALIDATE_PRODUCT = YES;
};
name = Release;
Expand Down
2 changes: 1 addition & 1 deletion sample_push.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
$push->connect();

// Instantiate a new Message with a single recipient
$message = new ApnsPHP_Message('1e82db91c7ceddd72bf33d74ae052ac9c84a065b35148ac401388843106a7485');
$message = new ApnsPHP_Message('19e4d2cb683e6302ff688b0fe9b6f562c40ea5a31a10d593f82b6d6bf1c88678');

// Set a custom identifier. To get back this identifier use the getCustomIdentifier() method
// over a ApnsPHP_Message object retrieved with the getErrors() message.
Expand Down
Loading

0 comments on commit a2d6a51

Please sign in to comment.