Sunday, March 2, 2014

ASP.NET web forms captcha

The whole purpose of this post is to show how simple it is to introduce some custom handler in ASP.NET (that old one, which's still used though). Reading the preparation book to TS 70-515 exam, I found handlers' examples there very impractical, so I decided to get more or less real case, where custom handlers are of use - captcha. To free UI thread, handler will be asynchronous. So let's assume you have a project called ASPWebApp, with the same namespace. Here's the operation IAsyncResult class, which will be performing the main work:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using System.Drawing;

    public class CaptchaAsyncOperation : IAsyncResult
    {
        private int IMG_WIDTH = 100;
        private int IMG_HEIGHT = 45;
        private int FONT_SIZE = 26;
        //List of fonts
        private string[] fontNames = {"Verdana","Tahoma","Times","Arial","Georgia"};

        private bool completed;
        private object state;
        private AsyncCallback ascCallback;
        private HttpContext context;

        public CaptchaAsyncOperation(AsyncCallback callback, HttpContext context, object state)
        {
            this.state = state;
            this.ascCallback = callback;
            this.context = context;
            this.completed = false;
        }

        public object AsyncState
        {
            get { return this.state; }
        }

        public WaitHandle AsyncWaitHandle
        {
            get { return null; }
        }

        public bool CompletedSynchronously
        {
            get { return false; }
        }

        public bool IsCompleted
        {
            get { return this.completed; }
        }

        public void StartAsync()
        {
            ThreadPool.QueueUserWorkItem(new WaitCallback(StartAsyncOperation), null);
        }

        public void StartAsyncOperation(object workItemState)
        {
            Random r = new Random();
            HttpResponse resp = this.context.Response;
            string textToDraw = r.Next(1000, 9999).ToString();

            using (Bitmap bmpOut = new Bitmap(IMG_WIDTH, IMG_HEIGHT))
            {    
                Graphics g = Graphics.FromImage(bmpOut);
                g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Low;
                g.FillRectangle(Brushes.Gray, 0, 0, IMG_WIDTH, IMG_HEIGHT);
                g.DrawString(textToDraw, 
                    new Font(fontNames[r.Next(0,fontNames.Length)], FONT_SIZE, FontStyle.Strikeout), new SolidBrush(Color.White), 0, 0);

                //Drawing lines to make it more difficult for bots
                g.DrawLine(new Pen(Brushes.Red, 2), new Point(0, r.Next(0,IMG_HEIGHT-20)), new Point(r.Next(IMG_WIDTH-15,IMG_WIDTH), IMG_HEIGHT));
                g.DrawLine(new Pen(Brushes.Brown, 2), new Point(r.Next(10, IMG_HEIGHT - 10)), new Point(r.Next(IMG_WIDTH - 5, IMG_WIDTH),
                    r.Next(IMG_HEIGHT-30, IMG_HEIGHT)));

               
                using (MemoryStream ms = new MemoryStream())
                {
                    bmpOut.Save(ms, System.Drawing.Imaging.ImageFormat.Png);
                    byte[] bmpBytes = ms.GetBuffer();
                    ms.Close();
                    resp.ContentType = "image/png";
                    resp.BinaryWrite(bmpBytes);
                    resp.End();
                    context.Session["captcha"] = textToDraw;
                }
            }

            completed = true;
            this.ascCallback(this);
        }
    }
So it's a picture of randomly chosen font from a list, struck out and with two lines upon it. As you see, string consists of 4 numbers (you might want to throw in some letters). Now handler's back-end.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;


namespace ASPWebApp
{
    public class AsyncCaptchaHandler : IHttpAsyncHandler, System.Web.SessionState.IRequiresSessionState
    {
        public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
        {
            CaptchaAsyncOperation cao = new CaptchaAsyncOperation(cb, context, extraData);
            cao.StartAsync();
            
            return cao;
        }

        public void EndProcessRequest(IAsyncResult result)
        {
            
        }

        public bool IsReusable
        {
            get { return false; }
        }

        public void ProcessRequest(HttpContext context)
        {
            throw new InvalidOperationException("Sync calls not supported");
        }
    }
}
Notice the System.Web.SessionState.IRequiresSessionState interface - it's needed to access Session object from handler. I preferred it to be separated from the actual *.ashx file, for clarity (as class has a clearly different name). So the Captcha.ashx itself is rather small:
<%@ WebHandler Language="C#"  CodeBehind="Captcha.ashx.cs" Class="ASPWebApp.AsyncCaptchaHandler" %>
And the final part - getting image on page, and checking user's input against the generated string. Front-end:
    <div id="captcha-holder">
        <asp:Image ID="testCaptcha" runat="server" ImageUrl="~/Captcha.ashx" />
        <br />
        <asp:TextBox ID="txtCaptchaString" runat="server" />
        <br />
        <asp:Button ID="btnCaptchaSubmit" runat="server" Text="Submit" 
            onclick="btnCaptchaSubmit_Click" />

        <asp:Label ID="lblStatus" runat="server" Text="" ForeColor="Red" />
    </div>
And the event handler in back-end:
        protected void btnCaptchaSubmit_Click(object sender, EventArgs e)
        {
            if (!String.IsNullOrEmpty(txtCaptchaString.Text))
            {
                lblStatus.Text = (txtCaptchaString.Text == (string)Session["captcha"]) ? "Right" : "Wrong;" + (string)Session["captcha"];
            }
            else
            {
                lblStatus.Text = "Input the value";
            }
        }
So here's completely usable captcha - not the synthetic example from preparation book. Of course, it's far from ideal, and you might want to improve it by adding letters or take a different approach against bots, like "find the word which doesn't fit in" riddles. But that's a completely different story.

No comments:

Post a Comment