ControlFreak
LANGUAGES: VB.NET | C#
ASP.NET VERSIONS: 2.x
Probing CAPTCHASP
An Examination of the Source Code of a CAPTCHA Web Control
By Steve C. Orr
In the September 2007 issue of asp.netPRO we examined the rise of bots and the solution of CAPTCHA technology (see CAPTCHASP) . We also examined how to use the free CAPTCHASP control. In this installment, we ll probe the source code of the CAPTCHASP control and examine its overall architecture, unique design traits, and major functions.
The CAPTCHASP control (shown in Figure 1) is a standard Web custom control compiled into a standalone DLL that can be added to the Visual Studio toolbox and dropped onto any ASP.NET Web form.
Figure 1: Let users in, keep bots
out! The CAPTCHASP control provides instant, highly customizable CAPTCHA
verification.
Securing the Page
The CAPTCHASP control s base class (WebControl) provides an OnInit event, which has been overridden with the code listed in Figure 2.
Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
If Me.DesignMode = False AndAlso Not _
Page.Request.QueryString("CAPTCHASP_Img" Is Nothing) Then
'A request for the CAPTCHA image has been intercepted
'(Detected by querystring parameter)
'Replace normal page/control output with CAPTCHA image
OutputImage()
Exit Sub
End If
InitControls() 'Initialize constituent controls
If Me.DesignMode=False AndAlso Page.IsPostBack=False Then
'Secure viewstate to prevent tampering
Page.RegisterRequiresViewStateEncryption()
If Page.Session.Mode <> _
SessionState.SessionStateMode.Off Then
Page.ViewStateUserKey = Page.Session.SessionID
End If
End If
End Sub
Figure 2: CAPTCHASP overrides its base class OnInit event with this code.
If the page s QueryString contains a parameter named CAPTCHASP_Img , then the OutputImage subroutine (more on this later) is called; otherwise, the InitControls subroutine (detailed in the next section) is invoked.
Finally, the page s security settings are increased because this control (and, thus, its page) is a likely target of hackers and bots. The page s RegisterRequiresViewStateEncryption method is called to inform the page that it should use the CPU cycles required to encrypt ViewState to discourage tampering. Similarly, the page s ViewStateUserKey property is set to the SessionID of the current user s session (if any) to help prevent cross-site scripting and session hijacking.
Parts & Pieces
The standard run-time layout of the CAPTCHASP instance shown in Figure 1 consists of four main elements: The CAPTCHA image, the instruction label underneath, the entry textbox, and the Submit button. (There are other optional elements, as well, but they are fairly redundant from a coding standpoint, so I won t cover them here.)
A slightly simplified version of CAPTCHASP s InitControls subroutine is listed in Figure 3. This subroutine is called from the base control class overridden OnInit event shown in Figure 2. InitControls creates each sub element of the CAPTCHASP user interface and initializes them with their default values.
'Initialize constituent controls with default values
Protected Sub InitControls()
Me.Controls.Clear()
'Create the Image tag
_imgMain = New System.Web.UI.WebControls.Image
_imgMain.AlternateText = "CAPTCHA"
_imgMain.Width = New Unit(100, UnitType.Percentage)
If Me.DesignMode Then
_imgMain.ImageUrl = _DefaultImgUrl
Else
With Page.Request.FilePath
'Create a quasi-callback URL
Dim SlashInd As Integer = .LastIndexOf("/")
Dim BaseUrl As String = .Substring(SlashInd + 1)
_imgMain.ImageUrl = BaseUrl & "?CAPTCHASP_Img=true"
End With
End If
'Create the instruction label
_lblInstruct = New Label()
InstructionStyle.Font.CopyFrom(Me.Font)
InstructionStyle.Width = _DefaultInstructionWidth
'Create the entry text box
_txtEntry = New TextBox
_txtEntry.ID = "EntryTextBox"
_txtEntry.Attributes.Add("AUTOCOMPLETE", "OFF")
'Create the submit button
_btnSubmit = New Button
_btnSubmit.ID = "CAPTCHASP_SubmitButton"
ButtonStyle.Width = New Unit(20, UnitType.Percentage)
_btnSubmit.CommandName = "CAPTCHASP_SubmitButton"
_btnSubmit.CommandArgument = "CAPTCHASP_SubmitButton"
'Create the Error label (only visible upon entry error)
_lblErr = New Label()
FailMessageStyle.Font.CopyFrom(Me.Font)
FailMessageStyle.ForeColor = Color.Red
'Add each control to the control collection with spacing
Controls.Add(_lblTitle)
Controls.Add(_imgMain)
Controls.Add(New
LiteralControl("
"))
Controls.Add(_lblInstruct)
Controls.Add(New
LiteralControl("
"))
Controls.Add(_txtEntry)
Controls.Add(New LiteralControl(" "))
Controls.Add(_btnSubmit)
Controls.Add(_lblErr)
End Sub
Figure 3: This subroutine is called from the OnInit event to initialize CAPTCHASP s constituent controls with their default values.
The first code block initializes an image tag that will be output in the page. This img tag will reference the CAPTCHA image generated by the control. This will be discussed more in the next section.
The rest of the code blocks are similar, initializing a label to display the instruction text, a textbox to hold the user s input, an error label that gets displayed only when the user enters an invalid code, and, finally, a submit button. The last code block of Figure 3 simply adds each control to the control collection of the base WebControl class. Appropriate spacing is inserted.
While the Init event triggers controls being created with their default values, the Render event is overridden to ensure customized (non-default) property values get rendered correctly, as shown in Figure 4.
Protected Overrides Sub Render(ByVal writer _
As System.Web.UI.HtmlTextWriter)
'Choose a codeword (if not already chosen)
If Me.CodeWord.Length = 0 Then
GenerateNewCodeWord()
End If
If Me.Controls.Count > 2 Then
_imgMain.ToolTip = Me.ImageToolTip
_lblInstruct.Text = Me.InstructionText
_lblInstruct.ApplyStyle(Me.InstructionStyle)
Me.TextEntryStyle.Width = _
New Unit(75, UnitType.Percentage)
_txtEntry.ApplyStyle(Me.TextEntryStyle)
_btnSubmit.Text = Me.SubmitButtonText
_btnSubmit.ApplyStyle(Me.ButtonStyle)
_btnSubmit.Visible = Me.ShowSubmitButton
_lblErr.Text = Me.ErrorMessage
_lblErr.ApplyStyle(Me.FailMessageStyle)
_lblErr.Visible = Me.ErrorMessage.Length > 3
End If
MyBase.Render(writer)
End Sub
Figure 4: The Render event is overridden to ensure non-default control settings are rendered appropriately.
The first code block of Figure 4 calls a function that randomly generates a new CAPTCHA CodeWord if one has not already been generated. Then customizable text, style, and visibility properties are applied to the constituent controls. Finally, the last line calls the base class Render method to ensure its functionality is executed normally.
Innovative Imaging
Custom Web controls that wish to render dynamic images have always been troublesome to create. This is because a control can typically output only HTML. So it s no problem to output an image tag in the HTML, but to what URL should it refer? Typically, such image-generating controls refer to a separate page or HTTP handler that generates the image, but this creates deployment problems because the user must remember to put that page in the right place or register that HTTP handler in their web.config file. If the user forgets or does it wrong, then things won t work and there is no easy way to help the user discover the problem. Sure, it can be documented that the user must take these extra steps, but not many people bother to read documentation. Most controls don t have requirements like this; instead, they work instantly as soon as the control is dropped on the page. Many users tend to move on if a control doesn t work right away, because they perceive the control to be broken or difficult to use.
Fellow MVP Oleg Tkachenko has documented an interesting technique for adding an HTTP handler reference to the web.config automatically at design time (http://www.tkachenko.com/blog/archives/000686.html), but this is complex code that only works if the control is viewed in the Visual Studio designer.
So I took another approach. Instead of referring to a separate page or HTTP handler, the image tag refers back to the page on which the CAPTCHASP component is hosted. The QueryString is replaced with a custom value, CAPTCHASP_Img=true, in this case. Figure 3 shows this URL alteration being done in the code block commented Create a quasi-callback URL . The first code block of the control s OnInit event (shown in Figure 2) reveals this request being intercepted. The result is that the OutputImage subroutine is called instead of the control s normal HTML output functionality. The OutputImage subroutine shown in Figure 5 clears the control s usual HTML output and replaces it with a custom-drawn CAPTCHA image.
'Write the CAPTCHA image to the output stream
'instead of the normal page/control contents
Protected Sub OutputImage()
Page.Response.Clear()
Page.Response.ContentType = "image/jpg"
Using Memstr As MemoryStream = New MemoryStream()
Using bmp As Bitmap = GenImage()
bmp.Save(Memstr, Imaging.ImageFormat.Jpeg)
Page.Response.BinaryWrite(Memstr.ToArray())
End Using
End Using
Page.Response.End()
End Sub
Figure 5: The OutputImage subroutine clears the CAPTCHASP control s normal HTML output and instead outputs an image.
This fairly unorthodox approach for having a control create its own image avoids dependencies on any external pages or HTTP handlers, and allows it to work the instant it is first dropped on a Web form without any additional configuration being required. There is no way the control s configuration or deployment can be botched so badly that the control will fail to work.
This technique may not work so well for a more general image-generation control that is meant to be used on a variety of pages, because some pages may implement some strange URL functionality of their own that could potentially conflict. However, a control such as CAPTCHASP is likely to be used on a small number of relatively simple pages that generally don t do much else besides CAPTCHA verification so it should work well in virtually all realistic situations. Additionally, the fact that the page s rendering process is intercepted very early (in the OnInit event) helps ensure that any custom page code will not significantly interfere.
Any existing HTML that may have been written to the page s output is cleared by the first line of code in the OutputImage subroutine. Then the content type of the new output is specified to be a JPEG image. A memory stream is then created to hold the bitmap generated by the GenImage function listed in Figure 6. Finally, the image is rendered into the page s Response object using the BinaryWrite method and the response is then explicitly ended to ensure no other page content gets written out. No further page code is executed.
'Generate the CAPTCHA image
Protected Function GenImage() As Bitmap
Dim Code As String = Me.CodeWord
'Calculate character spacing
Dim Rect As Rectangle = _
New Rectangle(0, 0, _ImageWidth, _ImageHeight)
_CharSpacing = CInt(((_ImageWidth - 20) / Code.Length))
'Generate the image
Using bmp As Bitmap = New Bitmap(Rect.Width, Rect.Height)
Using g As Graphics = Graphics.FromImage(bmp)
'Fill the image background with random coloring
Dim IsDarkClr As Boolean = _
Convert.ToBoolean(Int((2 * Rnd())))
Dim BackClr As Brush = _
GetRandBackgroundBrush(Rect, IsDarkClr)
g.FillRectangle(BackClr, Rect)
'Loop through and draw each character
_CharCount = 0
For Each ch As Char In Code
'Render the character into the image
DrawChar(g, Not IsDarkClr, BackClr, ch)
Next
End Using
Return CType(bmp.Clone(), Bitmap)
End Using
End Function
Figure 6: The GenImage function creates the actual CAPTCHA image that ultimately gets displayed.
The GenImage function first calculates the spacing that will be required for each character based on the length of the CodeWord and the size of the bitmap. A random brush is created to paint the background of the image. A gradient or patterned brush is randomly chosen with randomly selected colors to ensure a fairly unique image background is created each time. Then each character of the CodeWord is drawn on top of the background one by one. This character drawing is delegated to the DrawChar subroutine listed in Figure 7.
'Draw a character in the image
Protected Sub DrawChar(ByVal g As Graphics, _
ByVal UseDarkClr As Boolean, _
ByVal BackClr As Brush, _
ByVal Character As Char)
'random font
Dim fon As Font = GetRandFont()
'random size
Dim sz As SizeF = _
g.MeasureString(Character.ToString(), fon)
'random rotation
Dim ypos As Integer = _
CInt((_ImageHeight / 2) - (sz.Height / 2) - _
(RandRotate(g, _CharCount) * 2))
'Calculate character location
Dim xpos As Integer = CInt(_CharSpacing * _
(_CharCount) + (_CharSpacing / 3))
Dim RectCh As Rectangle = _
New Rectangle(xpos, ypos, _
CInt(sz.Width), CInt(sz.Height))
'Random text color
Dim BrushClr As Brush = GetRandSolidBrush(UseDarkClr)
'Draw the character
g.DrawString(Character.ToString(), _
fon, BrushClr, RectCh)
'clean up
UnRotate(g, BackClr)
_CharCount += Convert.ToByte(1)
End Sub
Figure 7: The DrawChar subroutine draws each character of the CAPTCHA CodeWord with random fonts, sizes, colors, and rotations.
The DrawChar subroutine first chooses a random font with which to draw the character. The font is picked from a carefully crafted list of standard fonts to ensure compatibility. A random size is then chosen for the font and it is rotated to a random degree. The character s location is then calculated semi-randomly in relation to the other characters. Then a random text color is chosen before the character is finally drawn onto the image using the DrawString method of the Graphics object. The imported System.Drawing namespace is used extensively for all of this image-creation code.
Conclusion
With more than 1,200 lines of source code in the CAPTCHASP control, and only a limited amount of space here to examine it, there is no way to cover every interesting function contained within the control. However, we have examined the most innovative and unique elements of the control; enough, certainly, to get you started on a similar CAPTCHA control of your own if you should choose to embark on such a journey.
If you re an asp.netPRO subscriber, you can download the complete source code and probe every nook and cranny of CAPTCHASP. Even if you re not an asp.netPRO subscriber, you can still download the compiled control and use it for free. You can download CAPTCHASP or try it out live right now from the demo pages I ve assembled at http://SteveOrr.net/demo/CAPTCHASP. Now your Web site can be immune from bot infestations.
Sample code accompanying this article is available for download.
Steve C. Orr is an ASPInsider, MCSD, Certified ScrumMaster, Microsoft MVP in ASP.NET, and author of the book Beginning ASP.NET 2.0 AJAX by Wrox. He s been developing software solutions for leading companies in the Seattle area for more than a decade. When he s not busy designing software systems or writing about them, he can often be found loitering at local user groups and habitually lurking in the ASP.NET newsgroup. Find out more about him at http://SteveOrr.net or e-mail him at mailto:[email protected].