%@ 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")
};
}
}