We are a software consultancy based in Berlin, Germany. We deliver
high quality web apps in short timespans.

Upstream Agile GmbH

Captchas with rails and multiple servers

August 17, 2007 by alex

captcha

On autoki.com we have this “Tell-a-friend” functionality, where people can enter the email addresses of their friends and have the link to a cool photo or car sent to them. Until recently this was only accessible to logged in users because we were afraid of spam bots using it to send emails to everyone using our servers. At some point we realized that this function could be much more useful if everyone would be able to use it - member or not. The solution we chose to fight the spam bots was pretty standard: Captchas - Completely Automated Public Turing tests to tell Computers and Humans Apart. (I just love this name)

There are a couple of plugins for rails to implement captchas. Most of them display an image with distorted letters and numbers and ask the user to enter these in a form field. The alternative is to ask the user a simple question that a human can easily answer but a computer (as in spam bot) can not. The latter has two advantages: they work for blind people / text browsers and we don’t have to generate and store images - so we went with this one. validates_captcha (sorry couldn’t find a working link) is a plugin we have used before and it supports both - images and questions (so called logic captchas).

Now the only problem was this: autoki runs on multiple servers and the captcha validation process involves multiple http requests, which can each go to a different server. Validates_captcha stores the captchas in a local file, so if the generation of the captcha occurs on one server but the validation on another, that other server can’t find the captcha. The solution we chose is actually pretty simple: rewrite validates_captcha to use memcache instead of the local file.

To accomplish this we only had to change one file, captcha_challenge.rb. All we had to do was replace all the occurrences of store[:captchas] with memcache. The modified file now looks like this:

require 'digest/sha1'

module FleskPlugins #:nodoc:

  # This is an abstract class. Use one of its subclasses.
  class CaptchaChallenge

    include CaptchaConfig
    extend CaptchaConfig

    DEFAULT_TTL = 1200#Lifetime in seconds. Default is 20 minutes.

    attr_reader :id, :created_at
    attr_accessor :ttl

    @@types = HashWithIndifferentAccess.new

    def initialize(options = {}) #:nodoc:
      self.id = generate_id
      options = {
        :ttl => config['default_ttl'] || DEFAULT_TTL
      }.update(options)
      self.ttl = options[:ttl]
      @created_at = Time.now
      self.class.prune
    end

    # Implement in subclasses.
    def correct? #:nodoc:
      raise NotImplementedError
    end

    # Has this challenge expired?
    def expired?
      Time.now > self.created_at+self.ttl
    end

    def ==(other) #:nodoc:
      other.is_a?(self.class) && other.id == self.id
    end

  private

    def generate_id #:nodoc:
      "captcha_#{self.id = Digest::SHA1.hexdigest(Time.now.to_s+rand.to_s)}"
    end

    def id=(i) #:nodoc:
      @id = i
    end

    def write_to_store #:nodoc:
      CACHE.set self.id, self, self.ttl
    end

    class << self

      #Get the challenge type (class) registered with +name+
      def get(name)
        @@types[name]
      end

      #Register a challenge type (class) with +name+
      def register_name(name, klass = self)
        @@types[name] = klass
      end

      # Find a challenge from the storage based on its ID.
      def find(id)
        CACHE.get id
      end

      # Delete a challenge from the storage based on its ID.
      def delete(id)
        CACHE.delete id
      end

      # Removes old instances from PStore
      def prune
      end#prune
    end#class << self
  end
end#module FleskPlugins

With this, all captchas now get stored on the central memcache server and all the application servers have access to it. And it’s even better than the original, because memcache supports time to live, so old captchas get removed from the store automatically.

Note: We are using the MemcacheClient gem to access memcache from rails. The CACHE constant is declared in the environment files, so in production.rb we have this:

require 'memcache'

memcache_options = {
   :compression => false,
   :debug => false,
   :namespace => "autoki_#{RAILS_ENV}",
   :readonly => false,
   :urlencode => false
}
memcache_servers = [ 'cache.autoki.com:11211']

cache_params = *([memcache_servers, memcache_options].flatten)
CACHE = MemCache.new *cache_params

For development and testing we use a simple hash based stub:

class TestCache

  def initialize
    @items = {}
  end

  def set(key, object, ttl = nil)
    @items[key] = object
  end

  def get(key)
    @items[key]
  end

  def [](key)
    @items[key]
  end

  def []=(key, value)
    @items[key] = value
  end

  def delete(key)
    @items.delete key
  end

  def clear
    @items.clear
  end
end

CACHE = TestCache.new

Now the exercise left would be to factor out the storage code so the choice of captcha storage can be configured in the validates_captcha configuration, anyone? :)