Do the Captcha

Monday, 5th of December, 2005

Inspired by the previous post and the apparent lack of good stuff out there, I've decided to post my own code for making Captcha's. The library is simple yet efficient in its approach by applying some layers of confusion:

  1. It requires the use of a TrueType font, which is a hell-of-a-lot better than something like Courier
  2. By default, a set of three predefined (yet manually configurable) contrasting colours are available, making up the foreground colour pool
  3. Each individual letter uses a random colour from the foreground colour pool
  4. The letters are individually rotated by an arbitrary amount of degrees, which can be manually configured
  5. The background consists of a bunch of diagonal lines, forming a grid; the line colours are randomly chosen from the foreground colour pool

The following example code can be used to create your very own Captcha:

<?php

require_once('../codefarm/captcha.inc.php');

try {
    
$captcha = new Captcha_Generator('capcha',array(
        
'font'=>'../fonts/georgiab.ttf',
    ));
    
header('Content-Type: image/gif');
    echo
$captcha->generate();
} catch (
Exception $e) {
    
print_r($e);
}

?>

The result (image looks different every time you refresh the page):

test captcha

A good Captcha implementation involves at least the following elements:

<?php

class Captcha_Generator
{
    var
$options = array(
        
'size_x' => 180, // width
        
'size_y' => 30, // height
                
'fontsize_range' => array(15,20), // font size range
                
'rotate_range' => array(-40,40), // text rotation angle range
        
'color_bg' => '#ffffff', // background colour
        
'color_txt' => array( // possible foreground colours
            
'#0000cc',
            
'#cc0000',
            
'#00cc00',
        ),
        
'font' => '/path/to/ttf_file', // TTF file to be used
    
);
    var
$pw;

    function
__construct($pw,$options=null)
    {
                
// $pw = password to generate
        
$this->pw = $pw;
        if (!
is_null($options)) {
            foreach (
$options as $k => $v) {
                
$this->options[$k] = $v;
            }
        }
    }

    function
generate()
    {
        
$colcount = count($this->options['color_txt']);
        
$im = imagecreate($this->options['size_x'],$this->options['size_y']);
        
// set color ids
        
$cid_bg = $this->setColor($im,$this->options['color_bg']);
        for (
$x=0;$x<$colcount;++$x) {
            
$cid_txt[$x] = $this->setColor($im,$this->options['color_txt'][$x]);
        }

        for (
$x = 0; $x < $this->options['size_x']; $x+=10) {
            
imageline($im,$x,0,$x+$this->options['size_y'],$this->options['size_y'],$cid_txt[rand(0,$colcount-1)]);
            
imageline($im,$x,0,$x-$this->options['size_y'],$this->options['size_y'],$cid_txt[rand(0,$colcount-1)]);
        }
        
$x_pos = 5;
        for (
$j = 0; $j < strlen($this->pw); ++$j) {
            
imagettftext($im,rand($this->options['fontsize_range'][0],$this->options['fontsize_range'][1]),rand($this->options['rotate_range'][0],$this->options['rotate_range'][1]),$x_pos,21,$cid_txt[rand(0,$colcount-1)],$this->options['font'],$this->pw{$j});
            
$x_pos += 30;
        }
        
ob_start();
        
imagecolortransparent($im,$cid_bg);
        
imagegif($im);
        
$tmp = ob_get_contents();
        
ob_end_clean();

        return
$tmp;
    }

    private function
setColor($im,$color)
    {
        
sscanf($color, "#%2x%2x%2x", $red, $green, $blue);
        return
imagecolorallocate($im,$red,$green,$blue);
    }
}

?>
captcha.inc.php

Welcome to the world of web based SMS

Tuesday, 15th of November, 2005

Who doesn't want to have a server side script being able to send "free" SMS messages to your handphone in case of emergencies? Be the first who gets the warning signals from a dying server; you receive new laptop, pay rise, promotion ... ahhh, definitely worth spending some time on!

Within Singapore, at least three phone providers offer web based SMS services: SingTel, M1 and Starhub. I'm on SingTel myself, so my goal went out to this provider first. The first steps are always to find out what makes the application tick; in this case, investigate the page for FORM and INPUT elements.

The next step is writing the necessary code to simulate a web browser and replicate its behaviour. A lot of this work has already been done by other PHP fanatics and can be found on the PEAR website. Now that the tedious elements have been covered, we can solely focus on the business logic.

singtel captchaclose-up of singtel captcha

One thing worth mentioning was the way SingTel protects their online SMS service; this is done by using a so called Captcha. A Captcha is used to distinguish between computers and human beings by presenting an image which only humans can comprehend. Though, when you look closely, you might recognize a remarkable feature in the font they've used: it's a so called fixed font, all digits are 5 x 9 and are placed at an equal distance from each other.

In theory the Captcha could be decoded by inspecting the vertical lines inside the image; every digit has their own unique pattern of black pixels per column. This generic algorithm is considered O(1) because of the limited steps required, though you still need to inspect all 200+ pixels. Blegh, what a resouce hog! Can we optimize this process? Yes we can!

My final algorithm is based on the minimal set of pixel tests needed to correctly guess a particular digit. My little research started by collecting all possible digits in Photoshop, putting them into separate layers. With all layers visible, I made each layer temporarily hidden, inspect the changes, and made it visible again. If a certain pixel changes from black into white by hiding a particular layer, a single test on that pixel is enough to extract the digit; i call this a special digit. This step is repeated over all the layers (10) after which you remove the "special digit" layers and start over again. The digit '3' remains being the undetectable digit until the end.

This completes the fastest possible algorithm, needing a maximum of 27 tests. The whole code has been written into a factory model to complete the range of Singapore based SMS services.

