-
Notifications
You must be signed in to change notification settings - Fork 862
/
Message.cs
499 lines (427 loc) · 18.3 KB
/
Message.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
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;
using Amazon.Runtime.Internal.Util;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using ThirdParty.BouncyCastle.OpenSsl;
namespace Amazon.SimpleNotificationService.Util
{
/// <summary>
/// This class reads in JSON formatted Amazon SNS messages into Message objects. The messages can also be verified using the IsMessageSignatureValid operation.
/// </summary>
public class Message
{
private const int MAX_RETRIES = 3;
private const string SHA1_SIGNATURE_VERSION = "1";
private const string SHA256_SIGNATURE_VERSION = "2";
/// <summary>
/// The value of the Type property for a subscription confirmation message
/// </summary>
public const string MESSAGE_TYPE_SUBSCRIPTION_CONFIRMATION = "SubscriptionConfirmation";
/// <summary>
/// The value of the Type property for a unsubscribe confirmation message
/// </summary>
public const string MESSAGE_TYPE_UNSUBSCRIPTION_CONFIRMATION = "UnsubscribeConfirmation";
/// <summary>
/// The value of the Type property for a notification message
/// </summary>
public const string MESSAGE_TYPE_NOTIFICATION = "Notification";
private Message()
{
}
/// <summary>
/// Parses the JSON message from Amazon SNS into the Message object.
/// </summary>
/// <param name="messageText">The JSON text from an Amazon SNS message</param>
/// <returns>The Message object with properties set from the JSON document</returns>
public static Message ParseMessage(string messageText)
{
var message = new Message();
var jsonData = JsonMapper.ToObject(messageText);
Func<string, string> 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] != null && jsonData[anyCaseKey].IsString)
return (string)jsonData[anyCaseKey];
return null;
});
message.MessageId = extractField("MessageId");
message.MessageText = extractField("Message");
message.Signature = extractField("Signature");
message.SignatureVersion = ValidateSignatureVersion(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;
}
/// <summary>
/// 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.
/// </summary>
public string MessageId
{
get;
private set;
}
/// <summary>
/// Gets the MessageText value specified when the notification was published to the topic.
/// </summary>
public string MessageText
{
get;
private set;
}
/// <summary>
/// Gets the Base64-encoded "SHA1withRSA" or "SHA256withRSA" signature of the Message, MessageId, Subject (if present), Type, Timestamp, and TopicArn values.
/// </summary>
public string Signature
{
get;
private set;
}
/// <summary>
/// Gets the Version of the Amazon SNS signature used.
/// </summary>
public string SignatureVersion
{
get;
private set;
}
/// <summary>
/// Gets the URL to the certificate that was used to sign the message.
/// </summary>
public string SigningCertURL
{
get;
private set;
}
/// <summary>
/// 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.
/// </summary>
public string Subject
{
get;
private set;
}
/// <summary>
/// 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.
/// </summary>
public string SubscribeURL
{
get;
private set;
}
/// <summary>
/// Gets the time (GMT) when the notification was published.
/// </summary>
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;
}
/// <summary>
/// Gets a value you can use with the ConfirmSubscription action to re-confirm the subscription. Alternatively, you can simply visit the SubscribeURL.
/// </summary>
public string Token
{
get;
private set;
}
/// <summary>
/// Gets the Amazon Resource Name (ARN) for the topic.
/// </summary>
public string TopicArn
{
get;
private set;
}
/// <summary>
/// Gets the type of message. Possible values are Notification, SubscriptionConfirmation, and UnsubscribeConfirmation.
/// </summary>
public string Type
{
get;
private set;
}
/// <summary>
/// Returns true if the message type is a subscription confirmation.
/// </summary>
/// <returns>True if the message type is a subscription confirmation.</returns>
public bool IsSubscriptionType
{
get { return this.Type == Message.MESSAGE_TYPE_SUBSCRIPTION_CONFIRMATION; }
}
/// <summary>
/// Returns true if the message type is a unsubscribe confirmation.
/// </summary>
/// <returns>True if the message type is a unsubscribe confirmation.</returns>
public bool IsUnsubscriptionType
{
get { return this.Type == Message.MESSAGE_TYPE_UNSUBSCRIPTION_CONFIRMATION; }
}
/// <summary>
/// Returns true if the message type is a notification message.
/// </summary>
/// <returns>True if the message type is a notification message.</returns>
public bool IsNotificationType
{
get { return this.Type == Message.MESSAGE_TYPE_NOTIFICATION;}
}
/// <summary>
/// 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.
/// </summary>
public string UnsubscribeURL
{
get;
private set;
}
/// <summary>
/// Verifies that the signing certificate url is from a recognizable source.
/// Returns the cert url if it cen be verified, otherwise throws an exception.
/// </summary>
/// <param name="certUrl"></param>
/// <returns></returns>
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.");
}
/// <summary>
/// Verifies the SignatureVersion is either 1 for SHA1 or 2 for SHA256
/// Returns true if is a valid value, otherwise throws an exception
/// </summary>
/// <param name="signatureVersion">SignatureVersion in a SNS message</param>
/// <returns>Returns the SignatureVersion if it's a valid value, otherwise throws an exception</returns>
private static string ValidateSignatureVersion(string signatureVersion)
{
if (signatureVersion == null)
{
throw new AmazonClientException("SignatureVersion is missing");
}
if (!signatureVersion.Equals(SHA1_SIGNATURE_VERSION) && !signatureVersion.Equals(SHA256_SIGNATURE_VERSION))
{
throw new AmazonClientException("SignatureVersion is not a valid value");
}
return signatureVersion;
}
#region Message Verification
/// <summary>
/// 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.
/// </summary>
/// <returns>Returns true if the message is authentic.</returns>
public bool IsMessageSignatureValid()
{
var bytesToSign = GetMessageBytesToSign();
var certificate = GetX509Certificate();
#if BCL
string cryptoConfig;
if (this.SignatureVersion.Equals(SHA1_SIGNATURE_VERSION))
cryptoConfig = CryptoConfig.MapNameToOID("SHA1");
else
cryptoConfig = CryptoConfig.MapNameToOID("SHA256");
var rsa = certificate.PublicKey.Key as RSACryptoServiceProvider;
return rsa.VerifyData(bytesToSign, cryptoConfig, Convert.FromBase64String(this.Signature));
#else
HashAlgorithmName hashAlgorithmName;
if (this.SignatureVersion.Equals(SHA1_SIGNATURE_VERSION))
hashAlgorithmName = HashAlgorithmName.SHA1;
else
hashAlgorithmName = HashAlgorithmName.SHA256;
var rsa = certificate.GetRSAPublicKey();
return rsa.VerifyData(bytesToSign, Convert.FromBase64String(this.Signature), hashAlgorithmName, 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;
}
/// <summary>
/// Build the string to sign for Notification messages.
/// </summary>
/// <returns>The string to sign</returns>
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();
}
/// <summary>
/// Build the string to sign for SubscriptionConfirmation and UnsubscribeConfirmation messages.
/// </summary>
/// <returns>The string to sign</returns>
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<string, X509Certificate2> certificateCache = new Dictionary<string, X509Certificate2>();
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
#if BCL
#region Subscribe/Unsubscribe Actions
/// <summary>
/// Uses the SubscribeURL property to subscribe to the topic
/// </summary>
public void SubscribeToTopic()
{
MakeGetRequest(this.SubscribeURL, "subscribe");
}
/// <summary>
/// Uses the UnsubscribeURL property to unsubscribe from the topic
/// </summary>
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
}
}