Taylohtio/CKS.FormsBasedAuthentication/Layouts/FBA/ImageHipChallenge.ashx

223 lines
9.4 KiB
Plaintext

<%@ 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
{
/// <summary>Provides cryptographically-strong pseudo-random numbers.</summary>
internal class RandomNumbers
{
/// <summary>Random number generator.</summary>
private RNGCryptoServiceProvider _rand = new RNGCryptoServiceProvider();
/// <summary>Used by NextRandom and NextRandomDouble.</summary>
/// <remarks>
/// 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.
/// </remarks>
private byte[] _rd4 = new byte[4], _rd8 = new byte[8];
/// <summary>Gets a random non-negative integer less than max.</summary>
/// <param name="max">The upper-bound for the random number.</param>
/// <returns>The generated random number.</returns>
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;
}
/// <summary>Gets a random number between min and max, inclusive.</summary>
/// <param name="min">The minimum possible value.</param>
/// <param name="max">The maximum possible value.</param>
/// <returns>The randomly generated number.</returns>
public int Next(int min, int max)
{
if (min > max) throw new ArgumentOutOfRangeException("max");
return Next(max - min + 1) + min;
}
/// <summary>Gets a randomly generated double between 0.0 and 1.1.</summary>
/// <returns>The random number.</returns>
public double NextDouble()
{
_rand.GetBytes(_rd8);
return BitConverter.ToUInt64(_rd8, 0) / (double)UInt64.MaxValue;
}
}
/// <summary>Handles requests for dynamic images from the ImageHipChallenge control.</summary>
public class ImageHipChallengeHandler : IHttpHandler
{
/// <summary>Default value for the RenderUrl property.</summary>
private const string RENDERURL_DEFAULT = "ImageHipChallenge.ashx";
/// <summary>Query string key for the image width.</summary>
internal const string WIDTH_KEY = "w";
/// <summary>Query string key for the image height.</summary>
internal const string HEIGHT_KEY = "h";
/// <summary>Query string key for challenge ID.</summary>
internal const string ID_KEY = "id";
private RandomNumbers _rand = new RandomNumbers();
/// <summary>Maximum width of an image to generate.</summary>
private const int MAX_IMAGE_WIDTH = 600;
/// <summary>Maximum height of an image to generate.</summary>
private const int MAX_IMAGE_HEIGHT = 600;
/// <summary>Gets whether this handler is reusable.</summary>
/// <remarks>This handler is not thread-safe (uses non thread-safe member variables), so it is not reusable.</remarks>
public bool IsReusable { get { return false; } }
/// <summary>Gets a random non-negative integer less than max.</summary>
/// <param name="max">The upper-bound for the random number.</param>
/// <returns>The generated random number.</returns>
protected int NextRandom(int max) { return _rand.Next(max); }
/// <summary>Gets a random number between min and max, inclusive.</summary>
/// <param name="min">The minimum possible value.</param>
/// <param name="max">The maximum possible value.</param>
/// <returns>The randomly generated number.</returns>
protected int NextRandom(int min, int max) { return _rand.Next(min, max); }
/// <summary>Gets a randomly generated double between 0.0 and 1.1.</summary>
/// <returns>The random number.</returns>
protected double NextRandomDouble() { return _rand.NextDouble(); }
/// <summary>Gets the challenge text for a particular ID.</summary>
/// <param name="challengeId">The ID of the challenge text to retrieve.</param>
/// <returns>The text associated with the specified ID; null if no text exists.</returns>
internal static string GetChallengeText(Guid challengeId)
{
HttpContext ctx = HttpContext.Current;
return (string)ctx.Cache[challengeId.ToString()];
}
/// <summary>Processes the image request and generates the appropriate image.</summary>
/// <param name="context">The current HttpContext.</param>
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);
}
}
}
/// <summary>Generates the challenge image.</summary>
/// <param name="text">The text to be rendered into the image.</param>
/// <param name="size">The size of the image to generate.</param>
/// <returns>A dynamically-generated challenge image.</returns>
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;
}
/// <summary>Distorts the image.</summary>
/// <param name="b">The image to be transformed.</param>
/// <param name="distortion">An amount of distortion.</param>
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));
}
}
}
}
/// <summary>List of fonts that can be used for rendering text.</summary>
/// <remarks>This list can be changed to include any families available on the current system.</remarks>
private static FontFamily [] _families = {
new FontFamily("Times New Roman")
};
}
}