summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2022-11-19 12:26:21 -0500
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2022-11-19 12:27:25 -0500
commit52554a1dcd9101fff5bea67f883f2d1ebe57d8e4 (patch)
treea61dc2b89ea16ae83bf50040c197a846adb198a3
parent10cce1e4d3f7849c5f65f99dc8940dd6e15afbc0 (diff)
[dynamic] Implement client-side of POW captcha
-rw-r--r--haunt/jakob/dynamic/capabilities/comment-form.scm8
-rw-r--r--haunt/jakob/dynamic/capabilities/comments.scm26
-rw-r--r--haunt/jakob/dynamic/captcha.scm3
-rw-r--r--haunt/static/css/style.css8
-rw-r--r--haunt/static/js/proof-of-work.js117
5 files changed, 120 insertions, 42 deletions
diff --git a/haunt/jakob/dynamic/capabilities/comment-form.scm b/haunt/jakob/dynamic/capabilities/comment-form.scm
index 1754dfd..f1cbd9e 100644
--- a/haunt/jakob/dynamic/capabilities/comment-form.scm
+++ b/haunt/jakob/dynamic/capabilities/comment-form.scm
@@ -21,6 +21,7 @@
#:use-module (jakob dynamic captcha)
#:use-module (jakob dynamic util)
#:use-module (jakob theme)
+ #:use-module (jakob utils sxml)
#:use-module (json)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-11)
@@ -56,7 +57,12 @@
(img (@ (src ,(format #f "data:image/jpeg;charset=utf-8;base64,~a"
(base64-encode captcha-image)))))
(input (@ (type "text") (id "captcha") (name "captcha") (size 24)))
- (input (@ (type "submit") (id "submit-form") (value "Submit")))))))))
+ (button (@ (id "pow-trigger") (hidden #t))
+ "Too hard? (Or unable to see the challenge?) Click here.")
+ (input (@ (type "text") (id "captcha-alt") (name "captcha-alt") (hidden #t)))
+ (input (@ (type "text") (id "captcha-alt-id") (name "captcha-alt-id") (hidden #t)))
+ (input (@ (type "submit") (id "submit-form") (value "Submit")))))
+ ,(script "proof-of-work.js")))))
(define (get-comment-form request body)
"API endpoint handler for querying for the comments on a particular post
diff --git a/haunt/jakob/dynamic/capabilities/comments.scm b/haunt/jakob/dynamic/capabilities/comments.scm
index 72067b4..54e1846 100644
--- a/haunt/jakob/dynamic/capabilities/comments.scm
+++ b/haunt/jakob/dynamic/capabilities/comments.scm
@@ -80,11 +80,18 @@ This is a wrapper around `get-comments-by-slug'."
(and (assoc "slug" form-data)
(assoc "name" form-data)
(assoc "comment" form-data)
- (assoc "captcha" form-data)
+ (or (assoc "captcha" form-data)
+ (and (assoc "captcha-alt" form-data)
+ (assoc "captcha-alt-id" form-data)))
(assoc "captcha-id" form-data)
- (validate-captcha!
- (assoc-value form-data "captcha")
- (string->number (assoc-value form-data "captcha-id")))))
+ (if (and (string? (assoc-value form-data "captcha-alt"))
+ (positive? (string-length (assoc-value form-data "captcha-alt"))))
+ (validate-proof-of-work!
+ (assoc-value form-data "captcha-alt")
+ (string->number (assoc-value form-data "captcha-alt-id")))
+ (validate-captcha!
+ (assoc-value form-data "captcha")
+ (string->number (assoc-value form-data "captcha-id"))))))
(define (insert-comment form-data)
(exec-query conn
"INSERT INTO comments (submitted, slug, name, subject,
@@ -132,7 +139,7 @@ This is a wrapper around `get-comments-by-slug'."
(form-data (if query-string
(decode-form query-string)
'())))
- (if (valid-reaction? form-data)
+ (if (valid-reaction? form-data)
(let ((id (assoc-value form-data "id"))
(reaction (assoc-value form-data "reaction"))
(reactions (comment-reactions id)))
@@ -149,12 +156,3 @@ This is a wrapper around `get-comments-by-slug'."
(scm->json-string
`((success . #f)
(error . "missing `id', or `reaction'")))))))
-
-;; (define (make-challenge request body)
-;; (let-values (((uuid value image) (new-captcha)))
-;; (hash-set! challenges uuid value)
-;; (hash-for-each (lambda (x y) (display x) (newline)) challenges)
-;; (values `((content-type . (application/base64))
-;; (access-control-allow-origin . "*")
-;; (x-captcha-id . ,uuid))
-;; (base64-encode image))))
diff --git a/haunt/jakob/dynamic/captcha.scm b/haunt/jakob/dynamic/captcha.scm
index ab90f55..0f6e0ba 100644
--- a/haunt/jakob/dynamic/captcha.scm
+++ b/haunt/jakob/dynamic/captcha.scm
@@ -255,7 +255,7 @@ internally-defined `time-to-live-seconds'."
(define (validate-proof-of-work! prefix challenge-id)
(define zero-prefix (string-join (map (lambda (_) "0") (iota %hardness)) ""))
- (unless (member challenge-id (id-queue-free pow-challenge-id-queue))
+ (when (member challenge-id (id-queue-free pow-challenge-id-queue))
(raise (condition (&message
(message "No such challenge ID")))))
(let* ((challenge (hash-ref pow-challenges challenge-id))
@@ -269,7 +269,6 @@ internally-defined `time-to-live-seconds'."
(when challenge
(release-id! challenge-id pow-challenge-id-queue))
(and (= 32 (string-length prefix))
- (not (member challenge-id (id-queue-free pow-challenge-id-queue)))
(string-prefix? zero-prefix hash-value))))
(define (make-pow-challenge! request body)
diff --git a/haunt/static/css/style.css b/haunt/static/css/style.css
index 215a08c..82bd8da 100644
--- a/haunt/static/css/style.css
+++ b/haunt/static/css/style.css
@@ -1,3 +1,5 @@
+[hidden] { display: none !important; }
+
html {
font-family: "Cantarell", sans-serif;
}
@@ -257,7 +259,9 @@ ul#webmention-container .comment .comment-source-identifier {
clear: left;
}
-#comment-form label, #comment-form input {
+#comment-form label,
+#comment-form input,
+#comment-form button {
margin-top: 1em;
}
@@ -342,7 +346,7 @@ tr:nth-child(even) {
/* Buttons. */
-button {
+.lang button {
display: inline;
padding: .3em .6em .3em;
font-size: 75%;
diff --git a/haunt/static/js/proof-of-work.js b/haunt/static/js/proof-of-work.js
index 5207ac6..1a57c2a 100644
--- a/haunt/static/js/proof-of-work.js
+++ b/haunt/static/js/proof-of-work.js
@@ -1,34 +1,105 @@
-const text = 'An obscure body in the S-K System, your majesty. The inhabitants refer to it as the planet Earth.';
+/*
+ * proof-of-work.js -- Alternative captcha based on SHA-256 proof-of-work.
+ * Copyright © 2022 Jakob L. Kreuze <zerodaysfordays@sdf.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
function makeid(length) {
- var result = '';
- var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
- var charactersLength = characters.length;
- for ( var i = 0; i < length; i++ ) {
- result += characters.charAt(Math.floor(Math.random() * charactersLength));
- }
- return result;
+ let result = '';
+ let alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ for (let i = 0; i < length; i++) {
+ result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
+ }
+ return result;
}
async function digestMessage(message) {
- const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
- const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message
- const hashArray = Array.from(new Uint8Array(hashBuffer)); // convert buffer to byte array
- const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); // convert bytes to hex string
+ const msgUtf8 = new TextEncoder().encode(message);
+ const hashBuffer = await crypto.subtle.digest('SHA-256', msgUtf8);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
-const HARDNESS = 4;
-
-async function findPrefix(challenge) {
- while (true) {
- let prefix = makeid(32);
- let digestHex = await digestMessage(prefix + challenge);
- if (digestHex.startsWith("0".repeat(HARDNESS))) {
- return [prefix + challenge, digestHex];
- }
+async function findPrefix(hardness, nonce) {
+ while (true) {
+ let prefix = makeid(32);
+ let digestHex = await digestMessage(prefix + nonce);
+ if (digestHex.startsWith("0".repeat(hardness))) {
+ return prefix;
}
+ }
+}
+
+function makeRequest (method, url) {
+ return new Promise(function (resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open(method, url);
+ xhr.onload = function () {
+ if (xhr.status >= 200 && xhr.status < 300) {
+ resolve(xhr.response);
+ } else {
+ reject({
+ status: xhr.status,
+ statusText: xhr.statusText
+ });
+ }
+ };
+ xhr.onerror = function () {
+ reject({
+ status: xhr.status,
+ statusText: xhr.statusText
+ });
+ };
+ xhr.send();
+ });
}
-const suffix = "p12IyPB2dZsmBHYZyaEMrR5OUxqNc5Z9Cijal+9/iuQ=";
-findPrefix(suffix).then(console.log);
+
+function raceEndpoint() {
+ return new Promise(function (resolve, reject) {
+ makeRequest("GET", "/api/challenge")
+ .then(function (data) {
+ let challengeData = JSON.parse(data);
+ findPrefix(challengeData.hardness, challengeData.nonce)
+ .then((prefix) => { resolve([prefix, challengeData["challenge-id"]]) } );
+ })
+ .catch(reject);
+ });
+}
+
+let trigger = document.getElementById("pow-trigger");
+trigger.hidden = false;
+trigger.addEventListener("click", (e) => {
+ trigger.innerHTML = "Please wait...";
+ trigger.disabled = true;
+ raceEndpoint()
+ .then((result) => {
+ let resultField = document.getElementById("captcha-alt");
+ let challengeIdField = document.getElementById("captcha-alt-id");
+ resultField.value = result[0];
+ challengeIdField.value = result[1];
+ trigger.innerHTML = "Proof-of-Work completed successfully!";
+ trigger.disabled = true;
+ })
+ .catch((err) => {
+ console.log(err);
+ trigger.innerHTML = "Proof-of-Work failed!";
+ trigger.disabled = true;
+ });
+ e.preventDefault();
+})
+