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.
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! |
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.
[ show source ]
# 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
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.
[ show source ]
# 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
Check if answer for challenge with given id is valid.
Also removes image file and challenge from the store.
[ show source ]
# 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