using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Amazon.Runtime;
using Amazon.Util;
using ThirdParty.Json.LitJson;
#if BCL || NETSTANDARD
using Amazon.Runtime.Internal.Util;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using ThirdParty.BouncyCastle.OpenSsl;
#endif
namespace Amazon.SimpleNotificationService.Util
{
///
/// This class reads in JSON formatted Amazon SNS messages into Message objects. The messages can also be verified using the IsMessageSignatureValid operation.
///
public class Message
{
private const int MAX_RETRIES = 3;
///
/// The value of the Type property for a subscription confirmation message
///
public const string MESSAGE_TYPE_SUBSCRIPTION_CONFIRMATION = "SubscriptionConfirmation";
///
/// The value of the Type property for a unsubscribe confirmation message
///
public const string MESSAGE_TYPE_UNSUBSCRIPTION_CONFIRMATION = "UnsubscribeConfirmation";
///
/// The value of the Type property for a notification message
///
public const string MESSAGE_TYPE_NOTIFICATION = "Notification";
private Message()
{
}
///
/// Parses the JSON message from Amazon SNS into the Message object.
///
/// The JSON text from an Amazon SNS message
/// The Message object with properties set from the JSON document
public static Message ParseMessage(string messageText)
{
var message = new Message();
var jsonData = JsonMapper.ToObject(messageText);
Func extractField = ((fieldName) =>
{
if (jsonData[fieldName] != null && jsonData[fieldName].IsString)
return (string)jsonData[fieldName];
// Check to see if the field can be found with a different case.
var anyCaseKey = jsonData.PropertyNames.FirstOrDefault(x => string.Equals(x, fieldName, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(anyCaseKey) && jsonData[anyCaseKey].IsString)
return (string)jsonData[anyCaseKey];
return null;
});
message.MessageId = extractField("MessageId");
message.MessageText = extractField("Message");
message.Signature = extractField("Signature");
message.SignatureVersion = extractField("SignatureVersion");
message.SigningCertURL = ValidateCertUrl(extractField("SigningCertURL"));
message.SubscribeURL = extractField("SubscribeURL");
message.Subject = extractField("Subject");
message.TimestampString = extractField("Timestamp");
message.Token = extractField("Token");
message.TopicArn = extractField("TopicArn");
message.Type = extractField("Type");
message.UnsubscribeURL = extractField("UnsubscribeURL");
return message;
}
///
/// Gets a Universally Unique Identifier, unique for each message published. For a notification that Amazon SNS resends during a retry, the message ID of the original message is used.
///
public string MessageId
{
get;
private set;
}
///
/// Gets the MessageText value specified when the notification was published to the topic.
///
public string MessageText
{
get;
private set;
}
///
/// Gets the Base64-encoded "SHA1withRSA" signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values.
///
public string Signature
{
get;
private set;
}
///
/// Gets the Version of the Amazon SNS signature used.
///
public string SignatureVersion
{
get;
private set;
}
///
/// Gets the URL to the certificate that was used to sign the message.
///
public string SigningCertURL
{
get;
private set;
}
///
/// Gets the Subject parameter specified when the notification was published to the topic. Note that this is an optional parameter.
/// If no Subject was specified, then this name/value pair does not appear in this JSON document.
///
public string Subject
{
get;
private set;
}
///
/// Gets the URL that you must visit in order to re-confirm the subscription. Alternatively, you can instead use the Token with the ConfirmSubscription action to re-confirm the subscription.
///
public string SubscribeURL
{
get;
private set;
}
///
/// Gets the time (GMT) when the notification was published.
///
public DateTime Timestamp
{
get
{
if (string.IsNullOrEmpty(this.TimestampString))
return DateTime.MinValue;
return DateTime.ParseExact(this.TimestampString, AWSSDKUtils.ISO8601DateFormat, System.Globalization.CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);
}
}
private string TimestampString
{
get;
set;
}
///
/// Gets a value you can use with the ConfirmSubscription action to re-confirm the subscription. Alternatively, you can simply visit the SubscribeURL.
///
public string Token
{
get;
private set;
}
///
/// Gets the Amazon Resource Name (ARN) for the topic.
///
public string TopicArn
{
get;
private set;
}
///
/// Gets the type of message. Possible values are Notification, SubscriptionConfirmation, and UnsubscribeConfirmation.
///
public string Type
{
get;
private set;
}
///
/// Returns true if the message type is a subscription confirmation.
///
/// True if the message type is a subscription confirmation.
public bool IsSubscriptionType
{
get { return this.Type == Message.MESSAGE_TYPE_SUBSCRIPTION_CONFIRMATION; }
}
///
/// Returns true if the message type is a unsubscribe confirmation.
///
/// True if the message type is a unsubscribe confirmation.
public bool IsUnsubscriptionType
{
get { return this.Type == Message.MESSAGE_TYPE_UNSUBSCRIPTION_CONFIRMATION; }
}
///
/// Returns true if the message type is a notification message.
///
/// True if the message type is a notification message.
public bool IsNotificationType
{
get { return this.Type == Message.MESSAGE_TYPE_NOTIFICATION;}
}
///
/// Gets a URL that you can use to unsubscribe the endpoint from this topic. If you visit this URL, Amazon SNS unsubscribes the endpoint and stops sending notifications to this endpoint.
///
public string UnsubscribeURL
{
get;
private set;
}
///
/// Verifies that the signing certificate url is from a recognizable source.
/// Returns the cert url if it cen be verified, otherwise throws an exception.
///
///
///
private static string ValidateCertUrl(string certUrl)
{
var uri = new Uri(certUrl);
if (uri.Scheme == "https" && certUrl.EndsWith(".pem", StringComparison.Ordinal))
{
const string pattern = @"^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$";
var regex = new Regex(pattern);
if (regex.IsMatch(uri.Host))
return certUrl;
}
throw new AmazonClientException("Signing certificate url is not from a recognised source.");
}
#if BCL || NETSTANDARD
#region Message Verification
///
/// Verifies the authenticity of a message sent by Amazon SNS. This is done by computing a signature from the fields in the message and then comparing
/// the signature to the signature provided as part of the message.
///
/// Returns true if the message is authentic.
public bool IsMessageSignatureValid()
{
var bytesToSign = GetMessageBytesToSign();
var certificate = GetX509Certificate();
#if BCL
var rsa = certificate.PublicKey.Key as RSACryptoServiceProvider;
return rsa.VerifyData(bytesToSign, CryptoConfig.MapNameToOID("SHA1"), Convert.FromBase64String(this.Signature));
#else
var rsa = certificate.GetRSAPublicKey();
return rsa.VerifyData(bytesToSign, Convert.FromBase64String(this.Signature), HashAlgorithmName.SHA1, RSASignaturePadding.Pkcs1);
#endif
}
private byte[] GetMessageBytesToSign()
{
string stringToSign = null;
if (this.IsNotificationType)
stringToSign = BuildNotificationStringToSign();
else if (this.IsSubscriptionType || this.IsUnsubscriptionType)
stringToSign = BuildSubscriptionStringToSign();
else
throw new AmazonClientException("Unknown message type: " + this.Type);
byte[] bytesToSign = UTF8Encoding.UTF8.GetBytes(stringToSign);
return bytesToSign;
}
///
/// Build the string to sign for Notification messages.
///
/// The string to sign
private string BuildSubscriptionStringToSign()
{
StringBuilder stringToSign = new StringBuilder();
stringToSign.Append("Message\n");
stringToSign.Append(this.MessageText);
stringToSign.Append("\n");
stringToSign.Append("MessageId\n");
stringToSign.Append(this.MessageId);
stringToSign.Append("\n");
stringToSign.Append("SubscribeURL\n");
stringToSign.Append(this.SubscribeURL);
stringToSign.Append("\n");
stringToSign.Append("Timestamp\n");
stringToSign.Append(this.TimestampString);
stringToSign.Append("\n");
stringToSign.Append("Token\n");
stringToSign.Append(this.Token);
stringToSign.Append("\n");
stringToSign.Append("TopicArn\n");
stringToSign.Append(this.TopicArn);
stringToSign.Append("\n");
stringToSign.Append("Type\n");
stringToSign.Append(this.Type);
stringToSign.Append("\n");
return stringToSign.ToString();
}
///
/// Build the string to sign for SubscriptionConfirmation and UnsubscribeConfirmation messages.
///
/// The string to sign
private string BuildNotificationStringToSign()
{
StringBuilder stringToSign = new StringBuilder();
stringToSign.Append("Message\n");
stringToSign.Append(this.MessageText);
stringToSign.Append("\n");
stringToSign.Append("MessageId\n");
stringToSign.Append(this.MessageId);
stringToSign.Append("\n");
if (this.Subject != null)
{
stringToSign.Append("Subject\n");
stringToSign.Append(this.Subject);
stringToSign.Append("\n");
}
stringToSign.Append("Timestamp\n");
stringToSign.Append(this.TimestampString);
stringToSign.Append("\n");
stringToSign.Append("TopicArn\n");
stringToSign.Append(this.TopicArn);
stringToSign.Append("\n");
stringToSign.Append("Type\n");
stringToSign.Append(this.Type);
stringToSign.Append("\n");
return stringToSign.ToString();
}
static Dictionary certificateCache = new Dictionary();
private X509Certificate2 GetX509Certificate()
{
lock (certificateCache)
{
if (certificateCache.ContainsKey(this.SigningCertURL))
{
return certificateCache[this.SigningCertURL];
}
else
{
for (int retries = 1; retries <= MAX_RETRIES; retries++)
{
try
{
HttpWebRequest request = HttpWebRequest.Create(this.SigningCertURL) as HttpWebRequest;
#if BCL
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
#else
// It's illegal to await an async method within a lock statement block.
// So just get the response on this thread.
using (HttpWebResponse response = AsyncHelpers.RunSync(request.GetResponseAsync) as HttpWebResponse)
#endif
using (var reader = new StreamReader(response.GetResponseStream()))
{
var content = reader.ReadToEnd().Trim();
var pemObject = new PemReader(new StringReader(content)).ReadPemObject();
X509Certificate2 certificate = new X509Certificate2(pemObject.Content);
certificateCache[this.SigningCertURL] = certificate;
return certificate;
}
}
catch(Exception e)
{
if (retries == MAX_RETRIES)
throw new AmazonClientException(string.Format(CultureInfo.InvariantCulture,
"Unable to download signing cert after {0} retries", MAX_RETRIES), e);
else
AWSSDKUtils.Sleep((int)(Math.Pow(4, retries) * 100));
}
}
}
throw new AmazonClientException(string.Format(CultureInfo.InvariantCulture,
"Unable to download signing cert after {0} retries", MAX_RETRIES));
}
}
#endregion
#endif
#if BCL
#region Subscribe/Unsubscribe Actions
///
/// Uses the SubscribeURL property to subscribe to the topic
///
public void SubscribeToTopic()
{
MakeGetRequest(this.SubscribeURL, "subscribe");
}
///
/// Uses the UnsubscribeURL property to unsubscribe from the topic
///
public void UnsubscribeFromTopic()
{
MakeGetRequest(this.UnsubscribeURL, "unsubscribe");
}
private static void MakeGetRequest(string url, string action)
{
for (int retries = 1; retries <= MAX_RETRIES; retries++)
{
try
{
HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;
var response = request.GetResponse() as HttpWebResponse;
response.Close();
return;
}
catch (Exception e)
{
if (retries == MAX_RETRIES)
throw new AmazonClientException(string.Format(CultureInfo.InvariantCulture,
"Unable to {0} after {1} retries", action, MAX_RETRIES), e);
else
AWSSDKUtils.Sleep((int)(Math.Pow(4, retries) * 100));
}
}
}
#endregion
#endif
}
}