Class: Turing::Challenge

Inherits:
Object
  • Object
show all
Defined in:
lib/turing/challenge.rb

Overview

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.

Defined Under Namespace

Classes: ChallengeObject, GeneratedChallenge

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Challenge

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.

Raises:

  • (ArgumentError)


50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/turing/challenge.rb', line 50

def initialize(opts = {}) # {{{
  raise ArgumentError, "Opts must be hash!" unless opts.kind_of? Hash

  tmpdir = ENV["TMPDIR"] || '/tmp'
  base = File.join(File.dirname(__FILE__), '..', '..', 'shared')
  @options = {
    :store => File.join(tmpdir, 'turing-challenges.pstore'),
    :dictionary => File.join(base, 'dictionary'),
    :lifetime => 10*60, # 10 minutes
    :outdir => tmpdir,
  }

  @options.merge!(opts)

  begin
    @store = PStore.new(@options[:store])
  rescue
    raise ArgumentError, "Failed to initialize store: #{$!}"
  end

  begin
    File.open(@options[:dictionary]) do |f|
      @dictionary = f.readlines.map! { |x| x.strip }
    end
  rescue
    raise ArgumentError, "Failed to load dictionary: #{$!}"
  end

  begin
    @ti = Turing::Image.new(@options)
  rescue
    raise ArgumentError, "Failed to initialize Turing::Image: #{$!}"
  end
end

Instance Method Details

#generate_challengeObject

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.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/turing/challenge.rb', line 98

def generate_challenge # {{{
  id = nil
  word = nil
  tries = 3
  err = nil
  fname = nil
  
  begin
    id = random_id
    fname = id + ".jpg"
    word = @dictionary[rand(@dictionary.size)]
    @ti.generate(fname, word)
  rescue Object => err
    tries -= 1
    retry if tries > 0
  end
  raise "Failed to generate: #{err}" unless err.nil?

  begin
    @store.transaction do
      @store[id] = ChallengeObject.new(word, Time.now)
    end
  rescue
    raise "Failed to save to store: #{$!}"
  end
  
  GeneratedChallenge.new(fname, id)
end

#valid_answer?(id, answer) ⇒ Boolean

Check if answer for challenge with given id is valid.

Also removes image file and challenge from the store.

Returns:

  • (Boolean)


130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/turing/challenge.rb', line 130

def valid_answer?(id, answer) # {{{
  ret = false
  begin
    @store.transaction do
      object = @store[id]

      # out if not found
      break if object.nil?

      # remove from store and delete img
      @store.delete(id)
      begin
        n = File.join(@options[:outdir], id + '.jpg')
        File.unlink(n)
      rescue Object
      end

      # true if it's ok
      if object.answer == answer && \
          Time.now < object.when + (@options[:lifetime] || 0)
        ret = true
      end
    end
  rescue
  end
  ret
end