active_hashcash 0.1.0

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