Showing posts with label Push Notification Service. Show all posts
Showing posts with label Push Notification Service. Show all posts

Wednesday, April 15, 2009

APNS : Pushing tweets to iPhone

see here :
http://arstechnica.com/apple/guides/2009/04/pushing-tweets-to-your-iphone-with-apple-push-notifications.ars

You can modify it to get the RSS feed as well. see code sample here

pushtweet.m Select all

while (1 > 0)
{

TreeNode *root = [[XMLParser sharedInstance] parseXMLFromURL: [NSURL URLWithString:URL_STRING]];
TreeNode *found = nil;
for (TreeNode *node in [root children])
{
if (![[node key] isEqualToString:@"channel"]) continue;
if ([[node key] isEqualToString:@"channel"])
{
found = nil;
for (TreeNode *node2 in [node children]) {
// [node2 dump];
if ([[node2 key] isEqualToString:@"item"]) {
found = node2;
break;
}
}
if (found) break;
}
}

if (found)
{
NSString *testString = [NSString stringWithFormat:@"%@:%@", [found leafForKey:@"title"], [found leafForKey:@"link"]];
NSString *prevString = [NSString stringWithContentsOfFile:TWEET_FILE encoding:NSUTF8StringEncoding error:nil];
if (![prevString isEqualToString:testString])
{
// Update with the new tweet information
NSLog(@"\nNew RSS title from %@:\n \"%@\"\n\"%@\"\n", [found leafForKey:@"title"], [[found leafForKey:@"description"] substringToIndex:30], [found leafForKey:@"link"]);

// Save the unmessed tweet to the ~/.tweet file
[testString writeToFile:TWEET_FILE atomically:YES encoding:NSUTF8StringEncoding error:nil];

// handle reserved stuff. There's got to be a better way to escape
testString = [testString stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
testString = [testString stringByReplacingOccurrencesOfString:@"'" withString:@""];
testString = [testString stringByReplacingOccurrencesOfString:@":" withString:@"-"];
testString = [testString stringByReplacingOccurrencesOfString:@"{" withString:@"("];
testString = [testString stringByReplacingOccurrencesOfString:@"}" withString:@")"];

// push it
system([PUSH_CMD UTF8String]);
}
}

[NSThread sleepForTimeInterval:(double) delay];
if (SHOW_TICK) printf("tick\n");
}


There is also a Mac Xcode project sample on how to push from Desktop App here
http://stefan.hafeneger.name/download/PushMeBabySource.zip
 
 
 

Monday, April 13, 2009

APNS : How to generate JSON payload in C

For the communication program with APNS, you have many implementation choices, either php, perl, python, ruby or even C#.

You may wonder why the sample of raw interface given by Apple is a C function.
static bool sendPayload(SSL *sslPtr, char *deviceTokenBinary, char *payloadBuff, size_t payloadLength)

Would there be many implementations that use C /C++ or Objective C ? I guess the number will be increasing if the developer is looking for scalability and performance.

If you would like to implement it in C variant, you need to implement a raw TLS/SSL socket program (and may be with threading) and the handling of JSON payload. One of the possibilities is to use open source JSON C Library, but I think it is too heavy to use it here as the communication program needs to construct the Payload message only. So I write this function (genPayloadData ) to generate the payload message for the APNS.

This source code includes the JSON escape string function and the C structure to generate the APNS Payload.

apns.c Select all

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <stdbool.h>

#define Debug 1
#define logerror dprintf
#define dprintf if (Debug) printf

#define DEVICE_BINARY_SIZE 32
#define MAXPAYLOAD_SIZE 256

typedef struct
{
char* alert;
int badge;
char* sound;
char* name_key[4]; //custom key
char* str_value[4]; // custom key value
int int_value[4]; // custom key int
char* action_loc_key; // for custom button label
char* loc_key; // formatted localized string
char* loc_args[4]; // formatted localized string arguments
} PayloadData;

/* string escaping */
const char *json_number_chars = "0123456789.+-eE";
const char *json_hex_chars = "0123456789abcdef";

char *json_escape_str(char *str)
{
char *results = (char*)malloc(200);
char *resultsPt = results;
int pos = 0, start_offset = 0;
unsigned char c;
do {
c = str[pos];
switch(c) {
case '\0':
break;
case '\b':
case '\n':
case '\r':
case '\t':
case '"':
case '\\':
case '/':
if(pos - start_offset > 0)
{memcpy(resultsPt, str + start_offset, pos - start_offset); resultsPt+=pos - start_offset;}
if(c == '\b') {memcpy(resultsPt, "\\b", 2); resultsPt+=2;}
else if(c == '\n') {memcpy(resultsPt, "\\n", 2); resultsPt+=2;}
else if(c == '\r') {memcpy(resultsPt, "\\r", 2); resultsPt+=2;}
else if(c == '\t') {memcpy(resultsPt, "\\t", 2); resultsPt+=2;}
else if(c == '"') {memcpy(resultsPt, "\\\"", 2); resultsPt+=2;}
else if(c == '\\') {memcpy(resultsPt, "\\\\", 2); resultsPt+=2;}
else if(c == '/') {memcpy(resultsPt, "\\/", 2); resultsPt+=2;}
start_offset = ++pos;
break;
default:
if(c < ' ') {
if(pos - start_offset > 0)
{memcpy(resultsPt, str + start_offset, pos - start_offset); resultsPt+=pos-start_offset;}
sprintf(resultsPt, "\\u00%c%c",
json_hex_chars[c >> 4],
json_hex_chars[c & 0xf]);
start_offset = ++pos;
} else pos++;
}
} while(c);
if(pos - start_offset > 0)
{memcpy(resultsPt, str + start_offset, pos - start_offset); resultsPt+=pos-start_offset;}
memcpy(resultsPt, "\0", 1);
dprintf("results:%s\n",results);
return results;
return 0;
}



#define APSHEAD "{\"aps\":{"
#define APSTAIL "}"


void genPayloadData(PayloadData myPaylodData, char *msgbuf) {
char *alert = (char*)malloc(200);
char *message = (char*)malloc(200);
char *sound = (char*)malloc(20);
char *badge = (char *)malloc(20);
char *msgbufPt = msgbuf;
bool isAlert=false, isBadge=false, isSound=false;
bool isToken=false;
int len;
int i;

if (myPaylodData.alert) {
if (myPaylodData.action_loc_key) {
sprintf(alert, "\"alert\":{\"body\":\"%s\",\"action-loc-key\":\"%s\"}",json_escape_str(myPaylodData.alert),json_escape_str(myPaylodData.action_loc_key));
}
else {
sprintf(alert, "\"alert\":\"%s\"",json_escape_str(myPaylodData.alert));
}
isAlert = true;
} else if (myPaylodData.loc_key) {
isToken=false;
sprintf(alert, "\"alert\":{\"loc-key\":\"%s\"",json_escape_str(myPaylodData.loc_key));
for (i=0 ; i < 4; i++) {
if (myPaylodData.loc_args[i]) {
if (isToken) {
sprintf(alert, "%s,\"%s\"",alert,json_escape_str(myPaylodData.loc_args[i]));
}
else {
sprintf(alert, "%s,\"loc-args\":[\"%s\"",alert,json_escape_str(myPaylodData.loc_args[i]));
}
isToken=true;
}
}
if (isToken) {
sprintf(alert, "%s]}",alert);
}
isAlert = true;
}
if (myPaylodData.badge > 0) {
sprintf(badge, "\"badge\":%d",myPaylodData.badge);
isBadge = true;
}
if (myPaylodData.sound) {
sprintf(sound, "\"sound\":\"%s\"",myPaylodData.sound);
isSound = true;
}
if (isAlert | isBadge | isSound) {
len = strlen(APSHEAD);
memcpy(msgbufPt, APSHEAD, len);
msgbufPt += len;
}
else {
memcpy(msgbufPt, "{", 1);
msgbufPt += 1;
}
if (isAlert) {
len = strlen(alert);
memcpy(msgbufPt, alert, len);
msgbufPt += len;
}
if (isBadge) {
if (isAlert) {
memcpy(msgbufPt++, ",", 1);
}
len = strlen(badge);
memcpy(msgbufPt, badge, len);
msgbufPt += len;
}
if (isSound) {
if (isAlert | isBadge) {
memcpy(msgbufPt++, ",", 1);
}
len = strlen(sound);
memcpy(msgbufPt, sound, len);
msgbufPt += len;
}
if (isAlert | isBadge | isSound) {
len = strlen(APSTAIL);
memcpy(msgbufPt, APSTAIL, len);
msgbufPt += len;
isToken = true;
}
isToken=false;
if (msgbufPt-msgbuf < MAXPAYLOAD_SIZE) {
for (i=0; i < 4; i++) {
if (myPaylodData.name_key[i]) {
if (myPaylodData.str_value[i]) {
sprintf(message, "%s\"%s\":\"%s\"",(isToken?",":""),json_escape_str(myPaylodData.name_key[i]),json_escape_str(myPaylodData.str_value[i]));
len = strlen(message);
if (msgbufPt-msgbuf+len<MAXPAYLOAD_SIZE) {
memcpy(msgbufPt, message, len);
msgbufPt += len;
isToken = true;
}
else {
dprintf("\n!!!!Warnings: Total Payload message overlimit (>%d) when processing %s",MAXPAYLOAD_SIZE,message);
}
}
if (myPaylodData.int_value[i]) {
sprintf(message, "%s\"%s\":%d",(isToken?",":""),json_escape_str(myPaylodData.name_key[i]),myPaylodData.int_value[i]);
len = strlen(message);
if (msgbufPt-msgbuf+len<MAXPAYLOAD_SIZE) {
memcpy(msgbufPt, message, len);
msgbufPt += len;
isToken = true;
}
else {
dprintf("\n!!!!Warnings: Total Payload message overlimit (>%d) when processing %s",MAXPAYLOAD_SIZE,message);
}
}
}
}
}
len = strlen(APSTAIL);
memcpy(msgbufPt, APSTAIL, len);
msgbufPt += len;
memcpy(msgbufPt, "\0", 1);
dprintf("\nconstructed message:%s\n",msgbuf);
if (strlen(msgbuf) > MAXPAYLOAD_SIZE) {
dprintf("\n!!!!Warnings: Payload (>%d) : %d\n",MAXPAYLOAD_SIZE,(unsigned)strlen(msgbuf));
}
else {
dprintf("\nPayload (<=%d) size : %d\n",MAXPAYLOAD_SIZE,(unsigned)strlen(msgbuf));
}
free(alert);
free(badge);
free(sound);
free(message);
}

int main(){
char msgbuf[512]; /* payload messages */

PayloadData myPaylodData = {0};
myPaylodData.badge = 3;
myPaylodData.alert = "Message from javacom";
// myPaylodData.action_loc_key = "";
// myPaylodData.loc_args[0] = "";
// myPaylodData.loc_args[1] = "";
// myPaylodData.loc_args[2] = "";
myPaylodData.sound = "received3.caf";
myPaylodData.name_key[0] = "test1";
myPaylodData.str_value[0] = "Hello iPhone";
genPayloadData(myPaylodData,msgbuf);
printf("%s\n",msgbuf);
return 0;

}


 
 
 

Friday, April 10, 2009

APNS Client Development Certificate Available Now



(1) You need to create an App ID without .* in the Program Portal (that means one cert for one app)

(2) Generate a certificate signing request from your Mac's keychain and save to disk

(3) Upload the CertificateSigningRequest.certSigningRequest to the Program Portal

(4) Wait for the generation of cert (about 1 min). Download the certificate (aps_developer_identity.cer) from the Program Portal (If you need to renew this cert, it is under the App ID that you created in step 1, and choose Action Configure)

(5) Keep (or rename them if you prefer) these 2 files (steps 2 and 4) in a safe place. You might need the CertificateSigningRequest.certSigningRequest file to request a new cert for a new app in the future or renew the old cert when expired.

(6) Suppose you have imported the aps_developer_identity.cer to the keychain. Then you have to export these new cert and the private key of this cert (not the public key) and saved as .p12 files.

(7) Then you use these commands to generate the cert and key in Mac's Terminal for PEM format (Privacy Enhanced Mail Security Certificate)

openssl pkcs12 -clcerts -nokeys -out cert.pem -in cert.p12
openssl pkcs12 -nocerts -out key.pem -in key.p12


(8) The cert.pem and key.pem files will be used by your own php script communicating with APNS.

(9) If you want to remove the passphase of private key in key.pem, do this

openssl rsa -in key.pem -out key.unencrypted.pem


Then combine the certificate and key

cat cert.pem key.unencrypted.pem > ck.pem


But please set the file permission of this unencrypted key by using chmod 400 and is only readable by root in a sever configuration.

(10) The testing APNS is at ssl://gateway.sandbox.push.apple.com:2195

(11) For the source codes to push payload message to the APNS, you can find them in the Developer Forum. This is the one that I used, for php script. Run this (after obtaining the device token from the testing device and with iPhone Client program setup)
php -f apns.php "My Message" 2

or if you put this php script and the ck.pem in a local web server, you can use this to test
http://127.0.0.1/apns/apns.php?message=test%20from%20javacom&badge=2&sound=received5.caf

Please be patient to get message from the sandbox server. Normally, you need 10 minutes+ to get the first message from push notification testing.

apns.php Select all

#!/usr/bin/env php
<?php
$deviceToken = '02da851dXXXXXXXXb4f2b5bfXXXXXXXXce198270XXXXXXXX0d3dac72bc87cd60'; // masked for security reason
// Passphrase for the private key (ck.pem file)
// $pass = '';

// Get the parameters from http get or from command line
$message = $_GET['message'] or $message = $argv[1] or $message = 'Message received from javacom';
$badge = (int)$_GET['badge'] or $badge = (int)$argv[2];
$sound = $_GET['sound'] or $sound = $argv[3];

// Construct the notification payload
$body = array();
$body['aps'] = array('alert' => $message);
if ($badge)
$body['aps']['badge'] = $badge;
if ($sound)
$body['aps']['sound'] = $sound;


/* End of Configurable Items */

$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'local_cert', 'ck.pem');
// assume the private key passphase was removed.
// stream_context_set_option($ctx, 'ssl', 'passphrase', $pass);

