In order to send a push notification to an iOS device we must contact Apple servers and delegate them to do the work (go to this page to find more details on the entire process).
Some months ago Apple published a new protocol that we can use to contact its servers. The previous protocol (the one we used for years) had in fact some problems. In particular after sending a push, we had to wait at least 1 second for a (possible) Apple response. This is ok if we must send a limited number of pushes, but it is not acceptable when we need to send thousands of that (to send a push to 10.000 users we would wait almost 3 hours, spent almost entirely … waiting and idling!) .
The new way to communicate with Apple servers is (finally) fast and efficient and it uses the HTTP/2 protocol.
In this post I assume that you have already prepared your app to receive Push Notification. There are many tutorials that explain this process and I will not repeat them (this seems to me a good one).
Requirements
Before writing our code we need some libraries installed on the system.
The library we will use to send data to the network is curl (the library name is libcurl). The minimum version of libcurl supporting HTTP2 is 7.38.0 and must be compiled with the flag –with-nghttp2.
To see the version of your curl library open a terminal and digit the command:
1 2 3 4 |
> curl -V curl 7.47.1 (x86_64-apple-darwin14.5.0) libcurl/7.47.1 OpenSSL/1.0.2g zlib/1.2.5 nghttp2/1.7.0 Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp Features: IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets |
(this is the output on my machine OSX 10.11.4).
Note that curl must be compiled with openssl version >=1.0.2 to fully support http/2, otherwise you will get the error: “?@@?HTTP/2 client preface string missing or corrupt…“.
The process of install the correct libcurl version is not straightforward. On MacOSX I found very useful the Homebrew tool.
Sending the push from the terminal
A test to check the installed software (before entering the PHP part) is to try to send a push from the terminal.
The command is:
1 |
> curl -d '{"aps":{"alert":"[MESSAGE]","sound":"default"}}' --cert "[PATH TO APS CERTIFICATE.pem]":"" -H "apns-topic: [BUNDLE IDENTIFIER]" --http2 https://api.development.push.apple.com/3/device/[TOKEN] |
For example this command sends a push notification with message “Hi!” to my SamplePush app (bundle “it.tabasoft.samplepush”) to my device (token “dbdaearrea6aaaaww61859fb4rr074c1c388eftt348987447”).
1 |
> curl -d '{"aps":{"alert":"Hi!","sound":"default"}}' --cert "~/valfer/certificates/samplepush/development.pem":"" -H "apns-topic: it.tabasoft.samplepush" --http2 https://api.development.push.apple.com/3/device/dbdaeae86abcdea6e7d0a61859fb41d2c7b2cbf074c1c389ef8053ec48987847 |
I f the test is successfull the command prints nothing. If it prints something … the debug begins.
For example this is the response if I mistype the path of the certificate:
1 |
curl: (58) could not load PEM client certificate, OpenSSL error error:02001002:system library:fopen:No such file or directory, (no key found, wrong pass phrase, or wrong file format?) |
The PHP code
We need a version of PHP >= 5.5.24 that uses the correct version of libcurl.
I installed php 7 with Homebrew with the command:
1 |
brew install php70 --with-homebrew-curl |
We can verify the correct version of curl typing in the terminal the command
1 |
php -i > phpinfo.txt |
that will create a file “phpinfo.txt” in the current directory containing some php infos. Open the file and verify the lines:
1 2 3 4 5 6 7 8 9 10 |
... curl cURL support => enabled cURL Information => 7.47.1 ... HTTP2 => Yes ... SSL Version => OpenSSL/1.0.2g ... |
OK!
So the following is the PHP function that sends the push notification:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/** * @param $http2ch the curl connection * @param $http2_server the Apple server url * @param $apple_cert the path to the certificate * @param $app_bundle_id the app bundle id * @param $message the payload to send (JSON) * @param $token the token of the device * @return mixed the status code */ function sendHTTP2Push($http2ch, $http2_server, $apple_cert, $app_bundle_id, $message, $token) { // url (endpoint) $url = "{$http2_server}/3/device/{$token}"; // certificate $cert = realpath($apple_cert); // headers $headers = array( "apns-topic: {$app_bundle_id}", "User-Agent: My Sender" ); // other curl options curl_setopt_array($http2ch, array( CURLOPT_URL => $url, CURLOPT_PORT => 443, CURLOPT_HTTPHEADER => $headers, CURLOPT_POST => TRUE, CURLOPT_POSTFIELDS => $message, CURLOPT_RETURNTRANSFER => TRUE, CURLOPT_TIMEOUT => 30, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSLCERT => $cert, CURLOPT_HEADER => 1 )); // go... $result = curl_exec($http2ch); if ($result === FALSE) { throw new Exception("Curl failed: " . curl_error($http2ch)); } // get response $status = curl_getinfo($http2ch, CURLINFO_HTTP_CODE); return $status; } |
Possible codes returned from Apple are:
1 2 3 4 5 6 7 8 9 |
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. |
Here are listed all the codes together with the descriptions (reason). In case of error the variable $result in the previous php code contains more details.
We call the function with the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// this is only needed with php prior to 5.5.24 if (!defined('CURL_HTTP_VERSION_2_0')) { define('CURL_HTTP_VERSION_2_0', 3); } // open connection $http2ch = curl_init(); curl_setopt($http2ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0); // send push $apple_cert = '/certificates/samplepush/development.pem'; $message = '{"aps":{"alert":"Hi!","sound":"default"}}'; $token = 'dbdaeae86abcde56rtyww1859fb41b2cby053ec48987847'; $http2_server = 'https://api.development.push.apple.com'; // or 'api.push.apple.com' if production $app_bundle_id = 'it.tabasoft.samplepush'; $status = sendHTTP2Push($http2ch, $http2_server, $apple_cert, $app_bundle_id, $message, $token); echo "Response from apple -> {$status}\n"; // close connection curl_close($http2ch); |
The server Apple to connect to depends on the version of the app (debug or production).
Note that we open the connection, send the push and close it. But when you send many pushes, be sure to open and close the connection only one time, as in:
1 2 3 4 5 6 7 8 |
// open connection // send all the pushes for ($i = 0; $i < $totSends; $i++) { // send push } // close connection |
This is important for two reasons:
- the open and close are time consuming, calling them every time will heavily slow our code
- we will get our IP banned if Apple considers this as a DOS attack
On my Mac (and a not so good network at this time: ~6Mb download, ~0.7Mb upload) my sendHTTP2Push takes an average 120ms to complete.
You can find here the code of this post here. (remember to set the parameters to that of your app).
Feel free to experiment the sendHTTP2Push code and let me know if all is clear (and working).
In the next post we will refactor the code and use Composer and Symfony/Console to build a command line tool.
I have my server all set up correctly and I have tested this code without the “for” loop which sends one request and it works just fine and I receive it on my device. But, when I try to send multiple through the same connection I get an error about an unknown HTTP2 protocol. Has anything changed with the newer version of CURL that causes multiple requests during the same connection to throw an error?
Does this code still work for you?
I just tried your code directly and it will finish the first loop and send the message, but when it tries to send the second message, I get this error:
PHP Fatal error: Uncaught exception ‘Exception’ with message ‘Curl failed with error: Unknown SSL protocol error in connection to api.development.push.apple.com:443
Hi Carl, yes my code still works.
Which versions exactly are you running?
To help getting the exact version of CURL/PHP check my new post “Which CURL version is PHP running?”: http://coding.tabasoft.it/php/which-curl-version-is-php-running/
Valerio, thank you very much for answering. I have also posted a detailed version of the problem with version information, sample code and curl verbose output on stack overflow at http://stackoverflow.com/questions/36611384/apns-provider-api-http-2-using-php-curl-causes-error-on-multiple-push-notificat
I will try to look into your suggested link and see if I can figure out the problem. I am running the php script from the command line (not from apache) and it does send the first message successfully, but when it tries the second one, it fails.
Also to add from phpinfo()
PHP Version => 7.0.5-2+deb.sury.org~wily+1
cURL support => enabled
cURL Information => 7.48.0
HTTP2 => Yes
Hello there! Thanks for your post.
I am trying to get my push notifications working but It’s seems that I am doing something wrong:
when I try to execute command line like this:
/usr/local/Cellar/curl/7.49.0/bin/curl -d ‘{“aps”:{“alert”:”[MESSAGE]”,”sound”:”default”}}’ –cert “/path/to/file.pem”:”” -H “apns-topic: [bundle.id]” –http2 https://api.development.push.apple.com/3/device/bdd07cc28c5c088cf5279200ef060c0a10397b4fc7b4ece1c4da99c67e82e12a
I obtain {“reason”:”BadDeviceToken”}.
I don’t know what the problem is beacuse de deviceToken it’s seems to be right.
Any ideas?
Thanks!
I’ve change my environment and now the device token seems to be right.
Problem with APNs over HTTP/2
I’ve used the phonegap-push-plugin with phonegap bulid cli 5.4.1
In Android seems to be alright and the push notifications are delivered sucessfully to the devices.
In iOS, I am trying to send push notification from commad line with this order:
curl -d ‘{“aps”:{“alert”:”Hi”,”sound”:”default”}}’ –cert “mycert.pem”:”” -H “apns-topic: es.mypush.example” –http2 https://api.push.apple.com/3/device/bdd07cc28c5c088cf5279200ef060c0a10397b4fc7b4ece1c4da99c67e82e12a
But I am always receiving this error:
{“reason”:”DeviceTokenNotForTopic”}
In my config.xml I’ve used this bundle id <widget id="es.mypush.example" …
with this command from the command line:
Not sure which is the topic is registering the device in apns service, I suposed that the phonegap cli was sending the widget id, but it does not seems that.
Can you help me please?
Best Regards
Hi Víctor,
I really don’t know the phonegap-push-plugin, but, as you can already see, it seems that token is not for the app ‘es.mypush.example’.
Thanks Valerio for your reply.
I am not sure what’s is wrong, it’s obvius that the device token is not registered with the bundle.id
However, this is de log of the curl command:
$ /usr/local/bin/curl -v -d ‘{“aps”:{“alert”:”Test Push”,”sound”:”default”}}’ \
> –cert /isotoolsmobiledev.pem:ClavePushAPN \
> -H “apns-topic: org.isotools.mobile” –http2 \
> https://api.development.push.apple.com/3/device/af5cdca3a1ed695b575bfa0d412933e566f81b2ee658ff1417195a37a8bb2425
* Trying 17.172.238.203…
* Connected to api.development.push.apple.com (17.172.238.203) port 443 (#0)
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS handshake, CERT verify (15):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* Server certificate:
* subject: CN=api.development.push.apple.com; OU=management:idms.group.533599; O=Apple Inc.; ST=California; C=US
* start date: Jun 19 01:49:43 2015 GMT
* expire date: Jul 18 01:49:43 2017 GMT
* subjectAltName: host “api.development.push.apple.com” matched cert’s “api.development.push.apple.com”
* issuer: CN=Apple IST CA 2 – G1; OU=Certification Authority; O=Apple Inc.; C=US
* SSL certificate verify ok.
> POST /3/device/af5cdca3a1ed695b575bfa0d412933e566f81b2ee658ff1417195a37a8bb2425 HTTP/1.1
> Host: api.development.push.apple.com
> User-Agent: curl/7.49.0
> Accept: */*
> apns-topic: org.isotools.mobile
> Content-Length: 47
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 47 out of 47 bytes
* TLSv1.2 (IN), TLS alert, Client hello (1):
* Connection #0 to host api.development.push.apple.com left intact
▒@@▒HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 504f5354202f332f6465766963652f616635636463613361[vtellez@localhost ~]$ PuTTY
The text in the log “?@@?HTTP/2 client preface string missing or corrupt…” should not appear. See the paragraph “Requirements” of my article.
hello I have got the same response from aspn, it’s {“reason”:”BadDeviceToken”}, how can I get the suitable device token?
Hi
Thanks for your blog, it is so difficult to find post talking about push notification for iOS.
I have sent with success push from terminal with this command :curl -v -d ‘{“aps”:{“alert”:”hi”,”sound”:”default”}}’ –cert “myKey.pem” -H “apns-topic:com.my-Topic” –http2 https://api.development.push.apple.com/3/device/6378ab97095297b832cfdcb269b48u8b671bc9d81c3f43df5b5f38dbdafecda8
but when i want tu use you php model, i received always :
Response from apple -> 0
and no push sent.
please can you tell me why iin command line it is sent with success but not with php ?
thanks for hekl and sorry for my poor englsih.
Sandy
i have put some echo, and here what i have received :
�@@�HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 504f5354202f332f6465766963652f363334396162393730status0Response from apple -> 0
See “Requirements” paragraph
Are you able to fix this problem ?
I am also getting same error message (?@@?HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 504f5354202f332f6465766963652f393762353130633536bool(true))
me too
all requirements were done but your php code did not work for me.
for help some others with the same issue, here the simple short php code that worked fine in my case :
function sendPushiOS($tAlert,$tToken){
$device_token = $tToken;
if(defined(‘CURL_HTTP_VERSION_2_0’))
{
$pem_file = ‘myKey.pem’;
$apns_topic = ‘com.myTopic’;
$sample_alert = ‘{“aps”:{“alert”:”‘.$tAlert.'”,”sound”:”default”}}’;
$url = “https://api.development.push.apple.com/3/device/$device_token” ;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POSTFIELDS, $sample_alert);
curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(“apns-topic: $apns_topic”));
curl_setopt($ch, CURLOPT_SSLCERT, $pem_file);
$response = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
//var_dump($response);
}
}
hope this helps.
thanks
Sandy
Valerio Ferrucci ,
When i run the below command in terminal i am receiving the push notification .
curl -d ‘{“aps”:{“alert”:”Hi!”,”sound”:”default”}}’ –cert ck.pem:”” -H “apns-topic: com.SMIT.RealEstate” –http2 https://api.development.push.apple.com/3/device/97b510c565e746a7d1fc120e6a4ef1b7f29e136a01414544a4f9628dd9e09c69
When i try to execute the Php code I am getting -?@@?HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 504f5354202f332f6465766963652f393762353130633536bool(true)
According to your paragraph I installed the curl –
curl 7.49.1 (x86_64-apple-darwin15.5.0) libcurl/7.49.1 OpenSSL/1.0.2h zlib/1.2.5 nghttp2/1.11.0
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp smb smbs smtp smtps telnet tftp
Features: IPv6 Largefile NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets
But when i check php -i > phpinfo.txt
cURL support enabled
cURL Information 7.43.0
HTTP2 => No.
How do i change native MAC apache. ?
Hi
Thanks for your blog.
I have sent with success push from terminal with this command :curl -v -d ‘{“aps”:{“alert”:”hi”,”sound”:”default”}}’ –cert “ck.pem” -H “apns-topic:com.my-Topic” –http2 https://api.development.push.apple.com/3/device/6378ab97095297b832cfdcb269b48u8b671bc9d81c3f43df5b5f38dbdafecda8
but when i try to use your php model, i received always :
?@@.{“reason”:”BadCertificateEnvironment”Response from apple -> 0
and no push notification sent.
Could you share with me the reason?
Des
dear all,
if you are not able to send push with php code that Valerio gave please try my php code. it will work.
good luck
sandy
Nice article!
I was calling from .net platform using WebClient but it could not create SSL/TLS secure channel.
Any idea on .net platform?
Hi, what’s the purpose of the “User-agent” header? Can I omit that? Thanks.
As of today I don’t know how Apple uses the user agent (probably it ignores it) but I think it is a good practice always to include it when making an http call.
Thank you 😉
Good code, I have update my old code.
I have one answer, after about 4200 token the php server return “500 internal server error”.
This is a big problem, I have a app with more 30000 token active.
Ideas?