active_hashcash 0.1.0

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: 47a914f25827b270ed0e1bb2f0fc222316846b48232cf5dceb255c99725f1467
4
+ data.tar.gz: f5aaf27f5d578cfc271624c4ddd3746365e81424247cd50ed43744cb534ea893
5
+ SHA512:
6
+ metadata.gz: 9799788e77f0f6d8415d0e69a153a8b88715e34d5742794ed64a0db3a30a41bd619bc54d9c840b1d4d37eff4f0f9cf90ec9bc4c552656aad8f086068997685e0
7
+ data.tar.gz: f439c8fb6265ae1a2999b6fe3995871762e09b8ab05b38a776dc4e0eafd4fe67d431396323425118f55d596c2f2270ff5160733ccc94fe7f1d47df0eba38ce9f
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-07-01
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Alexis Bernard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # ActiveHashcash
2
+
3
+ <img align="right" width="200px" src="logo.png" alt="Active Hashcash logo"/>
4
+
5
+ ActiveHashcash protects your Rails application against DoS and bots.
6
+
7
+ Hashcash is proof-of-work algorithm, invented by Adam Back in 1997, to protect systems against denial of service attacks.
8
+ ActiveHashcash is an easy way to protect any Rails application against brute force attacks and some bots.
9
+
10
+ The idea is to force clients to spend some time to solve a hard problem that is very easy to verify for the server.
11
+ We have developped ActiveHashcash after seeing brute force attacks against our Rails application monitoring service [RorVsWild](https://rorvswild.com).
12
+
13
+ The idea is to enable ActiveHashcash on sensitive forms such as login and registration. While the user is filling the form,
14
+ ActiveHashcash performs the work in JavaScript and set the result into a hidden input text. The form cannot be submitted while the proof of work has not been found.
15
+ The user submits the form, and the stamp is verified by the controller in a before action.
16
+
17
+ It blocks bots that do not interpret JavaScript since the proof of work is not computed. For the more sophisticated bots, we are happy to slow them down.
18
+
19
+ Here is a [demo on a login form](https://www.rorvswild.com/session) :
20
+
21
+ ![Active Hashcash GIF preview](demo.gif)
22
+
23
+ ## Limitations
24
+
25
+ The JavaScript implementation is 10 to 20 times slower than the official C version.
26
+ It needs some work and knowledges to be optimised. Unfortunately, I'm not a JavaScript expert.
27
+ Maybe you have good JS skills to optimize it ?
28
+
29
+ ## Installation
30
+
31
+ Add this line to your application's Gemfile:
32
+
33
+ ```ruby
34
+ gem "active_hashcash"
35
+ ```
36
+
37
+ Require hashcash from your JavaScript manifest.
38
+
39
+ ```js
40
+ //= require hashcash
41
+ ```
42
+
43
+ Add a Hashcash hidden field into the form you want to protect.
44
+
45
+ ```erb
46
+ <form>
47
+ <%= hashcash_hidden_field_tag %>
48
+ </form>
49
+ ```
50
+
51
+ Then you have to define a `before_action :check_hashcash` in you controller.
52
+
53
+ ```ruby
54
+ class SessionController < ApplicationController
55
+ include ActiveHashcash
56
+
57
+ # Only the action receiving the form needs to be protected
58
+ before_action :check_hashcash, only: :create
59
+ end
60
+ ```
61
+
62
+ To customize some behaviour, you can override most of the methods which begins with `hashcash_`.
63
+ Simply have a look to `active_hashcash.rb`.
64
+
65
+ You must have Redis in order to prevent double spent stamps. Otherwise it will be useless.
66
+ It automatically tries to connect with the environement variables `ACTIVE_HASHCASH_REDIS_URL` or `REDIS_URL`.
67
+ You can also manually set the URL with `ActiveHashcash.redis_url = redis://user:password@localhost:6379`.
68
+
69
+ ## Complexity
70
+
71
+ Complexity is the most important parameter. By default its value is 20 and requires most of the time 5 to 20 seconds to be solved on a decent laptop.
72
+ The user won't wait that long, since he needs to fill the form while the problem is solving.
73
+ Howevever, if your application includes people with slow and old devices, then consider lowering this value, to 16 or 18.
74
+
75
+ You can change the complexity, either with `ActiveHashcash.bits = 20` or by overriding the method `hashcash_bits` in you controller.
76
+
77
+ ## Contributing
78
+
79
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/active_hashcash.
80
+
81
+ ## License
82
+
83
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
84
+
85
+ Made by Alexis Bernard at [Base Secrète](https://basesecrete.com).
@@ -0,0 +1,150 @@
1
+ module ActiveHashcash
2
+ extend ActiveSupport::Concern
3
+
4
+ include ActionView::Helpers::FormTagHelper
5
+
6
+ included do
7
+ helper_method :hashcash_hidden_field_tag
8
+ end
9
+
10
+ mattr_accessor :bits, instance_accessor: false, default: 20
11
+ mattr_accessor :resource, instance_accessor: false
12
+ mattr_accessor :redis_url, instance_accessor: false
13
+
14
+ # TODO: protect_from_brute_force bits: 20, exception: ActionController::InvalidAuthenticityToken, with: :handle_failed_hashcash
15
+
16
+ # Call me via a before_action when the form is submitted : `before_action :chech_hashcash, only: :create`
17
+ def check_hashcash
18
+ if hashcash_stamp_is_valid? && !hashcash_stamp_spent?
19
+ hashcash_redis.sadd("active_hashcash_stamps".freeze, hashcash_param)
20
+ hashcash_after_success
21
+ else
22
+ hashcash_after_failure
23
+ end
24
+ end
25
+
26
+ # Override the methods below in your controller, to change any parameter of behaviour.
27
+
28
+ # By default the host name is used as the resource.
29
+ # It' should be good for most cases and prevent from reusing the same stamp between sites.
30
+ def hashcash_resource
31
+ ActiveHashcash.resource || request.host
32
+ end
33
+
34
+ # Define the complexity, the higher the slower it is. Consider lowering this value to not exclude people with old and slow devices.
35
+ # On a decent laptop, it takes around 30 seconds for the JavaScript implementation to solve a 20 bits complexity and few seconds when it's 16.
36
+ def hashcash_bits
37
+ ActiveHashcash.bits
38
+ end
39
+
40
+ # Override if you want to rename the hashcash param.
41
+ def hashcash_param
42
+ params[:hashcash]
43
+ end
44
+
45
+ # Override to customize message displayed on submit button while computing hashcash.
46
+ def hashcash_waiting_message
47
+ t("active_hashcash.waiting_label")
48
+ end
49
+
50
+ # Override to provide a different behaviour when hashcash failed
51
+ def hashcash_after_failure
52
+ raise ActionController::InvalidAuthenticityToken.new("Invalid hashcash #{hashcash_param}")
53
+ end
54
+
55
+ # Maybe you want something special when the hashcash is valid. By default nothing happens.
56
+ def hashcash_after_success
57
+ # Override me for your own needs.
58
+ end
59
+
60
+ # Call it inside the form that have to be protected and don't forget to initialize the JavaScript Hascash.setup().
61
+ # Unless you need something really special, you should not need to override this method.
62
+ def hashcash_hidden_field_tag(name = :hashcash)
63
+ options = {resource: hashcash_resource, bits: hashcash_bits, waiting_message: hashcash_waiting_message}
64
+ hidden_field_tag(name, "", "data-hashcash" => options.to_json)
65
+ end
66
+
67
+ def hashcash_redis
68
+ @hashcash_redis = Redis.new(url: hashcash_redis_url)
69
+ end
70
+
71
+ def hashcash_redis_url
72
+ ActiveHashcash.redis_url || ENV["ACTIVE_HASHCASH_REDIS_URL"] || ENV["REDIS_URL"]
73
+ end
74
+
75
+ def hashcash_stamp_is_valid?
76
+ stamp = Stamp.parse(hashcash_param)
77
+ stamp.valid? && stamp.bits >= hashcash_bits && stamp.parse_date >= Date.yesterday
78
+ end
79
+
80
+ def hashcash_stamp_spent?
81
+ hashcash_redis.sismember("active_hashcash_stamps".freeze, hashcash_param)
82
+ end
83
+
84
+
85
+
86
+ class Engine < ::Rails::Engine
87
+ config.assets.paths << File.expand_path("..", __FILE__)
88
+
89
+ config.after_initialize { load_translations }
90
+
91
+ def load_translations
92
+ if !I18n.backend.exists?(I18n.locale, "active_hashcash")
93
+ I18n.backend.store_translations(:de, {active_hashcash: {waiting_label: "Warten auf die Überprüfung ..."}})
94
+ I18n.backend.store_translations(:en, {active_hashcash: {waiting_label: "Waiting for verification ..."}})
95
+ I18n.backend.store_translations(:es, {active_hashcash: {waiting_label: "A la espera de la verificación ..."}})
96
+ I18n.backend.store_translations(:fr, {active_hashcash: {waiting_label: "En attente de vérification ..."}})
97
+ I18n.backend.store_translations(:it, {active_hashcash: {waiting_label: "In attesa di verifica ..."}})
98
+ I18n.backend.store_translations(:jp, {active_hashcash: {waiting_label: "検証待ち ..."}})
99
+ I18n.backend.store_translations(:pt, {active_hashcash: {waiting_label: "À espera de verificação ..."}})
100
+ end
101
+ end
102
+ end
103
+
104
+ class Stamp
105
+ attr_reader :version, :bits, :date, :resource, :extension, :rand, :counter
106
+
107
+ def self.parse(string)
108
+ args = string.split(":")
109
+ new(args[0], args[1], args[2], args[3], args[4], args[5], args[6])
110
+ end
111
+
112
+ def self.mint(resource, options = {})
113
+ new(
114
+ options[:version] || 1,
115
+ options[:bits] || ActiveHashcash.bits,
116
+ options[:date] || Date.today.strftime("%y%m%d"),
117
+ resource,
118
+ options[:ext],
119
+ options[:rand] || SecureRandom.alphanumeric(16),
120
+ options[:counter] || 0).work
121
+ end
122
+
123
+ def initialize(version, bits, date, resource, extension, rand, counter)
124
+ @version = version
125
+ @bits = bits.to_i
126
+ @date = date.respond_to?(:strftime) ? date.strftime("%y%m%d") : date
127
+ @resource = resource
128
+ @extension = extension
129
+ @rand = rand
130
+ @counter = counter
131
+ end
132
+
133
+ def valid?
134
+ Digest::SHA1.hexdigest(to_s).hex >> (160-bits) == 0
135
+ end
136
+
137
+ def to_s
138
+ [version, bits, date, resource, extension, rand, counter].join(":")
139
+ end
140
+
141
+ def parse_date
142
+ Date.strptime(date, "%y%m%d")
143
+ end
144
+
145
+ def work
146
+ @counter += 1 until valid?
147
+ self
148
+ end
149
+ end
150
+ end
data/lib/hashcash.js ADDED
@@ -0,0 +1,257 @@
1
+ // http://www.hashcash.org/docs/hashcash.html
2
+ // <input type="hiden" name="hashcash" data-hashcash="{resource: 'site.example', bits: 16}"/>
3
+ Hashcash = function(input) {
4
+ options = JSON.parse(input.getAttribute("data-hashcash"))
5
+ Hashcash.disableParentForm(input, options)
6
+ input.dispatchEvent(new CustomEvent("hashcash:mint", {bubbles: true}))
7
+
8
+ Hashcash.mint(options.resource, options, function(stamp) {
9
+ input.value = stamp.toString()
10
+ Hashcash.enableParentForm(input, options)
11
+ input.dispatchEvent(new CustomEvent("hashcash:minted", {bubbles: true, detail: {stamp: stamp}}))
12
+ })
13
+ }
14
+
15
+ Hashcash.setup = function() {
16
+ if (document.readyState != "loading") {
17
+ var input = document.querySelector("input#hashcash")
18
+ input && new Hashcash(input)
19
+ } else
20
+ document.addEventListener("DOMContentLoaded", Hashcash.setup )
21
+ }
22
+
23
+ Hashcash.disableParentForm = function(input, options) {
24
+ input.form.querySelectorAll("[type=submit]").forEach(function(submit) {
25
+ submit.originalValue = submit.value
26
+ options["waiting_message"] && (submit.value = options["waiting_message"])
27
+ submit.disabled = true
28
+ })
29
+ }
30
+
31
+ Hashcash.enableParentForm = function(input, options) {
32
+ input.form.querySelectorAll("[type=submit]").forEach(function(submit) {
33
+ submit.originalValue && (submit.value = submit.originalValue)
34
+ submit.disabled = null
35
+ })
36
+ }
37
+
38
+ Hashcash.default = {
39
+ version: 1,
40
+ bits: 20,
41
+ extension: null,
42
+ }
43
+
44
+ Hashcash.mint = function(resource, options, callback) {
45
+ // Format date to YYMMDD
46
+ var date = new Date
47
+ var year = date.getFullYear().toString()
48
+ year = year.slice(year.length - 2, year.length)
49
+ var month = (date.getMonth() + 1).toString().padStart(2, "0")
50
+ var day = date.getDate().toString().padStart(2, "0")
51
+
52
+ var stamp = new Hashcash.Stamp(
53
+ options.version || Hashcash.default.version,
54
+ options.bits || Hashcash.default.bits,
55
+ options.date || year + month + day,
56
+ resource,
57
+ options.extension || Hashcash.default.extension,
58
+ options.rand || Math.random().toString(36).substr(2, 10),
59
+ )
60
+ return stamp.work(callback)
61
+ }
62
+
63
+ Hashcash.Stamp = function(version, bits, date, resource, extension, rand, counter = 0) {
64
+ this.version = version
65
+ this.bits = bits
66
+ this.date = date
67
+ this.resource = resource
68
+ this.extension = extension
69
+ this.rand = rand
70
+ this.counter = counter
71
+ }
72
+
73
+ Hashcash.Stamp.parse = function(string) {
74
+ var args = string.split(":")
75
+ return new Hashcash.Stamp(args[0], args[1], args[2], args[3], args[4], args[5], args[6])
76
+ }
77
+
78
+ Hashcash.Stamp.prototype.toString = function() {
79
+ return [this.version, this.bits, this.date, this.resource, this.extension, this.rand, this.counter].join(":")
80
+ }
81
+
82
+ // Trigger the given callback when the problem is solved.
83
+ // In order to not freeze the page, setTimeout is called every 100ms to let some CPU to other tasks.
84
+ Hashcash.Stamp.prototype.work = function(callback) {
85
+ this.startClock()
86
+ var timer = performance.now()
87
+ while (!this.check())
88
+ if (this.counter++ && performance.now() - timer > 100)
89
+ return setTimeout(this.work.bind(this), 0, callback)
90
+ this.stopClock()
91
+ callback(this)
92
+ }
93
+
94
+ Hashcash.Stamp.prototype.check = function() {
95
+ var array = Hashcash.sha1(this.toString())
96
+ return array[0] >> (160-this.bits) == 0
97
+ }
98
+
99
+ Hashcash.Stamp.prototype.startClock = function() {
100
+ this.startedAt || (this.startedAt = performance.now())
101
+ }
102
+
103
+ Hashcash.Stamp.prototype.stopClock = function() {
104
+ this.endedAt || (this.endedAt = performance.now())
105
+ var duration = this.endedAt - this.startedAt
106
+ var speed = Math.round(this.counter * 1000 / duration)
107
+ console.debug("Hashcash " + this.toString() + " minted in " + duration + "ms (" + speed + " per seconds)")
108
+ }
109
+
110
+ /**
111
+ * Secure Hash Algorithm (SHA1)
112
+ * http://www.webtoolkit.info/
113
+ **/
114
+ Hashcash.sha1 = function(msg) {
115
+ var rotate_left = Hashcash.sha1.rotate_left
116
+ var Utf8Encode = Hashcash.sha1.Utf8Encode
117
+
118
+ var blockstart;
119
+ var i, j;
120
+ var W = new Array(80);
121
+ var H0 = 0x67452301;
122
+ var H1 = 0xEFCDAB89;
123
+ var H2 = 0x98BADCFE;
124
+ var H3 = 0x10325476;
125
+ var H4 = 0xC3D2E1F0;
126
+ var A, B, C, D, E;
127
+ var temp;
128
+ msg = Utf8Encode(msg);
129
+ var msg_len = msg.length;
130
+ var word_array = new Array();
131
+ for (i = 0; i < msg_len - 3; i += 4) {
132
+ j = msg.charCodeAt(i) << 24 | msg.charCodeAt(i + 1) << 16 |
133
+ msg.charCodeAt(i + 2) << 8 | msg.charCodeAt(i + 3);
134
+ word_array.push(j);
135
+ }
136
+ switch (msg_len % 4) {
137
+ case 0:
138
+ i = 0x080000000;
139
+ break;
140
+ case 1:
141
+ i = msg.charCodeAt(msg_len - 1) << 24 | 0x0800000;
142
+ break;
143
+ case 2:
144
+ i = msg.charCodeAt(msg_len - 2) << 24 | msg.charCodeAt(msg_len - 1) << 16 | 0x08000;
145
+ break;
146
+ case 3:
147
+ i = msg.charCodeAt(msg_len - 3) << 24 | msg.charCodeAt(msg_len - 2) << 16 | msg.charCodeAt(msg_len - 1) << 8 | 0x80;
148
+ break;
149
+ }
150
+ word_array.push(i);
151
+ while ((word_array.length % 16) != 14) word_array.push(0);
152
+ word_array.push(msg_len >>> 29);
153
+ word_array.push((msg_len << 3) & 0x0ffffffff);
154
+ for (blockstart = 0; blockstart < word_array.length; blockstart += 16) {
155
+ for (i = 0; i < 16; i++) W[i] = word_array[blockstart + i];
156
+ for (i = 16; i <= 79; i++) W[i] = rotate_left(W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16], 1);
157
+ A = H0;
158
+ B = H1;
159
+ C = H2;
160
+ D = H3;
161
+ E = H4;
162
+ for (i = 0; i <= 19; i++) {
163
+ temp = (rotate_left(A, 5) + ((B & C) | (~B & D)) + E + W[i] + 0x5A827999) & 0x0ffffffff;
164
+ E = D;
165
+ D = C;
166
+ C = rotate_left(B, 30);
167
+ B = A;
168
+ A = temp;
169
+ }
170
+ for (i = 20; i <= 39; i++) {
171
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0x6ED9EBA1) & 0x0ffffffff;
172
+ E = D;
173
+ D = C;
174
+ C = rotate_left(B, 30);
175
+ B = A;
176
+ A = temp;
177
+ }
178
+ for (i = 40; i <= 59; i++) {
179
+ temp = (rotate_left(A, 5) + ((B & C) | (B & D) | (C & D)) + E + W[i] + 0x8F1BBCDC) & 0x0ffffffff;
180
+ E = D;
181
+ D = C;
182
+ C = rotate_left(B, 30);
183
+ B = A;
184
+ A = temp;
185
+ }
186
+ for (i = 60; i <= 79; i++) {
187
+ temp = (rotate_left(A, 5) + (B ^ C ^ D) + E + W[i] + 0xCA62C1D6) & 0x0ffffffff;
188
+ E = D;
189
+ D = C;
190
+ C = rotate_left(B, 30);
191
+ B = A;
192
+ A = temp;
193
+ }
194
+ H0 = (H0 + A) & 0x0ffffffff;
195
+ H1 = (H1 + B) & 0x0ffffffff;
196
+ H2 = (H2 + C) & 0x0ffffffff;
197
+ H3 = (H3 + D) & 0x0ffffffff;
198
+ H4 = (H4 + E) & 0x0ffffffff;
199
+ }
200
+ return [H0, H1, H2, H3, H4]
201
+ }
202
+
203
+ Hashcash.hexSha1 = function(msg) {
204
+ var array = Hashcash.sha1(msg)
205
+ var cvt_hex = Hashcash.sha1.cvt_hex
206
+ return cvt_hex(array[0]) + cvt_hex(array[1]) + cvt_hex(array[2]) + cvt_hex(array3) + cvt_hex(array[4])
207
+ }
208
+
209
+ Hashcash.sha1.rotate_left = function(n, s) {
210
+ var t4 = (n << s) | (n >>> (32 - s));
211
+ return t4;
212
+ };
213
+
214
+ Hashcash.sha1.lsb_hex = function(val) {
215
+ var str = '';
216
+ var i;
217
+ var vh;
218
+ var vl;
219
+ for (i = 0; i <= 6; i += 2) {
220
+ vh = (val >>> (i * 4 + 4)) & 0x0f;
221
+ vl = (val >>> (i * 4)) & 0x0f;
222
+ str += vh.toString(16) + vl.toString(16);
223
+ }
224
+ return str;
225
+ };
226
+
227
+ Hashcash.sha1.cvt_hex = function(val) {
228
+ var str = '';
229
+ var i;
230
+ var v;
231
+ for (i = 7; i >= 0; i--) {
232
+ v = (val >>> (i * 4)) & 0x0f;
233
+ str += v.toString(16);
234
+ }
235
+ return str;
236
+ };
237
+
238
+ Hashcash.sha1.Utf8Encode = function(string) {
239
+ string = string.replace(/\r\n/g, '\n');
240
+ var utftext = '';
241
+ for (var n = 0; n < string.length; n++) {
242
+ var c = string.charCodeAt(n);
243
+ if (c < 128) {
244
+ utftext += String.fromCharCode(c);
245
+ } else if ((c > 127) && (c < 2048)) {
246
+ utftext += String.fromCharCode((c >> 6) | 192);
247
+ utftext += String.fromCharCode((c & 63) | 128);
248
+ } else {
249
+ utftext += String.fromCharCode((c >> 12) | 224);
250
+ utftext += String.fromCharCode(((c >> 6) & 63) | 128);
251
+ utftext += String.fromCharCode((c & 63) | 128);
252
+ }
253
+ }
254
+ return utftext;
255
+ };
256
+
257
+ Hashcash.setup()
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_hashcash
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Alexis Bernard
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-07-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.0.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.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.2.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.2.0
41
+ description: Potect your Rails application against DoS and bots.
42
+ email:
43
+ - alexis@basesecrete.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - CHANGELOG.md
49
+ - LICENSE.txt
50
+ - README.md
51
+ - lib/active_hashcash.rb
52
+ - lib/hashcash.js
53
+ homepage: https://github.com/BaseSecrete/active_hashcash
54
+ licenses:
55
+ - MIT
56
+ metadata:
57
+ homepage_uri: https://github.com/BaseSecrete/active_hashcash
58
+ source_code_uri: https://github.com/BaseSecrete/active_hashcash
59
+ changelog_uri: https://github.com/BaseSecrete/active_hashcash/CHANGELOG.md
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.4.0
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.2.22
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Potect your Rails application against DoS and bots.
79
+ test_files: []