turing 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/COPYRIGHT ADDED
@@ -0,0 +1,19 @@
1
+ Turing -- Ruby implementation of Captcha
2
+
3
+ Copyright (C) 2005 Michal Safranek <wejn@box.cz>
4
+
5
+ This file is part of http://turing.rubyforge.org/
6
+
7
+ Turing is free software; you can redistribute it and/or modify it under
8
+ the terms of the GNU General Public License as published by the Free
9
+ Software Foundation; either version 2 of the License, or (at your option)
10
+ any later version.
11
+
12
+ This program is distributed in the hope that it will be useful, but
13
+ WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
14
+ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15
+ for more details.
16
+
17
+ You should have received a copy of the GNU General Public License along
18
+ with this program; if not, write to the Free Software Foundation, Inc.,
19
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
data/README ADDED
@@ -0,0 +1,26 @@
1
+ = Turing - Ruby implementation of Captcha
2
+
3
+ == Purpose / mission
4
+ This project is aimed to provide easy-to-use and easy-to-extend
5
+ implementation of Captcha[http://captcha.net/].
6
+
7
+ == Download
8
+ Latest version of Turing can be found at
9
+
10
+ * http://rubyforge.org/frs/?group_id=1132
11
+
12
+ Unfortunately there's no public SCM repository as of now.
13
+
14
+ == Installation
15
+ This package is installed as gem, so simple
16
+
17
+ gem install --remote turing
18
+
19
+ should do the trick.
20
+
21
+ == Examples
22
+ Examples describing how to use this library can be found in class
23
+ descriptions and also in +samples+ directory.
24
+
25
+ == License
26
+ Turing is provided under GPL version 2, see COPYRIGHT for details.
data/Rakefile ADDED
@@ -0,0 +1,75 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rake/rdoctask'
4
+
5
+ $:.unshift 'lib'
6
+ require 'turing'
7
+
8
+ spec = Gem::Specification.new do |s|
9
+ s.name = "turing"
10
+ s.version = Turing::VERSION
11
+ s.author = "Michal Safranek"
12
+ s.email = "wejn@box.cz"
13
+ s.homepage = "http://turing.rubyforge.org/"
14
+ s.rubyforge_project = 'turing'
15
+ s.platform = Gem::Platform::RUBY
16
+ s.summary = "Another implementation of captcha (http://captcha.net/)"
17
+ s.description = 'Implementation of captcha (Completely Automated Public Turing-Test to Tell Computers and Humans Apart) that is both easy to use and easy to customize/extend.'
18
+
19
+ candidates = Dir.glob("{bin,lib,shared,tests}/**/*")
20
+ candidates = candidates.delete_if do |item|
21
+ item.include?(".svn") || item.include?("rdoc")
22
+ end
23
+ candidates += ['README', 'CHANGES', 'TODO', 'COPYING', 'COPYRIGHT', 'Rakefile', 'rdoc.jamis.rb']
24
+ s.files = candidates
25
+ s.executables = candidates.select { |x| x =~ /^bin\// }.
26
+ map { |x| File.basename(x) }
27
+
28
+ s.required_ruby_version = '>=1.8.2'
29
+
30
+ s.require_path = "lib"
31
+ s.autorequire = 'turing'
32
+
33
+ #s.test_file = "tests/ts_all.rb"
34
+
35
+ s.has_rdoc = true
36
+ s.rdoc_options << '--title' << 'Turing Documentation' << '--charset' \
37
+ << 'utf-8' << '--line-numbers' << '--inline-source' << '--main' \
38
+ << 'README' << '-T' << './rdoc.jamis.rb'
39
+ s.extra_rdoc_files = ["README"]
40
+
41
+ s.add_dependency("gd2", '>=1.0')
42
+ end
43
+
44
+ Rake::GemPackageTask.new(spec) do |pkg|
45
+ pkg.need_tar = true
46
+ end
47
+
48
+ Rake::RDocTask.new("doc") do |rdoc|
49
+ rdoc.rdoc_dir = 'doc/'
50
+ rdoc.title = "Turing Documentation"
51
+ rdoc.options << '--line-numbers --inline-source --charset utf-8 --main README'
52
+ rdoc.rdoc_files.include('README')
53
+ rdoc.rdoc_files.include('lib/**/*.rb')
54
+
55
+ rdoc.template = './rdoc.jamis.rb'
56
+ end
57
+
58
+ task :default => [:gem, :doc]
59
+
60
+ file "CHANGES" do
61
+ system "svn log . > CHANGES"
62
+ end
63
+
64
+ task :cleanup do
65
+ system "rm -rf pkg doc CHANGES"
66
+ end
67
+
68
+ task :release do
69
+ rel = "file:///home/wejn/data/SVN-repo/turing/branches/REL-#{Turing::VERSION}"
70
+ target = "turing-#{Turing::VERSION}"
71
+ system "svn co #{rel} #{target}"
72
+ system "svn log #{rel} > #{target}/CHANGES"
73
+ system "tar zcf #{target}.tgz #{target}"
74
+ system "rm -rf #{target}"
75
+ end
data/TODO ADDED
@@ -0,0 +1 @@
1
+ - unit tests (how?)
data/lib/turing.rb ADDED
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # Turing -- Ruby implementation of Captcha
4
+ #
5
+ # Copyright © 2005 Michal Šafránek <wejn@box.cz>
6
+ #
7
+ # This file is part of http://turing.rubyforge.org/
8
+ #
9
+ # Turing is free software; you can redistribute it and/or modify it under
10
+ # the terms of the GNU General Public License as published by the Free
11
+ # Software Foundation; either version 2 of the License, or (at your option)
12
+ # any later version.
13
+ #
14
+ # This program is distributed in the hope that it will be useful, but
15
+ # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16
+ # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
17
+ # for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along
20
+ # with this program; if not, write to the Free Software Foundation, Inc.,
21
+ # 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
22
+ #
23
+
24
+ # == Turing module
25
+ # This module contains three classes (built on top of each other) that might
26
+ # be used independently:
27
+ # * Turing::Image: Simple obfuscated image generator with plugin design.
28
+ # * Turing::Challenge: Captcha challenge generator and verifier.
29
+ # * Turing::CGIHandler: Simple Turing::Challenge wrapper designed to run as CGI.
30
+ module Turing # {{{
31
+ VERSION = "0.0.7".freeze
32
+ end # }}}
33
+
34
+ require 'turing/image'
35
+ require 'turing/challenge'
36
+ require 'turing/cgi_handler'
37
+
38
+ # vim: set ts=4 sw=4 :
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # Turing -- Ruby implementation of Captcha
4
+ #
5
+ # Copyright © 2005 Michal Šafránek <wejn@box.cz>
6
+ #
7
+ # This file is part of http://turing.rubyforge.org/
8
+ #
9
+ # See turing.rb in lib/ directory for license terms.
10
+ #
11
+ require 'cgi'
12
+ require 'turing'
13
+ require 'turing/challenge'
14
+ require 'erb'
15
+
16
+ # == Generic (Fast)CGI handler to ease integration into current systems
17
+ #
18
+ # Handles CGI requests using prepared HTML templates and Turing::Challenge
19
+ # object.
20
+ #
21
+ # Example of use:
22
+ # #!/usr/bin/ruby
23
+ # require 'rubygems'
24
+ # require_gem 'turing'
25
+ # tcgi_config = {
26
+ # :imagepath => "/imgs",
27
+ # :outdir => '/home/wejn/ap/htdocs/imgs',
28
+ # :store => '/home/wejn/ap/data/turing.pstore',
29
+ # :redirect_to => 'http://localhost:8000/secured/',
30
+ # }
31
+ # tcgi_config[:on_success] = proc do
32
+ # out = {}
33
+ # out[:headers] = {
34
+ # "cookie" => CGI::Cookie.new({
35
+ # 'name' => 'turing_passed',
36
+ # 'value' => 'true',
37
+ # 'path' => '/',
38
+ # 'expires' => Time.now + 3600*24,
39
+ # }),
40
+ # "dude" => "you_rock!",
41
+ # }
42
+ # out
43
+ # end
44
+ # Turing::CGIHandler.new(tcgi_config).handle
45
+ # This CGI script forces user to pass turing challenge. After he passes
46
+ # the test, he's given cookie +turing_passed+ with value +true+ and redirected
47
+ # to http://localhost:8000/secured/.
48
+ #
49
+ # <b>Please note</b>: Using this script verbatim is like having no turing
50
+ # challenge at all -- any non-braindead attacker will get around it in no
51
+ # time.
52
+ class Turing::CGIHandler # {{{
53
+ # Configure instance using options hash.
54
+ #
55
+ # *Warning*: Keys of this hash must be symbols.
56
+ #
57
+ # Accepted options:
58
+ # * +templatedir+: directory where templates (challenge.rhtml, error.rhtml, success.rhtml) reside, by default it's set to gem's <tt>shared/templates/</tt> directory.
59
+ # * +on_success+: proc object (or any object responding to +call+) which returns Hash. Recognized keys of the returned hash are +headers+ and +variables+. Former are used as HTTP response headers, latter are used as variables for success template (if no redirect given).
60
+ # * +redirect_to+: redirect to this location on success. This can be also done by returning +Location+ header in +on_success+'s +headers+ hash.
61
+ # * +imagepath+: path under which generated images are accessible.
62
+ #
63
+ # Given hash will be also used to initialize Turing::Challenge object.
64
+ def initialize(options = {}) # {{{
65
+ @options = options
66
+ base = File.join(File.dirname(__FILE__), '..', '..', 'shared')
67
+
68
+ if @options[:templatedir].nil?
69
+ @options[:templatedir] = File.join(base, 'templates')
70
+ end
71
+
72
+ begin
73
+ td = @options[:templatedir]
74
+ raise "not directory" unless FileTest.directory?(td)
75
+ rescue
76
+ error_response("Templatedir is invalid", $!)
77
+ end
78
+
79
+ begin
80
+ @tc = Turing::Challenge.new(@options)
81
+ rescue
82
+ error_response("Unable to initialize Turing::Challenge class", $!)
83
+ exit 1
84
+ end
85
+ end # }}}
86
+
87
+ # Handle incoming CGI request.
88
+ #
89
+ # If you don't pass one, it will create it using CGI.new
90
+ def handle(cgi = nil) # {{{
91
+ cgi ||= CGI.new
92
+
93
+ if "POST" == cgi.request_method
94
+ if @tc.valid_answer?(cgi["id"], cgi["solution"])
95
+ # process on_success
96
+ os = @options[:on_success]
97
+ if os.nil? || ! os.respond_to?(:call)
98
+ extra = nil
99
+ else
100
+ extra = verify_extra(os.call)
101
+ end
102
+
103
+ # is there redir from config or from on_success?
104
+ if @options[:redirect_to] || extra[:headers]["Location"]
105
+ redirect_to(cgi, extra[:headers]["Location"] || \
106
+ @options[:redirect_to], extra)
107
+ else
108
+ success_response(cgi, extra)
109
+ end
110
+ else
111
+ show_challenge(cgi, true)
112
+ end
113
+ else
114
+ show_challenge(cgi)
115
+ end
116
+ end # }}}
117
+
118
+ private
119
+
120
+ # generate error exception
121
+ def error_response(what, exc, cgi = nil) # {{{
122
+ cgi ||= CGI.new
123
+ $stderr.puts "What: #{$!.to_s}"
124
+ cgi.out do
125
+ begin
126
+ Template.new(template_file("error.rhtml"), {
127
+ :what => what,
128
+ :explanation => exc.to_s,
129
+ :backtrace => exc.backtrace.join("\n"),
130
+ }).render
131
+ rescue
132
+ err = [what, '', exc.to_s, '', exc.backtrace.join("\n")]
133
+ "<pre>" + CGI.escapeHTML(err.join("\n")) + "</pre>"
134
+ end
135
+ end
136
+ end # }}}
137
+
138
+ # generate challenge and show it
139
+ def show_challenge(cgi, bad_try = false) # {{{
140
+ c = nil
141
+ begin
142
+ c = @tc.generate_challenge
143
+ rescue
144
+ error_response("Unable to generate challenge", $!, cgi)
145
+ end
146
+
147
+ cgi.out do
148
+ begin
149
+ Template.new(template_file("challenge.rhtml"), {
150
+ :already_failed => bad_try,
151
+ :file => File.join(@options[:imagepath] || '', c.file),
152
+ :id => c.id,
153
+ }).render
154
+ rescue => exc
155
+ error_response("Unable to render template", exc, cgi)
156
+ end
157
+ end
158
+ end # }}}
159
+
160
+ # show success response
161
+ def success_response(cgi, extra = {}) # {{{
162
+ cgi.out(extra[:headers]) do
163
+ begin
164
+ Template.new(template_file("success.rhtml"),
165
+ extra[:variables]).render
166
+ rescue => exc
167
+ error_response("Unable to render template", exc, cgi)
168
+ end
169
+ end
170
+ end # }}}
171
+
172
+ # redirect to given location
173
+ def redirect_to(cgi, where, extra = {}) # {{{
174
+ params = {
175
+ "Location" => where,
176
+ }
177
+ params.merge!(extra[:headers])
178
+ cgi.out(params) do
179
+ <<-EOF
180
+ <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
181
+ <HTML><HEAD>
182
+ <META HTTP-EQUIV='refresh' CONTENT='1;url=#{CGI.escapeHTML(where.to_s)}'>
183
+ <TITLE>302 Found</TITLE>
184
+ </HEAD><BODY>
185
+ <H1>Found</H1>
186
+ The document has moved <A HREF="#{CGI.escapeHTML(where.to_s)}">here</A>.<P>
187
+ </BODY></HTML>
188
+ EOF
189
+ end
190
+
191
+ end # }}}
192
+
193
+ # verify/correct "extra" object returned from on_success
194
+ def verify_extra(extra) # {{{
195
+ out = {
196
+ :headers => {},
197
+ :variables => {},
198
+ }
199
+ extra = {} if extra.nil? || ! extra.kind_of?(Hash)
200
+ out[:headers] = extra[:headers] if extra[:headers].kind_of?(Hash)
201
+ out[:variables] = extra[:variables] if extra[:variables].kind_of?(Hash)
202
+ out
203
+ end # }}}
204
+
205
+ # template name to abs path to file
206
+ def template_file(name) # {{{
207
+ name += ".rhtml" unless name =~ /\.rhtml$/
208
+ File.join(@options[:templatedir], name)
209
+ end # }}}
210
+
211
+ public
212
+
213
+ # == Simple template engine
214
+ # Essentially just convenient wrapper around ERB which is used internally by Turing::CGIHandler.
215
+ class Template # {{{
216
+ # Specify template file (+what+) and +variables+ that will be pushed as instance variables to the Template.
217
+ def initialize(what, variables = nil) # {{{
218
+ (variables || {}).each do |k,v|
219
+ instance_variable_set("@" + k.to_s, v)
220
+ end
221
+ @__what__ = what
222
+ end # }}}
223
+
224
+ # render given template and return result as string.
225
+ def render # {{{
226
+ erb = ERB.new(File.open(@__what__).read, nil, '%-')
227
+ erb.result(binding)
228
+ rescue
229
+ raise "Failure rendering template #{@what}: #{$!}"
230
+ end # }}}
231
+
232
+ # shortcut for <tt>CGI.escapeHTML</tt>
233
+ #
234
+ # <i>can you say "Rails" ? :)</i>
235
+ def h(var) # {{{
236
+ CGI.escapeHTML(var)
237
+ end # }}}
238
+ end # }}}
239
+ end # }}}
240
+
241
+ # execute if called directly
242
+ if __FILE__ == $0 # {{{
243
+ Turing::CGIHandler.new({
244
+ :imagepath => '.',
245
+ :outdir => '.',
246
+ :store => 'store',
247
+ }).handle
248
+ exit 0 # 'turing' requires 'turing/challenge' which might make loop
249
+ end # }}}
250
+
251
+ # vim: set ts=4 sw=4 :
@@ -0,0 +1,214 @@
1
+ #!/usr/bin/ruby
2
+ #
3
+ # Turing -- Ruby implementation of Captcha
4
+ #
5
+ # Copyright © 2005 Michal Šafránek <wejn@box.cz>
6
+ #
7
+ # This file is part of http://turing.rubyforge.org/
8
+ #
9
+ # See turing.rb in lib/ directory for license terms.
10
+ #
11
+ require 'pstore'
12
+ require 'turing'
13
+ require 'turing/image'
14
+ require 'digest/sha2'
15
+
16
+ # == Captcha challenge generator and verifier
17
+ # Purpose of this class is to provide abstraction layer (on top of PStore
18
+ # and Turing::Image) you can use to build Captcha challenge/response
19
+ # mechanism.
20
+ #
21
+ # Example of use:
22
+ # tc = Turing::Challenge.new(:store => 'store', :outdir => '.')
23
+ # c = tc.generate_challenge
24
+ #
25
+ # system("xv", c.file)
26
+ #
27
+ # puts "Enter solution:"
28
+ # r = $stdin.gets.chomp
29
+ #
30
+ # if tc.valid_answer?(c.id, r)
31
+ # puts "That's right."
32
+ # else
33
+ # puts "I don't think so."
34
+ # end
35
+ # In this example records about generated challenges are stored in file
36
+ # +store+ which is simple PStore. Images are generated via Turing::Image
37
+ # to current directory and then displayed via "xv" image viewer.
38
+ class Turing::Challenge # {{{
39
+ # Configure instance using options hash.
40
+ #
41
+ # *Warning*: Keys of this hash must be symbols.
42
+ #
43
+ # Accepted options:
44
+ # * +store+: File to be used as PStore for challenges. Default: <tt>$TMPDIR/turing-challenges.pstore</tt>.
45
+ # * +dictionary+: Filename to be used as dictionary (base for random words). Default: gem's <tt>shared/dictionary</tt> file.
46
+ # * +lifetime+: Lifetime for generated challenge in seconds (to prevent "harvesting").
47
+ # * +outdir+: Outdir for images generated by Turing::Image. Default: <tt>$TMPDIR</tt>.
48
+ #
49
+ # Given hash will be also used to initialize Turing::Image object.
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 # }}}
84
+
85
+ # Challenge object we store
86
+ ChallengeObject = Struct.new(:answer, :when) # :nodoc:
87
+
88
+ # Generated challenge Struct -- returned from generate_challenge
89
+ GeneratedChallenge = Struct.new(:file, :id)
90
+
91
+ # Generate challenge (image containing random word from configured
92
+ # dictionary) and return +GeneratedChallenge+ containing +file+
93
+ # (basename) and +id+ of this challenge.
94
+ #
95
+ # Generation of challenge is retried three times -- to descrease possibility
96
+ # it will fail due to a bug in plugin. But if that happens, we just raise
97
+ # +RuntimeError+.
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 # }}}
126
+
127
+ # Check if +answer+ for challenge with given +id+ is valid.
128
+ #
129
+ # Also removes image file and challenge from the store.
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 # }}}
157
+
158
+ private
159
+ # SHA algorithm "strength" for +random_id+ generation
160
+ SHA_STRENGTH = 256 # 256, 384, 512
161
+
162
+ # Random device used for +random_id+ generation
163
+ #
164
+ # *Warning*: don't use /dev/random unless you have LOTS of entropy available!
165
+ RANDOM_DEVICE = '/dev/urandom' # '/dev/random'
166
+
167
+ # generate random ID using SHA* algo family
168
+ def random_id # {{{
169
+ strength = SHA_STRENGTH
170
+ algo = nil
171
+ begin
172
+ algo = Digest.const_get("SHA" + strength.to_s)
173
+ rescue
174
+ # We got wrong SHA_STRENGTH, fallback to default (256) if possible
175
+ unless strength == 256
176
+ strength = 256
177
+ retry
178
+ else
179
+ raise RuntimeError, "SHA hash algorithm family not available"
180
+ end
181
+ end
182
+ begin
183
+ algo.hexdigest(File.open(RANDOM_DEVICE).sysread(strength / 8 + 1))
184
+ rescue
185
+ # XXX: fall-back, but shouldn't happen
186
+ poor_randomness = [
187
+ Process.pid, rand(65535), Time.now.to_i, Time.now.usec,
188
+ rand(65535)].map { |x| x.to_s }.join(':')
189
+ algo.hexdigest(poor_randomness)
190
+ end
191
+ end # }}}
192
+ end # }}}
193
+
194
+ # test it if invoked directly
195
+ if __FILE__ == $0 # {{{
196
+ tc = Turing::Challenge.new(:store => 'store', :outdir => '.')
197
+
198
+ c = tc.generate_challenge
199
+
200
+ system("xv", c.file)
201
+
202
+ puts "Enter solution:"
203
+ r = $stdin.gets.chomp
204
+
205
+ if tc.valid_answer?(c.id, r)
206
+ puts "That's right."
207
+ else
208
+ puts "I don't think so."
209
+ end
210
+
211
+ exit 0
212
+ end # }}}
213
+
214
+ # vim: set ts=4 sw=4 :