captchah 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ad66eace82032f4bd0032a0a418dc3d110c5d90ae28ae72e7433eec12bb891bb
4
+ data.tar.gz: 5916bf46a222fce1aa04f069e4cab99ee2a2276cb4a5a9c179dd2eb385e27ab4
5
+ SHA512:
6
+ metadata.gz: 27ff37ca4328a81c6d7477ca54223c45cd4c1cd6cb25512c9b9d8648947113dfb8586208ddcc2bb3e7d5febbf41229ba7c88054e7720e7915b71bd2f9b0bec04
7
+ data.tar.gz: 2429ba66d17e070f54b1a057e8b007c9e7ac08a6f80f3a7dc6869c1c6b757fcb11a82ae38265346ae87809d6a1842440a03ec8fec1fa4bbaa80acebe0204808e
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Captchah
2
+
3
+ A Rails captcha gem that attempts to determine whether or not a user is human.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```
10
+ gem 'captchah', '~> 1.0'
11
+ ```
12
+
13
+ And execute:
14
+
15
+ ```
16
+ $ bundle
17
+ ```
18
+
19
+ ## Requirements
20
+
21
+ ImageMagick or GraphicsMagick command-line tool has to be installed. You can check if you have it installed by running:
22
+
23
+ ```
24
+ $ convert -version
25
+ ```
26
+
27
+ ## Dependencies
28
+
29
+ ```
30
+ gem 'rails', '~> 5.0'
31
+ ```
32
+
33
+ ```
34
+ gem 'mini_magick', '~> 4.0'
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ Include the Captchah module into your controller. Example:
40
+
41
+ ```
42
+ class YourController < ApplicationController
43
+ include Captchah
44
+ ```
45
+
46
+ Add the captchah_tag form helper to your form. Note, only 1 captchah_tag per form is allowed. Example:
47
+
48
+ ```
49
+ <%= form_tag('/your-path') do %>
50
+ <%= captchah_tag %>
51
+ ```
52
+
53
+ Once a user submits your form, you can verify if they have typed in the correct characters by calling the verify_captchah method inside your controller. Example:
54
+
55
+ ```
56
+ class YourController < ApplicationController
57
+ include Captchah
58
+
59
+ def create
60
+ redirect_to('/your-path') unless verify_captchah == :valid
61
+ end
62
+ ```
63
+
64
+ ## Details
65
+
66
+ The captchah_tag form helper accepts the following arguments:
67
+ ```
68
+ captchah_tag(
69
+ id: 'unique-id', # String value Default: (automatically generated)
70
+ difficulty: 3, # Integer value between 1 and 5 Default: 3
71
+ expiry: 10.minutes, # ActiveSupport::Duration object Default: 10.minutes
72
+ width: 140, # Integer value Default: 140(pixels)
73
+ action_label: 'Type...', # String value Default: 'Type the letters you see:'
74
+ reload_label: 'Reload', # String value Default: 'Reload'
75
+ reload_max: 5, # Integer value Default: 5
76
+ reload: true, # Boolean value Default: true
77
+ css: true # Boolean value Default: true
78
+ )
79
+ ```
80
+
81
+ The verify_captchah method returns the following statuses:
82
+ ```
83
+ :valid # The user has typed in the correct characters.
84
+ :invalid # The user has not typed in the correct characters.
85
+ :expired # The captcha has expired.
86
+ :no_params # params[:captchah] is empty.
87
+ ```
88
+
89
+ ## Running the tests
90
+
91
+ ```
92
+ $ bundle exec rspec
93
+ ```
94
+
95
+ ## Contributing
96
+
97
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/evgeniradev/captchah](https://github.com/evgeniradev/captchah).
98
+
99
+ ## License
100
+
101
+ Captchah is released under the MIT License. See LICENSE for details.
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captchah
4
+ class CaptchahController < ActionController::API
5
+ def new
6
+ return head(:forbidden) unless request.xhr?
7
+
8
+ render plain: captchah_tag
9
+ rescue StandardError => e
10
+ puts "Captchah #{e.class.name}: #{e.message}"
11
+ head :internal_server_error
12
+ end
13
+
14
+ private
15
+
16
+ def payload
17
+ @payload ||= begin
18
+ raise Error, 'Payload missing' if captchah_params.blank?
19
+
20
+ Encryptor.decrypt(captchah_params)
21
+ end
22
+ end
23
+
24
+ def captchah_tag
25
+ Generators::Captcha.call(captchah_arguments)
26
+ end
27
+
28
+ def captchah_arguments
29
+ {
30
+ id: payload[:id],
31
+ difficulty: payload[:difficulty],
32
+ expiry: payload[:expiry],
33
+ width: payload[:width],
34
+ action_label: payload[:action_label],
35
+ reload_label: payload[:reload_label],
36
+ reload_max: payload[:reload_max],
37
+ reload_count: payload[:reload_count] + 1,
38
+ reload: payload[:reload],
39
+ css: payload[:css]
40
+ }
41
+ end
42
+
43
+ def captchah_params
44
+ @captchah_params ||= params[:captchah]
45
+ end
46
+ end
47
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ post :captchah, to: 'captchah/captchah#new'
5
+ end
data/lib/captchah.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'captchah/version'
4
+ require 'captchah/generators/html'
5
+ require 'captchah/generators/puzzle'
6
+ require 'captchah/generators/truth'
7
+ require 'captchah/generators/captcha'
8
+ require 'captchah/base64_images'
9
+ require 'captchah/encryptor'
10
+ require 'captchah/verifier'
11
+
12
+ module Captchah
13
+ class Error < StandardError; end
14
+
15
+ class Engine < Rails::Engine
16
+ end
17
+
18
+ def self.included(base)
19
+ return unless base.respond_to?(:helper_method)
20
+
21
+ return unless base.ancestors.include?(ActionController::Base)
22
+
23
+ base.helper_method(:captchah_tag, :verify_captchah)
24
+ end
25
+
26
+ def captchah_tag(*args)
27
+ Generators::Captcha.call(*args)
28
+ end
29
+
30
+ def verify_captchah
31
+ Verifier.call(params[:captchah])
32
+ end
33
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captchah
4
+ class Base64Images
5
+ class << self
6
+ def loader
7
+ 'R0lGODlhHAAEAPcAACwsLC0tLS4uLi4uLi8vLzAwMDExMTIyMjMzMzMzMzQ0NDU1NTY' \
8
+ '2Njc3Nzg4ODg4ODk5OTo6Ojs7Ozw8PD09PT09PT4+Pj8/P0BAQEFBQUJCQkJCQkNDQ0' \
9
+ 'REREVFRUZGRkdHR0dHR0hISElJSUpKSktLS0xMTExMTE1NTU5OTk9PT1BQUFFRUVFRU' \
10
+ 'VJSUlNTU1RUVFVVVVZWVlZWVldXV1hYWFlZWVpaWltbW1tbW1xcXF1dXV5eXl9fX2Bg' \
11
+ 'YGBgYGFhYWJiYmNjY2RkZGRkZGVlZWZmZmdnZ2hoaGlpaWlpaWpqamtra2xsbG1tbW5' \
12
+ 'ubm5ubm9vb3BwcHFxcXJycnNzc3Nzc3R0dHV1dXZ2dnd3d3h4eHh4eHl5eXp6ent7e3' \
13
+ 'x8fH19fX19fX5+fn9/f4CAgIGBgYKCgoKCgoODg4SEhIWFhYaGhoeHh4eHh4iIiImJi' \
14
+ 'YqKiouLi4yMjIyMjI2NjY6Ojo+Pj5CQkJGRkZGRkZKSkpOTk5SUlJWVlZaWlpaWlpeX' \
15
+ 'l5iYmJmZmZqampqampubm5ycnJ2dnZ6enp+fn5+fn6CgoKGhoaKioqOjo6SkpKSkpKW' \
16
+ 'lpaampqenp6ioqKmpqampqaqqqqurq6ysrK2tra6urq6urq+vr7CwsLGxsbKysrOzs7' \
17
+ 'Ozs7S0tLW1tba2tre3t7i4uLi4uLm5ubq6uru7u7y8vL29vb29vb6+vr+/v8DAwMHBw' \
18
+ 'cLCwsLCwsPDw8TExMXFxcbGxsfHx8fHx8jIyMnJycrKysvLy8vLy8zMzM3Nzc7Ozs/P' \
19
+ 'z9DQ0NDQ0NHR0dLS0tPT09TU1NXV1dXV1dbW1tfX19jY2NnZ2dra2tra2tvb29zc3N3' \
20
+ 'd3d7e3t/f39/f3+Dg4OHh4eLi4uPj4+Tk5OTk5OXl5ebm5ufn5+jo6Onp6enp6erq6u' \
21
+ 'vr6+zs7O3t7e7u7u7u7u/v7/Dw8PHx8fLy8vPz8/Pz8/T09PX19fb29vf39/j4+Pj4+' \
22
+ 'Pn5+fr6+vv7+/z8/P39/f39/f7+/v///////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJ' \
23
+ 'CgD/ACwAAAAAHAAEAAAIKAATCRwo8N+/cggTIjSoUCFBggwbLjwosdzDgRElZmx4sSB' \
24
+ 'FjR8bBgQAIfkECQoA/wAsAAAAABwABAAACCwAAQgcKPDfv0QIEyI0WK6hw4YECRpUqJ' \
25
+ 'DhQ4cRB06kuPDfRYwZAWzkaPFjQAAh+QQJCgD/ACwAAAAAHAAEAAAIJwATCRwo8N8/A' \
26
+ 'AgTIjRIsKHDRAYVKmT4sGJEiQv/VbR4ECMAig4DAgAh+QQJCgD/ACwAAAAAHAAEAAAI' \
27
+ 'LADLCRwo8N+/RAgTIjQIoKHDhgQJGlSokOFDhxEHTqS48N9FjBnLbeRo8WNAACH5BAk' \
28
+ 'KAP8ALAAAAAAcAAQAAAgoAMsJHCjw3z+CBA0mWshwIcKBBh8W/NewocRyESUqrOhQ40' \
29
+ 'GPHBcGBAAh+QQJGQD/ACwAAAAAHAAEAAAIHwDLCRwo8N8/ggQNIlyIUCFDhwwfHpQYM' \
30
+ 'SLEhhMXBgQAOw=='
31
+ end
32
+
33
+ def puzzle_background
34
+ '/9j/4QBWRXhpZgAATU0AKgAAAAgABAESAAMAAAABAAEAAAEaAAUAAAABAAAAPgEbAAU' \
35
+ 'AAAABAAAARgEoAAMAAAABAAIAAAAAAAAAAAEsAAAAAQAAASwAAAAB/+AAEEpGSUYAAQ' \
36
+ 'EAAAEAAQAA/9sAQwABAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' \
37
+ 'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/9sAQwEBAQEBAQEBAQEBAQEBAQEBAQEB' \
38
+ 'AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB/8AAEQg' \
39
+ 'ANwCMAwERAAIRAQMRAf/EABwAAAMBAAMBAQAAAAAAAAAAAAcICQoEBQYDAf/EACwQAA' \
40
+ 'IDAQEAAQQCAgICAgMAAAQFAgMGAQcIERITFAkVABYXISIkIzElQkP/xAAUAQEAAAAAA' \
41
+ 'AAAAAAAAAAAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AXV94' \
42
+ 'j/pVO59MDaZbCYjLDrkSsglImyC5coAvvMbGO9sursayr0Apsv7ebARLUtuZSIUgIqF' \
43
+ 'cBKgJvm7vNfIGEfTlu1MeeVr3RjDMn5bLIltvq+wCqMIFWFlMVqwbmYxvQa0ydmwS1u' \
44
+ 'iTqFNoYohklBt4L4t1tJnoKrFN8eZk/wDW6f26s0LQ1YZqFjPRqaY0UHiytz99cJkMD' \
45
+ 'hK+MS7Y0fml0eucezgFvcSx7biEzEy9ahWBrZCPGBJhMuq7qaeQu4dwesnq6+gWyN99' \
46
+ 'khzPoLbXZG23tPZdAOIlLX1HOdZ4qtXMETQlM173sdUv0cMrF6DMj+gNADXDMKmNcjz' \
47
+ '/AMhYYY4sIxFuptrjM6sGK8U8y8mzVWr3dbklwwQumiwAqpX2VdLxibcdcDUUZCB7p0' \
48
+ 'MadfcXyugUPtlnIx5f2H5egY/H/E/MXe3ltc353Md5Kxh/b61k5Kn1s9Y2wtaNIqL5c' \
49
+ 'U3M+8r/AATIhzp1co9Eh/4w7/gLh6zrfH/Svkxt8j4XuOZT0/xYBEt9F2PnQ2ajeFsX' \
50
+ 'tls60TcogU2km4dT+uO3EYhGcGNhUKTR2nnJxCKvpvmP5PYWN+U0Wqr2en3nWrG1YVT' \
51
+ 'RI0vP3zm1YH4k5SSgu/t5wjEi8OcLRI2WMKTociPCINXm/WEGz9WzR/oH9M9jgP1kt+' \
52
+ 'aS4DS6XZFLpC9+kCI0mAglGLj+WF0dGJBJrnCJLOBw/ewgDBjenYjnoCduizWpqzVAz' \
53
+ 'e4LSTEyJM6nB/IzvHgGM5L4AHQvpmuIlS1LLjbZ2gmuM6/pACkH7V4Zo8kdoL+aFNE3' \
54
+ '9mvRCL1wTCohkFZ+CXT1Ib5QWReXzkftjeaR26uMPpL7auR4C8emeLeD+o5s5jlVOyz' \
55
+ 'LjS6Nbn69vjBx8g6GlbdK+TEo4dgcwKTrpxjcNRcfKutvWPD8lEvtvgCNIvaN15yt2n' \
56
+ 'xr2WsV79IU3MTH6DZEakfagLJklG8JfZbi+7ukvKlGNRrjPkQT3LZ18uAMW38ImAw9s' \
57
+ '8C8Kdo/NTPJd+p8V1KZpCq0WrUsCc5Ew4qs0pmhq0wZwONlEoKJFRtAsgiIclSX2uic' \
58
+ '5xDl/CvK5/y30j0crfeief8ApTELZZ9baftvvomPb6EdaEUZkas0sJS54TaaC9RSc0D' \
59
+ 'zFKdpdSlM0og4VlbqgKCfJn1DGtfN6cpgzFTytPSOd9omwwzwLLDi36ag9dFYnhQYH2' \
60
+ 'hQtAWU0zFraJpnZ8UEEBbToyyAkHs/JfQQdGwgsW0lBFcFYVW/t/rfZM8SgooT6/8AX' \
61
+ '7X9eXbev/b/AP6/q/T/APT/AAHX+fPwe9U9Y+TNXwZCMF2mM8/+x4mQoLI5AgZ6eJNc' \
62
+ '4M29IrMsBjmgKEKcjFMR1gumV4DUifhWMdQTaJaFWfjJ/Ch6zksbpvPqvakWURMYgKM' \
63
+ 'fxfkuE059ApVwXACHB0xoIZMJqViKh0f2OUbPnFB+ufv7S4XDMgRo3xkTnviv46vQk/' \
64
+ 'pfsir1IHArtmAJ1IJmBlOsa8ZsG+hoelaJkngZy3T/AGos/aKCzZLFd1S1SKKGcFw33' \
65
+ '8UKbTZx1ZutVnBXK1IgtfkIbLdShdDZmdp6obTIGqvO51uIIOxsNui7TF8pXki2fs1l' \
66
+ 'RotkB0o+HdUsHW7Ueg4nZoE2EnQwkvWxzyMrOxBMIJ+2OanplX4qaK5Q5BRRGjkbftH' \
67
+ 'r5/1H/A+uQwfk2aT6L0j1t7PJeUeYZEbbMmrNcXks/wAQ0CWzPKpiVZc7e2V2jyE/9h' \
68
+ 'WluItuo/RkfwkP8gZ8vmZ8fvlP/KNbz2Hyjear4sfG3Jzg2+OnkcH2iRnaGuqdo6L13' \
69
+ 'cTSdpMzjV01osYJuM6mVyVdcIJCFLK3seAHf4Ufj5lPF03zZU/LvW7bJeqpvYc3lWfF' \
70
+ 'bEF+buTr0FTIZ/eZqUZVrVe8Nu4QMxvcrLyO/fX0im3/AKkDrsPi0Ux9E0u4Nzey8/y' \
71
+ 'whK+IT/UZGetf29vj2trpUg6AOhKeFaLZXOOfAJmePTyVl9rMmPKeAJfXPhJ4mu3OcU' \
72
+ '4X5Sa3i/S0/ta3RYfydljfQ62UiYX8uDamGZ9lnl9Q8oziKVZN4TKvpK0mwL80KgM2B' \
73
+ '/iI9h8t9Yt9Tw3vAftrOy9PHceTekDN80Ns/LXEbZq526Fc+0JSTXWy7a4A0nQnRFxN' \
74
+ 'Vs2VRwJP/Qd783Pib8dPEclqhclv9Hb6ubPJ7NL5SueDN2uZRf2lI7xc+ForS8/RP5a' \
75
+ 'RGglrMhkPRRdcNH8dHI8CfYIOuShV6cp3ahB1y9e0ng0O0PHWZcwK8sO20qutisZsro' \
76
+ 'qp1dvu5FkvpacuL4p5bVXcYCZ6YMxt6NsT+7453qoF/wBGqL2Kxy9CQoXtM6LVSqs6F' \
77
+ 'dFbAvpF9IpkRBLOsqom/p19r5KwO59BrUAJ+PHWrI1SrM8juny9mXJnoUa/kL+DLArp' \
78
+ 'lCNgFvS6mKpNWfYi/Iussr5KXZcj0BPiEU9P6P5g+dZs+vEDMFqbm3LGrLXqtLVR0jM' \
79
+ 'XlBjUlBO21InbKP1hLCKl1JFFViduRXbfwKJebeKkaZ5LPqAUuw0p9BzFWrnIsY+qvp' \
80
+ 'JdaJzTAx/YvVCugZCVMUfFAlLExz+twUa+N1JgeC26b1uhivggyULBP6ij9mu6lvRIR' \
81
+ 'jws6BoEaw4/rwrAtj+pCuv/AKo5T+v/APVP+BbbAe32o/5K/kULjPBtu78V1Xyi0APq' \
82
+ 'fzM1Ml8cNiWeI8Ew6mXjggPEV5eXyGY2q/QLy9qzrGUwbtDg5Mz2zL9qQPbn+/yCV/I' \
83
+ 'bbWX+m/FfaeMZSbDgfi2Cz2jye4XqWld9WXg+9W5p9SSs2a+wYUkrvcmUuYdrmKIAuJ' \
84
+ 'IXOgQkh8ccs7R/yH6luOe20Wj2/vHsALW5s0XaMTMxthox1QiuTRLKwUAC1634VaKzD' \
85
+ 'rvrolUqXii2ljxDTif/AMS/Hnz7S+i6x3kvOfKsYGbqdzp9BavpQ9FgHVNg3KO+v0rJ' \
86
+ 'IvrEkR9Y96wM+vKI/sXU/UIY6n+XF9o/F/T/AElZ8NPXJ/Hz1d7o/OvKd35tqcc1sA8' \
87
+ '8kKNn6vXtZjTNAs1gqI9nM7SzuzOefcGzNo0z6ajOQsLCnOnWZ30fzrMPY+VD+w5dKm' \
88
+ 'TGLVazG4DSOtTAGQJq/uWj6KTmkIc/qJFhDs9ANO+mIjGuS8/lcehwfLsX6NtIb/0L0' \
89
+ 'jKNskA5ogq8z8cZUZ0c3D5UWdtY12rYZgt6EY+0R05MGcUJzRcir/GsoKa1Bd+8JI7d' \
90
+ 'f8n2HyP868w9J8H8Q8iX7l/odoBrPMPQGbG3f/8AGglIyAbQVlZ0DT48yKeYX7r9fEy' \
91
+ 'k2+2XK1A99fZzB8fMd/zQXE5d0kc36n0tnxexbMlGkTzikzVVi61eXnd6EA9iMt+k1n' \
92
+ 'HKIEaGgrnWyl9Y2fXoFXxnxHMGaXUMbRZlSzmgVkJbXKwFlxKQjLkWF1WW0oLa3qp0f' \
93
+ 'lHjRYH0eE485HkOR53oNDkEYgEnmkNq/a0Loy+LE06y0gydAll9a0Oi7pHaKVIQsvuW' \
94
+ 'g0Bi8H7cTz6d53/oJSfyW4Hy9t8afQWCsFCo1TfcIv3tkYi4+aFE2s1YLQsLk628RJQ' \
95
+ 'UStEAn0MeMCaPv7H/AO5dDJP8hPdbIuF/kmSD1OtLZ3nJaYZ4sU3URVU18KZmDLh7w5' \
96
+ 'XKndE6AyqZDA8kNIizhEejfZIBl8c8lvdRotIt3fnrPBewK6TrsXcSO4ar/wAY5wf9U' \
97
+ 'sz4QVbCuhhbYLZRoXMlMA+2QXTLMWc+y+kO098EK89LfC73PLFrDS0fpbsgzIjzrhZe' \
98
+ 'X9vBBnlVBLUjrCR5Mf1xqZRj0L75SjTYt7wDh5XrKdKhFFjTLi6kdB2tc76Cz6jt4Lw' \
99
+ 'qI45SsQSKkKvlcGLXtkJ2uQwwpMpcgFOPAtF/GE7I3fsOm/0ungjliA6xZmNkJNUaqe' \
100
+ 'xwTF2g1a7eCq7TFOcgzEZrqtOHQWKMZaKCz/GUekLgFXGXx69H1HRW+Z88eTUSrMX11' \
101
+ 'TzXnyKdBaZwzTs7Igu9A0a8CaswS3ye1izPZSRtlkGxH9vWfVUC2fxN/MrLfPrx70DY' \
102
+ 'iYB6mQEagvR+rDbbKoUOZ3XtvpFU/TvS1+MVyY6BuB5/k+OFGZXGmGHNtOOQoOJESct' \
103
+ '/u3gU18gR4rH7CvxfN+bZnC2YPz1Rq4MMvJUFm3Yj3StAMs0AsV9VtFVDkzKNmrRYfm' \
104
+ 'LzCDgJ0SYvKhaS3IZ/z9sL4P8AyHab3GhM5eI/PvWPRHmvVJJknOR/NDWF9GwKgogq6' \
105
+ 'S2Kz51/X6we22xq2NrGEC5bwmMJB+eyZ/5Efzve6gsBJ+w+Efxy+QM0HVniu886vx+v' \
106
+ '9r1FMIGaPTbsQ605fHHTpOsCz0ZltSBYC8oDyVJdpTfoWU9wVJvAvF2hGfWqVhHlWZx' \
107
+ 'VMKerJiLVWX1m8RY5uwXTAgZ+gxzeZkewsvhTcAOWNWW0okJK2voMb58WgxHieRT+XW' \
108
+ 'L9PnLVaZfmLcw7YiVMVjfsJWsQDKIsRiQ5j9Jt/PWcOCdOi2iFY9ttI/QGlTG/Hel7U' \
109
+ 'q3a6pU6csFTVOoIX8sC1CVUiVK2jKdAqM0eY8T6ryWd61YhaCFGROJjOuVU5ApPyzyr' \
110
+ '/T/Ij4SeyqBTSVeI3+wR6m4OsgouhFqc1+SxqB+IsYkzi29dXXyuEVxLXpFNcBybecp' \
111
+ 'kHc+YeIesge+2eqv/AE9nYt0ZeisW+bPynEy0eLOsrJCZ2gtCpwhaUTzsBFwoC+ayuc' \
112
+ 'oG8LI5+XgGrTa/eecevAV5Fx3TINXps2Ztcd9F556FOVGxW+br2QmkOgDcEL0c6CYlR' \
113
+ 'OZk+9qlC2UuURBidTshlOsRJ9OlEHXalQXaE+Kbs1xQxYNw/K/7aHAg+rhpA2ToFmZy' \
114
+ 'dUGPaa6fpyzv+BKn+WvSRTfGbTCnXZ+q5Q/Qk1ZhQ/X3sWNNl8OryWVP6H74ot3/AJc' \
115
+ '4eOXGmXYfrSnyU+8/wMq3mhKVrp9R6lr/ADtDVCnmQ7S36uMHIZJln4Lb+MaS2F9YVx' \
116
+ 'hk7oFdY8DpIWjC9hbEbs6agPHtny5zIal5e3KAzgvCjS8aa6rCBPqEjAlaP0bQov8Ab' \
117
+ 'i303fZ/qKv6S5VWbwge4fvJSI+gKDi/kNnt3EwHV+bb5YBO+1UcMVOOjoMXdvrpVaHv' \
118
+ 'NWAAzCMlbdczmCOyOu7Efp9N49lXLIh80n9pg2q7XxZVBqKNBclYX3nW8KJrd2clZw9' \
119
+ 'RXz9qnoxp8U9TI36ih2Wf9RojZ0agLcfED170nw9Ocxx3mtVahvq8+Sl9dIi3YzLzgj' \
120
+ 'm3UUfYmTJF2ZZsnGdRySTSl6Ue21V1ozviVJD0oANK13zBYIFmZlrHCpS7eZhQ/KVwV' \
121
+ '1FwX2G1zpIEjYojoQ4fhMEKjKHHbO2U/uutJ723lcAQz+FXyAPzT+PL42NcyoroL9QS' \
122
+ 'meg60IBXSDOnR6qRVMwrhmf1IvsyCyCbE8r79GEOZ8FcX/4Kfp0KZbG3MeWaz0z3DUA' \
123
+ 'XWYxB5vhVmsMUkGTIW5zzzTeiOnbowZIvLucps8Fqz2bFbyfJVUo2ceQ+kI9mGR/3j5' \
124
+ '1Z9Z/Iq7zvhepXa4rQelGFjBKcYwUVr8RpIoXU0u9X7aKT+6MYt7c/+uKqIClWPmmdp' \
125
+ 'LugOu9XcF0/4vffD/SvNfRM9sdKy1+6wZ9rlwMCpBssNFfkH3p6qet9SDA8bn6n1SSH' \
126
+ 'rVrvvnOLQyBNJ3AQe5o5NhSp2ujw8slePuk2dLJaMVrcmxI6qNSryGv6fQQF0iGRgwd' \
127
+ 'qao1qRO6+URLi5G1ymBsraoMgrZsnPRrOU3szZk1XCIv67OEXWRCsJlUWDQCmUQ/ZXg' \
128
+ 'En3wsrtnTD7ZF2T50E3M1HPX31S/K7NYwUYsy0SuhPVZ/c5e1jRfAwt0HtRAiDzjgbY' \
129
+ 'WCr2PCxxwZQKCJKpMh24ET+cHhmvuS4Fj5yW9atcn6fnNwv86A01CIHUXISJk0QuTLo' \
130
+ 'ZvPF12jVHWW0zpJoAus7ZSOTbR9nQcH436byz295u93mWuuYb5DxTjH4DYc0ClDbXT+' \
131
+ '/dmh1xbYGkhSKddbyJRaNM3v/ABdu7WVTbURIOB7fgoFFmx/tXPkWiXk0K/Ot1mpGqb' \
132
+ 'WWnLF/POh0XaKzRHLLyvwjQzukCOBYc+vL7IQnXzgCTG/EP3z2sz+l+S/yWM9UTYl2w' \
133
+ 'TO0iHLaTzaWwTNxRmfVeqjjtQFkdHn4311QrVErqBZB1kUM0NtsqbLgV3+ULwbz/wAP' \
134
+ '+KuwpxAYoqlVs8izJx6xYGavsbM5XLhHNVv6ldtJQ9fe8LkTeGFULG3slpd3K76Azue' \
135
+ 'bueq8KxEAuIJbuldwBIgxKt4BWWfz7Zkux+PanXCixquHDUirh7JREJv7Cnk+wtBMln' \
136
+ 'iUWurc6htVojlqT+nozqz8Co3KLE8rbgxh4rG1DApawttt4wpstKibCy0j9LkI2Ed6B' \
137
+ 'vzOf7OTPKiYPS9IV1KPwiLhFShMFWTZCy+cqo6kiZcrefYAyuo4NRRZb+ZOYFTX0moD' \
138
+ '/p/NVIsbWm7Yos2vXlVZuuos1XWSbbxfXn2KklYWpPlPjn9+cbxD6Ab6bpTHVn8cUrI' \
139
+ 'UhpP/AINKPIvVPDfYhaU6kxAJ6CGGdkrwK7Orv+OGz7Hojpj2z+21YSZm4Hplg1nRqY' \
140
+ 'kSrMs6RDvf8CqWn+LOqvdGsPOvbtrhUba65mQpTZzxQqgpiTfbzrAg7YYdi7PK6urXA' \
141
+ 'fsXGW1VirxQ6u/+rL6h3PlXg+FwGTzmNwlcrfOcouuUrkIxPSAFVLw5fbymIvf/AFhV' \
142
+ 'lMyOuKTgZ00KpV/l7dVMqEogiv8AJp8nvjD8Ifjqo+O/sfoTDygD5d5v1fx7Ea9vVpt' \
143
+ 'SJif380zsVnbY1cI3PXZZc9dq872+m8wRaCwtmspLSIORBBPjvCfGND81cT6cD58p9P' \
144
+ 'xHoL7LtVm+zMmDnDatXrARxbXEXoRYaR4jqEhUaPO2/o8qI2DwHL6PAO8LD+YZDFeIe' \
145
+ 'nbHypblcWAuehEbbF6JKJWqaEq0ZQywjHPBaQahKRsrU/EVZ0hYTZcZRwgokUXttf3h' \
146
+ '0/yKo0ev85IG8/nm81o8m0Sa6BrTk2ipyFh21WgMDt6TJZOUTuhf1d5dpBn9aRVyzvL' \
147
+ 'OL/t6AP2/tfrlQFNT/wAFXr1MJdVaP0m3QstAf+RgumZQ3wnknl4261r0wSy+JV9Llx' \
148
+ 'kpAXzqYVQMhVKyIHfJn4J5nO2Ydqn0tj1PFK82i0GukqLsMSrsxzlv4BiOsg7bYyuXM' \
149
+ 'IyYiSrnQ2ushHsv8DyXqnk0Wt/mzdOQV27DPBG98Vx/M0IbWJT2i0ixbQeXQRXXKUbI' \
150
+ 'jcpiNURdKdX0q7dXMJ5aL5b4r4/Ee57XCeC/3bFjsB1wrDy1IgCaeg7i4qC4a7R32UG' \
151
+ 'dtmQ450Oo0sCasICgxtImkHn14DLfGPzz5Bejhar3P5Pajmdea7OzXZzzDIPW9uGw64' \
152
+ '4jh5E6ilhF5zjWMa41iEaEAFP+ryHRkigJfb+QkPXJtJovAPSWgoSHR6vz3036O62Cy' \
153
+ '0xu8X6EHgonB6UbsEpy24SEPZbdUJEZlZKm221daXLlcwWL+W/Robfitqjrdwiz0Xrb' \
154
+ 'J9aDbMQ9JMunpXRlAf8AU3gW2qeHEmcGiZoJJaK+dslbEK6qdcAypYHK4n0J6kci6k5' \
155
+ 'PPMgmLL3OQLvWjlUJ77uSP+gWpvAsX9H5H9VksCHKjRaON986OWF1hz/Q7lGQzGL/AN' \
156
+ 'T2DPZQf0Utr7dYBcpffpQjXaK4nVQjqp/X5DkhrOuftheNCq+uB5VtdkgEPbvMG/8At' \
157
+ 'Oq9DKL40eHLTKiEWkq+jEoc6nlw6ocF4uvGjKsuJXFkldymhfG4LnOfl73gU+8jw/df' \
158
+ 'ixNq+5lDhTQHgKpdYwgjJKf1IjKQUCZSRVGTFm0dGLyaCFK1lVWQvhMkqcIyBLB6f4O' \
159
+ 'PYMf8fPaPb/A9Hv4ptN6xBLpcn5lr9KjoWI9T9rBxoaaXVJCuQBjqLMjs0IYbeoph0J' \
160
+ '2EdUS/ep1wa0x0sbao2EGGBWT5zvaQzuV1/XkYxs7P7u87ZbC3llMrfp9LI1Q7z/Ajv' \
161
+ '6VlWgv8qnxlphsfSMZmWXw/9uaaDznIbBokwe33Hmnq3mCZGRtcmpdwROlyxb6VsTE8' \
162
+ 'ia7egda9DmMVVCwa4MxP8k3mdH8tX8zfqXmTjat6fil8K8KmxekrTWRzDPJRhmjXWtE' \
163
+ 'yIDJC0La6C3aksbSyZqrU1qJPKK9wXfSjFJDWd/HR4R/xT8T8t4EGSvaYzzP/AGzI+N' \
164
+ '66TJ9PTF+b3umEQ6XQzSJ1gRaKyshWPSO1FqOTggVxGW8nIekIk/yTfyC/IcL07w/3b' \
165
+ '4jiPVec849p9T8sZ6I2rBSSOvHcZTkp+sni5/TMVuur0jJh563tQhF86FYvgrJnAVxd' \
166
+ '2AQXa9I9iCHXAYTzvL16Hdb7MxZaPQtqKw0Hn6LWBziUYw/dd1NGpzs46gVIsyy9gMI' \
167
+ 'Vw0lySnX1DQNDzvlHmcPHWXsXq22oUkY3UczxohYlVbF6gmcnXAOHU7h1ys7vD25E+/' \
168
+ '14USLBKuxtClRDtlEALOmHVYtMkc0gCEhHNE4DkvvSaNDFLo7eh0FWF0fSk3/5rxuSD' \
169
+ 'l91naSJSu/+SqXP8DiafW49RtkfnDs6y8/R0VjLlUhWn4Cwh/xkTnO8T6B8Opt/FOj9' \
170
+ 'qyivsvpOVvO1/ZMFR9886z+H9Q+N/urFSoZIub1x5rq0ZUCTexp9E52jN7BWqLgSnFc' \
171
+ 'pGYnOsbF9Yhxgl/aKyYwjLv8AgPaWI8zam8ZqTfYGOJYLSy/YjWwaWys7XVWQtCnNYP' \
172
+ 'RDpNVFdv39JjRRzs5/d3/sBwTimDha2z9enIQ7BfbB4pZREi7LXfqFjh1WVEHWfqXVH' \
173
+ 'EWXhw7PtBAlPKpSojGuJHQgX/Mn5gSV8cwMO1O2rFdqN4rC/wBz1W+L12//ALthI7/8' \
174
+ 'VcvYVSyKfJRDgWZ9ybpZ1ZtAkYr7rK4E/wCBAtV4KgXKB1FJ86V2brDV8axKYi9oIJn' \
175
+ 'TWMuHHS1qjo3fbXOy0r81qv7vr+QQjkuV/wCB7rM+ODvWDjQEaDRvDF6m1cBo3LmbNu' \
176
+ 'DWJUxMHGXUEigBApU1VH6IKscSmM43F3d52y775B4BxifNdC+C17/NZ4XQ/vdvhzO5p' \
177
+ 'dEGgdUZZfQCCA2HIBNsU1CDjyteVTqZTtld37Ox+vAKGj8w9b2nrec2zjYTF8NEz2jW' \
178
+ 'YToNCG0vM63W5N4kxrHQZ2xcMQT/AHXozBSt0FmYIAsWZg1tMM2HOV84F1Pg9/Ex4p7' \
179
+ 'Kvd6b1XSt9R6j5UlRZ+vQQCvTPEe3LJcaSzVqtCO8Y3F1zTaVMunaZMlu7r6uI0BPWY' \
180
+ 'zi6QXc0/xj8g9FvUt/k5nUXq3pyxAszl2trSlCUEqFELID11hDOVX6ltrG5qyY122NP' \
181
+ 'ytmLAsQ+laQCpVh/9k='
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captchah
4
+ class Encryptor
5
+ class << self
6
+ def encrypt(value)
7
+ Base64.strict_encode64(encryptor.encrypt_and_sign(value))
8
+ end
9
+
10
+ def decrypt(value)
11
+ encryptor.decrypt_and_verify(Base64.strict_decode64(value))
12
+ end
13
+
14
+ private
15
+
16
+ def encryptor
17
+ secret_key_base = Rails.application.secrets.secret_key_base
18
+
19
+ ActiveSupport::MessageEncryptor.new(secret_key_base)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captchah
4
+ module Generators
5
+ class Captcha
6
+ DEFAULT_DIFFICULTY = 3
7
+ DEFAULT_EXPIRY = 10.minutes
8
+ DEFAULT_WIDTH = 140
9
+ DEFAULT_ACTION_LABEL = 'Type the letters you see:'
10
+ DEFAULT_RELOAD_LABEL = 'Reload'
11
+ DEFAULT_RELOAD_MAX = 5
12
+ DEFAULT_RELOAD_COUNT = 1
13
+
14
+ def self.call(*args)
15
+ new(*args).send(:call)
16
+ end
17
+
18
+ def initialize(args = {})
19
+ @id = args[:id] || SecureRandom.uuid
20
+ @difficulty = args[:difficulty] || DEFAULT_DIFFICULTY
21
+ @expiry = args[:expiry] || DEFAULT_EXPIRY
22
+ @width = (args[:width] || DEFAULT_WIDTH).to_i
23
+ @action_label = args[:action_label] || DEFAULT_ACTION_LABEL
24
+ @reload_label = args[:reload_label] || DEFAULT_RELOAD_LABEL
25
+ @reload_max = args[:reload_max] || DEFAULT_RELOAD_MAX
26
+ @reload_count = args[:reload_count] || DEFAULT_RELOAD_COUNT
27
+ @reload = args[:reload] == false ? false : allow_reload?
28
+ @css = (args[:css] != false)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader(
34
+ :id,
35
+ :difficulty,
36
+ :expiry,
37
+ :width,
38
+ :action_label,
39
+ :reload_label,
40
+ :reload_max,
41
+ :reload_count,
42
+ :reload,
43
+ :css
44
+ )
45
+
46
+ def call
47
+ arguments_check
48
+
49
+ Html.call(
50
+ id: id,
51
+ puzzle: puzzle,
52
+ width: width,
53
+ action_label: action_label,
54
+ truth_payload: truth_payload,
55
+ reload_payload: reload_payload,
56
+ reload_label: reload_label,
57
+ reload: reload,
58
+ css: css
59
+ )
60
+ end
61
+
62
+ def puzzle
63
+ Puzzle.call(truth, difficulty)
64
+ end
65
+
66
+ def truth
67
+ @truth ||= Truth.call(difficulty)
68
+ end
69
+
70
+ def truth_payload
71
+ Encryptor.encrypt(truth: truth, timestamp: Time.current + expiry)
72
+ end
73
+
74
+ def reload_payload
75
+ return unless reload
76
+
77
+ Encryptor.encrypt(
78
+ id: id,
79
+ difficulty: difficulty,
80
+ expiry: expiry,
81
+ width: width,
82
+ action_label: action_label,
83
+ reload_label: reload_label,
84
+ reload_max: reload_max,
85
+ reload_count: reload_count,
86
+ reload: reload,
87
+ css: css
88
+ )
89
+ end
90
+
91
+ def allow_reload?
92
+ @reload_count <= @reload_max
93
+ end
94
+
95
+ def arguments_check
96
+ unless difficulty.is_a?(Integer) && difficulty.between?(1, 5)
97
+ raise Error, "'difficulty' must be an Integer value between 1 and 5."
98
+ end
99
+
100
+ return if expiry.is_a?(ActiveSupport::Duration)
101
+
102
+ raise Error, "'expiry' must be an ActiveSupport::Duration object."
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captchah
4
+ module Generators
5
+ class Html
6
+ ATTR_NAMES = %i[
7
+ id
8
+ puzzle
9
+ width
10
+ action_label
11
+ truth_payload
12
+ reload_payload
13
+ reload_label
14
+ reload
15
+ css
16
+ ].freeze
17
+
18
+ def self.call(*args)
19
+ new(*args).send(:call)
20
+ end
21
+
22
+ def initialize(args)
23
+ ATTR_NAMES.each do |attr_name|
24
+ instance_variable_set("@#{attr_name}", args[attr_name])
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader(*ATTR_NAMES)
31
+
32
+ def call
33
+ tags = [
34
+ style_tag,
35
+ action_tag,
36
+ truth_tag,
37
+ guess_tag,
38
+ puzzle_tag,
39
+ reload_animation_tag,
40
+ reload_tag,
41
+ javascript_tag
42
+ ].join
43
+
44
+ "<div id='#{container_id}' class='captchah'>#{tags}</div>"
45
+ .gsub(/(\s\s)*/, '')
46
+ .html_safe
47
+ end
48
+
49
+ def style_tag
50
+ return unless css
51
+
52
+ "<style type='text/css'>
53
+ ##{container_id} {
54
+ background-color: #f9f9f9;
55
+ border-radius: 2px;
56
+ border: 1px solid #d3d3d3;
57
+ color: black;
58
+ font-family: 'Verdana';
59
+ font-size: 11px;
60
+ letter-spacing: 0;
61
+ max-width: #{width}px;
62
+ padding: 10px;
63
+ }
64
+
65
+ ##{container_id} .captchah-guess {
66
+ background-color: white;
67
+ border-radius: 2px;
68
+ border: 1px solid #c1c1c1;
69
+ font-size: 13px;
70
+ height: 25px;
71
+ margin: 5px 0;
72
+ max-width: #{width - 12}px;
73
+ min-width: 0;
74
+ outline: none;
75
+ padding: 0 5px;
76
+ width: 100%;
77
+ }
78
+
79
+ ##{container_id} .captchah-reload-animation {
80
+ margin-top: 11px;
81
+ transform: translateY(-50%);
82
+ }
83
+
84
+ ##{container_id} .captchah-puzzle,
85
+ ##{container_id} .captchah-guess {
86
+ display: block;
87
+ }
88
+
89
+ ##{container_id} .captchah-action {
90
+ margin: 0;
91
+ padding: 0;
92
+ }
93
+
94
+ ##{container_id} .captchah-puzzle {
95
+ margin: 0
96
+ padding: 0;
97
+ width: 100%;
98
+ }
99
+
100
+ ##{container_id} .captchah-reload {
101
+ color: #5e5e5e;
102
+ cursor: pointer;
103
+ display: inline-block;
104
+ line-height: 1;
105
+ margin-top: 5px;
106
+ text-decoration: none;
107
+ transition: 0.3s;
108
+ }
109
+
110
+ ##{container_id} .captchah-reload:hover {
111
+ color: black;
112
+ }
113
+ </style>"
114
+ end
115
+
116
+ def action_tag
117
+ "<p class='captchah-action'>#{action_label}</p>"
118
+ end
119
+
120
+ def truth_tag
121
+ '<input ' \
122
+ "type='hidden' " \
123
+ "name='captchah[truth]' " \
124
+ "value='#{truth_payload}' " \
125
+ "class='captchah-truth'>"
126
+ end
127
+
128
+ def guess_tag
129
+ '<input ' \
130
+ "type='text' " \
131
+ "autocomplete='off' " \
132
+ "name='captchah[guess]' " \
133
+ "class='captchah-guess'>"
134
+ end
135
+
136
+ def puzzle_tag
137
+ '<img ' \
138
+ "src='#{puzzle}' " \
139
+ "class='captchah-puzzle'>"
140
+ end
141
+
142
+ def reload_animation_tag
143
+ '<img ' \
144
+ "src='#{"data:image/gif;base64,#{Base64Images.loader}"}' " \
145
+ "style='display: none;' " \
146
+ "class='captchah-reload-animation'>"
147
+ end
148
+
149
+ def reload_tag
150
+ return unless reload
151
+
152
+ '<span ' \
153
+ "onclick='captchah(this)' " \
154
+ "data-payload='#{reload_payload}' " \
155
+ "class='captchah-reload'>" \
156
+ "#{reload_label}" \
157
+ '</span>'
158
+ end
159
+
160
+ def javascript_tag
161
+ return unless reload
162
+
163
+ "<script type='text/javascript'>
164
+ var captchah = function(reload) {
165
+ var captchahLoaderAnimation = function(el_show, el_hide){
166
+ el_hide.style.display='none';
167
+ el_show.style.display='inline-block';
168
+ };
169
+
170
+ captchahLoaderAnimation(reload.previousSibling, reload);
171
+
172
+ var xhr = new XMLHttpRequest();
173
+ xhr.open('POST', '/captchah');
174
+ xhr.setRequestHeader('Content-Type', 'application/json');
175
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
176
+ xhr.onload = function() {
177
+ if (xhr.status === 200) {
178
+ reload.parentNode.outerHTML = xhr.responseText;
179
+ }
180
+ else if (xhr.status !== 200) {
181
+ console.log('Error: Unable to change captcha.');
182
+ captchahLoaderAnimation(reload, reload.previousSibling);
183
+ }
184
+ };
185
+ xhr.send(JSON.stringify({captchah: reload.dataset.payload}));
186
+ };
187
+ </script>"
188
+ end
189
+
190
+ def container_id
191
+ @container_id ||= "captchah-#{id}"
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captchah
4
+ module Generators
5
+ class Puzzle
6
+ def self.call(*args)
7
+ new(*args).send(:call)
8
+ end
9
+
10
+ def initialize(truth, difficulty)
11
+ @truth = truth
12
+ @difficulty = difficulty
13
+
14
+ @font = 'Verdana'
15
+ @color1 = '44,44,44'
16
+ @color2 = '235,235,235'
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :truth, :difficulty, :font, :color1, :color2
22
+
23
+ def call
24
+ image.combine_options do |c|
25
+ c.font(font)
26
+ c.pointsize(23)
27
+ c.blur("0x#{difficulty < 4 ? 3 : 4}")
28
+ c.fill(rgba(color2, opacity))
29
+ c.distort(
30
+ 'Shepards',
31
+ "#{rand(-108..0)},0 " \
32
+ "0,#{rand(-108..0)} " \
33
+ "#{rand(0..108)},0 " \
34
+ "#{rand(0..108)},8"
35
+ )
36
+ c.draw("text 23,#{rand(21..30)} '#{truth[0..2]}'")
37
+ c.distort(
38
+ 'Shepards',
39
+ '30,-50 ' \
40
+ "0,#{rand(-7..-3)} " \
41
+ "#{rand(8..17)},#{rand(2..3)} " \
42
+ "#{difficulty > 2 ? 15 : 10},#{rand(-5..-1)}"
43
+ )
44
+ c.draw("line 0,#{rand(25..35)} 290,30") if difficulty > 1
45
+ c.fill(rgba(color1, opacity))
46
+ c.pointsize(26)
47
+ c.draw("text 82,#{rand(25..40)} '#{truth[3..-1]}'")
48
+ c.draw("line 0,#{rand(10..20)} 400,20") if difficulty > 1
49
+
50
+ if difficulty < 3
51
+ c.fill(rgba(color2, -0.1))
52
+ c.draw('circle 0,0 300,0')
53
+ end
54
+
55
+ if difficulty > 3
56
+ c.pointsize(20)
57
+ c.fill(rgba(color1, -1.3))
58
+ (difficulty == 5 ? 2 : 1).times do
59
+ c.draw("text #{rand(43..55)},#{rand(18..48)} ',./ -,_'")
60
+ end
61
+ c.fill(rgba(color1, -0.2))
62
+ c.draw("text #{rand(68..75)},#{rand(10..28)} '/,\ ^._ -'")
63
+ end
64
+
65
+ if difficulty == 5
66
+ c.pointsize(40)
67
+ c.draw("text #{rand(0..36)},#{rand(38..48)} '. \ _ . -'")
68
+ end
69
+
70
+ c.blur("1x#{difficulty - 2}") if difficulty > 2
71
+ c.blur('1x0.1')
72
+ c.quality(100)
73
+ end
74
+
75
+ base64_encode
76
+ ensure
77
+ image&.destroy!
78
+ end
79
+
80
+ def base64_encode
81
+ image_content = File.open(image.path, &:read)
82
+
83
+ "data:image/jpeg;base64,#{Base64.strict_encode64(image_content)}"
84
+ end
85
+
86
+ def image
87
+ @image ||=
88
+ begin
89
+ base64_image = Base64Images.puzzle_background
90
+ decoded_image = Base64.strict_decode64(base64_image)
91
+ MiniMagick::Image.read(decoded_image)
92
+ rescue NameError => e
93
+ raise Error, 'Missing MiniMagick.' if e.to_s.include?('MiniMagick')
94
+ end
95
+ end
96
+
97
+ def opacity
98
+ @opacity ||= difficulty > 2 ? -1.3 : 1.2
99
+ end
100
+
101
+ def rgba(rgb, alpha)
102
+ "rgba(#{rgb},#{alpha})"
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captchah
4
+ module Generators
5
+ class Truth
6
+ def self.call(difficulty)
7
+ chrs = []
8
+
9
+ (rand(0..1).positive? ? 4 : 5).times do
10
+ up_or_down = rand(0..1).positive? && difficulty > 1 && difficulty != 3
11
+ chr = (up_or_down ? rand(97..122) : rand(65..90)).chr
12
+ chrs << (%w[i j l r].any? { |c| c == chr } ? chr.upcase : chr)
13
+ end
14
+
15
+ chrs.join
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captchah
4
+ class Verifier
5
+ def self.call(params)
6
+ return :no_params unless params.present?
7
+
8
+ return :invalid if params[:guess].blank? || params[:truth].blank?
9
+
10
+ truth_payload = Encryptor.decrypt(params[:truth])
11
+
12
+ guess = params[:guess].downcase.delete(' ')
13
+
14
+ return :expired unless truth_payload[:timestamp] >= Time.current
15
+
16
+ return :valid if guess == truth_payload[:truth].downcase
17
+
18
+ :invalid
19
+ rescue ArgumentError, MessageEncryptor::InvalidMessage
20
+ :invalid
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Captchah
4
+ VERSION = '1.0.5'
5
+ end
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: captchah
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.5
5
+ platform: ruby
6
+ authors:
7
+ - Evgeni Radev
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-08-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mini_magick
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.8'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rails
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.3'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.3'
69
+ description:
70
+ email:
71
+ - evgeniradev@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - README.md
77
+ - app/controllers/captchah/captchah_controller.rb
78
+ - config/routes.rb
79
+ - lib/captchah.rb
80
+ - lib/captchah/base64_images.rb
81
+ - lib/captchah/encryptor.rb
82
+ - lib/captchah/generators/captcha.rb
83
+ - lib/captchah/generators/html.rb
84
+ - lib/captchah/generators/puzzle.rb
85
+ - lib/captchah/generators/truth.rb
86
+ - lib/captchah/verifier.rb
87
+ - lib/captchah/version.rb
88
+ homepage: https://github.com/evgeniradev/captchah
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ allowed_push_host: https://rubygems.org
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements:
108
+ - You must have ImageMagick or GraphicsMagick installed.
109
+ rubyforge_project:
110
+ rubygems_version: 2.7.9
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: A Rails captcha gem that attempts to determine whether or not a user is human.
114
+ test_files: []