Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Amazon Simple Notification Service Script

4.60/5 (4 votes)
18 Apr 2011Ms-PL6 min read 46.1K   945  
Automatically respond to Amazon Simple Notification Service messages

Introduction

Amazon Simple Notification Service (SNS) is a web service that allows you to notify distributed applications by pushing a message to them.

Say you have an application that runs on many servers. In a traditional application, the computers would continually poll a queue or database looking for a job to process. Many computers continually polling will reduce performance of the queue and result in higher usage fees. You can increase the time between polls to reduce load but this decreases the performance of your application.

One way to have your computers process a job immediately and eliminate the need to poll a queue is to use Amazon’s Simple Notification Service. First, you set up a web server on each of the computers that run your application. Next, set up a SNS subscription in Amazon to send messages to your computers. Then create an application on your web servers to do something when they receive an SNS message. Finally, configure your application to call SNS when something needs to be done.

Here is a representation of the flow.
Your application à Amazon SNS à Your web servers à Do work

To get familiar with Amazon SNS, you can log into the AWS Console with your Amazon credentials. Start by creating a Topic and add a Subscription that will specify where the messages will go. You can send a message to an email address, web site URL, or an Amazon Simple Queue Service queue.

When you create a subscription, you will need to confirm you have permission to send messages there. With an email subscription you will be sent an email and you will need to click a link in that email. When you create a web URL subscription, Amazon sends a message to that URL. You will need to capture this message and respond to it by loading a special URL in the message or calling the API with a token that is included in the message. If you do not confirm the subscription, Amazon will not send messages to it.

The process of configuring a web URL to receive a SNS message is tricky. You need to configure the web server before you set up the subscription. The following describes how to set up a Windows web server to respond to Amazon SNS messages. The main points of the article are setting up a script to automatically confirm a SNS subscription and to verify that the SNS message came from Amazon.

Using the Code

Start by downloading the attached Visual Studio project. You will need Visual Studio 2010 to work with it. The project contains a file called AutoConfirm.aspx. This is the script you will point your SNS subscription to. At the beginning of the code, you will see variables that hold your Amazon AccessKeyId and SecretAccessKey. Fill these variables with your values. Next, you will see variables related to email settings. The script is configured to send an email whenever a message is received. Fill these values with your settings.

C#
String AWSAccessKeyId = "xxxxxxxxxxxxxxxxxxxxx";
String AWSSecretAccessKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

String EmailFromAddress = "account@gmail.com";
String EmailToAddress = "me@company.com";
String EmailSmtpHost = "smtp.gmail.com";
int EmailSmtpPort = 587;
Boolean EmailSmtpEnableSsl = true;
String EmailSmtpUserName = "account@gmail.com";
String EmailSmtpPassword = "mypassowrd";       

The script starts by gathering all information that was sent to it. An interesting fact of SNS messages is that you cannot access the data sent to you through the typical Request.Form or Request.QueryString methods. Information sent to your script will only be available through Request.InputStream. The code demonstrated how to convert Request.InputStream into a string.

C#
//The LogMessage will contain information about what the script did. 
//It will be sent through email at the end.
String LogMessage = "";
LogMessage += "DateTime = " + DateTime.Now + Environment.NewLine;
LogMessage += "SERVER_NAME = " + Request.ServerVariables["SERVER_NAME"] + 
		Environment.NewLine;
LogMessage += "LOCAL_ADDR = " + Request.ServerVariables["LOCAL_ADDR"] + 
		Environment.NewLine;
LogMessage += "REMOTE_ADDR = " + Request.ServerVariables["REMOTE_ADDR"] + 
		Environment.NewLine;
LogMessage += "HTTP_USER_AGENT = " + Request.ServerVariables["HTTP_USER_AGENT"] + 
		Environment.NewLine;
LogMessage += "CONTENT_TYPE = " + Request.ServerVariables["CONTENT_TYPE"] + 
		Environment.NewLine;