$fp = stream_socket_client('ssl://gateway.sandbox.push.apple.com:2195', $err, $errstr, 60, STREAM_CLIENT_CONNECT, $ctx);
// for production change the server to ssl://gateway.push.apple.com:2195
if (!$fp) {
print "Failed to connect $err $errstr\n";
return;
}
else {
print "Connection OK\n";
}

$payload = json_encode($body);
$msg = chr(0) . pack("n",32) . pack('H*', str_replace(' ', '', $deviceToken)) . pack("n",strlen($payload)) . $payload;
print "sending message :" . $payload . "\n";
fwrite($fp, $msg);
fclose($fp);
?>


(12) For iPhone Client Program, you need to edit the bundle identifier to the App ID that you created and imported the new provisioning profile for that APP ID to the XCode and iPhone. And codesign with that new provisioning profile. Then implement the following methods in AppDelegate to Build & Go

AppDelegate.m Select all

- (void)applicationDidFinishLaunching:(UIApplication *)application {
NSLog(@"Registering Remote Notications");

[[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)];

// Override point for customization after app launch
[window addSubview:viewController.view];
[window makeKeyAndVisible];
}


// Delegation methods
- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
const void *devTokenBytes = [devToken bytes];
// self.registered = YES;
NSLog(@"deviceToken: %@", devToken);
// [self sendProviderDeviceToken:devTokenBytes]; // custom method
}

- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
NSLog(@"Error in registration. Error: %@", err);
}



(13) Additional tips for sandbox testing
- The feedback service is feedback.sandbox.push.apple.com:2195
- Send your messages to gateway.sandbox.push.apple.com:2195



Here is the feedback server request php code. For the sandbox feedback server, you have to create a second dummy app to make the first one works. May be the sandbox feedback server is buggy as the production push and feedback servers does not have this problem.

php -f feedback.php

feedback.php Select all

#!/usr/bin/env php
<?php

$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'local_cert', 'ck.pem');
stream_context_set_option($ctx, 'ssl', 'verify_peer', false);
// assume the private key passphase was removed.
// stream_context_set_option($ctx, 'ssl', 'passphrase', $pass);


$fp = stream_socket_client('ssl://feedback.sandbox.push.apple.com:2196', $error, $errorString, 60, STREAM_CLIENT_CONNECT, $ctx);
// production server is ssl://feedback.push.apple.com:2196

if (!$fp) {
print "Failed to connect feedback server: $err $errstr\n";
return;
}
else {
print "Connection to feedback server OK\n";
}

print "APNS feedback results\n";
while ($devcon = fread($fp, 38))
{
$arr = unpack("H*", $devcon);
$rawhex = trim(implode("", $arr));
$feedbackTime = hexdec(substr($rawhex, 0, 8));
$feedbackDate = date('Y-m-d H:i', $feedbackTime);
$feedbackLen = hexdec(substr($rawhex, 8, 4));
$feedbackDeviceToken = substr($rawhex, 12, 64);
print "TIMESTAMP:" . $feedbackDate . "\n";
print "DEVICE ID:" . $feedbackDeviceToken. "\n\n";
}
fclose($fp);
?>



