Using Apple Push Notifications from Windows Azure

In my MIX11 session last week I demonstrated how to create push notifications to iPhone and iPad devices from Wndows Azure.  I’ve put together this blog post to share more detail and the source code for how this works.

Firstly, if you haven’t already, you will need to register your iPhone/iPad application for push notifications.  To do this, log into the iOS developer center (you’ll need to be a registered Apple Developer) and in the provisioning portal setup a new App ID, enabling it for push notifications.  Here’s the App ID for my MIX demo:

image

With the development certificate that you downloaded during this process, create a new Azure worker role and import the certificate into a folder called “certs”:  In addition, you’ll need to configure the properties of the certificate file such that the build action is set to “Content”.

image

(I’ve deliberately skimmed over the previous points of creating and App ID and Azure Worker role as they are both well documented by Apple and Microsoft).

 

To start configuring the worker role for push notifications, first add a reference to the Windows Azure Storage Client (Microsoft.WindowsAzure.StorageClient) library.  In the OnRun section of the worker role, access the Azure queue that messages are going to be placed in. 

StorageCredentials creds = new StorageCredentialsAccountAndKey("YOUR ACCOUNT NAME", "YOUR ACCOUNT KEY");
  
CloudQueueClient cqc = new CloudQueueClient(""YOUR QUEUE URL”, creds);
var testQueue = cqc.ListQueues().First(q => q.Name.StartsWith("YOUR QUEUE NAME"));

Then, still within the OnRun method, create a routine that checks the queue for incoming messages and sets up the connection to the APN (Apple Push Notification) service.

while (true)
            {
                Thread.Sleep(10000);

                if (testQueue.RetrieveApproximateMessageCount() != 0)
                {

                    List messages = testQueue.GetMessages(testQueue.RetrieveApproximateMessageCount()).ToList();
                    foreach (CloudQueueMessage message in messages)
                    {
                        Trace.WriteLine("Retrieved message from Queue: " + message.AsString);
                        // open the APN connection
                        InitializeAPN();
                        // send message
                        string session = message.AsString.Substring(0, message.AsString.IndexOf(':'));
                        SendAPNMessage(message.AsString, session);
                        // tear down the APN connection
                        CloseAPN();

                        testQueue.DeleteMessage(message);
                    }
                }
            }

You’ll probably want to do something a little more elegant than “while (true)” but this works for the purposes of this post.  Also, as I mentioned in the talk, you may or may not want to setup and tear down the connection to the APN for each message that you send.  If you are planning to send a large volume of messages to a large number of devices, Apple may view this as a denial of service attack and refuse your connection.  A more prescriptive approach in this scenario would be to instead open the connection in the OnStart method and keep it alive during OnRun.

Within the worker role, setup the following declarations.  Most of these should be straightforward, and you’ll need to replace a number of them with your own details. 

private static string HOST = "gateway.sandbox.push.apple.com";
  
private static int PORT = 2195;
private static string CERT_PASSWORD = "YOUR PASSWORD";
private static X509Certificate2 CLIENT_CERT = new X509Certificate2(Environment.GetEnvironmentVariable("RoleRoot") + @"approotcertsmix11_dev_cert.p12", CERT_PASSWORD);

private static X509Certificate2Collection CLIENT_CERT_COLLECTION = new X509Certificate2Collection(CLIENT_CERT);
private static string DEVICE_TOKEN = "YOUR DEVICE TOKEN";  //Replace this with the Device token we obtain later on in this example

private TcpClient client;
private SslStream sslStream;

With these declarations in place, we can now start writing the APN code.  First, create an IntializeAPN method, responsible for setting up the connection to the APN.

private void InitializeAPN()
  
{
    client = new TcpClient(HOST, PORT);
    sslStream = new SslStream(client.GetStream(), false);

    try
    {
        sslStream.AuthenticateAsClient(HOST, CLIENT_CERT_COLLECTION, SslProtocols.Tls, false);
    }
    catch (AuthenticationException ex)
    {
        Trace.WriteLine("Could not open APN connection: " + ex.ToString());
    }
    Trace.WriteLine("APN connection opened successfully.");
}

Then, create a method called SendAPNMessage which will construct and sent the push notification message in the correct format. 

private void SendAPNMessage(string message, string session)
  
        {
            try
            {
                MemoryStream memoryStream = new MemoryStream();
                BinaryWriter binaryWriter = new BinaryWriter(memoryStream);

                // construct the message
                binaryWriter.Write((byte)0); 
                binaryWriter.Write((byte)0); 
                binaryWriter.Write((byte)32);

                // convert to hex and write
                byte[] deviceToken = new byte[DEVICE_TOKEN.Length / 2];
                for (int i = 0; i < deviceToken.Length; i++)
                    deviceToken[i] = byte.Parse(DEVICE_TOKEN.Substring(i * 2, 2), System.Globalization.NumberStyles.HexNumber);
                binaryWriter.Write(deviceToken);

                // construct payload within JSON message framework
                String payload = "{"aps":{"alert":"" + message + "","session":""+session+"","badge":1}}";

                // write payload data
                binaryWriter.Write((byte)0);                 
                binaryWriter.Write((byte)payload.Length);    
                byte[] payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload);
                binaryWriter.Write(payloadBytes);
                binaryWriter.Flush();

                // send across the wire
                byte[] array = memoryStream.ToArray();
                sslStream.Write(array);
                sslStream.Flush();
            }
            catch (Exception ex)
            {
                Trace.WriteLine(ex.ToString());
            }
            Trace.WriteLine("Message successfully sent.");
        }

