Utilizing the MPNS Push Service via REST API and Notification Hubs
Technorati Tags: Azure,NotificationHubs,REST,MPNS,Push,Notifications,Tessel,Node.js,ServiceBus
My team recently staged an internal hackfest where we were given 2 hours to code up a use case involving the Tessel (https://tessel.io) device. Azure Mobile Services was on my mind and so I thought to try getting the Tessel to send a push to my Windows Phone 8.1 device based on some external event, such as physically rotating the Tessel with an Accelerometer module plugged in and configured.
Notification Hubs enables push notifications to be sent through a specific service, such as MPNS, but it also can be used as an abstraction layer between a sending program and devices on the other side of several services (MPNS, APN, WNS, GCM, ADM, NXN) by using the template technique.
The Tessel is essentially a Node.js execution environment, so I started off by using Node.js on my PC – a Surface Pro 2 device running Windows 8.1. I usually keep my development tools segregated off on a Hyper-V VM on that machine, but had to load up Node.js on the bare metal because I needed to hook up the Tessel to USB. Hyper-V doesn’t currently enable capturing the USB port.
Because the goal was to execute the code on the Tessel, I wanted to use as few libraries as possible. The Tessel only has 30mb of RAM to work with, and the Azure SDK for Node.js is 37mb. When working with devices like this you can’t make any assumptions about library support. Hence the decision to go with the REST API.
The bits:
- Node.js for Windows, version 0.10.31
- Notification Hubs REST API version 2013-08 (see https://msdn.microsoft.com/en-us/library/dn495827.aspx for details)
- Visual Studio 2013 Update 2 (used to build a test app for the phone)
The set up
To register the phone to receive notifications I created a trivial Windows Phone Silverlight 8.1 app using Visual Studio. This is based on the “Blank App (Windows Phone Silverlight)” template. I selected Windows Phone 8.1 as the phone’s O.S. The only code added to the template went into the Application_Launching event of App.xaml.cs: (line breaks inserted for clarity)
private void Application_Launching(object sender, LaunchingEventArgs e)
{
var channel = HttpNotificationChannel.Find("MyPushChannel");
if (channel == null)
{
channel = new HttpNotificationChannel("MyPushChannel");
channel.Open();
channel.BindToShellToast();
}
channel.ChannelUriUpdated += new EventHandler<NotificationChannelUriEventArgs>(async (o, args) =>
{
var hub = new NotificationHub(
"<my-hub-name>",
"Endpoint=sb://<my-namespace>.servicebus.windows.net/;
SharedAccessKeyName=DefaultListenSharedAccessSignature;SharedAccessKey=<my-key>");
await hub.RegisterNativeAsync(args.ChannelUri.ToString());
});
}
In testing I used the Hyper-V based emulator as well as my actual phone device, a Nokia 920.
Because I was working in an unfamiliar environment (Node.js, Tessel, etc) I used a “known working” technique to test: a .NET command line app. This app uses the NotificationHubClient class to send a message to the phone. It allows me to test the push registration. Here’s the code: (line breaks inserted for clarity)
class Program
{
static void Main(string[] args)
{
SendNotificationAsync();
Console.ReadLine();
}
private static async void SendNotificationAsync()
{
NotificationHubClient hub = NotificationHubClient.CreateClientFromConnectionString(
"Endpoint=sb://<my-namespace>.servicebus.windows.net/;
SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=<my-key>",
"<my-hub-name>");
string toast = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
"<wp:Notification xmlns:wp=\"WPNotification\">" +
" <wp:Toast>" +
" <wp:Text1>Hello from a .NET App!</wp:Text1>" +
" </wp:Toast> " +
"</wp:Notification>";
await hub.SendMpnsNativeNotificationAsync(toast);
}
}
With these two snippets I now have a working push system and am ready to venture into unknown territory.
To test the Node.js code on my Windows box, I opened a command window with Node in the path and Nodepad++ to edit. Here’s the code:
var https = require('https');
var options = {
host: '<my-namespace>.servicebus.windows.net',
path: '/<my-hub-name>/messages',
method: 'POST',
headers: {
'Authorization': 'SharedAccessSignature sr=https%3a%2f%2f<my-namespace>.servicebus.windows.net%2f<my-hub-name>%2fmessages&sig=<my-SAS-key>&skn=DefaultFullSharedAccessSignature',
'Content-Type': 'application/xml;charset=utf-8',
'ServiceBusNotification-Format': 'windowsphone',
'X-NotificationClass': '2',
'X-WindowsPhone-Target': 'toast'
}
}
var req = https.request(options, function(res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
// do nothing
});
});
req.on('error', function(e) {
console.log('problem with request: ' + e.message);
});
var toast = '<?xml version="1.0" encoding="utf-8"?><wp:Notification xmlns:wp="WPNotification"><wp:Toast><wp:Text1>Hello from a Node.js App!</wp:Text1></wp:Toast></wp:Notification>';
// write data to request body
req.write(toast);
req.end();
The only thing about this code that isn’t straightforward is the Shared Access Signature in the Authorization header. Notification Hubs is part of Service Bus. Service Bus accepts either ACS credentials or a Shared Access Signature to authenticate the user. (see https://msdn.microsoft.com/en-us/library/azure/dn170478.aspx for details.) The details for generating a SAS are here: https://msdn.microsoft.com/en-us/library/azure/dn170477.aspx. But I used a command line program created by a colleague (Brent Stineman @ https://brentdacodemonkey.wordpress.com/feed/) to make life simpler. Here’s the code:
class Program
{
static void Main(string[] args)
{
Console.WriteLine("What is your service bus namespace?");
string sbNamespace = Console.ReadLine();
Console.WriteLine("What is the path?");
string sbPath = Console.ReadLine();
Console.WriteLine("What existing shared access policy would you like to use to generate your SAS?");
string sbPolicy = Console.ReadLine();
Console.WriteLine("What is that policy's shared access key (primary or secondary)?");
string sbKey = Console.ReadLine();
Console.WriteLine("When should this expire (MM/DD/YY HH, GMT)? (press enter for 10/31/2020 12:00)");
string sbExpiry = Console.ReadLine();
if (sbExpiry.Length == 0) sbExpiry = "10/31/2020 12";
// convert the string into a timespan...
DateTime tmpDT;
DateTime.TryParseExact(sbExpiry, "MM/dd/YY HH", null, DateTimeStyles.None, out tmpDT);
TimeSpan expiry = DateTime.UtcNow.Subtract(tmpDT);
var serviceUri = ServiceBusEnvironment.CreateServiceUri("https", sbNamespace, sbPath).ToString().Trim('/');
string generatedSaS = SharedAccessSignatureTokenProvider.GetSharedAccessSignature(sbPolicy, sbKey, serviceUri, expiry);
Console.WriteLine("Your SAS is:\n{0}", generatedSaS);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
Having generated the SAS and included it in the headers, the code is ready to run. And – the same code works great from the Tessel.
Cheers!