captcha 1.2.1

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