You’ll notice that my SendAPNMessage method signature contains a “session” value.  For the purposes of the demo, I was sending across the session code that had changed as an explicit value in the notification message.  Feel free to change or remove this as you need.

Finally, the close method is called to close the connection.

private void CloseAPN()
  
{
    client.Close();
}

At this point, you might be wondering how you obtain the DEVICE_TOKEN value for the above.  This is not the UDID of the device, but instead a separate token that is generated by the phone itself.  To get this token, and to handle incoming push notifications, let’s turn our attention to the XCode project.  For my demo I was receiving push notifications within a PhoneGap application, but this code will work equally in a regular native client application.

First, we need to instruct the application to register for APN messages.  This is done using the registerForRemoteNotificationTypes method.  You’ll need to call this method when the application first starts up (for PhoneGap projects, this can be in the init method of the AppDelegate).

NSLog(@"Registering for APN");
  
[[UIApplication sharedApplication] registerForRemoteNotificationTypes: (UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];

This method has three callbacks that it can take advantage of.  One to indicate that registration was successful (we also get the Device ID from here), one to indicate that something went wrong (e.g. if we are running in the simulator, which doesn’t support push notifications), and one for when we actually receive a message).

The first two are easy to handle:

- (void)application:(UIApplication *)app didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
  
   
    NSString *str = [NSString stringWithFormat:@"Device Token=%@",deviceToken];
    NSLog(@"%@",str);
}

- (void)application:(UIApplication *)app didFailToRegisterForRemoteNotificationsWithError:(NSError *)err {
   
    NSString *str = [NSString stringWithFormat: @"Error: %@", err];
    NSLog(@"%@",str);   
}

Note how the first method (didRegisterForRemoteNotificationsWithDeviceToken) is where we actually extract the DEVICE_TOKEN string required in the worker role.  You’ll have to run this once, and copy and paste appropriately.  Of course, in a production environment, we would likely pass this value to the service via a separate call. 

The third callback gets called when the device actually receives a message.

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
  
   
    for (id key in userInfo) {
        NSLog(@"key: %@, value: %@", key, [userInfo objectForKey:key]);
        NSString *payload = [NSString stringWithFormat:@"%@",[userInfo objectForKey:key]];
       
        // work out the session code from the JSON payload
        NSRegularExpression* regex;
        NSTextCheckingResult* result;
        NSError* error = nil;
        NSString* regexStr = @"session = ([^']*);";
        NSString* value = nil;
        regex = [NSRegularExpression regularExpressionWithPattern:regexStr options:NSRegularExpressionCaseInsensitive error:&error];
        result = [regex firstMatchInString:payload options:0 range:NSMakeRange(0, payload.length)];
       
        if(result && [result numberOfRanges] == 2)
        {
            NSRange r = [result rangeAtIndex:1];
            value = [payload substringWithRange:r];
        }
        if(value)
        {
            NSLog(@"Found session value in payload: %@",value);
            NSString* jsString = [NSString stringWithFormat:@"handleOpenURL("http://URLHERE.cloudapp.net/Session/Lookup?session=%@");",value];
            [webView stringByEvaluatingJavaScriptFromString:jsString];
        }
    }       
}

 

As you can see above, this method parses the payload of the message, tries to work out the session code, and if one is found, creates a new javascript call to a method called handleOpenURL which instructs PhoneGap to call the method of the same name.  Of course, you are going to want to configure this for your own scenario, but hopefully this gives you a sense of how to pass a value as part of the message, and then take an action on that accordingly. 

Well, that wraps up this post.  I hope you enjoyed the talk at MIX, and that this code is useful if you have services in Windows Azure that have a need to push notification messages to iPhone and iPad devices.


					