LogMessage += "SCRIPT_NAME = " + Request.ServerVariables["SCRIPT_NAME"] + 
		Environment.NewLine;
LogMessage += "REQUEST_METHOD = " + Request.ServerVariables["REQUEST_METHOD"] + 
		Environment.NewLine;
LogMessage += "Request.QueryString = " + Request.QueryString.ToString() + 
		Environment.NewLine;
LogMessage += "Request.Form = " + Request.Form.ToString() + Environment.NewLine;

//Convert the Request.InputStream into a string.
byte[] MyByteArray = new byte[Request.InputStream.Length];
Request.InputStream.Read(MyByteArray, 0, Convert.ToInt32(Request.InputStream.Length));

String InputStreamContents;
InputStreamContents = System.Text.Encoding.UTF8.GetString(MyByteArray);

At this point, we have the data that was sent to us. Amazon sends the message in JSON format. The script converts the JSON message into a Dictionary using the System.Web.Script.Serialization.JavaScriptSerializer class. You can access the class by adding a reference to System.Web.Extensions.dll in your project.  The class is only available in .NET Framework 4.0. If your web server does not support .NET 4.0, you will need to rewrite this part of the code to parse the variables from the message using another method.

C#
//Convert the JSON data into a Dictionary.  
//Use of the System.Web.Script namespace requires 
//a reference to System.Web.Extensions.dll.
System.Web.Script.Serialization.JavaScriptSerializer MyJavaScriptSerializer = 
	new System.Web.Script.Serialization.JavaScriptSerializer();
            
System.Collections.Generic.Dictionary<String, Object> MyObjectDictionary;
MyObjectDictionary = MyJavaScriptSerializer.DeserializeObject(InputStreamContents) 
			as System.Collections.Generic.Dictionary<String, Object>;

Now that all the variables are in a dictionary, it is simple to access them. For example, use MyObjectDictionary["TopicArn"].ToString() to give you the value of TopicArn in the message.

Next, the code validates that the message really did come from Amazon. This is done by taking the values in the message, and then constructing a string with the values in a specific order. The construction of this string is described at http://sns-public-resources.s3.amazonaws.com/SNS_Message_Signing_Release_Note_Jan_25_2011.pdf. Amazon constructed the same string on their end and signed the string before sending the message to you. The signed string value is included in the Signature value of the message. You will use these values to confirm the signature is valid.

C#
StringBuilder MyStringBuilder = new StringBuilder();
MyStringBuilder.Append("Message\n");
MyStringBuilder.Append(MyObjectDictionary["Message"].ToString()).Append("\n");
MyStringBuilder.Append("MessageId\n");
MyStringBuilder.Append(MyObjectDictionary["MessageId"].ToString()).Append("\n");
MyStringBuilder.Append("SubscribeURL\n");
MyStringBuilder.Append(MyObjectDictionary["SubscribeURL"].ToString()).Append("\n");
MyStringBuilder.Append("Timestamp\n");
MyStringBuilder.Append(MyObjectDictionary["Timestamp"].ToString()).Append("\n");
MyStringBuilder.Append("Token\n");
MyStringBuilder.Append(MyObjectDictionary["Token"].ToString()).Append("\n");
MyStringBuilder.Append("TopicArn\n");
MyStringBuilder.Append(MyObjectDictionary["TopicArn"].ToString()).Append("\n");
MyStringBuilder.Append("Type\n");
MyStringBuilder.Append(MyObjectDictionary["Type"].ToString()).Append("\n");

String GeneratedMessage;
GeneratedMessage = MyStringBuilder.ToString();        

The VerifySignature function downloads Amazon’s public certificate key and uses it to create a hash of the variables in the message. If the hash matches the Signature value, then the message did come from Amazon. Only Amazon has the private certificate key which is required to generate signatures.

