From a2d6a51d34ecd189fe738cf7bd944a6df6e25862 Mon Sep 17 00:00:00 2001 From: Aldo Armiento Date: Wed, 1 Jun 2016 13:05:19 +0200 Subject: [PATCH] First HTTP/2 Protocol implementation. --- ApnsPHP/Abstract.php | 76 +++++++++++- ApnsPHP/Message.php | 23 ++++ ApnsPHP/Push.php | 110 ++++++++++++++---- Objective-C Demo/Classes/DemoAppDelegate.m | 2 + .../Demo.xcodeproj/project.pbxproj | 15 ++- sample_push.php | 2 +- sample_push_http.php | 88 ++++++++++++++ 7 files changed, 284 insertions(+), 32 deletions(-) create mode 100644 sample_push_http.php diff --git a/ApnsPHP/Abstract.php b/ApnsPHP/Abstract.php index 43a5eb93..b3c32289 100644 --- a/ApnsPHP/Abstract.php +++ b/ApnsPHP/Abstract.php @@ -42,6 +42,9 @@ 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. */ @@ -49,8 +52,10 @@ abstract class ApnsPHP_Abstract 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. */ @@ -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( @@ -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; @@ -357,7 +370,12 @@ 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; } @@ -365,14 +383,60 @@ public function disconnect() /** * 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}..."); /** @@ -405,7 +469,7 @@ protected function _connect() return true; } - + /** * Logs a message through the Logger. * diff --git a/ApnsPHP/Message.php b/ApnsPHP/Message.php index e0ce3a56..3a7f5395 100644 --- a/ApnsPHP/Message.php +++ b/ApnsPHP/Message.php @@ -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. * @@ -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; + } } diff --git a/ApnsPHP/Push.php b/ApnsPHP/Push.php index 6bf50543..c85ba138 100644 --- a/ApnsPHP/Push.php +++ b/ApnsPHP/Push.php @@ -52,6 +52,19 @@ 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( @@ -59,6 +72,11 @@ class ApnsPHP_Push extends ApnsPHP_Abstract '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. */ @@ -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; } } @@ -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); @@ -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 { @@ -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. * diff --git a/Objective-C Demo/Classes/DemoAppDelegate.m b/Objective-C Demo/Classes/DemoAppDelegate.m index 3a8977ea..6aca91d6 100644 --- a/Objective-C Demo/Classes/DemoAppDelegate.m +++ b/Objective-C Demo/Classes/DemoAppDelegate.m @@ -40,6 +40,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( bundle:nil]; self.window.rootViewController = self.viewController; + NSLog(@"Started."); + return YES; } diff --git a/Objective-C Demo/Demo.xcodeproj/project.pbxproj b/Objective-C Demo/Demo.xcodeproj/project.pbxproj index ec8e9dfb..bebc77b3 100755 --- a/Objective-C Demo/Demo.xcodeproj/project.pbxproj +++ b/Objective-C Demo/Demo.xcodeproj/project.pbxproj @@ -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"; @@ -222,14 +227,17 @@ 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; }; @@ -237,12 +245,15 @@ 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; diff --git a/sample_push.php b/sample_push.php index 06b7cd09..dd0c9917 100644 --- a/sample_push.php +++ b/sample_push.php @@ -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. diff --git a/sample_push_http.php b/sample_push_http.php new file mode 100644 index 00000000..aea79482 --- /dev/null +++ b/sample_push_http.php @@ -0,0 +1,88 @@ +setWriteInterval(0); + +// Set the Provider Certificate passphrase +// $push->setProviderCertificatePassphrase('test'); + +// Connect to the Apple Push Notification Service +$push->connect(); + +// Instantiate a new Message with a single recipient +$message = new ApnsPHP_Message('19e4d2cb683e6302ff688b0fe9b6f562c40ea5a31a10d593f82b6d6bf1c88678'); + +// Set the topic of the remote notification (the bundle ID for your app) +$message->setTopic('com.armiento.test'); + +// Set a custom identifier. To get back this identifier use the getCustomIdentifier() method +// over a ApnsPHP_Message object retrieved with the getErrors() message. +$message->setCustomIdentifier("Message-Badge-3"); + +// Set badge icon to "3" +$message->setBadge(3); + +// Set a simple welcome text +$message->setText('Hello APNs-enabled device!'); + +// Play the default sound +$message->setSound(); + +// Set a custom property +$message->setCustomProperty('acme2', array('bang', 'whiz')); + +// Set another custom property +$message->setCustomProperty('acme3', array('bing', 'bong')); + +// Set the expiry value to 30 seconds +$message->setExpiry(30); + +// Add the message to the message queue +$push->add($message); + +// Send all messages in the message queue +$push->send(); + +// Disconnect from the Apple Push Notification Service +$push->disconnect(); + +// Examine the error message container +$aErrorQueue = $push->getErrors(); +if (!empty($aErrorQueue)) { + var_dump($aErrorQueue); +}