19 thoughts on “Using Apple Push Notifications from Windows Azure

  1. Craig

    Hi,

    this is just what I need for my app! However, the line:

    sslStream.AuthenticateAsClient(HOST, CLIENT_CERT_COLLECTION, SslProtocols.Tls, false);

    fails to authenticate and crashes. I think that my certificate is OK. Apple notes in their remote notifications guide that an Entrust Secure CA root certificate must be installed on the provider’s server. Maybe this is the problem? It this installed on Azure?

    Any advice or even some code would be most welcome!

    Reply
  2. Simon Post author

    Craig – does the certificate that you are using contain the private key? What kind of exception are you getting? A general SSPI failure?

    Reply
  3. MinGyu

    There is a problem you have missed.

    If you want to send messages in English, you don’t have to fix this code.
    It completly works.
    However, if you are trying to send messages written in other languages like Korean, you must know this.

    Because of it, I spent too much time to fix it.
    I wanna share how to fix it.
    I was trying to send push messages in Korean.
    But the connection to push notification server kept closing and the messages never sent.

    The problem is this line

    binaryWriter.Write((byte)payload.Length);

    The length of the character you write is calculated as 1.
    The payload is encoded by UTF-8 so that characters in other languages need more than one byte.

    One Korean character need 3 bytes for UTF-8.

    There is fixed code.

    byte[] payloadBytes = System.Text.Encoding.UTF8.GetBytes(payload);

    // write payload data
    binaryWriter.Write((byte)0);
    binaryWriter.Write((byte)payloadBytes.Length);
    binaryWriter.Write(payloadBytes);

    It will work with any languages.

    Reply
      1. Sam

        The sample code still isn’t quite right as you’re using the size of the original string, not the UTF8 encoded string in bytes. This will again cause a problem for UTF8 characters. I just came across this problem and changing the above code to the following fixed it:

        byte[] payloadBytes = System.Text.Encoding.UTF8.GetBytes(json);

        // write payload data
        binaryWriter.Write((byte)0);
        binaryWriter.Write((byte)payloadBytes.Length);
        binaryWriter.Write(payloadBytes);
        binaryWriter.Flush();

        Other than that, great article!

        Reply
  4. Matt M.

    Very useful code, however I seem to be running into an issue when calling:

    private static X509Certificate2 CLIENT_CERT = new X509Certificate2(Environment.GetEnvironmentVariable(“RoleRoot”) + @”approotcertsmycert_key_dev_private.p12″, CERT_PASSWORD);

    I get the exception: “The parameter is incorrect”.

    any ideas on why this is happening?

    Reply
  5. Pingback: Using Microsoft Windows Azure to Host Web Services and Apple Push Notifications for your iOS Apps. | The Kitchen Drawer

    1. Simon Post author

      This is the URL of your Azure queue. Something like abc.queue.windows.net – you’ll find this URL in the Azure portal under storage section.

      Reply
  6. Simon

    Hi Simon,

    I wish you’d pad out and update this example for all the simpletons like me. Its the ONLY reference I can find on the web for how to do this. I’m getting lost in all the certificate stuff!

    Reply
  7. Simon

    I don’t understand the line that (presumably) is creating a certificate class from the Apple certificate.

    The line is:

    private static X509Certificate2 CLIENT_CERT = new X509Certificate2(Environment.GetEnvironmentVariable(“RoleRoot”) + @”approotcertsmix11_dev_cert.p12″, CERT_PASSWORD);

    Yet you advised us to put our certificate in a folder called “Certs”.
    I just can’t understand how this is working or even if this IS the Apple certificate?

    Reply
    1. Simon Post author

      Simon – happy to try and help. Are you trying to get this working using Windows Azure, or just from a local machine? The cert path in my code relates to Windows Azure – the environment variable is used to determine whether the certificate is uploaded to.

      Reply
      1. Simon

        Hi (wiser) Simon!,

        Sorry for all the questions and thanks for the offer of help.

        I think I’ve got it to connect. It says it is now anyway!

        There were two issues, your code example was missing the folder separators “” I think, it should be:

        private static X509Certificate2 CLIENT_CERT = new X509Certificate2(Environment.GetEnvironmentVariable(“RoleRoot”) + @”approotcertsmix11_dev_cert.p12″, CERT_PASSWORD);

        This might be obvious to anyone that knows Azure but I’m a complete beginner.

        However, I finally got my code working by moving my certificate into the root and using:

        private static X509Certificate2 CLIENT_CERT = new X509Certificate2(Path.Combine(appRoot + @””, @”approotmy_cert_name.p12″), CERT_PASSWORD);

        The other issue was my fault, but perhaps something worth stipulating. As well as marking the certificate as “Content” you must also set “Copy to Output Directory” to “Copy always” or “Copy if Newer”. As a newbie, I’d left it at “Do not copy”.

        I now get a successful connection to APN with my certificate. I’m still testing locally and now moving onto the code to actually send the message. Slow progress as I’m really wanting to understand what’s happening here rather than just cutting and pasting your code.

        NB: I’m totally confident with .NET and web page development, but completely new to Azure and Certificates.

        Reply
        1. Simon

          OK, I got it all running – its says it connected OK, and the messages were sent OK (no exceptions anyway). However, I get no notifications to my app :-(

          I double-checked the DEVICE TOKEN and it was fine.

          Will this actually work when running in the Azure emulator on my laptop?

          Since your example uses the “Simple” notification format for the message to the APN server, I can’t get any feedback from the APN server. And I can’t use the “enhanced” format as I haven’t got a clue how i’d intercept the usual error response.

          This is all so opaque !

          Reply
          1. Simon Post author

            Hi Simon – sorry to hear you are still having troubles. If I’m correct, you can get the code working directly from a C# application – but once you put it in an Azure role, things stop working (even on the development fabric on your local machine)?

          2. Simon

            Hi Simon,

            Just thought I’d update you. Thanks to your website I did eventually persevere and managed to get this all up and working. I plan to post the code on my blog once I’ve tidied it up a little.

  8. vikram jain

    This code running at local even on local or azure emulator. But when upload on azure server is not running , so please update your code also for azure server.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>