winton-captcha 1.1.0 → 1.2.0

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/MIT-LICENSE ADDED
@@ -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.
data/README.markdown CHANGED
@@ -1,8 +1,19 @@
1
1
  captcha
2
2
  =======
3
3
 
4
- An Rmagick based, Google-style captcha generator.
4
+ A Google-style captcha for enterprise Rails apps
5
5
 
6
+ Goals
7
+ -----
8
+
9
+ * Batch generate captchas
10
+ * Use ciphered filenames (no need to store filename/captcha code pairs)
11
+ * Captchas refresh automatically
12
+ * Easy configuration
13
+ ** Number of captchas
14
+ ** Period for captcha refresh
15
+ ** Colors, wave, implode
16
+ * Handle lots of users
6
17
 
7
18
  Install
8
19
  -------
@@ -12,7 +23,9 @@ Install
12
23
  ### Create lib/captcha_config.rb (optional)
13
24
 
14
25
  <pre>
15
- {
26
+ Captcha::Config.new(
27
+ # Used for filename cipher
28
+ :password => 'something-unique',
16
29
  # Captcha colors
17
30
  :colors => {
18
31
  :background => '#FFFFFF',
@@ -23,8 +36,8 @@ Install
23
36
  # Where to write captchas
24
37
  :destination => "#{RAILS_ROOT}/public/images/captchas",
25
38
  # Generate new batch every day
26
- :generate_every => RAILS_ENV == 'production' ? 24 * 60 * 60 : 10 ** 8
27
- }
39
+ :generate_every => 24 * 60 * 60
40
+ )
28
41
  </pre>
29
42
 
30
43
  See <code>lib/captcha/config.rb</code> for more options.
@@ -35,50 +48,40 @@ See <code>lib/captcha/config.rb</code> for more options.
35
48
  public/images/captchas/*
36
49
  </pre>
37
50
 
38
- ### Create a captcha controller
39
-
40
- script/generate controller Captchas
41
-
42
- ### Add the resource to your routes.rb
43
-
44
- map.resource :captcha
45
-
46
- ### Add acts\_as\_captcha to captchas_controller.rb
51
+ ### Add acts\_as\_captcha to application_controller.rb
47
52
 
48
53
  <pre>
49
- class CaptchasController < ApplicationController
54
+ class ApplicationController < ActionController::Base
50
55
  acts_as_captcha
51
56
  end
52
- </pre>
57
+ </pre>
58
+
59
+ You may now use the <code>reset_captcha</code> method in any controller.
53
60
 
54
61
  ### Add acts\_as\_captcha to your model
55
62
 
56
63
  <pre>
57
- acts_as_captcha "does not have the correct code"
64
+ class User < ActiveRecord::Base
65
+ acts_as_captcha :base => "base error when captcha fails", :field => "field error when captcha fails"
66
+ end
58
67
  </pre>
59
68
 
60
- The parameter is either the error added to the captcha input or nil to add a generic error to base.
69
+ No parameters behaves like <code>:field => true</code>, a default error is added to the "captcha" field.
70
+
71
+ Use <code>:base => true</code> to activate a default error for base.
61
72
 
62
73
  ### In your view
63
74
 
64
75
  <pre>
65
- <img src="/captcha?<%= Time.now.to_i %>" onclick="this.src = '/captcha/new?' + (new Date()).getTime()" />
76
+ &lt;img src="/images/captchas/<%= session[:captcha] %>.jpg" /&gt;
66
77
  <%= text_field_tag(:captcha) %>
67
78
  </pre>
68
79
 
69
80
  ### In your controller
70
81
 
71
82
  <pre>
72
- model_instance.known_captcha = session[:captcha]
73
- model_instance.captcha = params[:captcha]
74
- </pre>
75
-
76
- ### More information
77
-
78
- Captchas will generate and re-generate automatically when a Rails instance starts. Captcha keeps the old batch of captchas around until the next re-generate so users do not get 404s.
79
-
80
- We used the following URLs in our view:
81
- * <code>/captcha</code> to retrieve the image
82
- * <code>/captcha/new</code> to grab a new image
83
-
84
- Use <code>session[:captcha]</code> to store, retrieve, and reset the captcha code.
83
+ user = User.new
84
+ user.known_captcha = session[:captcha]
85
+ user.captcha = params[:captcha]
86
+ user.save
87
+ </pre>
data/captcha.gemspec CHANGED
@@ -1,10 +1,10 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'captcha'
3
- s.version = '1.1.0'
4
- s.date = '2009-03-24'
3
+ s.version = '1.2.0'
4
+ s.date = '2009-03-27'
5
5
 
6
- s.summary = "An Rmagick based, Google-style captcha generator"
7
- s.description = "An Rmagick based, Google-style captcha generator"
6
+ s.summary = "A Google-style captcha for enterprise Rails apps"
7
+ s.description = "A Google-style captcha for enterprise Rails apps"
8
8
 
9
9
  s.author = 'Winton Welsh'
10
10
  s.email = 'mail@wintoni.us'
@@ -14,24 +14,22 @@ Gem::Specification.new do |s|
14
14
 
15
15
  # = MANIFEST =
16
16
  s.files = %w[
17
+ MIT-LICENSE
17
18
  README.markdown
18
19
  Rakefile
19
20
  captcha.gemspec
20
21
  init.rb
21
22
  lib/captcha.rb
22
23
  lib/captcha/action.rb
23
- lib/captcha/actions.rb
24
- lib/captcha/captcha.rb
24
+ lib/captcha/cipher.rb
25
25
  lib/captcha/config.rb
26
26
  lib/captcha/generator.rb
27
27
  lib/captcha/image.rb
28
- lib/captcha/routes.rb
28
+ lib/captcha/model.rb
29
29
  resources/captcha.ttf
30
- resources/captchas.rb
31
30
  spec/lib/captcha_spec.rb
32
31
  spec/spec.opts
33
32
  spec/spec_helper.rb
34
- tasks/captcha.rake
35
33
  ]
36
34
  # = MANIFEST =
37
35
  end
@@ -7,22 +7,26 @@ module Captcha
7
7
 
8
8
  module ClassMethods
9
9
  def acts_as_captcha
10
- include Captcha::Action::InstanceMethods
10
+ unless included_modules.include? InstanceMethods
11
+ include InstanceMethods
12
+ end
13
+ before_filter :assign_captcha
11
14
  end
12
15
  end
13
16
 
14
17
  module InstanceMethods
15
- def new
16
- files = Captcha::Config.newest_captchas
17
- session[:captcha] = File.basename(files[rand(files.length)], '.jpg')
18
- redirect_to(:action => :show)
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
19
25
  end
20
-
21
- def show
22
- new and return unless session[:captcha]
23
- file = "#{Captcha::Config.options[:destination]}/#{session[:captcha]}.jpg"
24
- new and return unless File.exists?(file)
25
- send_file(file, :disposition => 'inline', :type => 'image/jpeg')
26
+
27
+ def reset_captcha
28
+ session[:captcha] = nil
29
+ assign_captcha
26
30
  end
27
31
  end
28
32
 
@@ -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
@@ -1,11 +1,19 @@
1
1
  module Captcha
2
2
  class Config
3
3
 
4
- PRODUCTION = defined?(RAILS_ENV) ?
5
- RAILS_ENV == 'production' || RAILS_ENV == 'staging' : false
6
- ROOT = defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/" : ''
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
7
12
 
13
+ @@exists = {}
14
+ @@exists_since = Time.now
8
15
  @@options = {
16
+ :password => 'captcha',
9
17
  :colors => {
10
18
  :background => '#FFFFFF',
11
19
  :font => '#080288'
@@ -19,7 +27,7 @@ module Captcha
19
27
  # canvas width (px)
20
28
  :width => 110
21
29
  },
22
- :generate_every => PRODUCTION ? 24 * 60 * 60 : 10 ** 8,
30
+ :generate_every => PRODUCTION ? ONE_DAY * 1 : ONE_DAY * 10000,
23
31
  # http://www.imagemagick.org/RMagick/doc/image2.html#implode
24
32
  :implode => 0.2,
25
33
  :letters => {
@@ -27,7 +35,7 @@ module Captcha
27
35
  :baseline => 25,
28
36
  # number of letters in captcha
29
37
  :count => 6,
30
- :ignore => ['a','e','i','o','u','l','j','q'],
38
+ :ignore => ['a','e','i','o','u','l','j','q','v'],
31
39
  # font size (pts)
32
40
  :points => 38,
33
41
  # width of a character (used to decrease or increase space between characters) (px)
@@ -47,29 +55,25 @@ module Captcha
47
55
  @@options.merge!(options)
48
56
  end
49
57
 
50
- def self.captchas_ordered_by_modified
51
- return unless @@options
52
- files_modified = Dir["#{@@options[:destination]}/*.jpg"].collect do |file|
53
- [ file, File.mtime(file) ]
54
- end
55
- # Youngest to oldest
56
- files_modified.sort! { |a, b| b[1] <=> a[1] }
57
- files_modified.collect { |f| f[0] }
58
+ def self.captchas
59
+ Dir["#{@@options[:destination]}/*.jpg"]
58
60
  end
59
-
61
+
60
62
  def self.codes
61
- self.captchas_ordered_by_modified.collect do |f|
63
+ self.captchas.collect do |f|
62
64
  File.basename f, '.jpg'
63
65
  end
64
66
  end
65
67
 
66
- def self.newest_captchas
67
- captchas = self.captchas_ordered_by_modified
68
- if captchas
69
- captchas[0..@@options[:count]-1]
70
- else
71
- []
68
+ def self.exists?(code)
69
+ if Time.now - @@exists_since > 60 * 60
70
+ @@exists = {}
71
+ @@exists_since = Time.now
72
+ end
73
+ unless @@exists[code]
74
+ @@exists[code] = File.exists?("#{@@options[:destination]}/#{code}.jpg")
72
75
  end
76
+ @@exists[code]
73
77
  end
74
78
 
75
79
  def self.options
@@ -77,8 +81,12 @@ module Captcha
77
81
  end
78
82
 
79
83
  def self.last_modified
80
- youngest = self.captchas_ordered_by_modified.first
81
- youngest ? File.mtime(youngest) : nil
84
+ file = self.captchas.first
85
+ if file
86
+ File.mtime(file)
87
+ else
88
+ nil
89
+ end
82
90
  end
83
91
  end
84
92
  end
@@ -7,23 +7,20 @@ module Captcha
7
7
  def generate
8
8
  return unless Config.options
9
9
  return if Config.last_modified && Config.last_modified > Time.now - Config.options[:generate_every]
10
+ path = Config.options[:destination]
11
+ if File.exists?(path)
12
+ FileUtils.rm_rf Config.options[:destination]
13
+ end
10
14
  FileUtils.mkdir_p Config.options[:destination]
11
15
  (1..Config.options[:count]).each do |x|
12
- c = Image.new Config.options
13
- File.open("#{Config.options[:destination]}/#{c.code}.jpg", 'w') do |f|
14
- f << c.image
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
15
21
  end
16
22
  end
17
- destroy_if_over_limit
18
23
  GC.start
19
24
  end
20
-
21
- def destroy_if_over_limit
22
- if Config.codes.length >= Config.options[:count] * 3
23
- Config.captchas_ordered_by_modified[(Config.options[:count] * 2)..-1].each do |file|
24
- FileUtils.rm_f(file)
25
- end
26
- end
27
- end
28
25
  end
29
26
  end
data/lib/captcha/image.rb CHANGED
@@ -4,13 +4,12 @@ module Captcha
4
4
  class Image
5
5
 
6
6
  include Magick
7
- attr_reader :code, :image
7
+ attr_reader :code, :data
8
8
 
9
9
  def initialize(o)
10
- @code = generate_code o
10
+ generate_code o
11
11
 
12
- canvas = Magick::ImageList.new
13
- canvas.new_image(o[:dimensions][:width], o[:dimensions][:height]) {
12
+ canvas = Magick::Image.new(o[:dimensions][:width], o[:dimensions][:height]) {
14
13
  self.background_color = o[:colors][:background]
15
14
  }
16
15
 
@@ -30,17 +29,19 @@ module Captcha
30
29
  canvas = canvas.wave(o[:wave][:amplitude], rand(w.last - w.first) + w.first)
31
30
  canvas = canvas.implode(o[:implode])
32
31
 
33
- @code = @code.to_s
34
- @image = canvas.to_blob { self.format = "JPG" }
32
+ @code = @code.to_s
33
+ @data = canvas.to_blob { self.format = "JPG" }
34
+ canvas.destroy!
35
35
  end
36
36
 
37
37
  private
38
38
 
39
39
  def generate_code(o)
40
40
  chars = ('a'..'z').to_a - o[:letters][:ignore]
41
- code_array = []
42
- 1.upto(o[:letters][:count]) { code_array << chars[rand(chars.length)] }
43
- code_array
41
+ @code = []
42
+ 1.upto(o[:letters][:count]) do
43
+ @code << chars[rand(chars.length)]
44
+ end
44
45
  end
45
46
  end
46
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
data/lib/captcha.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require File.dirname(__FILE__) + "/captcha/action.rb"
2
2
  require File.dirname(__FILE__) + "/captcha/image.rb"
3
3
  require File.dirname(__FILE__) + "/captcha/config.rb"
4
+ require File.dirname(__FILE__) + "/captcha/cipher.rb"
4
5
  require File.dirname(__FILE__) + "/captcha/generator.rb"
@@ -17,47 +17,15 @@ describe :captcha do
17
17
  it "should generate captchas" do
18
18
  Captcha::Config.codes.length.should == 10
19
19
  end
20
- it "should only generate more captchas if the youngest file is older than the generate_every option" do
21
- @generator.generate
22
- Captcha::Config.codes.length.should == 10
23
- sleep @delay
24
- @generator.generate
25
- Captcha::Config.codes.length.should == 20
26
- end
27
- it "should not allow more than twice the captcha limit to exist" do
28
- sleep @delay
29
- @generator.generate
30
- Captcha::Config.codes.length.should == 20
31
- sleep @delay
32
- @generator.generate
33
- Captcha::Config.codes.length.should == 20
34
- end
35
- it "should only destroy the oldest captchas" do
20
+ it "should generate fresh captchas if the files are older than the generate_every option" do
36
21
  codes = Captcha::Config.codes
37
22
  sleep @delay
38
23
  @generator.generate
39
- codes2 = Captcha::Config.codes
40
- Captcha::Config.codes.length.should == 20
41
- Captcha::Config.codes[9..-1].should_include_all_from(codes)
24
+ codes.should_not == Captcha::Config.codes
25
+ end
26
+ it "should not allow more than the captcha limit to exist" do
42
27
  sleep @delay
43
28
  @generator.generate
44
- Captcha::Config.codes.length.should == 20
45
- Captcha::Config.codes[9..-1].should_include_all_from(codes2)
46
- Captcha::Config.codes.should_not_include_any_from(codes)
47
- @generator.generate
48
- Captcha::Config.codes.length.should == 20
49
- Captcha::Config.codes.should_not_include_any_from(codes2)
50
- end
51
- class Array
52
- def should_include_all_from(array)
53
- included_all = true
54
- array.each { |a| included_all = false unless self.include?(a) }
55
- included_all
56
- end
57
- def should_not_include_any_from(array)
58
- included_any = false
59
- array.each { |a| included_any = true if self.include?(a) }
60
- included_any
61
- end
29
+ Captcha::Config.codes.length.should == 10
62
30
  end
63
31
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: winton-captcha
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Winton Welsh
@@ -9,11 +9,11 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-03-24 00:00:00 -07:00
12
+ date: 2009-03-27 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
16
- description: An Rmagick based, Google-style captcha generator
16
+ description: A Google-style captcha for enterprise Rails apps
17
17
  email: mail@wintoni.us
18
18
  executables: []
19
19
 
@@ -22,24 +22,22 @@ extensions: []
22
22
  extra_rdoc_files: []
23
23
 
24
24
  files:
25
+ - MIT-LICENSE
25
26
  - README.markdown
26
27
  - Rakefile
27
28
  - captcha.gemspec
28
29
  - init.rb
29
30
  - lib/captcha.rb
30
31
  - lib/captcha/action.rb
31
- - lib/captcha/actions.rb
32
- - lib/captcha/captcha.rb
32
+ - lib/captcha/cipher.rb
33
33
  - lib/captcha/config.rb
34
34
  - lib/captcha/generator.rb
35
35
  - lib/captcha/image.rb
36
- - lib/captcha/routes.rb
36
+ - lib/captcha/model.rb
37
37
  - resources/captcha.ttf
38
- - resources/captchas.rb
39
38
  - spec/lib/captcha_spec.rb
40
39
  - spec/spec.opts
41
40
  - spec/spec_helper.rb
42
- - tasks/captcha.rake
43
41
  has_rdoc: false
44
42
  homepage: http://github.com/winton/captcha
45
43
  post_install_message:
@@ -65,6 +63,6 @@ rubyforge_project:
65
63
  rubygems_version: 1.2.0
66
64
  signing_key:
67
65
  specification_version: 2
68
- summary: An Rmagick based, Google-style captcha generator
66
+ summary: A Google-style captcha for enterprise Rails apps
69
67
  test_files: []
70
68