Captcha challenge generator and verifier

Purpose of this class is to provide abstraction layer (on top of PStore and Turing::Image) you can use to build Captcha challenge/response mechanism.

Example of use:

  tc = Turing::Challenge.new(:store => 'store', :outdir => '.')
  c = tc.generate_challenge

  system("xv", c.file)

  puts "Enter solution:"
  r = $stdin.gets.chomp

  if tc.valid_answer?(c.id, r)
      puts "That's right."
  else
      puts "I don't think so."
  end

In this example records about generated challenges are stored in file store which is simple PStore. Images are generated via Turing::Image to current directory and then displayed via "xv" image viewer.

Methods
Constants
GeneratedChallenge = Struct.new(:file, :id)
  Generated challenge Struct — returned from generate_challenge
SHA_STRENGTH = 256
  SHA algorithm "strength" for random_id generation
RANDOM_DEVICE = '/dev/urandom'
  Random device used for random_id generation

Warning: don‘t use /dev/random unless you have LOTS of entropy available!

Public Class methods
new(opts = {})

Configure instance using options hash.

Warning: Keys of this hash must be symbols.

Accepted options:

  • store: File to be used as PStore for challenges. Default: $TMPDIR/turing-challenges.pstore.
  • dictionary: Filename to be used as dictionary (base for random words). Default: gem‘s shared/dictionary file.
  • lifetime: Lifetime for generated challenge in seconds (to prevent "harvesting").
  • outdir: Outdir for images generated by Turing::Image. Default: $TMPDIR.

Given hash will be also used to initialize Turing::Image object.

    # File lib/turing/challenge.rb, line 50
50:         def initialize(opts = {}) # {{{
51:                 raise ArgumentError, "Opts must be hash!" unless opts.kind_of? Hash
52: 
53:                 tmpdir = ENV["TMPDIR"] || '/tmp'
54:                 base = File.join(File.dirname(__FILE__), '..', '..', 'shared')
55:                 @options = {
56:                         :store => File.join(tmpdir, 'turing-challenges.pstore'),
57:                         :dictionary => File.join(base, 'dictionary'),
58:                         :lifetime => 10*60, # 10 minutes
59:                         :outdir => tmpdir,
60:                 }
61: 
62:                 @options.merge!(opts)
63: 
64:                 begin
65:                         @store = PStore.new(@options[:store])
66:                 rescue
67:                         raise ArgumentError, "Failed to initialize store: #{$!}"
68:                 end
69: 
70:                 begin
71:                         File.open(@options[:dictionary]) do |f|
72:                                 @dictionary = f.readlines.map! { |x| x.strip }
73:                         end
74:                 rescue
75:                         raise ArgumentError, "Failed to load dictionary: #{$!}"
76:                 end
77: 
78:                 begin
79:                         @ti = Turing::Image.new(@options)
80:                 rescue
81:                         raise ArgumentError, "Failed to initialize Turing::Image: #{$!}"
82:                 end
83:         end
Public Instance methods
generate_challenge(

Generate challenge (image containing random word from configured dictionary) and return GeneratedChallenge containing file (basename) and id of this challenge.

Generation of challenge is retried three times — to descrease possibility it will fail due to a bug in plugin. But if that happens, we just raise RuntimeError.

     # File lib/turing/challenge.rb, line 98
 98:         def generate_challenge # {{{
 99:                 id = nil
100:                 word = nil
101:                 tries = 3
102:                 err = nil
103:                 fname = nil
104:                 
105:                 begin
106:                         id = random_id
107:                         fname = id + ".jpg"
108:                         word = @dictionary[rand(@dictionary.size)]
109:                         @ti.generate(fname, word)
110:                 rescue Object => err
111:                         tries -= 1
112:                         retry if tries > 0
113:                 end
114:                 raise "Failed to generate: #{err}" unless err.nil?
115: 
116:                 begin
117:                         @store.transaction do
118:                                 @store[id] = ChallengeObject.new(word, Time.now)
119:                         end
120:                 rescue
121:                         raise "Failed to save to store: #{$!}"
122:                 end
123:                 
124:                 GeneratedChallenge.new(fname, id)
125:         end
valid_answer?(id, answer)

Check if answer for challenge with given id is valid.

Also removes image file and challenge from the store.

     # File lib/turing/challenge.rb, line 130
130:         def valid_answer?(id, answer) # {{{
131:                 ret = false
132:                 begin
133:                         @store.transaction do
134:                                 object = @store[id]
135: 
136:                                 # out if not found
137:                                 break if object.nil?
138: 
139:                                 # remove from store and delete img
140:                                 @store.delete(id)
141:                                 begin
142:                                         n = File.join(@options[:outdir], id + '.jpg')
143:                                         File.unlink(n)
144:                                 rescue Object
145:                                 end
146: 
147:                                 # true if it's ok
148:                                 if object.answer == answer && \
149:                                                 Time.now < object.when + (@options[:lifetime] || 0)
150:                                         ret = true
151:                                 end
152:                         end
153:                 rescue
154:                 end
155:                 ret
156:         end