Hints: If you want to test the production push and feedback servers, use the adhoc distribution certificate and adhoc build to your devices. Moreover, the device tokens are different for the same device under sandbox and production servers.





Monday, March 23, 2009

Apple Push Notification Service (APNS)

The Apple Push Notification Service will be launched in this summer and this is one of interesting features of iPhone OS 3.0.

Since the SSL certificate for the APNS for the developer's app is not yet available (update: available now on April 9) in the developer program portal, we need to simulate and test it in our environment at this moment. To prepare for this, we need a client program and a server program (for testing with the client) implementing the TLS communication protocol.

(1) Requirements
The requirement of communication with Apple Push Notification Service (APNS) is described in the "Apple Push Notification Service Programming Guide" and for the developer's view, we need to have a client program to communicate with and send the payload to the APNS

- a "raw" (binary) socket interface which is asynchronous and one-way from developer to APNS
- The binary interface is available through gateway.push.apple.com, port 2195
- use TLS (or SSL) to establish a secured communications channel
- The SSL certificate (will be provided from Apple per Application ID from developer portal) is required for these connections
- should present this certificate to APNS at connection time using peer-to-peer authentication.
- should also retain connections with APNS across multiple notifications.

(2) Payload
The maximum size allowed for a notification payload is 256 bytes;
Example of the notification payload

