winton-captcha 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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