C#
private static Boolean VerifySignature
(String GeneratedMessage, String SignatureFromAmazon, String SigningCertURL)
{
System.Uri MyUri = new System.Uri(SigningCertURL);

//Check if the domain name in the SigningCertURL is an Amazon URL.
if (MyUri.Host.EndsWith(".amazonaws.com") == true)
{
  byte[] SignatureBytes;
  SignatureBytes = Convert.FromBase64String(SignatureFromAmazon);

  //Check the cache for the Amazon signing cert.
  byte[] PEMFileBytes;
  PEMFileBytes = (byte[])System.Web.HttpContext.Current.Cache[SigningCertURL];

  if (PEMFileBytes == null)
  {
    //Download the Amazon signing cert and save it to cache.
    System.Net.WebClient MyWebClient = new System.Net.WebClient();
    PEMFileBytes = MyWebClient.DownloadData(SigningCertURL);

    System.Web.HttpContext.Current.Cache[SigningCertURL] = PEMFileBytes;
  }

  System.Security.Cryptography.X509Certificates.X509Certificate2 MyX509Certificate2 = 
    new System.Security.Cryptography.X509Certificates.X509Certificate2(PEMFileBytes);

  System.Security.Cryptography.RSACryptoServiceProvider MyRSACryptoServiceProvider;
  MyRSACryptoServiceProvider = 
  (System.Security.Cryptography.RSACryptoServiceProvider)MyX509Certificate2.PublicKey.Key;

  System.Security.Cryptography.SHA1Managed MySHA1Managed = 
			new System.Security.Cryptography.SHA1Managed();
  byte[] HashBytes = MySHA1Managed.ComputeHash(Encoding.UTF8.GetBytes(GeneratedMessage));

  return MyRSACryptoServiceProvider.VerifyHash
	(HashBytes, System.Security.Cryptography.CryptoConfig.MapNameToOID("SHA1"), 
	SignatureBytes); 
}
else
{
  return false;
}
} 

If the signature is valid, the code automatically responds to ConfirmSubscription requests. You get a ConfirmSubscription request as soon as you create a subscription. The message contains a special token you will use to confirm the subscription. The code does this by calling the ConfirmSubscription API method. Information on this method can be found at http://docs.amazonwebservices.com/sns/latest/api/API_ConfirmSubscription.html. The code uses the SprightlySoft AWS Component to make the API request. This component is free and it makes it easy to send the required authorization parameters to the API. See http://sprightlysoft.com/AWSComponent/ for more information.

C#
Boolean RetBool;
int ErrorNumber = 0;
String ErrorDescription = "";
String LogData = "";
Dictionary<String, String> RequestHeaders = new Dictionary<String, String>();
int ResponseStatusCode = 0;
String ResponseStatusDescription = "";
Dictionary<String, String> ResponseHeaders = new Dictionary<String, String>();
String ResponseString = "";

//This a SubscriptionConfirmation message. Get the token.
String Token;
Token = MyObjectDictionary["Token"].ToString();

//Get the endpoint from the SigningCertURL value.
String TopicEndpoint;
TopicEndpoint = MyObjectDictionary["SigningCertURL"].ToString();
TopicEndpoint = TopicEndpoint.Substring
		(0, TopicEndpoint.IndexOf(".amazonaws.com/") + 15);

//Amazon Simple Notification Service, ConfirmSubscription: 
<a href="http://docs.amazonwebservices.com/sns/latest/api/
API_ConfirmSubscription.htmlString">http://docs.amazonwebservices.com/
sns/latest/api/API_ConfirmSubscription.html
String</a> RequestURL;
RequestURL = TopicEndpoint + "?Action=ConfirmSubscription&Version=2010-03-31";
RequestURL += "&Token=" + System.Uri.EscapeDataString(Token);
RequestURL += "&TopicArn=" + System.Uri.EscapeDataString(TopicArn);

//Use the SprightlySoft AWS Component to call ConfirmSubscription on Amazon.
RetBool = MakeAmazonSignatureVersion2Request(AWSAccessKeyId, 
AWSSecretAccessKey, RequestURL, "GET", null, "", 3, ref ErrorNumber, 
ref ErrorDescription, ref LogData, ref RequestHeaders, ref ResponseStatusCode, 
ref ResponseStatusDescription, ref ResponseHeaders, ref ResponseString); ;

