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