<%@ WebHandler Language="C#" Class="CKS.FormsBasedAuthentication.HIP.ImageHipChallengeHandler" %> using System; using System.Web; using System.Drawing; using System.Drawing.Imaging; using System.Drawing.Drawing2D; using System.Collections.Specialized; using System.Security.Cryptography; namespace CKS.FormsBasedAuthentication.HIP { /// Provides cryptographically-strong pseudo-random numbers. internal class RandomNumbers { /// Random number generator. private RNGCryptoServiceProvider _rand = new RNGCryptoServiceProvider(); /// Used by NextRandom and NextRandomDouble. /// /// RNGCryptoServiceProvider is not thread-safe. Therefore, we only call to on a single thread. /// As a result, we can resuse the same byte arrays for every call. /// private byte[] _rd4 = new byte[4], _rd8 = new byte[8]; /// Gets a random non-negative integer less than max. /// The upper-bound for the random number. /// The generated random number. public int Next(int max) { if (max <= 0) throw new ArgumentOutOfRangeException("max"); _rand.GetBytes(_rd4); int val = BitConverter.ToInt32(_rd4, 0) % max; if (val < 0) val = -val; return val; } /// Gets a random number between min and max, inclusive. /// The minimum possible value. /// The maximum possible value. /// The randomly generated number. public int Next(int min, int max) { if (min > max) throw new ArgumentOutOfRangeException("max"); return Next(max - min + 1) + min; } /// Gets a randomly generated double between 0.0 and 1.1. /// The random number. public double NextDouble() { _rand.GetBytes(_rd8); return BitConverter.ToUInt64(_rd8, 0) / (double)UInt64.MaxValue; } } /// Handles requests for dynamic images from the ImageHipChallenge control. public class ImageHipChallengeHandler : IHttpHandler { /// Default value for the RenderUrl property. private const string RENDERURL_DEFAULT = "ImageHipChallenge.ashx"; /// Query string key for the image width. internal const string WIDTH_KEY = "w"; /// Query string key for the image height. internal const string HEIGHT_KEY = "h"; /// Query string key for challenge ID. internal const string ID_KEY = "id"; private RandomNumbers _rand = new RandomNumbers(); /// Maximum width of an image to generate. private const int MAX_IMAGE_WIDTH = 600; /// Maximum height of an image to generate. private const int MAX_IMAGE_HEIGHT = 600; /// Gets whether this handler is reusable. /// This handler is not thread-safe (uses non thread-safe member variables), so it is not reusable. public bool IsReusable { get { return false; } } /// Gets a random non-negative integer less than max. /// The upper-bound for the random number. /// The generated random number. protected int NextRandom(int max) { return _rand.Next(max); } /// Gets a random number between min and max, inclusive. /// The minimum possible value. /// The maximum possible value. /// The randomly generated number. protected int NextRandom(int min, int max) { return _rand.Next(min, max); } /// Gets a randomly generated double between 0.0 and 1.1. /// The random number. protected double NextRandomDouble() { return _rand.NextDouble(); } /// Gets the challenge text for a particular ID. /// The ID of the challenge text to retrieve. /// The text associated with the specified ID; null if no text exists. internal static string GetChallengeText(Guid challengeId) { HttpContext ctx = HttpContext.Current; return (string)ctx.Cache[challengeId.ToString()]; } /// Processes the image request and generates the appropriate image. /// The current HttpContext. public void ProcessRequest(HttpContext context) { // Retrieve query parameters and challenge text NameValueCollection queryString = context.Request.QueryString; int width = Convert.ToInt32(queryString[WIDTH_KEY]); if (width <= 0 || width > MAX_IMAGE_WIDTH) throw new ArgumentOutOfRangeException(WIDTH_KEY); int height = Convert.ToInt32(queryString[HEIGHT_KEY]); if (height <= 0 || height > MAX_IMAGE_HEIGHT) throw new ArgumentOutOfRangeException(HEIGHT_KEY); string text = GetChallengeText(new Guid(queryString[ID_KEY])); if (text != null) { // We successfully retrieved the information, so generate the image and send it to the client. HttpResponse resp = context.Response; resp.Clear(); resp.ContentType = "image/jpeg"; using (Bitmap bmp = GenerateImage(text, new Size(width, height))) { bmp.Save(resp.OutputStream, ImageFormat.Jpeg); } } } /// Generates the challenge image. /// The text to be rendered into the image. /// The size of the image to generate. /// A dynamically-generated challenge image. public Bitmap GenerateImage(string text, Size size) { // Create the new Bitmap of the specified size and render to it Bitmap bmp = new Bitmap(size.Width, size.Height); using (Graphics g = Graphics.FromImage(bmp)) { // Draw the background as a random linear gradient using(Brush b = new LinearGradientBrush( new Rectangle(0,0,size.Width,size.Height), Color.FromArgb(NextRandom(256),NextRandom(256),NextRandom(256)), Color.FromArgb(NextRandom(256),NextRandom(256),NextRandom(256)), (float)(NextRandomDouble()*360),false)) { g.FillRectangle(b, 0, 0, bmp.Width, bmp.Height); } // Select a font family and create the default sized font. We then need to shrink // the font size until the text fits. FontFamily ff = _families[NextRandom(_families.Length)]; int emSize = (int)(size.Width*2 / text.Length); Font f = new Font(ff, emSize); try { // Make sure that the font size we have will fit with the selected text. SizeF measured = new SizeF(0,0); SizeF workingSize = new SizeF(size.Width, size.Height); while (emSize > 2 && (measured = g.MeasureString(text, f)).Width > workingSize.Width || measured.Height > workingSize.Height) { f.Dispose(); f = new Font(ff, emSize -= 2); } // Select a color and draw the string into the center of the image using(StringFormat fmt = new StringFormat()) { fmt.Alignment = fmt.LineAlignment = StringAlignment.Center; using(Brush b = new LinearGradientBrush( new Rectangle(0,0,size.Width/2,size.Height/2), Color.FromArgb(NextRandom(256),NextRandom(256),NextRandom(256)), Color.FromArgb(NextRandom(256),NextRandom(256),NextRandom(256)), (float)(NextRandomDouble()*360),false)) { g.DrawString(text, f, b, new Rectangle(0,0,bmp.Width,bmp.Height), fmt); } } } finally { // Clean up f.Dispose(); } } // Distort the final image and return it. This distortion amount is fairly arbitrary. DistortImage(bmp, NextRandom(5, 10) * (NextRandom(2) == 1 ? 1 : -1) ); return bmp; } /// Distorts the image. /// The image to be transformed. /// An amount of distortion. private static void DistortImage(Bitmap b, double distortion) { int width = b.Width, height = b.Height; // Copy the image so that we're always using the original for source color using (Bitmap copy = (Bitmap)b.Clone()) { // Iterate over every pixel for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // Adds a simple wave int newX = (int)(x + (distortion * Math.Sin(Math.PI * y / 64.0))); int newY = (int)(y + (distortion * Math.Cos(Math.PI * x / 64.0))); if (newX < 0 || newX >= width) newX = 0; if (newY < 0 || newY >= height) newY = 0; b.SetPixel(x, y, copy.GetPixel(newX, newY)); } } } } /// List of fonts that can be used for rendering text. /// This list can be changed to include any families available on the current system. private static FontFamily [] _families = { new FontFamily("Times New Roman") }; } }