summaryrefslogtreecommitdiffstats
path: root/src/DotNetOpenAuth.Messaging/Messaging/Bindings/NonceMemoryStore.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/DotNetOpenAuth.Messaging/Messaging/Bindings/NonceMemoryStore.cs')
-rw-r--r--src/DotNetOpenAuth.Messaging/Messaging/Bindings/NonceMemoryStore.cs136
1 files changed, 136 insertions, 0 deletions
diff --git a/src/DotNetOpenAuth.Messaging/Messaging/Bindings/NonceMemoryStore.cs b/src/DotNetOpenAuth.Messaging/Messaging/Bindings/NonceMemoryStore.cs
new file mode 100644
index 0000000..6e64acc
--- /dev/null
+++ b/src/DotNetOpenAuth.Messaging/Messaging/Bindings/NonceMemoryStore.cs
@@ -0,0 +1,136 @@
+//-----------------------------------------------------------------------
+// <copyright file="NonceMemoryStore.cs" company="Andrew Arnott">
+// Copyright (c) Andrew Arnott. All rights reserved.
+// </copyright>
+//-----------------------------------------------------------------------
+
+namespace DotNetOpenAuth.Messaging.Bindings {
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Text;
+ using DotNetOpenAuth.Messaging.Bindings;
+
+ /// <summary>
+ /// An in-memory nonce store. Useful for single-server web applications.
+ /// NOT for web farms.
+ /// </summary>
+ internal class NonceMemoryStore : INonceStore {
+ /// <summary>
+ /// How frequently we should take time to clear out old nonces.
+ /// </summary>
+ private const int AutoCleaningFrequency = 10;
+
+ /// <summary>
+ /// The maximum age a message can be before it is discarded.
+ /// </summary>
+ /// <remarks>
+ /// This is useful for knowing how long used nonces must be retained.
+ /// </remarks>
+ private readonly TimeSpan maximumMessageAge;
+
+ /// <summary>
+ /// A list of the consumed nonces.
+ /// </summary>
+ private readonly SortedDictionary<DateTime, List<string>> usedNonces = new SortedDictionary<DateTime, List<string>>();
+
+ /// <summary>
+ /// A lock object used around accesses to the <see cref="usedNonces"/> field.
+ /// </summary>
+ private object nonceLock = new object();
+
+ /// <summary>
+ /// Where we're currently at in our periodic nonce cleaning cycle.
+ /// </summary>
+ private int nonceClearingCounter;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NonceMemoryStore"/> class.
+ /// </summary>
+ internal NonceMemoryStore()
+ : this(StandardExpirationBindingElement.MaximumMessageAge) {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NonceMemoryStore"/> class.
+ /// </summary>
+ /// <param name="maximumMessageAge">The maximum age a message can be before it is discarded.</param>
+ internal NonceMemoryStore(TimeSpan maximumMessageAge) {
+ this.maximumMessageAge = maximumMessageAge;
+ }
+
+ #region INonceStore Members
+
+ /// <summary>
+ /// Stores a given nonce and timestamp.
+ /// </summary>
+ /// <param name="context">The context, or namespace, within which the <paramref name="nonce"/> must be unique.</param>
+ /// <param name="nonce">A series of random characters.</param>
+ /// <param name="timestamp">The timestamp that together with the nonce string make it unique.
+ /// The timestamp may also be used by the data store to clear out old nonces.</param>
+ /// <returns>
+ /// True if the nonce+timestamp (combination) was not previously in the database.
+ /// False if the nonce was stored previously with the same timestamp.
+ /// </returns>
+ /// <remarks>
+ /// The nonce must be stored for no less than the maximum time window a message may
+ /// be processed within before being discarded as an expired message.
+ /// If the binding element is applicable to your channel, this expiration window
+ /// is retrieved or set using the
+ /// <see cref="StandardExpirationBindingElement.MaximumMessageAge"/> property.
+ /// </remarks>
+ public bool StoreNonce(string context, string nonce, DateTime timestamp) {
+ if (timestamp.ToUniversalTimeSafe() + this.maximumMessageAge < DateTime.UtcNow) {
+ // The expiration binding element should have taken care of this, but perhaps
+ // it's at the boundary case. We should fail just to be safe.
+ return false;
+ }
+
+ // We just concatenate the context with the nonce to form a complete, namespace-protected nonce.
+ string completeNonce = context + "\0" + nonce;
+
+ lock (this.nonceLock) {
+ List<string> nonces;
+ if (!this.usedNonces.TryGetValue(timestamp, out nonces)) {
+ this.usedNonces[timestamp] = nonces = new List<string>(4);
+ }
+
+ if (nonces.Contains(completeNonce)) {
+ return false;
+ }
+
+ nonces.Add(completeNonce);
+
+ // Clear expired nonces if it's time to take a moment to do that.
+ // Unchecked so that this can int overflow without an exception.
+ unchecked {
+ this.nonceClearingCounter++;
+ }
+ if (this.nonceClearingCounter % AutoCleaningFrequency == 0) {
+ this.ClearExpiredNonces();
+ }
+
+ return true;
+ }
+ }
+
+ #endregion
+
+ /// <summary>
+ /// Clears consumed nonces from the cache that are so old they would be
+ /// rejected if replayed because it is expired.
+ /// </summary>
+ public void ClearExpiredNonces() {
+ lock (this.nonceLock) {
+ var oldNonceLists = this.usedNonces.Keys.Where(time => time.ToUniversalTimeSafe() + this.maximumMessageAge < DateTime.UtcNow).ToList();
+ foreach (DateTime time in oldNonceLists) {
+ this.usedNonces.Remove(time);
+ }
+
+ // Reset the auto-clean counter so that if this method was called externally
+ // we don't auto-clean right away.
+ this.nonceClearingCounter = 0;
+ }
+ }
+ }
+}