|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Contents
Introduction
I've heard it said that laziness is a virtue of an effective programmer. Now while the literal interpretation of this is dubious, there is a measure of truth in it. I have found that laziness can sometimes be a driving force towards innovation. Well, not laziness exactly, but an inclination for finding the solution of least effort. As Lee (2002) points out, it is a "natural disposition that results in being economic". Case in point: a little while ago I needed to create an email driven registration confirmation subsystem, and I didn't want to go through the trouble of creating and managing a table of users with pending registrations. I had this idea: what if one could encode all the information required to complete the registration into the actual confirmation link? The downside is that you end up with a long and not so pretty URL, the upside is that you end up with a new level of ease and flexibility. This is not only for confirming account registration, but also for passing data from emails and between pages etc. This project consists of a URL Object Serialization component that provides serialization, compression, and encryption of CLR objects so that they can be embedded within URLs, a user-account purging component that performs the periodic removal of unconfirmed user accounts, and a demonstration website that shows the use of the components in an ASP.NET user-account confirmation system. Serializing CLR Objects to Query StringsThe RFC Specification specifies that URLs consist of only US ASCII characters. Any characters that are not present in the ASCII character set must be encoded. For characters within the UTF-8 character set this is done by using a percentage symbol and two hexadecimal digits. For us though, we won't use escape encoding, instead we will use Base64 encoding. Base64 encoding consists of the characters A–Z, a–z, and 0–9 (Wikipedia, 2008). This is the format that the As an aside, note that IIS also supports non-standard %u encoding, allowing all Unicode characters to be represented, which is more than the UTF-8 escape encoding described in the standard (Ollmann, 2007). We do not, however, make use of this fact. Instead we stick to the Base64 encoding. URL Length LimitationsWhen generating URLs, we must be aware that some browsers and Web servers have a limit on URL length. URLs using the GET method in Internet Explorer are limited to 2,083 characters. The POST method also limits the URL length to 2,083, but this does not include query string parameters (http://support.microsoft.com/kb/208427). This point is important when intending to serialize large object graphs, or instances with a lot of member data. Safari, Firefox, and Opera (version 9 and above) appear to have no such limit. Older browsers such as Netscape 6, support around 2,000 characters. As far as Web servers go, IIS supports up to 16,384 characters. For those using Mono and Apache, however, Apache supports up to 4,000 characters (Boutell, 2006). So, the short story is, if you wish to maintain compatibility with most browsers, then you should ensure that all URLs remain under 2,000 characters. This gives us about 8000 bytes or 7.8 KBs to work with. Not too shabby. Serializing a CLR Object for URL EmbeddingThe process of serializing an object to a URL is comprised of 3 stages. Firstly, we serialize the object using a The following diagram illustrates the URL object serialization and deserialization processes in more detail. We see that the compression and encryption strategies are used to place the serialized object into a format that is readily transmissible.
Figure: URL Object Encode/Decode sequence.
The The following class diagram shows the composition of the
Figure:
UrlEncoder class diagram.The default /// <summary>
/// Compresses the specified stream.
/// </summary>
/// <param name="stream">The stream.</param>
/// <returns>The compressed data.</returns>
public byte[] Compress(Stream stream)
{
using (MemoryStream resultStream = new MemoryStream())
{
using (GZipStream writeStream = new GZipStream(
resultStream, CompressionMode.Compress, true))
{
CopyBuffered(stream, writeStream);
}
return resultStream.ToArray();
}
}
static void CopyBuffered(Stream readStream, Stream writeStream)
{
byte[] bytes = new byte[bufferSize];
int byteCount;
while ((byteCount = readStream.Read(bytes, 0, bytes.Length)) != 0)
{
writeStream.Write(bytes, 0, byteCount);
}
}
The The following shows the encoding process within the /// <summary>
/// Serializes the specified data, and returns a string
/// that can later be deserialized.
/// </summary>
/// <param name="data">The data to serialize.</param>
/// <returns>The data serialized to a URL encoded string.</returns>
public string Encode(object data)
{
if (data == null)
{
throw new ArgumentNullException("data");
}
BinaryFormatter formatter = new BinaryFormatter();
byte[] dataBytes;
/* Serialize the data to a byte array. */
using (MemoryStream stream = new MemoryStream())
{
formatter.Serialize(stream, data);
dataBytes = stream.ToArray();
}
/* Compress the serialized data. */
byte[] compressedBytes;
using (MemoryStream stream = new MemoryStream(dataBytes))
{
compressedBytes = compressionStrategy.Compress(stream);
}
/* Encrypt the data. */
byte[] encryptedBytes = encryptionProvider.Encrypt
(compressedBytes, encryptionPassPhrase);
/* URL encode the result. */
return HttpServerUtility.UrlTokenEncode(encryptedBytes);
}
To recover the serialized object, we simply reverse the process: Decode -> Decrypt -> Uncompress -> Deserialize. /// <summary>
/// Deserializes the specified value.
/// </summary>
/// <param name="value">The URL encoded string representing
/// the object return value.</param>
/// <returns>The object that was initially serialized
/// using the <see cref="Encode"/> method.</returns>
public object Decode(string value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}
/* Decode the data. */
byte[] decoded = HttpServerUtility.UrlTokenDecode(value);
/* Decrypt the data. */
byte[] unencrypted = encryptionStrategy.Decrypt(decoded, encryptionPassPhrase);
byte[] uncompressedBytes;
/* Decompress the data. */
using (MemoryStream stream = new MemoryStream(unencrypted))
{
uncompressedBytes = compressionStrategy.Decompress(stream);
}
/* Reinstantiate the object instance. */
BinaryFormatter formatter = new BinaryFormatter();
object deserialized;
using (MemoryStream stream = new MemoryStream(uncompressedBytes))
{
deserialized = formatter.Deserialize(stream);
}
return deserialized;
}
URL Object Serialization: A Practical ExampleAs mentioned above, the reason why I came up with the URL object serialization was to implement a fire and forget user account confirmation system. The example website included in the download demonstrates the use of the user-account confirmation system.
Figure: User-account confirmation.
A user begins by registering his or her account. Once the user submits the data via the
Figure: User registration sequence.
When a user completes the first step, via the
Figure:
CreateUserWizard designer properties.The Once the user navigates back to the protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
string cipherText = Request.QueryString["Data"];
if (cipherText == null)
{
ShowConfirmationFailed();
return;
}
/* Reinstate the EmailConfirmation
* from the query string parameter. */
EmailConfirmation confirmation;
UrlEncoder encoder = new UrlEncoder(Settings.PassPhrase);
try
{
confirmation = (EmailConfirmation)encoder.Decode(cipherText);
}
catch (Exception ex)
{
Page.Trace.Write("Default", "Unable to deserialize confirmation: "
+ cipherText, ex);
ShowConfirmationFailed();
return;
}
if (confirmation.UserId == Guid.Empty)
{
Page.Trace.Write("User trying to confirm registration failed. "
+ "The guid UserId is empty. providerUserKey: "
+ cipherText);
ShowConfirmationFailed();
return;
}
MembershipUser user = Membership.GetUser(confirmation.UserId);
if (user == null)
{
Page.Trace.Write("User attempted confirmation of registration "
+ "and MembershipUser was null. UserId: "
+ confirmation.UserId);
ShowConfirmationFailed();
return;
}
/* Complete the action for the specified confirmation type.
* We may have more types here,
* such as a password change confirmation.*/
switch (confirmation.ConfirmationType)
{
case ConfirmationType.UserRegistration:
if (user.IsApproved)
{
ShowUserAlreadyConfirmed();
return;
}
user.IsApproved = true;
Membership.UpdateUser(user);
break;
}
ShowConfirmationSuccess();
bool rememberUser = Request.Cookies[FormsAuthentication.FormsCookieName] != null;
if (rememberUser)
{
FormsAuthentication.SetAuthCookie(user.UserName, true);
}
if (!string.IsNullOrEmpty(confirmation.ContinueUrl))
{
Panel_Continue.Visible = true;
HyperLink_Continue.Text = confirmation.ContinueTitle;
HyperLink_Continue.NavigateUrl = string.Format("{0}?Data={1}",
confirmation.ContinueUrl, cipherText);
}
}
}
User Purging SubsystemWhat do you do when someone signs up and never completes their registration? It's kind of a DOS attack against new registrants. The username will be unavailable until the account is purged. To solve this problem, we have a class named The
Figure:
UserPurging class diagram.In the provided example, and with the The configuration for the <!-- User Purger - provider configuration. [DV]
purgeOlderThanMinutes:Users that are not approved, and are older
than this value, will be deleted.
periodMinutes:The time between purges.
-->
<UserPurging
defaultProvider="AspNetMembershipUserPurger"
purgeOlderThanMinutes="5.5"
periodMinutes="5">
<providers>
<clear />
<!-- ASP.NET Membership UserPurger. -->
<add name="AspNetMembershipUserPurger"
type="Orpius.Web.AspNetMembershipUserPurger,
Orpius.Web.UserPurging" />
</providers>
</UserPurging>
The My rule of thumb is: if you need something to hang around then use a TestingI have included a Unit Testing project as part of the download. Apart from the User account confirmation example, the unit test demonstrates the serialization of a much larger object instance with a child instance. /// <summary>
/// A test for Serialize
/// </summary>
[TestMethod()]
public void SerializeTest()
{
string parentName = "Parent";
string childName = "Child";
UrlEncoder target = new UrlEncoder("Password");
SerializableTestClass data = new SerializableTestClass()
{ Name = parentName };
data.Child = new SerializableTestClass() { Name = childName };
string serialized = target.Encode(data); /* 1772 characters. */
SerializableTestClass deserialized =
(SerializableTestClass)target.Decode(serialized);
Assert.AreEqual(parentName, deserialized.Name);
Assert.AreEqual(childName, deserialized.Child.Name);
}
ConclusionSerializing objects to URLs is a novel approach to passing data to and from ASP.NET applications, and while there exist URL length constraints in some browsers, such constraints do not prohibit its use in scenarios where object graphs are not overly large and contain only a moderate amount of member data. This approach provides a secure yet flexible way to encapsulate and relay private workflow information to and from clients. I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better. References
HistoryJanuary 2008
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||