The code looks at the response from the API. A successful request will result in an XML document with the SubscriptionArn value.

C#
if (RetBool == true)
{
  System.Xml.XmlDocument MyXmlDocument;
  System.Xml.XmlNamespaceManager MyXmlNamespaceManager;
  System.Xml.XmlNode MyXmlNode;

  MyXmlDocument = new System.Xml.XmlDocument();
  MyXmlDocument.LoadXml(ResponseString);

  MyXmlNamespaceManager = new System.Xml.XmlNamespaceManager(MyXmlDocument.NameTable);
  MyXmlNamespaceManager.AddNamespace("amz", "http://sns.amazonaws.com/doc/2010-03-31/");

  MyXmlNode = MyXmlDocument.SelectSingleNode
	("amz:ConfirmSubscriptionResponse/amz:ConfirmSubscriptionResult/
	amz:SubscriptionArn", MyXmlNamespaceManager);

  LogMessage += "ConfirmSubscription was successful. SubscriptionArn = " + 
		MyXmlNode.InnerText + Environment.NewLine;
  LogMessage += Environment.NewLine;
}
else
{
  LogMessage += "ConfirmSubscription failed." + Environment.NewLine;
  LogMessage += "Response Status Code: " + ResponseStatusCode + Environment.NewLine;
  LogMessage += "Response Status Description: " + ResponseStatusDescription + 
		Environment.NewLine;
  LogMessage += "Response String: " + ResponseString + Environment.NewLine;
  LogMessage += Environment.NewLine;
}

The script finishes by sending an email with information on what took place. This email makes it easy to see when your script is called and what took place.

C#
//Send an email with the log information.
System.Net.Mail.SmtpClient MySmtpClient = new System.Net.Mail.SmtpClient();
MySmtpClient.Host = EmailSmtpHost;
MySmtpClient.Port = EmailSmtpPort;
MySmtpClient.EnableSsl = EmailSmtpEnableSsl;
if (EmailSmtpUserName != "")
{
  System.Net.NetworkCredential MyNetworkCredential = new System.Net.NetworkCredential();
  MyNetworkCredential.UserName = EmailSmtpUserName;
  MyNetworkCredential.Password = EmailSmtpPassword;
  MySmtpClient.Credentials = MyNetworkCredential;
}
System.Net.Mail.MailAddress FromMailAddress = 
	new System.Net.Mail.MailAddress(EmailFromAddress);
System.Net.Mail.MailAddress ToMailAddress = 
	new System.Net.Mail.MailAddress(EmailToAddress);
System.Net.Mail.MailMessage MyMailMessage = 
	new System.Net.Mail.MailMessage(FromMailAddress, ToMailAddress);

if (TopicArn == "")
{
  MyMailMessage.Subject = "Log Information For " + 
      Request.ServerVariables["SERVER_NAME"] + Request.ServerVariables["SCRIPT_NAME"];
}
else
{
  MyMailMessage.Subject = "Log Information For " + TopicArn;
}

MyMailMessage.Body = LogMessage;

MySmtpClient.Send(MyMailMessage);

Now that you know how the script works, it’s time to publish it to a web server. The script requires a Windows web server running the .NET Framework 4.0. The web server must be accessible from the Internet in order for Amazon to send messages to it.

When you have published the script, you can set up an SNS subscription in Amazon. Go to the AWS Console (http://aws.amazon.com/console/) and create a new topic. Once you have a topic, create a subscription under it. For protocol, select HTTP. Under endpoint, enter the URL to the script you published.

SNSConsole.png

If everything went right, your subscription will immediately be confirmed and you will receive an email from the script with details. You will then be able to publish messages to your subscription.

About SprightlySoft

This article has been brought to you by SprightlySoft. SprightlySoft develops tools and techniques for Microsoft developers to more easily work with Amazon Web Services. Visit http://sprightlysoft.com/ for more information.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)