captcha 1.2.1

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.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Winton Welsh
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,89 @@
1
+ captcha
2
+ =======
3
+
4
+ A Google-style captcha for enterprise Rails apps
5
+
6
+ Goals
7
+ -----
8
+
9
+ * Batch generate captchas
10
+ * Use ciphered filenames (no need to store filename/captcha pairs)
11
+ * Easy configuration
12
+ * Number of captchas
13
+ * Period for captcha refresh
14
+ * Colors, wave, implode
15
+ * Handle lots of users
16
+
17
+ Install
18
+ -------
19
+
20
+ script/plugin install git://github.com/winton/captcha.git
21
+
22
+ ### Create lib/captcha_config.rb (optional)
23
+
24
+ <pre>
25
+ Captcha::Config.new(
26
+ # Used for filename cipher
27
+ :password => 'something-unique',
28
+ # Captcha colors
29
+ :colors => {
30
+ :background => '#FFFFFF',
31
+ :font => '#080288'
32
+ },
33
+ # Number of captcha images to generate
34
+ :count => RAILS_ENV == 'production' ? 500 : 10,
35
+ # Where to write captchas
36
+ :destination => "#{RAILS_ROOT}/public/images/captchas",
37
+ # Generate new batch every day
38
+ :generate_every => 24 * 60 * 60
39
+ )
40
+ </pre>
41
+
42
+ See <code>lib/captcha/config.rb</code> for more options.
43
+
44
+ ### application_controller.rb
45
+
46
+ <pre>
47
+ class ApplicationController < ActionController::Base
48
+ acts_as_captcha
49
+ end
50
+ </pre>
51
+
52
+ You may now use the <code>reset_captcha</code> method in any controller.
53
+
54
+ ### user.rb
55
+
56
+ <pre>
57
+ class User < ActiveRecord::Base
58
+ acts_as_captcha :base => "base error when captcha fails", :field => "field error when captcha fails"
59
+ end
60
+ </pre>
61
+
62
+ With no parameters, a default error is added to the "captcha" field (<code>:field => true</code>).
63
+
64
+ Specify <code>:base => true</code> to use a default error for base.
65
+
66
+ ### In your view
67
+
68
+ <pre>
69
+ &lt;img src="/images/captchas/<%= session[:captcha] %>.jpg" /&gt;
70
+ <%= text_field_tag(:captcha) %>
71
+ </pre>
72
+
73
+ ### In your controller
74
+
75
+ <pre>
76
+ user = User.new
77
+ user.known_captcha = session[:captcha]
78
+ user.captcha = params[:captcha]
79
+ user.save
80
+ reset_captcha
81
+ </pre>
82
+
83
+ ### crontab
84
+
85
+ <pre>
86
+ 0 0 * * * cd /path/to/rails/app && /usr/bin/rake RAILS_ENV=production captcha:generate
87
+ </pre>
88
+
89
+ Your config file sets the captcha refresh period. The rake task just checks if its time to repopulate, and does so if necessary.
@@ -0,0 +1,32 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/gempackagetask'
4
+ require 'spec/rake/spectask'
5
+ require 'gemspec'
6
+
7
+ desc "Generate gemspec"
8
+ task :gemspec do
9
+ File.open("#{Dir.pwd}/#{GEM_NAME}.gemspec", 'w') do |f|
10
+ f.write(GEM_SPEC.to_ruby)
11
+ end
12
+ end
13
+
14
+ desc "Install gem"
15
+ task :install do
16
+ Rake::Task['gem'].invoke
17
+ `sudo gem uninstall #{GEM_NAME} -x`
18
+ `sudo gem install pkg/#{GEM_NAME}*.gem`
19
+ `rm -Rf pkg`
20
+ end
21
+
22
+ desc "Package gem"
23
+ Rake::GemPackageTask.new(GEM_SPEC) do |pkg|
24
+ pkg.gem_spec = GEM_SPEC
25
+ end
26
+
27
+ desc "Run specs"
28
+ Spec::Rake::SpecTask.new do |t|
29
+ t.rcov = true
30
+ t.spec_opts = ["--format", "specdoc", "--colour"]
31
+ t.spec_files = FileList["spec/**/*_spec.rb"]
32
+ end
@@ -0,0 +1,21 @@
1
+ GEM_NAME = 'captcha'
2
+ GEM_FILES = FileList['**/*'] - FileList[
3
+ 'coverage', 'coverage/**/*',
4
+ 'pkg', 'pkg/**/*'
5
+ ]
6
+ GEM_SPEC = Gem::Specification.new do |s|
7
+ # == CONFIGURE ==
8
+ s.author = "Winton Welsh"
9
+ s.email = "mail@wintoni.us"
10
+ s.homepage = "http://github.com/winton/#{GEM_NAME}"
11
+ s.summary = "A Google-style captcha for enterprise Rails apps"
12
+ # == CONFIGURE ==
13
+ s.add_dependency('rmagick', '>=2.9.2')
14
+ s.extra_rdoc_files = [ "README.markdown" ]
15
+ s.files = GEM_FILES.to_a
16
+ s.has_rdoc = false
17
+ s.name = GEM_NAME
18
+ s.platform = Gem::Platform::RUBY
19
+ s.require_path = "lib"
20
+ s.version = "1.2.1"
21
+ end
data/init.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'captcha'
2
+ if defined?(RAILS_ROOT) && File.exists?("#{RAILS_ROOT}/lib/captcha_config.rb")
3
+ require "#{RAILS_ROOT}/lib/captcha_config"
4
+ end
5
+
6
+ ActionController::Base.send :include, Captcha::Action
7
+ ActiveRecord::Base.send :include, Captcha::Model
@@ -0,0 +1,5 @@
1
+ require File.dirname(__FILE__) + "/captcha/action.rb"
2
+ require File.dirname(__FILE__) + "/captcha/image.rb"
3
+ require File.dirname(__FILE__) + "/captcha/config.rb"
4
+ require File.dirname(__FILE__) + "/captcha/cipher.rb"
5
+ require File.dirname(__FILE__) + "/captcha/generator.rb"
@@ -0,0 +1,34 @@
1
+ module Captcha
2
+ module Action
3
+
4
+ def self.included(base)
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_captcha
10
+ unless included_modules.include? InstanceMethods
11
+ include InstanceMethods
12
+ end
13
+ before_filter :assign_captcha
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ private
19
+
20
+ def assign_captcha
21
+ unless session[:captcha] && Captcha::Config.exists?(session[:captcha])
22
+ files = Captcha::Config.captchas
23
+ session[:captcha] = File.basename(files[rand(files.length)], '.jpg')
24
+ end
25
+ end
26
+
27
+ def reset_captcha
28
+ session[:captcha] = nil
29
+ assign_captcha
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,40 @@
1
+ require 'openssl'
2
+ require 'digest/sha1'
3
+
4
+ module Captcha
5
+ class Cipher
6
+ @@key = Digest::SHA1.hexdigest(Config.options[:password])
7
+ @@iv = 'captchas'*2
8
+
9
+ def self.encrypt(text)
10
+ # Encrypt
11
+ cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
12
+ cipher.encrypt
13
+ cipher.key = @@key
14
+ cipher.iv = @@iv
15
+ encrypted = cipher.update(text)
16
+ encrypted << cipher.final
17
+ # Turn into chr codes separated by underscores
18
+ # 135_14_163_53_43_135_172_31_1_23_169_81_49_110_49_230
19
+ encrypted = (0..encrypted.length-1).collect do |x|
20
+ encrypted[x]
21
+ end
22
+ encrypted.join('_')
23
+ end
24
+
25
+ def self.decrypt(text)
26
+ # Decode chr coded string
27
+ encrypted = text.split('_').collect do |x|
28
+ x.to_i.chr
29
+ end
30
+ encrypted = encrypted.join('')
31
+ # Decrypt
32
+ cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
33
+ cipher.decrypt
34
+ cipher.key = @@key
35
+ cipher.iv = @@iv
36
+ decrypted = cipher.update(encrypted)
37
+ decrypted << cipher.final
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,83 @@
1
+ module Captcha
2
+ class Config
3
+
4
+ if defined?(RAILS_ENV)
5
+ PRODUCTION = RAILS_ENV == 'production' || RAILS_ENV == 'staging'
6
+ ROOT = "#{RAILS_ROOT}/"
7
+ else
8
+ PRODUCTION = false
9
+ ROOT = ""
10
+ end
11
+ ONE_DAY = 24 * 60 * 60
12
+
13
+ @@options = {
14
+ :password => 'captcha',
15
+ :colors => {
16
+ :background => '#FFFFFF',
17
+ :font => '#080288'
18
+ },
19
+ # number of captcha images to generate
20
+ :count => PRODUCTION ? 500 : 10,
21
+ :destination => "#{ROOT}public/images/captchas",
22
+ :dimensions => {
23
+ # canvas height (px)
24
+ :height => 32,
25
+ # canvas width (px)
26
+ :width => 110
27
+ },
28
+ :generate_every => PRODUCTION ? ONE_DAY * 1 : ONE_DAY * 10000,
29
+ # http://www.imagemagick.org/RMagick/doc/image2.html#implode
30
+ :implode => 0.2,
31
+ :letters => {
32
+ # text baseline (px)
33
+ :baseline => 25,
34
+ # number of letters in captcha
35
+ :count => 6,
36
+ :ignore => ['a','e','i','o','u','l','j','q','v'],
37
+ # font size (pts)
38
+ :points => 38,
39
+ # width of a character (used to decrease or increase space between characters) (px)
40
+ :width => 17
41
+ },
42
+ :ttf => File.expand_path("#{File.dirname(__FILE__)}/../../resources/captcha.ttf"),
43
+ # http://www.imagemagick.org/RMagick/doc/image3.html#wave
44
+ :wave => {
45
+ # range is used for randomness (px)
46
+ :wavelength => (40..70),
47
+ # distance between peak and valley of sin wave (px)
48
+ :amplitude => 3
49
+ }
50
+ }
51
+
52
+ def initialize(options={})
53
+ @@options.merge!(options)
54
+ end
55
+
56
+ def self.captchas
57
+ Dir["#{@@options[:destination]}/*.jpg"]
58
+ end
59
+
60
+ def self.codes
61
+ self.captchas.collect do |f|
62
+ File.basename f, '.jpg'
63
+ end
64
+ end
65
+
66
+ def self.exists?(code)
67
+ File.exists?("#{@@options[:destination]}/#{code}.jpg")
68
+ end
69
+
70
+ def self.options
71
+ @@options
72
+ end
73
+
74
+ def self.last_modified
75
+ file = self.captchas.first
76
+ if file && File.exists?(file)
77
+ File.mtime(file)
78
+ else
79
+ nil
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,26 @@
1
+ module Captcha
2
+ class Generator
3
+ def initialize
4
+ generate
5
+ end
6
+
7
+ def generate
8
+ return unless Config.options
9
+ return if Config.last_modified && Config.last_modified > Time.now - Config.options[:generate_every]
10
+ path = Config.options[:destination]
11
+ Config.captchas.each do |captcha|
12
+ FileUtils.rm_f captcha
13
+ end
14
+ FileUtils.mkdir_p path
15
+ (1..Config.options[:count]).each do |x|
16
+ image = Image.new Config.options
17
+ path = "#{Config.options[:destination]}/#{Cipher.encrypt(image.code)}.jpg"
18
+ next if File.exists?(path)
19
+ File.open(path, 'w') do |f|
20
+ f << image.data
21
+ end
22
+ end
23
+ GC.start
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ require 'RMagick'
2
+
3
+ module Captcha
4
+ class Image
5
+
6
+ include Magick
7
+ attr_reader :code, :data
8
+
9
+ def initialize(o)
10
+ generate_code o
11
+
12
+ canvas = Magick::Image.new(o[:dimensions][:width], o[:dimensions][:height]) {
13
+ self.background_color = o[:colors][:background]
14
+ }
15
+
16
+ text = Magick::Draw.new
17
+ text.font = File.expand_path o[:ttf]
18
+ text.pointsize = o[:letters][:points]
19
+
20
+ cur = 0
21
+ @code.each { |c|
22
+ text.annotate(canvas, 0, 0, cur, o[:letters][:baseline], c) {
23
+ self.fill = o[:colors][:font]
24
+ }
25
+ cur += o[:letters][:width]
26
+ }
27
+
28
+ w = o[:wave][:wavelength]
29
+ canvas = canvas.wave(o[:wave][:amplitude], rand(w.last - w.first) + w.first)
30
+ canvas = canvas.implode(o[:implode])
31
+
32
+ @code = @code.to_s
33
+ @data = canvas.to_blob { self.format = "JPG" }
34
+ canvas.destroy!
35
+ end
36
+
37
+ private
38
+
39
+ def generate_code(o)
40
+ chars = ('a'..'z').to_a - o[:letters][:ignore]
41
+ @code = []
42
+ 1.upto(o[:letters][:count]) do
43
+ @code << chars[rand(chars.length)]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,56 @@
1
+ module Captcha
2
+ module Model
3
+ def self.included(base)
4
+ base.extend ActMethods
5
+ end
6
+
7
+ module ActMethods
8
+ def acts_as_captcha(options={})
9
+ extend ClassMethods
10
+ include InstanceMethods
11
+ attr_reader :captcha, :known_captcha
12
+ cattr_accessor :captcha_options
13
+ self.captcha_options = options
14
+ validate :captcha_must_match_known_captcha
15
+ end
16
+ end
17
+
18
+ module ClassMethods
19
+ end
20
+
21
+ module InstanceMethods
22
+ def captcha=(c)
23
+ @captcha = c || ''
24
+ end
25
+
26
+ def known_captcha=(c)
27
+ @known_captcha = c || ''
28
+ end
29
+
30
+ def captcha_must_match_known_captcha
31
+ return true if self.captcha.nil? || self.known_captcha.nil?
32
+ if self.captcha.strip.downcase != Captcha::Cipher.decrypt(self.known_captcha)
33
+ if self.captcha_options[:base]
34
+ self.errors.add_to_base(
35
+ case self.captcha_options[:base]
36
+ when true
37
+ "Enter the correct text in the image (6 characters)"
38
+ else
39
+ self.captcha_options[:base]
40
+ end
41
+ )
42
+ else
43
+ self.errors.add(:captcha,
44
+ case self.captcha_options[:field]
45
+ when true, nil
46
+ "text does not match the text in the image."
47
+ else
48
+ self.captcha_options[:field]
49
+ end
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
Binary file
@@ -0,0 +1,37 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe :captcha do
4
+ before(:each) do
5
+ @delay = 2 # 2 seconds
6
+ Captcha::Config.new(
7
+ :count => 10,
8
+ :destination => File.expand_path(File.dirname(__FILE__) + "/../tmp"),
9
+ :generate_every => @delay
10
+ )
11
+ FileUtils.rm_rf Captcha::Config.options[:destination]
12
+ @generator = Captcha::Generator.new
13
+ end
14
+ after(:all) do
15
+ FileUtils.rm_rf Captcha::Config.options[:destination]
16
+ end
17
+ it "should generate captchas" do
18
+ Captcha::Config.codes.length.should == 10
19
+ end
20
+ it "should generate fresh captchas if the files are older than the generate_every option" do
21
+ codes = Captcha::Config.codes
22
+ sleep @delay
23
+ @generator.generate
24
+ codes.should_not == Captcha::Config.codes
25
+ end
26
+ it "should not regenerate before the files are older than the generate_every option" do
27
+ codes = Captcha::Config.codes
28
+ sleep 1
29
+ @generator.generate
30
+ codes.should == Captcha::Config.codes
31
+ end
32
+ it "should not allow more than the captcha limit to exist" do
33
+ sleep @delay
34
+ @generator.generate
35
+ Captcha::Config.codes.length.should == 10
36
+ end
37
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,5 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'rubygems'
4
+ require 'captcha'
5
+ require 'spec'
@@ -0,0 +1,6 @@
1
+ namespace :captcha do
2
+ desc 'Generate a batch of captchas'
3
+ task :generate => :environment do
4
+ Captcha::Generator.new
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: captcha
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Winton Welsh
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-06 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rmagick
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 2.9.2
24
+ version:
25
+ description:
26
+ email: mail@wintoni.us
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - README.markdown
33
+ files:
34
+ - gemspec.rb
35
+ - init.rb
36
+ - lib/captcha/action.rb
37
+ - lib/captcha/cipher.rb
38
+ - lib/captcha/config.rb
39
+ - lib/captcha/generator.rb
40
+ - lib/captcha/image.rb
41
+ - lib/captcha/model.rb
42
+ - lib/captcha.rb
43
+ - MIT-LICENSE
44
+ - Rakefile
45
+ - README.markdown
46
+ - resources/captcha.ttf
47
+ - spec/lib/captcha_spec.rb
48
+ - spec/spec.opts
49
+ - spec/spec_helper.rb
50
+ - tasks/captcha.rake
51
+ has_rdoc: true
52
+ homepage: http://github.com/winton/captcha
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: "0"
71
+ version:
72
+ requirements: []
73
+
74
+ rubyforge_project:
75
+ rubygems_version: 1.3.5
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: A Google-style captcha for enterprise Rails apps
79
+ test_files: []
80
+