active_hashcash 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +101 -14
  4. data/app/assets/config/active_hashcash_manifest.js +1 -0
  5. data/app/assets/javascripts/hashcash.js +257 -0
  6. data/app/assets/stylesheets/active_hashcash/application.css +15 -0
  7. data/app/controllers/active_hashcash/addresses_controller.rb +7 -0
  8. data/app/controllers/active_hashcash/application_controller.rb +4 -0
  9. data/app/controllers/active_hashcash/assets_controller.rb +34 -0
  10. data/app/controllers/active_hashcash/stamps_controller.rb +11 -0
  11. data/app/helpers/active_hashcash/addresses_helper.rb +4 -0
  12. data/app/helpers/active_hashcash/application_helper.rb +4 -0
  13. data/app/helpers/active_hashcash/stamps_helper.rb +4 -0
  14. data/app/jobs/active_hashcash/application_job.rb +4 -0
  15. data/app/mailers/active_hashcash/application_mailer.rb +6 -0
  16. data/app/models/active_hashcash/application_record.rb +5 -0
  17. data/app/models/active_hashcash/stamp.rb +70 -0
  18. data/app/views/active_hashcash/addresses/index.html.erb +17 -0
  19. data/app/views/active_hashcash/assets/_logo.svg.erb +1 -0
  20. data/app/views/active_hashcash/assets/_style.css +148 -0
  21. data/app/views/active_hashcash/assets/application.css.erb +1 -0
  22. data/app/views/active_hashcash/assets/ariato.css.erb +2 -0
  23. data/app/views/active_hashcash/assets/favicon.ico +0 -0
  24. data/app/views/active_hashcash/assets/favicon.svg.erb +1 -0
  25. data/app/views/active_hashcash/assets/vendor/_ariato_base.css +1297 -0
  26. data/app/views/active_hashcash/assets/vendor/_ariato_extra.css +1206 -0
  27. data/app/views/active_hashcash/stamps/_filters.html.erb +39 -0
  28. data/app/views/active_hashcash/stamps/index.html.erb +25 -0
  29. data/app/views/active_hashcash/stamps/show.html.erb +21 -0
  30. data/app/views/layouts/active_hashcash/application.html.erb +36 -0
  31. data/config/locales/de.yml +4 -0
  32. data/config/locales/en.yml +4 -0
  33. data/config/locales/es.yml +4 -0
  34. data/config/locales/fr.yml +4 -0
  35. data/config/locales/it.yml +4 -0
  36. data/config/locales/jp.yml +4 -0
  37. data/config/locales/pt.yml +4 -0
  38. data/config/routes.rb +6 -0
  39. data/db/migrate/20240215143453_create_active_hashcash_stamps.rb +25 -0
  40. data/lib/active_hashcash/engine.rb +2 -14
  41. data/lib/active_hashcash/version.rb +1 -1
  42. data/lib/active_hashcash.rb +35 -21
  43. data/lib/tasks/active_hashcash_tasks.rake +4 -0
  44. metadata +47 -26
  45. data/lib/active_hashcash/stamp.rb +0 -52
  46. data/lib/active_hashcash/store.rb +0 -25
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81d808ca54efb58e16c61524bde0aee355f5a7b003ef4cb6cda2e9b672c7a284
4
- data.tar.gz: 6ec6f99dd02e353bb4e86bb9db8e65ecf35dc46e13051e401b62608700b7fde5
3
+ metadata.gz: dbc710b7b2cc6fef0667915ac1a95ccfdd6d1cb3eaa1fa9b97d29ef81e79985d
4
+ data.tar.gz: 323c22e60aa27d2d87e39ee8f5c356d2fc6e8190426e49831a29d762d2e74f28
5
5
  SHA512:
6
- metadata.gz: 17f8b181f15b009b850fea2e1ead3a5cb904218d8877b31d9b6a2e5f2578972a6a1a2837cdbda46013987911a88198d543dd7f3a79f68b455a969a005a3fbfc8
7
- data.tar.gz: 2b02561bf0516b2accb42ac64fe8d93b7bf956bb9474fcadec93db08f4be551d9eea430884d133d08b92156a9d10440ac8f48067207c95d3b72e4268752748dd
6
+ metadata.gz: ed74328b7b1f91eb2dece7a29efb5b9d68079872694e589caca3e79de1a06e6d0bd5d2556dcab12f92fcede4c08b075a4dc4b9077e79c8511cb4045ce509c82c
7
+ data.tar.gz: e8f580f0668529b3bd7e220f2bdbd4b2eae6bc63272228b9cb4d1fe75ef0e30202983e3e06d91664558d36239137b69d7ba96996c80b370f4b1638300f77781a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # Changelog of ActiveHashcash
2
+
3
+ ## Unrelease
4
+
5
+ - Fix gem spec list files
6
+
7
+ ## 0.3.0 - 2024-03-14
8
+
9
+ - Increase complexity automatically to slowdown brute force attacks
10
+ - Add mountable dashboard to list latest stamps and most frequent IP addresses
11
+ - Store stamps into the database instead of Redis
12
+ - Fix ActiveHashcash::Store#add? by converting stamp to a string
13
+
1
14
  ## 0.2.0 - 2022-08-02
