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/CHANGES +138 -0
- data/COPYING +340 -0
- data/COPYRIGHT +19 -0
- data/README +26 -0
- data/Rakefile +75 -0
- data/TODO +1 -0
- data/lib/turing.rb +38 -0
- data/lib/turing/cgi_handler.rb +251 -0
- data/lib/turing/challenge.rb +214 -0
- data/lib/turing/image.rb +245 -0
- data/lib/turing/image_plugins/__squaring_helper.rb +66 -0
- data/lib/turing/image_plugins/black_squaring.rb +23 -0
- data/lib/turing/image_plugins/blending.rb +44 -0
- data/lib/turing/image_plugins/random_noise.rb +32 -0
- data/lib/turing/image_plugins/spiral.rb +56 -0
- data/lib/turing/image_plugins/white_squaring.rb +23 -0
- data/rdoc.jamis.rb +591 -0
- data/shared/README +9 -0
- data/shared/bgs/04.jpeg +0 -0
- data/shared/bgs/06.jpeg +0 -0
- data/shared/bgs/07.jpeg +0 -0
- data/shared/bgs/08.jpeg +0 -0
- data/shared/bgs/09.jpeg +0 -0
- data/shared/bgs/13.jpeg +0 -0
- data/shared/bgs/18.jpeg +0 -0
- data/shared/bgs/19.jpeg +0 -0
- data/shared/bgs/21.jpeg +0 -0
- data/shared/bgs/26.jpeg +0 -0
- data/shared/bgs/28.jpeg +0 -0
- data/shared/bgs/29.jpeg +0 -0
- data/shared/dictionary +411 -0
- data/shared/fonts/cour.ttf +0 -0
- data/shared/fonts/georgiai.ttf +0 -0
- data/shared/templates/challenge.rhtml +40 -0
- data/shared/templates/error.rhtml +32 -0
- data/shared/templates/success.rhtml +26 -0
- metadata +102 -0
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/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 :
|