captchah 1.0.5

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.
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: []