Command : 1 byte (0x00)
Device Token length : 2 bytes (0x00 0x20)
Device Token(binary) : 32 bytes
Payload length : 2 bytes
Payload Content (max 256 bytes): e.g.

{
"aps" : {
"alert" : {"loc-key":"%@ and %@ have invited you to Dinner","loc-args":["Jenna","Frank"]},
"badge" : 9
"sound" : "happysound.aiff"
},
"acme1" : "custom property",
"acme2" : 42,
"acme3" : [5,8]
}


The following example shows the payload message of
{"aps":{"alert":"Message from javacom","badge":9}}
in HexEdit Screen




#define DEVICE_BINARY_SIZE 32
#define MAXPAYLOAD_SIZE 256


(3) Pseudocode for this client side program (My iniitial draft for C Code)


Client code for TLS v1 protocol
Needs openssl crypto lib

At first
Initialisation of TLS Context and return SSL Context
SSL_CTX *ctx;
SSL *ssl;

SSL_CTX object is created as a framework to establish TLS/SSL enabled connections
use SSL_CTX_new to create a new context
SSL_CTX should include the tls stuff for SSL certificate handling
use SSL_CTX_set_client_cert_cb(ctx, client_cert_cb) to provide client SSL cert.

socket is created for the APNS
int sock;
struct sockaddr_in server;

connect socket
connect(sock, (struct sockaddr *)&server, sizeof(server))

Then associate the network connection with the object

create a new SSL structure for a connection
SSL_new(ctx)))

connect the SSL object with a socket file descriptor
SSL_set_app_data(ssl, conn);
SSL_set_fd(ssl, sock)

Set ssl to work in client mode (that is to connect)
SSL_set_connect_state(ssl);

Then the TLS/SSL handshake is performed
Connect SSL
SSL_connect
If connect successful
Compose Payload
sendPayload using SSL_write (this part was given by Apple)

If connect unsuccessful, handle error

If Payload sent or reach maximum retries
Then Shut down the TLS/SSL connection.
SSL_shutdown(ssl);
SSL_free(ssl);
Destroy SSL Context
SSL_CTX_free(ctx); ctx = NULL
close(sock);




(4) Other aspects (TO-DOs)
- Keep the definitive count of data items remaining to be downloaded.
- Regularly connect with the feedback web server and fetch the current list of those devices that have repeatedly reported failed-delivery attempts.
- iPhone client program

see update here
http://iphonesdkdev.blogspot.com/2009/04/apns-client-development-certificate.html
.
.
.