1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
# frozen_string_literal: true
require 'open3'
require 'base64'
require 'securerandom'
class Captcha
attr_reader :image_base64, :solution, :solution_id
NUMBERS = %w[1 2 3 4 5 6].freeze
IMAGES = %w[1 2 3 4 5 6].freeze
CAPTCHA_ASSETS_BASE_PATH = 'app/assets/images/captcha'
NUMBER_FILES = {
'1' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '1.png').to_s,
'2' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '2.png').to_s,
'3' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '3.png').to_s,
'4' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '4.png').to_s,
'5' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '5.png').to_s,
'6' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, '6.png').to_s
}.freeze
IMAGE_FILES = {
'1' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i1.png').to_s,
'2' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i2.png').to_s,
'3' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i3.png').to_s,
'4' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i4.png').to_s,
'5' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i5.png').to_s,
'6' => Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'i6.png').to_s
}.freeze
BACKGROUND_FILE = Rails.root.join(CAPTCHA_ASSETS_BASE_PATH, 'background.png').to_s
GEOMETRY = {
1 => '+0+0', 2 => '+120+0', 3 => '+240+0',
4 => '+0+120', 5 => '+120+120', 6 => '+240+120',
7 => '+0+240', 8 => '+120+240', 9 => '+240+240'
}.freeze
DISTORTION_1 = [
%w[-implode .1].freeze,
%w[-implode -.1].freeze
].freeze
DISTORTION_2 = [
%w[-swirl 10].freeze,
%w[-swirl -10].freeze,
%w[-swirl 20].freeze,
%w[-swirl -20].freeze
].freeze
DISTORTION_3 = [
%w[-wave 5x180].freeze,
%w[-wave 5x126].freeze,
%w[-wave 10x180].freeze,
%w[-wave 10x126].freeze
].freeze
def initialize
@solution = NUMBERS.zip(IMAGES.shuffle).to_h
# 3x3 render grid
grid = [*NUMBERS, nil, nil, nil].shuffle
# Base arguments
imagemagick_cmd = [
'-page', '360x660', BACKGROUND_FILE
]
# Add individual grid tiles
grid.each_with_index do |num, i|
next if num.nil?
imagemagick_cmd += [IMAGE_FILES[@solution[num]], '-geometry', GEOMETRY[i + 1], '-composite']
imagemagick_cmd += [NUMBER_FILES[num], '-geometry', GEOMETRY[i + 1], '-composite']
end
imagemagick_cmd += [DISTORTION_1.sample, DISTORTION_2.sample, DISTORTION_3.sample].shuffle.flatten
imagemagick_cmd += ['-quality', '8', 'jpeg:-']
image_data, = Open3.capture2('convert', *imagemagick_cmd)
@image_base64 = Base64.encode64(image_data)
# Store solution in redis so that we can prevent reuse
@solution_id = "cp#{SecureRandom.hex(12)}"
$redis.setex(@solution_id, 10.minutes.to_i, @solution.to_json)
end
def self.validate(solution_id, solution)
# Delete key immediately. This may race, but the
# impact should be minimal if a race succeeds.
sol = $redis.get(solution_id)
$redis.del(solution_id)
return false if sol.blank?
sol = JSON.parse(sol)
# Ruby hash equality tests key/value equality, but not
# key order equality, so this works.
sol == solution
rescue StandardError
false
end
def self.options
@@options ||= NUMBERS.map { |n| [I18n.translate("captcha.solutions.#{n}"), n] }.freeze
end
end
|