2
15
 
3
16
  - Add ActiveHashcash::Store#clean to removed expired stamps
data/README.md CHANGED
@@ -2,29 +2,34 @@
2
2
 
3
3
  <img align="right" width="200px" src="logo.png" alt="Active Hashcash logo"/>
4
4
 
5
- ActiveHashcash protects your Rails application against brute force attacks, DoS and bots.
5
+ ActiveHashcash protects Rails applications against bots and brute force attacks without annoying humans.
6
6
 
7
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.
8
+ ActiveHashcash is an easy way to protect any Rails application against brute force attacks and bots.
9
9
 
10
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
11
  We have developped ActiveHashcash after seeing brute force attacks against our Rails application monitoring service [RorVsWild](https://rorvswild.com).
12
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.
13
+ ActiveHashcash is ideal to set up on sensitive forms such as login and registration.
14
+ While the user is filling the form, the problem is solved in JavaScript and set the result into a hidden input text.
15
+ The form cannot be submitted while the proof of work has not been found.
16
+ Then the user submits the form, and the stamp is verified by the controller in a before action.
16
17
 
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
+ It blocks bots that do not interpret JavaScript since the proof of work is not computed.
19
+ More sophisticated bots and brute force attacks are slow down.
20
+ Moreover the complexity increases automatically for IP addresses sending many requests.
21
+ Thus it becomes very CPU costly for attackers.
18
22
 
19
- Here is a [demo on a registration form](https://www.rorvswild.com/account/new) :
23
+ Finally legitimate users are not annoyed by asking to solve a puzzle or clicking on the all images containing a bus.
24
+ Here is a [demo on a registration form](https://www.rorvswild.com/session) :
20
25
 
21
26
  ![Active Hashcash GIF preview](demo.gif)
22
27
 
23
- ## Limitations
28
+ ---
24
29
 
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 ?
30
+ <img align="left" height="24px" src="rorvswild_logo.jpg" alt="RorVsWild logo"/>Made by <a href="https://www.rorvswild.com">RorVsWild</a>, performances & exceptions monitoring for Ruby on Rails applications.
31
+
32
+ ---
28
33
 
29
34
  ## Installation
30
35
 
@@ -62,26 +67,108 @@ end
62
67
  To customize some behaviour, you can override most of the methods which begins with `hashcash_`.
63
68
  Simply have a look to `active_hashcash.rb`.
64
69
 
70
+ Stamps are stored into into the database to prevents from spending them more than once.
71
+ You must run a migration:
72
+
73
+ ```
74
+ rails active_hashcash:install:migrations
75
+ rails db:migrate
76
+ ```
77
+
78
+ ### Dashboard
79
+
80
+ There is a mountable dahsboard which allows to see all spent stamps.
81
+ It's not mandatory, but useful for monitoring purpose.
82
+
83
+ ![ActiveHashcash dashboard](active_hashcash_dashboard.png "ActiveHashcash dashboard")
84
+
85
+ ```ruby
86
+ # config/routes.rb
87
+ mount ActiveHashcash::Engine, at: "hashcash"
88
+ ```
89
+
90
+ ActiveHashcash cannot guess how you handle user authentication, because it is different for all Rails applications.
91
+ So you have to monkey patch `ActiveHashcash::ApplicationController` in order to inject your own mechanism.
92
+ The patch can be saved wherever you want.
93
+ For example, I like to have all the patches in one place, so I put them in `lib/patches`.
94
+
95
+ ```ruby
96
+ # lib/patches/active_hashcash.rb
97
+
98
+ ActiveHashcash::ApplicationController.class_eval do
99
+ before_action :require_admin
100
+
101
+ def require_admin
102
+ # This example supposes there are current_user and User#admin? methods
103
+ raise ActionController::RoutingError.new("Not found") unless current_user.try(:admin?)
104
+ end
105
+ end
106
+ end
107
+ ```
108
+
109
+ Then you have to require the monkey patch.
110
+ Because it's loaded via require, it won't be reloaded in development.
111
+ Since you are not supposed to change this file often, it should not be an issue.
112
+
113
+ ```ruby
114
+ # config/application.rb
115
+ config.after_initialize do
116
+ require "patches/active_hashcash"
117
+ end
118
+ ```
119
+
120
+ If you use Devise, you can check the permission directly from routes.rb:
121
+
122
+ ```ruby
123
+ # config/routes.rb
124
+ authenticate :user, -> (u) { u.admin? } do # Supposing there is a User#admin? method
125
+ mount ActiveHashcash::Engine, at: "hashcash" # http://localhost:3000/hashcash
126
+ end
127
+ ```
128
+
129
+ ### Before version 0.3.0
130
+
65
131
  You must have Redis in order to prevent double spent stamps. Otherwise it will be useless.
66
132
  It automatically tries to connect with the environement variables `ACTIVE_HASHCASH_REDIS_URL` or `REDIS_URL`.
67
133
  You can also manually set the URL with `ActiveHashcash.redis_url = redis://user:password@localhost:6379`.
68
134
 
69
135
  You should call `ActiveHashcash::Store#clean` once a day, to remove expired stamps.
70
136
 
137
+ To upgrade from 0.2.0 you must run the migration :
138
+
139
+ ```
140
+ rails active_hashcash:install:migrations
141
+ rails db:migrate
142
+ ```
143
+
71
144
  ## Complexity
72
145
 
73
146
  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.
74
147
  The user won't wait that long, since he needs to fill the form while the problem is solving.
75
148
  Howevever, if your application includes people with slow and old devices, then consider lowering this value, to 16 or 18.
76
149
 
77
- You can change the complexity, either with `ActiveHashcash.bits = 20` or by overriding the method `hashcash_bits` in you controller.
150
+ You can change the minimum complexity with `ActiveHashcash.bits = 20`.
151
+
152
+ Since version 0.3.0, the complexity increases with the number of stamps spent during le last 24H from the same IP address.
153
+ Thus it becomes very efficient to slow down brute force attacks.
154
+
155
+ ## Limitations
156
+
157
+ The JavaScript implementation is 10 to 20 times slower than the official C version.
158
+ I first used the SubtleCrypto API but it is surprisingly slower than a custom SHA1 implementation.
159
+ Maybe I did in an unefficient way 2df3ba5?
160
+ Another idea would be to compile the work algorithm in wasm.
161
+
162
+ Unfortunately, I'm not a JavaScript expert.
163
+ Maybe you have good JS skills to optimize it?
164
+ Any help would be appreciate to better fights bots and brute for attacks!
78
165
 
79
166
  ## Contributing
80
167
 
81
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/active_hashcash.
168
+ Bug reports and pull requests are welcome on GitHub at https://github.com/BaseSecrete/active_hashcash.
82
169
 
83
170
  ## License
84
171
 
85
172
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
86
173
 
87
- Made by Alexis Bernard at [Base Secrète](https://basesecrete.com).
174
+ Made by Alexis Bernard at [RorVsWild](https://www.rorvswild.com).
@@ -0,0 +1 @@
1
+ //= link_directory ../stylesheets/active_hashcash .css
@@ -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()
@@ -0,0 +1,15 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
@@ -0,0 +1,7 @@
1
+ module ActiveHashcash
2
+ class AddressesController < ApplicationController
3
+ def index
4
+ @addresses = Stamp.filter_by(params).group(:ip_address).order(count_all: :desc).limit(1000).count
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,34 @@
1
+ module ActiveHashcash
2
+ class AssetsController < ApplicationController
3
+ protect_from_forgery except: :show
4
+
5
+ Mime::Type.register "image/x-icon", :ico
6
+
7
+ def show
8
+ if endpoints.include?(file_name = File.basename(request.path))
9
+ file_path = ActiveHashcash::Engine.root.join / "app/views/active_hashcash/assets" / file_name
10
+ if File.exists?("#{file_path}.erb")
11
+ render(params[:id], mime_type: mime_type)
12
+ else
13
+ render(file: file_path)
14
+ end
15
+ expires_in(1.day, public: true)
16
+ else
17
+ raise ActionController::RoutingError.new
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def endpoints
24
+ return @endpoints if @endpoints
25
+ folder = ActiveHashcash::Engine.root.join("app/views", controller_path)
26
+ files = folder.each_child.map { |path| File.basename(path).delete_suffix(".erb") }
27
+ @endpoints = files.delete_if { |str| str.start_with?("_") }
28
+ end
29
+
30
+ def mime_type
31
+ Mime::Type.lookup_by_extension(File.extname(request.path).delete_prefix("."))
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveHashcash
2
+ class StampsController < ApplicationController
3
+ def index
4
+ @stamps = Stamp.filter_by(params).order(created_at: :desc).limit(1000)
5
+ end
6
+
7
+ def show
8
+ @stamp = Stamp.find(params[:id])
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ module AddressesHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ module StampsHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ActiveHashcash
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveHashcash
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveHashcash
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveHashcash
4
+ class Stamp < ApplicationRecord
5
+ validates_presence_of :version, :bits, :date, :resource, :rand, :counter
6
+
7
+ scope :created_from, -> (date) { where(created_at: date..) }
8
+ scope :created_to, -> (date) { where(created_at: ..date) }
9
+
10
+ scope :bits_from, -> (value) { where(bits: value..) }
11
+ scope :bits_to, -> (value) { where(bits: ..value) }
12
+
13
+ scope :ip_address_starts_with, -> (string) { where("ip_address LIKE ?", sanitize_sql_like(string) + "%") }
14
+ scope :request_path_starts_with, -> (string) { where("request_path LIKE ?", sanitize_sql_like(string) + "%") }
15
+
16
+ def self.filter_by(params)
17
+ scope = all
18
+ scope = scope.request_path_starts_with(params[:request_path_starts_with]) if params[:request_path_starts_with].present?
19
+ scope = scope.ip_address_starts_with(params[:ip_address_starts_with]) if params[:ip_address_starts_with].present?
20
+ scope = scope.created_from(params[:created_from]) if params[:created_from].present?
21
+ scope = scope.created_to(params[:created_to]) if params[:created_to].present?
22
+ scope = scope.bits_from(params[:bits_from]) if params[:bits_from].present?
23
+ scope = scope.bits_to(params[:bits_to]) if params[:bits_to].present?
24
+ scope
25
+ end
26
+
27
+ def self.spend(string, resource, bits, date, options = {})
28
+ return false unless stamp = parse(string)
29
+ stamp.attributes = options
30
+ stamp.verify(resource, bits, date) && stamp.save
31
+ rescue ActiveRecord::RecordNotUnique
32
+ false
33
+ end
34
+
35
+ def self.parse(string)
36
+ return unless string
37
+ args = string.split(":")
38
+ return if args.size != 7
39
+ new(version: args[0], bits: args[1], date: Date.strptime(args[2], ActiveHashcash.date_format), resource: args[3], ext: args[4], rand: args[5], counter: args[6])
40
+ end
41
+
42
+ def self.mint(resource, attributes = {})
43
+ new({
44
+ version: 1,
45
+ bits: ActiveHashcash.bits,
46
+ date: Date.today.strftime(ActiveHashcash.date_format),
47
+ resource: resource,
48
+ rand: SecureRandom.alphanumeric(16),
49
+ counter: 0,
50
+ }.merge(attributes)).work
51
+ end
52
+
53
+ def work
54
+ counter.next! until authentic?
55
+ self
56
+ end
57
+
58
+ def authentic?
59
+ Digest::SHA1.hexdigest(to_s).hex >> (160-bits) == 0
60
+ end
61
+
62
+ def verify(resource, bits, date)
63
+ self.resource == resource && self.bits >= bits && self.date >= date && !self.date.future? && authentic?
64
+ end
65
+
66
+ def to_s
67
+ [version.to_i, bits, date.strftime("%y%m%d"), resource, ext, rand, counter].join(":")
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,17 @@
1
+ <details>
2
+ <summary>Filters</summary>
3
+ <%= render "active_hashcash/stamps/filters" %>
4
+ </details>
5
+
6
+ <table>
7
+ <tr>
8
+ <th><%= ActiveHashcash::Stamp.human_attribute_name(:ip_address) %></th>
9
+ <th><%= ActiveHashcash::Stamp.model_name.human.pluralize %></th>
10
+ </tr>
11
+ <% for (address, count) in @addresses %>
12
+ <tr>
13
+ <td><%= link_to address, stamps_path(ip_address_starts_with: address) %></td>
14
+ <td><%= number_with_delimiter count %></td>
15
+ </tr>
16
+ <% end %>
17
+ </table>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 50.4 50.4" xmlns="http://www.w3.org/2000/svg"><g fill="#08773c"><path d="m25.2 0c-13.9 0-25.2 11.3-25.2 25.2s11.3 25.2 25.2 25.2 25.2-11.3 25.2-25.2-11.31-25.2-25.2-25.2zm0 46.4c-11.69 0-21.2-9.51-21.2-21.2s9.51-21.2 21.2-21.2 21.2 9.51 21.2 21.2-9.51 21.2-21.2 21.2z"/><path d="m33 12.04h-4v7.58h-7.89v-7.58h-4v7.58h-4.98v4h4.98v2.69h-4.98v4h4.98v7.58h4v-7.58h7.89v7.58h4v-7.58h4.98v-4h-4.98v-2.69h4.98v-4h-4.98zm-4 14.27h-7.89v-2.69h7.89z"/></g></svg>