<?php

abstract class SMS_Sender {
    function
__construct()
    {
        if (
false===@include_once('HTTP/Client.php')) {
            throw new
Exception('HTTP_Client not installed');
        }
    }

    function
factory($driver)
    {
        
$driver = strtolower($driver);
        
$class = "SMS_Sender_{$driver}";

        if (
class_exists($class)) {
            
$sender = new $class();
            return
$sender;
        } else {
            throw new
Exception("Driver '$driver' not found.");
        }
    }

    abstract function
send($nr,$msg);
}

class
SMS_Sender_singtel extends SMS_Sender
{
    function
__construct()
    {
        
parent::__construct();
        if (!
function_exists('imagecreatefromstring')) {
            throw new
Exception('GD library not installed');
        }
    }

    public function
send($nr,$msg)
    {
        
$client = new HTTP_Client();
        
$status = $client->get('http://home.singtel.com/consumer/msg_center/internet_sms.asp');
        if (
PEAR::isError($status)) {
            throw new
Exception($status->getMessage());
        }

        
$status = $client->get('http://home.singtel.com/consumer/msg_center/spamcode.asp?time_='.date('YmdHis'));
        if (
PEAR::isError($status)) {
            throw new
Exception($status->getMessage());
        }
        
$res = $client->currentResponse();

        
$data = array(
            
'typed_spam_code' => $this->ocr($res['body']),
            
'mobile_no' => $nr,
            
'message' => $msg,
        );
        
$status = $client->post('http://home.singtel.com/consumer/msg_center/internet_sms_process.asp',$data);
        if (
PEAR::isError($status)) {
            throw new
Exception($status->getMessage());
        }
        
$res = $client->currentResponse();
        if (
preg_match('/is being processed/',$res['body'])) {
            return
true;
        } else {
            
file_put_contents('response.txt',print_r($res,true));
            if (
preg_match('/not a SingTel Mobile Number/',$res['body'])) {
                throw new
Exception("$nr is not a SingTel Mobile Customer");
            } else {
                throw new
Exception('Message failed (Unknown reason)');
            }
        }
        return
true;
    }

    private function
ocr($buf)
    {
        
$im = imagecreatefromstring($buf);
        
$fg = 0; $txt = '';

        for (
$i = 0; $i != 3; $i++) {
            
$x = $i * 7 + 1;
            if (
imagecolorat($im,$x+0,0)===$fg) {
                
$txt .= '7';
            } elseif (
imagecolorat($im,$x+1,5)===$fg) {
                
$txt .= '9';
            } elseif (
imagecolorat($im,$x+1,1)===$fg) {
                
$txt .= '5';
            } elseif (
imagecolorat($im,$x+2,2)===$fg) {
                
$txt .= '4';
            } elseif (
imagecolorat($im,$x+0,8)===$fg/* || imagecolorat($im,$x+4,8)===$fg*/) {
                
$txt .= '2';
            } elseif (
imagecolorat($im,$x+1,2)===$fg) {
                
$txt .= '1';
            } elseif (
imagecolorat($im,$x+2,3)===$fg/* || imagecolorat($im,$x+3,3)===$fg*/) {
                
$txt .= '6';
            } elseif (
imagecolorat($im,$x+1,4)===$fg) {
                
$txt .= '8';
            } elseif (
imagecolorat($im,$x+4,4)===$fg) {
                
$txt .= '0';
            } else {
                
$txt .= '3';
            }
        }
        return
$txt;
    }
}

class
SMS_Sender_m1 extends SMS_Sender
{
    public function
send($nr,$msg)
    {
        
$client = new HTTP_Client();

        
$args = array(
            
'msisdn' => $nr,
            
'msg' => $msg,
        );

        
$status = $client->post(
            
"http://msgctr.m1.com.sg/MessageCentre/jsp_en/processMessage.jsp?msisdn=$nr",
            
$args
        
);
        if (
PEAR::isError($status)) {
            throw new
Exception($status->getMessage());
        }
        
$res = $client->currentResponse();
        if (
preg_match('#sendsms_nonsub.jsp\?notice=nonsub#',$res['body'])) {
            throw new
Exception("$nr is not an M1 subscribed customer");
        }
        return
true;
    }
}

class
SMS_Sender_starhub extends SMS_Sender
{
    public function
send($nr,$msg)
    {
        
$client = new HTTP_Client();

        
$client->setMaxRedirects(0);

        
$args = array(
            
'mobileR' => $nr,
            
'isNewSession' => 'true',
            
'name' => 'sender',
            
'msg' => $msg,
            
'new' => '1',
            
'numLeft' => 143 - strlen($msg),
        );

        
$status = $client->post(
            
"http://websms.starhub.com/websms/sendSMS/p_sendSMS.jsp",
            
$args
        
);
        if (
PEAR::isError($status)) {
            throw new
Exception($status->getMessage());
        }
        
$res = $client->currentResponse();
        if (
preg_match('#valid 8 digit StarHub Mobile number#',$res['body'])) {
            throw new
Exception("$nr is not a StarHub Mobile customer");
        }
        
file_put_contents('response.txt',print_r($res,true));
        return
true;
    }
}

try {
    
$sms = SMS_Sender::factory('singtel');
    
$sms->send('12345678','the quick brown fox jumped over the lazy dog.');
} catch (
Exception $e) {
    echo
"Exception {$e->getMessage()}\n";
}

?>
SMS_Sender.php

Valid XHTML 1.0 Transitional

This repository contains a collection of my professional work over the years as a PHP developer as well as personal projects.

My employers include triptic and muvee Technologies