mirror of
https://github.com/sequentialread/pow-captcha.git
synced 2025-03-30 15:08:29 +00:00
rename captcha -> bot deterrent
This commit is contained in:
parent
b7b983eb94
commit
90c777a7e1
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
|
||||
wasm_build/node_modules
|
||||
wasm_build/scrypt-wasm
|
||||
PoW_Captcha_API_Tokens/*
|
||||
!PoW_Captcha_API_Tokens/README.md
|
||||
PoW_Bot_Deterrent_API_Tokens/*
|
||||
!PoW_Bot_Deterrent_API_Tokens/README.md
|
||||
|
11
Dockerfile
11
Dockerfile
@ -9,12 +9,13 @@ RUN apk add --update --no-cache ca-certificates git
|
||||
COPY go.mod go.mod
|
||||
COPY go.sum go.sum
|
||||
COPY main.go main.go
|
||||
RUN go get && go build -v $GO_BUILD_ARGS -o /build/sequentialread-pow-captcha .
|
||||
RUN go get && go build -v $GO_BUILD_ARGS -o /build/pow-bot-deterrent .
|
||||
|
||||
FROM alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /build/sequentialread-pow-captcha /app/sequentialread-pow-captcha
|
||||
COPY --from=build /build/pow-bot-deterrent /app/pow-bot-deterrent
|
||||
COPY static /app/static
|
||||
COPY PoW_Captcha_API_Tokens /app/PoW_Captcha_API_Tokens
|
||||
RUN chmod +x /app/sequentialread-pow-captcha
|
||||
ENTRYPOINT ["/app/sequentialread-pow-captcha"]
|
||||
COPY PoW_Bot_Deterrent_API_Tokens /app/PoW_Bot_Deterrent_API_Tokens
|
||||
RUN chmod +x /app/pow-bot-deterrent
|
||||
ENTRYPOINT ["/app/pow-bot-deterrent"]
|
||||
|
||||
|
5
PoW_Bot_Deterrent_API_Tokens/README.md
Normal file
5
PoW_Bot_Deterrent_API_Tokens/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# PoW_Bot_Deterrent_API_Tokens folder
|
||||
|
||||
💥PoW! Bot Deterrent will store API tokens here. You may place this folder either in the current working directory from which the application is started, or you may place it next to the application binary.
|
||||
|
||||
If you run 💥PoW! Bot Deterrent in a linux container, you will want to mount this folder to a persistent volume so you don't lose your API tokens when you upgrade the container to a new image!
|
@ -1,5 +0,0 @@
|
||||
# PoW_Captcha_API_Tokens folder
|
||||
|
||||
💥PoW! Captcha will store API tokens here. You may place this folder either in the current working directory from which the application is started, or you may place it next to the application binary.
|
||||
|
||||
If you run 💥PoW! Captcha in a linux container, you will want to mount this folder to a persistent volume so you don't lose your API tokens when you upgrade the container to a new image!
|
239
README.md
239
README.md
@ -1,30 +1,31 @@
|
||||
# 💥PoW! Captcha
|
||||
# 💥PoW! Bot Deterrent
|
||||
|
||||
A proof of work based captcha similar to [mCaptcha](https://mcaptcha.org/).
|
||||
A proof-of-work based bot deterrent. Lightweight, self-hosted and copyleft licensed.
|
||||
|
||||

|
||||
|
||||
Compared to mainstream captchas like recaptcha, hcaptcha, friendlycaptcha, this one is better for a few reasons:
|
||||
|
||||
- It is lightweight & all dependencies are included; total front-end unminified gzipped file size is about 50KB.
|
||||
- Free as in Freedom, and Free as in Free Beer
|
||||
- It is lightweight & all dependencies are included; total file size is about 68KB unminified / uncompressed, and 23kb gzipped.
|
||||
- It is self-hosted. It does not spy on you or your users; you can tell because you run it on your own server, you wholly own and control it.
|
||||
- If you wish to use the one that I host instead of running it yourself, just let me know. Maybe we can work something out.
|
||||
- It is fully GPLv3 licensed. It is legally structured to protect your freedom to own and operate the software in perpetuity.
|
||||
|
||||
Compared to other proof of work captchas like mCaptcha, I believe that this one is better because:
|
||||
Compared to other proof of work bot deterrents like mCaptcha, I believe that this one is better because:
|
||||
|
||||
- It uses a multi-threaded [WASM (Web Assembly)](https://webassembly.org/) [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) running the [Scrypt hash function](https://en.wikipedia.org/wiki/Scrypt) instead of [SHA256](https://en.wikipedia.org/wiki/SHA-2). Because of this, it's less succeptible to hash-farming attacks.
|
||||
- It uses a multi-threaded [WASM (Web Assembly)](https://webassembly.org/) [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) running the [Scrypt hash function](https://en.wikipedia.org/wiki/Scrypt) instead of [SHA256](https://en.wikipedia.org/wiki/SHA-2). Because of this, it's:
|
||||
- 1. Fundamentally harder to accelerate.
|
||||
- 1. More likely to stop bots; a basic headless browser with JS execution might not be enough.
|
||||
- It is optimized for production use; its API minimizes the number of requests and amount of latency that you have to add to your system.
|
||||
|
||||
|
||||
|
||||
### Table of Contents
|
||||
|
||||
1. [How it works](#how-it-works)
|
||||
1. [What is Proof of Work?](#what-is-proof-of-work)
|
||||
1. [Overview sequence diagram](#overview-sequence-diagram)
|
||||
1. [Configuring](#configuring)
|
||||
1. [HTTP Captcha API](#http-captcha-api)
|
||||
1. [HTTP Challenge API](#http-challenge-api)
|
||||
1. [HTTP Admin API](#http-admin-api)
|
||||
1. [HTML DOM API](#html-dom-api)
|
||||
1. [Running the example app](#running-the-example-app)
|
||||
@ -36,11 +37,11 @@ Compared to other proof of work captchas like mCaptcha, I believe that this one
|
||||
|
||||
This application was designed to be a drop-in replacement for ReCaptcha by Google. It works pretty much the same way;
|
||||
|
||||
1. Your web application requests a captcha (in this case, a batch of captchas) from the captcha HTTP API
|
||||
2. Your web application displays an HTML page which includes a form, and passes the captcha data to the form
|
||||
3. The HTML page includes the JavaScript part of the Captcha app, this JavaScript draws the Captcha on the page
|
||||
4. When the Captcha is complete, its JavaScript will fire off a callback to your JavaScript (usually used to enable the submit button on the form)
|
||||
5. When the form is submitted, your web application submites the captcha result to the captcha HTTP API for validation
|
||||
1. Your web application requests a challenge (in this case, a batch of challenges) from the challenge HTTP API
|
||||
2. Your web application displays an HTML page which includes a form, and passes the challenge data to the form
|
||||
3. The HTML page includes the JavaScript part of the Bot Deterrent app, this JavaScript draws a progress bar on the page
|
||||
4. When the Proof of Work is complete, its JavaScript will fire off a callback to your JavaScript
|
||||
5. When the form is submitted, your web application submits the nonce (solution) to the challenge HTTP API for validation
|
||||
|
||||
# What is Proof of Work?
|
||||
|
||||
@ -50,7 +51,7 @@ PoW does not require any 3rd party or authority to enforce rules, it is based on
|
||||
|
||||
PoW works fairly well as a deterrent against spam, a PoW requirement makes sending high-volume spam computationally expensive.
|
||||
|
||||
It is impossible to predict how long a given Proof of Work will take to calculate. It could take no time at all (got it on the first try 😎 ), or it could take an abnormally long time (got unlucky and took forever to find the right hash 😟 ). You can think of it like flipping coins until you get a certain # of heads in a row. This **DOES** matter in terms of user interface and usability, so you will want to make sure that the difficulty is low enough that users are extremely unlikely to be turned away by an unlucky "takes forever" captcha.
|
||||
It is impossible to predict how long a given Proof of Work will take to calculate. It could take no time at all (got it on the first try 😎 ), or it could take an abnormally long time (got unlucky and took forever to find the right hash 😟 ). You can think of it like flipping coins until you get a certain # of heads in a row. This **DOES** matter in terms of user interface and usability, so you will want to make sure that the difficulty is low enough that users are extremely unlikely to be turned away by an unlucky "takes forever" challenge.
|
||||
|
||||
The word ["Nonce"](https://en.wikipedia.org/wiki/Cryptographic_nonce#Hashing) in this document refers to "Number Used Once", in the context of hashing and proof of work.
|
||||
|
||||
@ -65,9 +66,9 @@ To edit it, download the <a download href="readme/sequence.drawio">diagram file<
|
||||
|
||||
# Configuring
|
||||
|
||||
💥PoW! Captcha gets all of its configuration from environment variables.
|
||||
💥PoW! Bot Deterrent gets all of its configuration from environment variables.
|
||||
|
||||
#### `POW_CAPTCHA_ADMIN_API_TOKEN`
|
||||
#### `POW_BOT_DETERRENT_ADMIN_API_TOKEN`
|
||||
|
||||
⚠️ **REQUIRED**
|
||||
|
||||
@ -75,23 +76,23 @@ This token allows control of the Admin API & allows the bearer to create, list,
|
||||
|
||||
----
|
||||
|
||||
#### `POW_CAPTCHA_BATCH_SIZE`
|
||||
#### `POW_BOT_DETERRENT_BATCH_SIZE`
|
||||
|
||||
💬 *OPTIONAL* default value is 1000
|
||||
|
||||
How many captcha challenges to return at once.
|
||||
How many challenges to return at once.
|
||||
|
||||
----
|
||||
|
||||
#### `POW_CAPTCHA_DEPRECATE_AFTER_BATCHES`
|
||||
#### `POW_BOT_DETERRENT_DEPRECATE_AFTER_BATCHES`
|
||||
|
||||
💬 *OPTIONAL* default value is 10
|
||||
|
||||
How many "batches-old" captcha challenges can be before being dropped from memory.
|
||||
How many "batches-old" challenges can be before being dropped from memory.
|
||||
|
||||
----
|
||||
|
||||
#### `POW_CAPTCHA_LISTEN_PORT`
|
||||
#### `POW_BOT_DETERRENT_LISTEN_PORT`
|
||||
|
||||
💬 *OPTIONAL* default value is 2730
|
||||
|
||||
@ -99,7 +100,7 @@ Which TCP port should the server listen on.
|
||||
|
||||
----
|
||||
|
||||
#### `POW_CAPTCHA_SCRYPT_CPU_AND_MEMORY_COST`
|
||||
#### `POW_BOT_DETERRENT_SCRYPT_CPU_AND_MEMORY_COST`
|
||||
|
||||
💬 *OPTIONAL* default value is 4096
|
||||
|
||||
@ -107,7 +108,7 @@ Allows you to tweak how difficult each individual hash in the proof of work will
|
||||
|
||||
----
|
||||
|
||||
# HTTP Captcha API
|
||||
# HTTP Challenge API
|
||||
|
||||
#### `POST /GetChallenges?difficultyLevel=<int>`
|
||||
|
||||
@ -115,7 +116,7 @@ Required Header: `Authorization: Bearer <api-token>`
|
||||
|
||||
Return type: `application/json`
|
||||
|
||||
`GetChallenges` returns a JSON array of 1000 strings. The Captcha server will remember each one of these challeges until it is
|
||||
`GetChallenges` returns a JSON array of 1000 strings. The Bot Deterrent server will remember each one of these challeges until it is
|
||||
restarted, or until GetChallenges has been called 10 more times. Each challenge can only be used once.
|
||||
|
||||
The difficultyLevel parameter specifies how many bits of difficulty the challenges should have.
|
||||
@ -135,7 +136,7 @@ Return type: `text/plain` (error/status messages only)
|
||||
- `GetChallenges` hasn't been called 10 or more times since this challenge was originally returned.
|
||||
- `Verify` has not been called on this challenge before.
|
||||
- The provided hexadecimal nonce solves the challenge.
|
||||
- (The winning nonce string will be passed to the function you specify in [data-sqr-captcha-callback](#data-sqr-captcha-callback). You just have to make sure to post it to your server so your server can include it when it calls `/Verify`)
|
||||
- (The winning nonce string will be passed to the function you specify in [data-pow-bot-deterrent-callback](#data-pow-bot-deterrent-callback). You just have to make sure to post it to your server so your server can include it when it calls `/Verify`)
|
||||
|
||||
|
||||
Otherwise it returns 404, 400, or 500.
|
||||
@ -147,11 +148,11 @@ Return type: depends on file
|
||||
|
||||
Files:
|
||||
|
||||
- captcha.js
|
||||
- captcha.css
|
||||
- pow-bot-deterrent.js
|
||||
- pow-bot-deterrent.css
|
||||
- proofOfWorker.js
|
||||
|
||||
You only need to include `captcha.js` in your page, it will pull in the other files automatically if they are not already present in the page.
|
||||
You only need to include `pow-bot-deterrent.js` in your page, it will pull in the other files automatically if they are not already present in the page.
|
||||
See below for a more detailed implementation walkthrough.
|
||||
|
||||
# HTTP Admin API
|
||||
@ -183,29 +184,29 @@ Revokes an existing API token.
|
||||
|
||||
# HTML DOM API
|
||||
|
||||
In order to set up 💥PoW! Captcha on your page, you just need to load/include `captcha.js` and one or more html elements
|
||||
In order to set up 💥PoW! Bot Deterrent on your page, you just need to load/include `pow-bot-deterrent.js` and one or more html elements
|
||||
with all 3 of the following properties:
|
||||
|
||||
#### `data-sqr-captcha-url`
|
||||
#### `data-pow-bot-deterrent-url`
|
||||
|
||||
This is the base url from which `captcha.js` will attempt to load additional resources `captcha.css` and `proofOfWorker.js`.
|
||||
This is the base url from which `pow-bot-deterrent.js` will attempt to load additional resources `pow-bot-deterrent.css` and `proofOfWorker.js`.
|
||||
|
||||
> 💬 *INFO* In our examples, we passed the captcha server URL down to the HTML page and used it as the value for this property.
|
||||
However, that's not required. The HTML page doesn't need to talk to the captcha server at all, it just needs to know where it can
|
||||
download the `captcha.css` and `proofOfWorker.js` files. There is nothing stopping you from simply hosting those files on your own server or CDN and placing the corresponding URL into the `data-sqr-captcha-url` property.
|
||||
> 💬 *INFO* In our examples, we passed the Bot Deterrent server URL down to the HTML page and used it as the value for this property.
|
||||
However, that's not required. The HTML page doesn't need to talk to the Bot Deterrent server at all, it just needs to know where it can
|
||||
download the `pow-bot-deterrent.css` and `proofOfWorker.js` files. There is nothing stopping you from simply hosting those files on your own server or CDN and placing the corresponding URL into the `data-pow-bot-deterrent-url` property.
|
||||
|
||||
#### `data-sqr-captcha-challenge`
|
||||
#### `data-pow-bot-deterrent-challenge`
|
||||
|
||||
Set this property to one of the challenge strings returned by `GetChallenges`. It must be unique, each challenge can only be used once.
|
||||
|
||||
⚠️ **NOTE** that the element with the 3 `sqr-captcha-xyz` data properties **MUST** be placed **inside a form element**. This is required, to allow the captcha to know which input elements it needs to trigger on. We only want the captcha to trigger when the user actually intends to submit the form; otherwise we are wasting a lot of their CPU cycles for no reason!
|
||||
⚠️ **NOTE** that the element with the 3 `pow-botdeterrent-xyz` data properties **MUST** be placed **inside a form element**. This is required, to allow the bot deterrent to know which input elements it needs to trigger on. We only want it to trigger when the user actually intends to submit the form; otherwise we are wasting a lot of their CPU cycles for no reason!
|
||||
|
||||
#### `data-sqr-captcha-callback`
|
||||
#### `data-pow-bot-deterrent-callback`
|
||||
|
||||
This is the name of a function in the global namespace which will be called & passed the winning nonce once the Proof of Work
|
||||
is completed. So, for example, if you had:
|
||||
|
||||
`<div ... data-sqr-captcha-callback="myCallbackFunction"></div>`
|
||||
`<div ... data-pow-bot-deterrent-callback="myCallbackFunction"></div>`
|
||||
|
||||
Then you would provide your callback like so:
|
||||
|
||||
@ -219,7 +220,7 @@ Then you would provide your callback like so:
|
||||
|
||||
> 💬 *INFO* You may also nest the callback inside object(s) if you wish:
|
||||
|
||||
`<div ... data-sqr-captcha-callback="myApp.myCallbackFunction"></div>`
|
||||
`<div ... data-pow-bot-deterrent-callback="myApp.myCallbackFunction"></div>`
|
||||
|
||||
```
|
||||
<script>
|
||||
@ -231,39 +232,39 @@ Then you would provide your callback like so:
|
||||
</script>
|
||||
```
|
||||
|
||||
When `captcha.js` runs, if it finds an element with `data-sqr-captcha-challenge` & `data-sqr-captcha-callback`, but the callback function is not defined yet, it will print a warning message. If the callback is still not defined when the Proof of Work is completed, it will throw an error.
|
||||
When `pow-bot-deterrent.js` runs, if it finds an element with `data-pow-bot-deterrent-challenge` & `data-pow-bot-deterrent-callback`, but the callback function is not defined yet, it will print a warning message. If the callback is still not defined when the Proof of Work is completed, it will throw an error.
|
||||
|
||||
> 💬 *INFO* the element with the `sqr-captcha` data properties should probably be styled to have a very small font size. When I was designing the css for the captcha element, I made everything scale based on the font size (by using `em`). But because the page I was testing it on had a small font by default, I accidentally made it huge when it is rendered on a default HTML page. So for now you will want to make the font size of the element which contains it fairly small, like `10px` or `11px`.
|
||||
> 💬 *INFO* the element with the `pow-botdeterrent` data properties should probably be styled to have a very small font size. When I was designing the css for the bot deterrent element, I made everything scale based on the font size (by using `em`). But because the page I was testing it on had a small font by default, I accidentally made it huge when it is rendered on a default HTML page. So for now you will want to make the font size of the element which contains it fairly small, like `10px` or `11px`.
|
||||
|
||||
#### `window.sqrCaptchaInit`
|
||||
#### `window.botBotDeterrentInit`
|
||||
|
||||
The captcha event listeners, elements, css, & webworkers **won't be loaded until this function is called**.
|
||||
The bot deterrent event listeners, elements, css, & webworkers **won't be loaded until this function is called**.
|
||||
|
||||
**`captcha.js` will call this function automatically** if there's at least one DOM element with `data-sqr-captcha-challenge` already when `captcha.js` loads. Otherwise, it is up to you to call this function after you render the DOM elements & add the `data-sqr-captcha-challenge` property to them.
|
||||
**`pow-bot-deterrent.js` will call this function automatically** if there's at least one DOM element with `data-pow-bot-deterrent-challenge` already when `pow-bot-deterrent.js` loads. Otherwise, it is up to you to call this function after you render the DOM elements & add the `data-pow-bot-deterrent-challenge` property to them.
|
||||
|
||||
This function will throw an error if it is called more than once without calling `window.sqrCaptchaReset()` in between.
|
||||
This function will throw an error if it is called more than once without calling `window.powBotDeterrentReset()` in between.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
<script>
|
||||
window.sqrCaptchaInit();
|
||||
window.botBotDeterrentInit();
|
||||
</script>
|
||||
```
|
||||
|
||||
#### `window.sqrCaptchaReset`
|
||||
#### `window.powBotDeterrentReset`
|
||||
|
||||
Resets the captchas, stops the webworkers, etc. Use this if you have updated the page and you need to call `window.sqrCaptchaInit` again.
|
||||
Resets the bot deterrent(s), stops the webworkers, etc. Use this if you have updated the page and you need to call `window.botBotDeterrentInit` again.
|
||||
|
||||
#### `window.sqrCaptchaInitDone`
|
||||
#### `window.botBotDeterrentInitDone`
|
||||
|
||||
A boolean variable that `captcha.js` uses internally, so it can know if it has already been initialized or not.
|
||||
A boolean variable that `pow-bot-deterrent.js` uses internally, so it can know if it has already been initialized or not.
|
||||
|
||||
----
|
||||
|
||||
If you wanted to integrate 💥PoW! Captcha with a JavaScript driven front-end app, like a React-based app for example, you can install it via npm:
|
||||
If you wanted to integrate 💥PoW! Bot Deterrent with a JavaScript driven front-end app, like a React-based app for example, you can install it via npm:
|
||||
|
||||
`npm install git+https://git.sequentialread.com/forest/pow-captcha.git`
|
||||
`npm install git+https://git.sequentialread.com/forest/pow-bot-deterrent.git`
|
||||
|
||||
and use it like this:
|
||||
|
||||
@ -272,15 +273,15 @@ import {React, useEffect, useState} from 'react';
|
||||
|
||||
...
|
||||
|
||||
import '../node_modules/sequentialread-pow-captcha/static/captcha.css'
|
||||
import '../node_modules/sequentialread-pow-captcha/static/captcha.js'
|
||||
import '../node_modules/pow-bot-deterrent/static/pow-bot-deterrent.css'
|
||||
import '../node_modules/pow-bot-deterrent/static/pow-bot-deterrent.js'
|
||||
|
||||
// assumes that this component gets passed the captchaUrl and challenge as props
|
||||
// assumes that this component gets passed the botDeterrentURL and challenge as props
|
||||
// these would be loaded/passed from the server somehow. Especially the challenge, it has to be unique each time.
|
||||
function MyComponent({captchaUrl, challenge}) {
|
||||
function MyComponent({botDeterrentURL, challenge}) {
|
||||
|
||||
// When the component is created, set a unique string to be used as the callback in the global namespace (window)
|
||||
const [uniqueCallback] = useState(`pow-captcha-callback-${String(Math.random()).substring(6)}`);
|
||||
const [uniqueCallback] = useState(`pow-bot-deterrent-callback-${String(Math.random()).substring(6)}`);
|
||||
|
||||
// when the nonce is calculated, we will call setNonce
|
||||
const [nonce, setNonce] = useState("");
|
||||
@ -295,10 +296,10 @@ function MyComponent({captchaUrl, challenge}) {
|
||||
// Maybe less clear than the above, but JavaScript heads might enjoy this more:
|
||||
// window[uniqueCallback] = setNonce;
|
||||
|
||||
if(window.sqrCaptchaInitDone) {
|
||||
window.sqrCaptchaReset();
|
||||
if(window.botBotDeterrentInitDone) {
|
||||
window.powBotDeterrentReset();
|
||||
}
|
||||
window.sqrCaptchaInit();
|
||||
window.botBotDeterrentInit();
|
||||
}, [uniqueCallback]);
|
||||
|
||||
return (
|
||||
@ -308,10 +309,10 @@ function MyComponent({captchaUrl, challenge}) {
|
||||
<form>
|
||||
<input type="text" name="item" />
|
||||
<input type="submit" disabled={nonce === ""} value="Add" />
|
||||
<div className="captcha-container"
|
||||
data-sqr-captcha-url={captchaUrl}
|
||||
data-sqr-captcha-challenge={challenge}
|
||||
data-sqr-captcha-callback={uniqueCallback}>
|
||||
<div className="botdeterrent-container"
|
||||
data-pow-bot-deterrent-url={botDeterrentURL}
|
||||
data-pow-bot-deterrent-challenge={challenge}
|
||||
data-pow-bot-deterrent-callback={uniqueCallback}>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@ -324,50 +325,50 @@ function MyComponent({captchaUrl, challenge}) {
|
||||
|
||||
# Running the example app
|
||||
|
||||
The `example` folder in this repository contains an example app that demonstrates how to implement the 💥PoW! Captcha
|
||||
The `example` folder in this repository contains an example app that demonstrates how to implement the 💥PoW! Bot Deterrent
|
||||
in as simple of a fashion as possible.
|
||||
|
||||
If you wish to run the example app, you will have to run both the 💥PoW! Captcha server and the example app server.
|
||||
If you wish to run the example app, you will have to run both the 💥PoW! Bot Deterrent server and the example app server.
|
||||
|
||||
The easiest way to do this would probably be to open two separate terminal windows or tabs and run each app in its own terminal.
|
||||
|
||||
#### `terminal 1`
|
||||
```
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha$ go run main.go
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent$ go run main.go
|
||||
|
||||
panic: can't start the app, the POW_CAPTCHA_ADMIN_API_TOKEN environment variable is required
|
||||
panic: can't start the app, the POW_BOT_DETERRENT_ADMIN_API_TOKEN environment variable is required
|
||||
|
||||
goroutine 1 [running]:
|
||||
main.main()
|
||||
/home/forest/Desktop/git/sequentialread-pow-captcha/main.go:84 +0xf45
|
||||
/home/forest/Desktop/git/pow-bot-deterrent/main.go:84 +0xf45
|
||||
exit status 2
|
||||
```
|
||||
As you can see, the server requires an admin API token to be set. This is the token we will use authenticate and create
|
||||
individual tokens for different apps or different people who all might want to use the captcha server.
|
||||
individual tokens for different apps or different people who all might want to use the bot deterrent server.
|
||||
|
||||
Once we provide this admin API token environment variable, it will run just fine:
|
||||
|
||||
```
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha$ POW_CAPTCHA_ADMIN_API_TOKEN="example_admin" go run main.go
|
||||
2021/02/25 16:24:00 💥 PoW! Captcha server listening on port 2370
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent$ POW_BOT_DETERRENT_ADMIN_API_TOKEN="example_admin" go run main.go
|
||||
2021/02/25 16:24:00 💥 PoW! Bot Deterrent server listening on port 2370
|
||||
```
|
||||
|
||||
Now let's try to launch the example Todo List application:
|
||||
|
||||
#### `terminal 2`
|
||||
```
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha$ cd example/
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha/example$ go run main.go
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent$ cd example/
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent/example$ go run main.go
|
||||
|
||||
panic: can't start the app, the CAPTCHA_API_TOKEN environment variable is required
|
||||
panic: can't start the app, the BOT_DETERRENT_API_TOKEN environment variable is required
|
||||
|
||||
goroutine 1 [running]:
|
||||
main.main()
|
||||
/home/forest/Desktop/git/sequentialread-pow-captcha/example/main.go:40 +0x488
|
||||
/home/forest/Desktop/git/pow-bot-deterrent/example/main.go:40 +0x488
|
||||
exit status 2
|
||||
```
|
||||
|
||||
It's a similar story for the example app, except this time we can't just make up any old token, we have to ask the captcha server to generate a new API token for the example app. I will do this by manually sending it an http request with `curl`:
|
||||
It's a similar story for the example app, except this time we can't just make up any old token, we have to ask the Bot Deterrent server to generate a new API token for the example app. I will do this by manually sending it an http request with `curl`:
|
||||
|
||||
```
|
||||
$ curl -X POST -H "Authorization: Bearer example_admin" http://localhost:2370/Tokens/Create
|
||||
@ -380,7 +381,7 @@ b804f221e8a9053b2e6e89de83c5d7a4
|
||||
Now we can use this token to start the example Todo List app:
|
||||
|
||||
```
|
||||
$ CAPTCHA_API_TOKEN="b804f221e8a9053b2e6e89de83c5d7a4" go run main.go
|
||||
$ BOT_DETERRENT_API_TOKEN="b804f221e8a9053b2e6e89de83c5d7a4" go run main.go
|
||||
2021/02/25 16:38:32 📋 Todo List example application listening on port 8080
|
||||
```
|
||||
|
||||
@ -388,9 +389,9 @@ Then, you should be able to visit the example Todo List application in the brows
|
||||
|
||||
# Implementation walkthrough via example app
|
||||
|
||||
Lets walk through how example app works and how it integrates the 💥PoW! Captcha.
|
||||
Lets walk through how example app works and how it integrates the 💥PoW! Bot Deterrent.
|
||||
|
||||
The Todo List app has three pieces of configuration related to the captcha: the API token, the url, and the difficulty.
|
||||
The Todo List app has three pieces of configuration related to the bot deterrent: the API token, the url, and the difficulty.
|
||||
Currently the url and difficulty are hardcoded into the Todo List app's code, while the API token is provideded via an environment variable.
|
||||
|
||||
```
|
||||
@ -398,35 +399,35 @@ Currently the url and difficulty are hardcoded into the Todo List app's code, wh
|
||||
//
|
||||
// 7 bits of difficulty would be fine for apps that are never used on mobile phones, 5 is better suited for mobile apps
|
||||
//
|
||||
const captchaDifficultyLevel = 5
|
||||
const difficultyLevel = 5
|
||||
|
||||
...
|
||||
|
||||
apiToken := os.ExpandEnv("$CAPTCHA_API_TOKEN")
|
||||
apiToken := os.ExpandEnv("$BOT_DETERRENT_API_TOKEN")
|
||||
if apiToken == "" {
|
||||
panic(errors.New("can't start the app, the CAPTCHA_API_TOKEN environment variable is required"))
|
||||
panic(errors.New("can't start the app, the BOT_DETERRENT_API_TOKEN environment variable is required"))
|
||||
}
|
||||
|
||||
captchaAPIURL, err = url.Parse("http://localhost:2370")
|
||||
powAPIURL, err = url.Parse("http://localhost:2370")
|
||||
```
|
||||
|
||||
When the Todo List app starts, it has a few procedures it runs through to ensure it's ready to run, including
|
||||
retrieving a batch of captcha challenges from the captcha API:
|
||||
retrieving a batch of challenges from the bot deterrent API:
|
||||
|
||||
```
|
||||
func main() {
|
||||
|
||||
...
|
||||
|
||||
err = loadCaptchaChallenges()
|
||||
err = loadChallenges()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "can't start the app because could not loadCaptchaChallenges():"))
|
||||
panic(errors.Wrap(err, "can't start the app because could not loadChallenges():"))
|
||||
}
|
||||
```
|
||||
|
||||
`loadCaptchaChallenges()` calls the `GetChallenges` API & sets the global variable `captchaChallenges`.
|
||||
`loadChallenges()` calls the `GetChallenges` API & sets the global variable `powChallenges`.
|
||||
|
||||
It's a good idea to do this when your app starts, to ensure that it can talk to the captcha server before it starts serving content to users.
|
||||
It's a good idea to do this when your app starts, to ensure that it can talk to the Bot Deterrent server before it starts serving content to users.
|
||||
|
||||
The Todo List app only has one route: `/`.
|
||||
|
||||
@ -442,14 +443,14 @@ This route displays a basic HTML page with a form, based on the template `index.
|
||||
|
||||
This route does 4 things:
|
||||
|
||||
1. If it was a `POST` request, call the `Verify` endpoint to ensure that a valid captcha challenge and nonce were posted.
|
||||
- see `validateCaptcha` on line 202.
|
||||
1. If it was a `POST` request, call the `Verify` endpoint to ensure that a valid challenge and nonce were posted.
|
||||
- see `validatePow` on line 202.
|
||||
2. If it was a *valid* `POST` request, add the posted `item` string to the global list variable `items`.
|
||||
3. Check if the global `captchaChallenges` list is running out, if it is, kick off a background process to grab more from the `GetChallenges` API.
|
||||
- see `loadCaptchaChallenges` on line 155.
|
||||
4. Consume one challenge string from the global `captchaChallenges` list variable and output an HTML page containing that challenge.
|
||||
3. Check if the global `powChallenges` list is running out, if it is, kick off a background process to grab more from the `GetChallenges` API.
|
||||
- see `loadChallenges` on line 155.
|
||||
4. Consume one challenge string from the global `powChallenges` list variable and output an HTML page containing that challenge.
|
||||
|
||||
The captcha API (`GetChallenges` and `Verify`) was designed this way to optimize the performance of your application; instead of calling something like *GetCaptchaChallenge* for every single request, your application can load batches of captcha challenges asychronously in the background, and always have a challenge loaded into local memory & ready to go.
|
||||
The challenge API (`GetChallenges` and `Verify`) was designed this way to optimize the performance of your application; instead of calling something like *GetChallenge* for every single request, your application can load batches of challenges asychronously in the background, and always have a challenge loaded into local memory & ready to go.
|
||||
|
||||
However, you have to make sure that you are using it right:
|
||||
|
||||
@ -468,39 +469,39 @@ There are two main important parts, the form and the javascript at the bottom:
|
||||
<input type="hidden" name="challenge" value="{{ .Challenge }}" />
|
||||
<input type="hidden" name="nonce" />
|
||||
<input type="submit" disabled="true" value="Add" />
|
||||
<div class="captcha-container"
|
||||
data-sqr-captcha-url="{{ .CaptchaURL }}"
|
||||
data-sqr-captcha-challenge="{{ .Challenge }}"
|
||||
data-sqr-captcha-callback="myCaptchaCallback">
|
||||
<div class="botdeterrent-container"
|
||||
data-pow-bot-deterrent-url="{{ .PowAPIURL }}"
|
||||
data-pow-bot-deterrent-challenge="{{ .Challenge }}"
|
||||
data-pow-bot-deterrent-callback="myPowCallback">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
...
|
||||
|
||||
<script>
|
||||
window.myCaptchaCallback = (nonce) => {
|
||||
window.myPowCallback = (nonce) => {
|
||||
document.querySelector("form input[name='nonce']").value = nonce;
|
||||
document.querySelector("form input[type='submit']").disabled = false;
|
||||
};
|
||||
</script>
|
||||
<script src="{{ .CaptchaURL }}/static/captcha.js"></script>
|
||||
<script src="{{ .PowAPIURL }}/static/pow-bot-deterrent.js"></script>
|
||||
```
|
||||
|
||||
⚠️ **NOTE** that the element with the `sqr-captcha` data properties is placed **inside a form element**. This is required because the captcha needs to know which input elements it should trigger on. We only want the captcha to trigger when the user actually intends to submit the form; otherwise we are wasting a lot of their CPU cycles for no reason!
|
||||
⚠️ **NOTE** that the element with the `pow-botdeterrent` data properties is placed **inside a form element**. This is required because the bot deterrent needs to know which input elements it should trigger on. We only want it to trigger when the user actually intends to submit the form; otherwise we are wasting a lot of their CPU cycles for no reason!
|
||||
|
||||
> 💬 *INFO* The double curly brace elements like `{{ .Challenge }}` are Golang string template interpolations. They are specific to the example app & how it renders the page.
|
||||
|
||||
When the page loads, the `captcha.js` script will execute, querying the page for all elements with the `data-sqr-captcha-challenge`
|
||||
property. It will then validate each element to make sure it also has the `data-sqr-captcha-url` and `data-sqr-captcha-callback` properties. For each element it found, it will locate the `<form>` parent/grandparent enclosing the element. If none are found, it will throw an error. Otherwise, it will set up an event listener on every input element inside that form, so that as soon as the user starts filling out the form, the captcha display will pop up and the Proof of Work will begin.
|
||||
When the page loads, the `pow-bot-deterrent.js` script will execute, querying the page for all elements with the `data-pow-bot-deterrent-challenge`
|
||||
property. It will then validate each element to make sure it also has the `data-pow-bot-deterrent-url` and `data-pow-bot-deterrent-callback` properties. For each element it found, it will locate the `<form>` parent/grandparent enclosing the element. If none are found, it will throw an error. Otherwise, it will set up an event listener on every input element inside that form, so that as soon as the user starts filling out the form, the bot deterrent display will pop up and the Proof of Work will begin.
|
||||
|
||||
When the Proof of Work finishes, `captcha.js` will call the function specified by `data-sqr-captcha-callback`, passing the winning nonce as the first argument, or throw an error if that function is not defined.
|
||||
When the Proof of Work finishes, `pow-bot-deterrent.js` will call the function specified by `data-pow-bot-deterrent-callback`, passing the winning nonce as the first argument, or throw an error if that function is not defined.
|
||||
|
||||
> 💬 *INFO* the element with the `sqr-captcha` data properties also has a class that *WE* defined, called `captcha-container`.
|
||||
This class has a very small font size. When I was designing the css for the captcha element, I made everything scale based on the font size (by using `em`). But because the page I was testing it on had a small font by default, I accidentally made it huge when it is rendered on a default HTML page. So for now you will want to make the font size of the element which contains it fairly small.
|
||||
> 💬 *INFO* the element with the `pow-botdeterrent` data properties also has a class that *WE* defined, called `botdeterrent-container`.
|
||||
This class has a very small font size. When I was designing the css for the bot deterrent element, I made everything scale based on the font size (by using `em`). But because the page I was testing it on had a small font by default, I accidentally made it huge when it is rendered on a default HTML page. So for now you will want to make the font size of the element which contains it fairly small.
|
||||
|
||||
```
|
||||
<style>
|
||||
.captcha-container {
|
||||
.botdeterrent-container {
|
||||
margin-top: 1em;
|
||||
font-size: 10px;
|
||||
}
|
||||
@ -510,11 +511,11 @@ This class has a very small font size. When I was designing the css for the capt
|
||||
</style>
|
||||
```
|
||||
|
||||
I think that concludes the walkthrough! In the Todo App, as soon as `captcha.js` calls `myCaptchaCallback`, the form will be completely filled out and the submit button will be enabled. When the form is posted, the browser will make a `POST` request to the server, and the server logic we already discussed will take over, closing the loop.
|
||||
I think that concludes the walkthrough! In the Todo App, as soon as `pow-bot-deterrent.js` calls `myPowCallback`, the form will be completely filled out and the submit button will be enabled. When the form is posted, the browser will make a `POST` request to the server, and the server logic we already discussed will take over, closing the loop.
|
||||
|
||||
# Implementation Details for Developers
|
||||
|
||||
💥PoW! Captcha uses [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)s and [WebAssembly (WASM)](https://developer.mozilla.org/en-US/docs/WebAssembly) to calculate Proof of Work in the browser as efficiently as possible. WebWorkers allow the application to run code on multiple threads and take advantage of multi-core CPUs. WebAssembly gives us access to *actual integers* (😲) and more low-level memory operations that have been historically missing from JavaScript.
|
||||
💥PoW! Bot Deterrent uses [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers)s and [WebAssembly (WASM)](https://developer.mozilla.org/en-US/docs/WebAssembly) to calculate Proof of Work in the browser as efficiently as possible. WebWorkers allow the application to run code on multiple threads and take advantage of multi-core CPUs. WebAssembly gives us access to *actual integers* (😲) and more low-level memory operations that have been historically missing from JavaScript.
|
||||
|
||||
I measured the performance of the application with and without WebWorker / WebAssembly on a variety of devices.
|
||||
|
||||
@ -527,7 +528,7 @@ I tried two different implementations of the scrypt hash function, one from the
|
||||
| Macbook Air 2018 | not tested | not tested | ~ 32h/s |
|
||||
| Google Pixel 3a | not tested | not tested | ~ 24h/s |
|
||||
|
||||
I had some trouble getting the WASM module loaded properly inside the WebWorkers. In my production environment, the web application server and the captcha server are running on separate subdomains, so I was getting cross-origin security violation issues.
|
||||
I had some trouble getting the WASM module loaded properly inside the WebWorkers. In my production environment, the web application server and the Bot Deterrent server are running on separate subdomains, so I was getting cross-origin security violation issues.
|
||||
|
||||
I ended up embedding the WASM binary inside the WebWorker javascript `proofOfWorker.js` using a boutique binary encoding called [base32768](https://github.com/qntm/base32768). I set up a custom build process for this in the `wasm_build` folder. It even includes the scripts necessary to clone the github.com/MyEtherWallet/scrypt-wasm repo and install the Rust compiler! You are welcome! However, this script does assume that you are running on a Linux computer. I have not tested it outside of Linux.
|
||||
|
||||
@ -538,20 +539,20 @@ I ended up embedding the WASM binary inside the WebWorker javascript `proofOfWor
|
||||
When you calculate the hash of a file or a piece of data, you get this random string of characters:
|
||||
|
||||
```
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha$ sha256sum LICENSE.md
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md
|
||||
119ba12858fcf041fc43bb3331eaeaf313e1d01e278d5cc911fd2c60dc1c503f LICENSE.md
|
||||
```
|
||||
|
||||
Here, I have called the SHA256 hash function on the GPLv3 `LICENSE.md` file in this repo. The result is displayed as a hexidecimal string, that is, each character can have one of 16 possible values, 0-9 and a-f. You can think of it like rolling a whole bunch of 16-sided dice, however, it's not random like dice are, its *pseudorandom*, meaning that given the same input file, if we execute the same hash function multiple times, it will return the same output. All the dice will land the same way every time:
|
||||
|
||||
```
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha$ sha256sum LICENSE.md
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md
|
||||
119ba12858fcf041fc43bb3331eaeaf313e1d01e278d5cc911fd2c60dc1c503f LICENSE.md
|
||||
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha$ sha256sum LICENSE.md
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md
|
||||
119ba12858fcf041fc43bb3331eaeaf313e1d01e278d5cc911fd2c60dc1c503f LICENSE.md
|
||||
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha$ sha256sum LICENSE.md
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md
|
||||
119ba12858fcf041fc43bb3331eaeaf313e1d01e278d5cc911fd2c60dc1c503f LICENSE.md
|
||||
```
|
||||
|
||||
@ -559,10 +560,10 @@ However, If I change the input, even if I only change it a tiny bit, say, append
|
||||
|
||||
```
|
||||
# append the letter a to the end of the file
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha$ echo 'a' >> LICENSE.md
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent$ echo 'a' >> LICENSE.md
|
||||
|
||||
# calculate the SHA256 hash again
|
||||
forest@thingpad:~/Desktop/git/sequentialread-pow-captcha$ sha256sum LICENSE.md
|
||||
forest@thingpad:~/Desktop/git/pow-bot-deterrent$ sha256sum LICENSE.md
|
||||
67e0e2cc3429b799036bfa95e2bd7854a0e468939d6cb9d4a3e9d32c3b6615dc LICENSE.md
|
||||
```
|
||||
|
||||
@ -575,11 +576,11 @@ The number or string of "`a`"s, whatever it is you use to change the file before
|
||||
|
||||
This is exactly how Bitcoin mining works, Bitcoin requires miners to search for SHA256 hashes that end in a rediculously unlikely number of zeros, like flipping 100 coins and getting 100 heads in a row.
|
||||
|
||||
💥PoW! Captcha uses a different hash function called [Scrypt](https://en.wikipedia.org/wiki/Scrypt). Scrypt was designed to take an arbitrarily long amount of time to execute on a computer, and to be hard to optimize.
|
||||
💥PoW! Bot Deterrent uses a different hash function called [Scrypt](https://en.wikipedia.org/wiki/Scrypt). Scrypt was designed to take an arbitrarily long amount of time to execute on a computer, and to be hard to optimize.
|
||||
|
||||
A modified version of Scrypt is used by the crypto currency [Litecoin](https://en.wikipedia.org/wiki/Litecoin).
|
||||
|
||||
Like I mentioned in the condensed "What is Proof of Work" section, because of this pseudorandom behaviour, we can't predict how long a given captcha will take to complete. The UI does have a "progress bar" but the behaviour of the bar is more related to probability than to progress. In fact, it displays the "probability that we should have found the answer already", which is related to the amount of work done so far, but it's not exactly a linear relationship.
|
||||
Like I mentioned in the condensed "What is Proof of Work" section, because of this pseudorandom behaviour, we can't predict how long a given challenge will take to complete. The UI does have a "progress bar" but the behaviour of the bar is more related to probability than to progress. In fact, it displays the "probability that we should have found the answer already", which is related to the amount of work done so far, but it's not exactly a linear relationship.
|
||||
|
||||
Here is a screenshot of a plot I generated using WolframAlpha while I was developing this progress bar, given the formula for the progress bar's width:
|
||||
|
||||
|
@ -17,25 +17,25 @@ sed -E 's/GOARCH=/GOARCH=amd64/' -i dockerbuild/Dockerfile-amd64
|
||||
sed -E 's/GOARCH=/GOARCH=arm/' -i dockerbuild/Dockerfile-arm
|
||||
sed -E 's/GOARCH=/GOARCH=arm64/' -i dockerbuild/Dockerfile-arm64
|
||||
|
||||
docker build -f dockerbuild/Dockerfile-amd64 -t sequentialread/pow-captcha:$VERSION-amd64 .
|
||||
docker build -f dockerbuild/Dockerfile-arm -t sequentialread/pow-captcha:$VERSION-arm .
|
||||
docker build -f dockerbuild/Dockerfile-arm64 -t sequentialread/pow-captcha:$VERSION-arm64 .
|
||||
docker build -f dockerbuild/Dockerfile-amd64 -t sequentialread/pow-bot-deterrent:$VERSION-amd64 .
|
||||
docker build -f dockerbuild/Dockerfile-arm -t sequentialread/pow-bot-deterrent:$VERSION-arm .
|
||||
docker build -f dockerbuild/Dockerfile-arm64 -t sequentialread/pow-bot-deterrent:$VERSION-arm64 .
|
||||
|
||||
docker push sequentialread/pow-captcha:$VERSION-amd64
|
||||
docker push sequentialread/pow-captcha:$VERSION-arm
|
||||
docker push sequentialread/pow-captcha:$VERSION-arm64
|
||||
docker push sequentialread/pow-bot-deterrent:$VERSION-amd64
|
||||
docker push sequentialread/pow-bot-deterrent:$VERSION-arm
|
||||
docker push sequentialread/pow-bot-deterrent:$VERSION-arm64
|
||||
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
|
||||
docker manifest create sequentialread/pow-captcha:$VERSION \
|
||||
sequentialread/pow-captcha:$VERSION-amd64 \
|
||||
sequentialread/pow-captcha:$VERSION-arm \
|
||||
sequentialread/pow-captcha:$VERSION-arm64
|
||||
docker manifest create sequentialread/pow-bot-deterrent:$VERSION \
|
||||
sequentialread/pow-bot-deterrent:$VERSION-amd64 \
|
||||
sequentialread/pow-bot-deterrent:$VERSION-arm \
|
||||
sequentialread/pow-bot-deterrent:$VERSION-arm64
|
||||
|
||||
docker manifest annotate --arch amd64 sequentialread/pow-captcha:$VERSION sequentialread/pow-captcha:$VERSION-amd64
|
||||
docker manifest annotate --arch arm sequentialread/pow-captcha:$VERSION sequentialread/pow-captcha:$VERSION-arm
|
||||
docker manifest annotate --arch arm64 sequentialread/pow-captcha:$VERSION sequentialread/pow-captcha:$VERSION-arm64
|
||||
docker manifest annotate --arch amd64 sequentialread/pow-bot-deterrent:$VERSION sequentialread/pow-bot-deterrent:$VERSION-amd64
|
||||
docker manifest annotate --arch arm sequentialread/pow-bot-deterrent:$VERSION sequentialread/pow-bot-deterrent:$VERSION-arm
|
||||
docker manifest annotate --arch arm64 sequentialread/pow-bot-deterrent:$VERSION sequentialread/pow-bot-deterrent:$VERSION-arm64
|
||||
|
||||
docker manifest push sequentialread/pow-captcha:$VERSION
|
||||
docker manifest push sequentialread/pow-bot-deterrent:$VERSION
|
||||
|
||||
rm -rf dockerbuild || true
|
@ -5,7 +5,7 @@
|
||||
<title>📋 Todo List</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
.captcha-container {
|
||||
.botdeterrent-container {
|
||||
margin-top: 1em;
|
||||
font-size: 10px;
|
||||
}
|
||||
@ -32,22 +32,22 @@
|
||||
<input type="hidden" name="challenge" value="{{ .Challenge }}" />
|
||||
<input type="hidden" name="nonce" />
|
||||
<input type="submit" disabled="true" value="Add" />
|
||||
<div class="captcha-container"
|
||||
data-sqr-captcha-url="http://localhost:8080/"
|
||||
data-sqr-captcha-challenge="{{ .Challenge }}"
|
||||
data-sqr-captcha-callback="myCaptchaCallback">
|
||||
<div class="botdeterrent-container"
|
||||
data-pow-bot-deterrent-url="http://localhost:8080/"
|
||||
data-pow-bot-deterrent-challenge="{{ .Challenge }}"
|
||||
data-pow-bot-deterrent-callback="myPowCallback">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</li>
|
||||
</ol>
|
||||
<script>
|
||||
window.myCaptchaCallback = (nonce) => {
|
||||
window.myPowCallback = (nonce) => {
|
||||
document.querySelector("form input[name='nonce']").value = nonce;
|
||||
document.querySelector("form input[type='submit']").disabled = false;
|
||||
};
|
||||
</script>
|
||||
<script src="/static/captcha.js"></script>
|
||||
<script src="/static/botdeterrent.js"></script>
|
||||
<!-- <script src='./static/scrypt_wasm.js'></script>
|
||||
<script>
|
||||
const { scrypt } = wasm_bindgen;
|
||||
|
@ -18,15 +18,15 @@ import (
|
||||
)
|
||||
|
||||
var httpClient *http.Client
|
||||
var captchaAPIURL *url.URL
|
||||
var captchaChallenges []string
|
||||
var powAPIURL *url.URL
|
||||
var powChallenges []string
|
||||
|
||||
var items []string
|
||||
|
||||
// 5 bits of difficulty, 1 in 2^6 (1 in 32) tries will succeed on average.
|
||||
//
|
||||
// 8 bits of difficulty would be ok for apps that are never used on mobile phones, 6 is better suited for mobile apps
|
||||
const captchaDifficultyLevel = 5
|
||||
const powDifficultyLevel = 5
|
||||
|
||||
func main() {
|
||||
|
||||
@ -34,20 +34,20 @@ func main() {
|
||||
Timeout: time.Second * time.Duration(5),
|
||||
}
|
||||
|
||||
apiToken := os.ExpandEnv("$CAPTCHA_API_TOKEN")
|
||||
apiToken := os.ExpandEnv("$BOT_DETERRENT_API_TOKEN")
|
||||
if apiToken == "" {
|
||||
panic(errors.New("can't start the app, the CAPTCHA_API_TOKEN environment variable is required"))
|
||||
panic(errors.New("can't start the app, the BOT_DETERRENT_API_TOKEN environment variable is required"))
|
||||
}
|
||||
|
||||
var err error
|
||||
captchaAPIURL, err = url.Parse("http://localhost:2370")
|
||||
powAPIURL, err = url.Parse("http://localhost:2370")
|
||||
if err != nil {
|
||||
panic(errors.New("can't start the app because can't parse captchaAPIURL"))
|
||||
panic(errors.New("can't start the app because can't parse powAPIURL"))
|
||||
}
|
||||
|
||||
err = loadCaptchaChallenges(apiToken)
|
||||
err = loadChallenges(apiToken)
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "can't start the app because could not loadCaptchaChallenges():"))
|
||||
panic(errors.Wrap(err, "can't start the app because could not loadChallenges():"))
|
||||
}
|
||||
|
||||
_, err = ioutil.ReadFile("index.html")
|
||||
@ -62,11 +62,11 @@ func main() {
|
||||
// The user submitted a POST request, attempting to add a new item to the list
|
||||
if request.Method == "POST" {
|
||||
|
||||
// Ask the captcha server if the user's proof of work result is legit,
|
||||
// Ask the botdeterrent server if the user's proof of work result is legit,
|
||||
// and if not, return HTTP 400 Bad Request
|
||||
err := request.ParseForm()
|
||||
if err == nil {
|
||||
err = validateCaptcha(apiToken, request.Form.Get("challenge"), request.Form.Get("nonce"))
|
||||
err = validatePow(apiToken, request.Form.Get("challenge"), request.Form.Get("nonce"))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@ -82,24 +82,24 @@ func main() {
|
||||
// if it looks like we will run out of challenges soon, then kick off a goroutine to go get more in the background
|
||||
// note that in a real application in production, you would want to use a lock or mutex to ensure that
|
||||
// this only happens once if lots of requests come in at the same time
|
||||
if len(captchaChallenges) > 0 && len(captchaChallenges) < 5 {
|
||||
go loadCaptchaChallenges(apiToken)
|
||||
if len(powChallenges) > 0 && len(powChallenges) < 5 {
|
||||
go loadChallenges(apiToken)
|
||||
}
|
||||
|
||||
// if we somehow completely ran out of challenges, load more synchronously
|
||||
if captchaChallenges == nil || len(captchaChallenges) == 0 {
|
||||
err = loadCaptchaChallenges(apiToken)
|
||||
if powChallenges == nil || len(powChallenges) == 0 {
|
||||
err = loadChallenges(apiToken)
|
||||
if err != nil {
|
||||
log.Printf("loading captcha challenges failed: %v", err)
|
||||
log.Printf("loading botdeterrent challenges failed: %v", err)
|
||||
responseWriter.WriteHeader(500)
|
||||
responseWriter.Write([]byte("captcha api error"))
|
||||
responseWriter.Write([]byte("botdeterrent api error"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// This gets & consumes the next challenge from the begining of the slice
|
||||
challenge := captchaChallenges[0]
|
||||
captchaChallenges = captchaChallenges[1:]
|
||||
challenge := powChallenges[0]
|
||||
powChallenges = powChallenges[1:]
|
||||
|
||||
// render the page HTML & output the result to the web browser
|
||||
htmlBytes, err := renderPageTemplate(challenge)
|
||||
@ -136,13 +136,13 @@ func renderPageTemplate(challenge string) ([]byte, error) {
|
||||
// constructing an instance of an anonymous struct type to contain all the data
|
||||
// that we need to pass to the template
|
||||
pageData := struct {
|
||||
Challenge string
|
||||
Items []string
|
||||
CaptchaURL string
|
||||
Challenge string
|
||||
Items []string
|
||||
PowAPIURL string
|
||||
}{
|
||||
Challenge: challenge,
|
||||
Items: items,
|
||||
CaptchaURL: captchaAPIURL.String(),
|
||||
Challenge: challenge,
|
||||
Items: items,
|
||||
PowAPIURL: powAPIURL.String(),
|
||||
}
|
||||
var outputBuffer bytes.Buffer
|
||||
err = pageTemplate.Execute(&outputBuffer, pageData)
|
||||
@ -153,25 +153,25 @@ func renderPageTemplate(challenge string) ([]byte, error) {
|
||||
return outputBuffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func loadCaptchaChallenges(apiToken string) error {
|
||||
func loadChallenges(apiToken string) error {
|
||||
|
||||
query := url.Values{}
|
||||
query.Add("difficultyLevel", strconv.Itoa(captchaDifficultyLevel))
|
||||
query.Add("difficultyLevel", strconv.Itoa(powDifficultyLevel))
|
||||
|
||||
loadURL := url.URL{
|
||||
Scheme: captchaAPIURL.Scheme,
|
||||
Host: captchaAPIURL.Host,
|
||||
Path: filepath.Join(captchaAPIURL.Path, "GetChallenges"),
|
||||
Scheme: powAPIURL.Scheme,
|
||||
Host: powAPIURL.Host,
|
||||
Path: filepath.Join(powAPIURL.Path, "GetChallenges"),
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
captchaRequest, err := http.NewRequest("POST", loadURL.String(), nil)
|
||||
captchaRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken))
|
||||
request, err := http.NewRequest("POST", loadURL.String(), nil)
|
||||
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := httpClient.Do(captchaRequest)
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -183,48 +183,48 @@ func loadCaptchaChallenges(apiToken string) error {
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return fmt.Errorf(
|
||||
"load proof of work captcha challenges api returned http %d: %s",
|
||||
"load proof of work botdeterrent challenges api returned http %d: %s",
|
||||
response.StatusCode, string(responseBytes),
|
||||
)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(responseBytes, &captchaChallenges)
|
||||
err = json.Unmarshal(responseBytes, &powChallenges)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(captchaChallenges) == 0 {
|
||||
return errors.New("proof of work captcha challenges api returned empty array")
|
||||
if len(powChallenges) == 0 {
|
||||
return errors.New("proof of work botdeterrent challenges api returned empty array")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCaptcha(apiToken, challenge, nonce string) error {
|
||||
func validatePow(apiToken, challenge, nonce string) error {
|
||||
query := url.Values{}
|
||||
query.Add("challenge", challenge)
|
||||
query.Add("nonce", nonce)
|
||||
|
||||
verifyURL := url.URL{
|
||||
Scheme: captchaAPIURL.Scheme,
|
||||
Host: captchaAPIURL.Host,
|
||||
Path: filepath.Join(captchaAPIURL.Path, "Verify"),
|
||||
Scheme: powAPIURL.Scheme,
|
||||
Host: powAPIURL.Host,
|
||||
Path: filepath.Join(powAPIURL.Path, "Verify"),
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
captchaRequest, err := http.NewRequest("POST", verifyURL.String(), nil)
|
||||
captchaRequest.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken))
|
||||
request, err := http.NewRequest("POST", verifyURL.String(), nil)
|
||||
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiToken))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := httpClient.Do(captchaRequest)
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if response.StatusCode != 200 {
|
||||
return errors.New("proof of work captcha validation failed")
|
||||
return errors.New("proof of work botdeterrent validation failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
2
go.mod
2
go.mod
@ -1,4 +1,4 @@
|
||||
module git.sequentialread.com/forest/pow-captcha
|
||||
module git.sequentialread.com/forest/pow-bot-deterrent
|
||||
|
||||
go 1.16
|
||||
|
||||
|
30
main.go
30
main.go
@ -49,39 +49,39 @@ func main() {
|
||||
deprecateAfterBatches := 10
|
||||
portNumber := 2370
|
||||
scryptCPUAndMemoryCost := 4096
|
||||
batchSizeEnv := os.ExpandEnv("$POW_CAPTCHA_BATCH_SIZE")
|
||||
deprecateAfterBatchesEnv := os.ExpandEnv("$POW_CAPTCHA_DEPRECATE_AFTER_BATCHES")
|
||||
portNumberEnv := os.ExpandEnv("$POW_CAPTCHA_LISTEN_PORT")
|
||||
scryptCPUAndMemoryCostEnv := os.ExpandEnv("$POW_CAPTCHA_SCRYPT_CPU_AND_MEMORY_COST")
|
||||
batchSizeEnv := os.ExpandEnv("$POW_BOT_DETERRENT_BATCH_SIZE")
|
||||
deprecateAfterBatchesEnv := os.ExpandEnv("$POW_BOT_DETERRENT_DEPRECATE_AFTER_BATCHES")
|
||||
portNumberEnv := os.ExpandEnv("$POW_BOT_DETERRENT_LISTEN_PORT")
|
||||
scryptCPUAndMemoryCostEnv := os.ExpandEnv("$POW_BOT_DETERRENT_SCRYPT_CPU_AND_MEMORY_COST")
|
||||
if batchSizeEnv != "" {
|
||||
batchSize, err = strconv.Atoi(batchSizeEnv)
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "can't start the app because the POW_CAPTCHA_BATCH_SIZE '%s' can't be converted to an integer", batchSizeEnv))
|
||||
panic(errors.Wrapf(err, "can't start the app because the POW_BOT_DETERRENT_BATCH_SIZE '%s' can't be converted to an integer", batchSizeEnv))
|
||||
}
|
||||
}
|
||||
if deprecateAfterBatchesEnv != "" {
|
||||
deprecateAfterBatches, err = strconv.Atoi(deprecateAfterBatchesEnv)
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "can't start the app because the POW_CAPTCHA_DEPRECATE_AFTER_BATCHES '%s' can't be converted to an integer", deprecateAfterBatchesEnv))
|
||||
panic(errors.Wrapf(err, "can't start the app because the POW_BOT_DETERRENT_DEPRECATE_AFTER_BATCHES '%s' can't be converted to an integer", deprecateAfterBatchesEnv))
|
||||
}
|
||||
}
|
||||
if portNumberEnv != "" {
|
||||
portNumber, err = strconv.Atoi(portNumberEnv)
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "can't start the app because the POW_CAPTCHA_LISTEN_PORT '%s' can't be converted to an integer", portNumberEnv))
|
||||
panic(errors.Wrapf(err, "can't start the app because the POW_BOT_DETERRENT_LISTEN_PORT '%s' can't be converted to an integer", portNumberEnv))
|
||||
}
|
||||
}
|
||||
if scryptCPUAndMemoryCostEnv != "" {
|
||||
scryptCPUAndMemoryCost, err = strconv.Atoi(scryptCPUAndMemoryCostEnv)
|
||||
if err != nil {
|
||||
panic(errors.Wrapf(err, "can't start the app because the POW_CAPTCHA_SCRYPT_CPU_AND_MEMORY_COST '%s' can't be converted to an integer", scryptCPUAndMemoryCostEnv))
|
||||
panic(errors.Wrapf(err, "can't start the app because the POW_BOT_DETERRENT_SCRYPT_CPU_AND_MEMORY_COST '%s' can't be converted to an integer", scryptCPUAndMemoryCostEnv))
|
||||
}
|
||||
}
|
||||
|
||||
apiTokensFolder := locateAPITokensFolder()
|
||||
adminAPIToken := os.ExpandEnv("$POW_CAPTCHA_ADMIN_API_TOKEN")
|
||||
adminAPIToken := os.ExpandEnv("$POW_BOT_DETERRENT_ADMIN_API_TOKEN")
|
||||
if adminAPIToken == "" {
|
||||
panic(errors.New("can't start the app, the POW_CAPTCHA_ADMIN_API_TOKEN environment variable is required"))
|
||||
panic(errors.New("can't start the app, the POW_BOT_DETERRENT_ADMIN_API_TOKEN environment variable is required"))
|
||||
}
|
||||
|
||||
scryptParameters := ScryptParameters{
|
||||
@ -416,7 +416,7 @@ func main() {
|
||||
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
|
||||
|
||||
log.Printf("💥 PoW! Captcha server listening on port %d", portNumber)
|
||||
log.Printf("💥 PoW! Bot Deterrent server listening on port %d", portNumber)
|
||||
|
||||
err = http.ListenAndServe(fmt.Sprintf(":%d", portNumber), nil)
|
||||
|
||||
@ -444,8 +444,8 @@ func locateAPITokensFolder() string {
|
||||
log.Fatalf("locateAPITokensFolder(): can't getCurrentExecDir(): %v", err)
|
||||
}
|
||||
|
||||
nextToExecutable := filepath.Join(executableDirectory, "PoW_Captcha_API_Tokens")
|
||||
inWorkingDirectory := filepath.Join(workingDirectory, "PoW_Captcha_API_Tokens")
|
||||
nextToExecutable := filepath.Join(executableDirectory, "PoW_Bot_Deterrent_API_Tokens")
|
||||
inWorkingDirectory := filepath.Join(workingDirectory, "PoW_Bot_Deterrent_API_Tokens")
|
||||
|
||||
nextToExecutableStat, err := os.Stat(nextToExecutable)
|
||||
foundKeysNextToExecutable := err == nil && nextToExecutableStat.IsDir()
|
||||
@ -453,7 +453,7 @@ func locateAPITokensFolder() string {
|
||||
foundKeysInWorkingDirectory := err == nil && inWorkingDirectoryStat.IsDir()
|
||||
if foundKeysNextToExecutable && foundKeysInWorkingDirectory && workingDirectory != executableDirectory {
|
||||
log.Fatalf(`locateAPITokensFolder(): Something went wrong with your installation,
|
||||
I found two PoW_Captcha_API_Tokens folders and I'm not sure which one to use.
|
||||
I found two PoW_Bot_Deterrent_API_Tokens folders and I'm not sure which one to use.
|
||||
One of them is located at %s
|
||||
and the other is at %s`, inWorkingDirectory, nextToExecutable)
|
||||
}
|
||||
@ -463,7 +463,7 @@ func locateAPITokensFolder() string {
|
||||
return nextToExecutable
|
||||
}
|
||||
|
||||
log.Fatalf(`locateAPITokensFolder(): I didn't find a PoW_Captcha_API_Tokens folder
|
||||
log.Fatalf(`locateAPITokensFolder(): I didn't find a PoW_Bot_Deterrent_API_Tokens folder
|
||||
in the current working directory (in %s) or next to the executable (in %s)`, workingDirectory, executableDirectory)
|
||||
|
||||
return ""
|
||||
|
14
package.json
14
package.json
@ -1,25 +1,25 @@
|
||||
{
|
||||
"name": "pow-captcha",
|
||||
"name": "pow-bot-deterrent",
|
||||
"version": "0.0.1",
|
||||
"description": "A proof of work based captcha similar to friendly captcha, but lightweight and FLOSS",
|
||||
"main": "static/captcha.js",
|
||||
"description": "A proof of work based bot deterrent. lightweight, selfhosted, and copyleft licensed",
|
||||
"main": "static/bot-deterrent.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://git.sequentialread.com/forest/pow-captcha.git"
|
||||
"url": "git+https://git.sequentialread.com/forest/pow-bot-deterrent.git"
|
||||
},
|
||||
"author": "forest johnson <forest.n.johnson@gmail.com>",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"bugs": {
|
||||
"url": "https://git.sequentialread.com/forest/pow-captcha/issues"
|
||||
"url": "https://git.sequentialread.com/forest/pow-bot-deterrent/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"wasm-build": "cd wasm_build && ./build_wasm.sh",
|
||||
"wasm_build": "cd wasm_build && ./build_wasm.sh",
|
||||
"wasmbuild": "cd wasm_build && ./build_wasm.sh"
|
||||
},
|
||||
"homepage": "https://git.sequentialread.com/forest/pow-captcha",
|
||||
"homepage": "https://git.sequentialread.com/forest/pow-bot-deterrent",
|
||||
"keywords": [
|
||||
"captcha",
|
||||
"bot-deterrent",
|
||||
"proof of work",
|
||||
"scrypt",
|
||||
"webworker"
|
||||
|
@ -1,4 +1,4 @@
|
||||
.sqr-captcha {
|
||||
.pow-botdeterrent {
|
||||
background-color: #ddd;
|
||||
border: 1px solid #9359fa;
|
||||
border-radius: 1em;
|
||||
@ -10,7 +10,7 @@
|
||||
min-width: 37em;
|
||||
}
|
||||
|
||||
.sqr-captcha-link {
|
||||
.pow-botdeterrent-link {
|
||||
color: #452775;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
@ -19,28 +19,28 @@
|
||||
}
|
||||
|
||||
@media screen and (max-width: 410px) {
|
||||
.sqr-captcha {
|
||||
.pow-botdeterrent {
|
||||
min-width: 25em;
|
||||
}
|
||||
.sqr-captcha-icon {
|
||||
.pow-botdeterrent-icon {
|
||||
height: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
.sqr-captcha-link span {
|
||||
.pow-botdeterrent-link span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sqr-captcha-link:hover,
|
||||
.sqr-captcha-link:active,
|
||||
.sqr-captcha-link:visited {
|
||||
.pow-botdeterrent-link:hover,
|
||||
.pow-botdeterrent-link:active,
|
||||
.pow-botdeterrent-link:visited {
|
||||
color: #452775;
|
||||
}
|
||||
|
||||
|
||||
.sqr-captcha-row {
|
||||
.pow-botdeterrent-row {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
@ -48,14 +48,14 @@
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.sqr-captcha-icon-container {
|
||||
.pow-botdeterrent-icon-container {
|
||||
margin-left: 1.5em;
|
||||
margin-top: 0.2em;
|
||||
margin-bottom: -2em;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
.sqr-captcha-best-hash {
|
||||
.pow-botdeterrent-best-hash {
|
||||
font-family: monospace;
|
||||
background: #585a29;
|
||||
color: #f6ff72;
|
||||
@ -70,17 +70,17 @@
|
||||
float: right;
|
||||
}
|
||||
|
||||
.sqr-captcha-best-hash-done {
|
||||
.pow-botdeterrent-best-hash-done {
|
||||
background: #3b6262;
|
||||
color: #53f65d;
|
||||
}
|
||||
|
||||
.sqr-captcha-description {
|
||||
.pow-botdeterrent-description {
|
||||
margin-top: 1em;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.sqr-captcha-progress-bar-container {
|
||||
.pow-botdeterrent-progress-bar-container {
|
||||
border-radius: 1em;
|
||||
background: #444;
|
||||
height: 1em;
|
||||
@ -90,7 +90,7 @@
|
||||
|
||||
}
|
||||
|
||||
.sqr-captcha-progress-bar {
|
||||
.pow-botdeterrent-progress-bar {
|
||||
background: #f6ff72;
|
||||
height: 1em;
|
||||
width: 0;
|
||||
@ -98,15 +98,15 @@
|
||||
transition: width 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
.sqr-captcha-icon {
|
||||
.pow-botdeterrent-icon {
|
||||
height: 4em;
|
||||
}
|
||||
|
||||
.sqr-captcha-hidden {
|
||||
.pow-botdeterrent-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sqr-checkmark-icon-checkmark {
|
||||
.pow-checkmark-icon-checkmark {
|
||||
fill:none;
|
||||
stroke: #31bd82;
|
||||
stroke-width: 6em;
|
||||
@ -114,11 +114,11 @@
|
||||
stroke-dashoffset: 74em;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
animation: 0.8s normal forwards ease-in-out sqr-draw-checkmark;
|
||||
animation: 0.8s normal forwards ease-in-out pow-draw-checkmark;
|
||||
animation-play-state: inherit;
|
||||
}
|
||||
|
||||
.sqr-checkmark-icon-border {
|
||||
.pow-checkmark-icon-border {
|
||||
fill:none;
|
||||
stroke: #aaa;
|
||||
stroke-width: 3em;
|
||||
@ -126,23 +126,23 @@
|
||||
stroke-dashoffset: 110em;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
animation: 0.8s normal forwards ease-in-out sqr-draw-checkmark-border;
|
||||
animation: 0.8s normal forwards ease-in-out pow-draw-checkmark-border;
|
||||
animation-play-state: inherit;
|
||||
}
|
||||
|
||||
.sqr-gears-icon-gear-large {
|
||||
.pow-gears-icon-gear-large {
|
||||
fill: #9359fa;
|
||||
animation: 4s linear infinite sqr-spinning-gears-large;
|
||||
animation: 4s linear infinite pow-spinning-gears-large;
|
||||
animation-play-state: running;
|
||||
}
|
||||
.sqr-gears-icon-gear-small {
|
||||
.pow-gears-icon-gear-small {
|
||||
fill: #9359fa;
|
||||
animation: 4s linear infinite sqr-spinning-gears-small;
|
||||
animation: 4s linear infinite pow-spinning-gears-small;
|
||||
animation-play-state: running;
|
||||
}
|
||||
|
||||
|
||||
@keyframes sqr-draw-checkmark-border {
|
||||
@keyframes pow-draw-checkmark-border {
|
||||
0% {
|
||||
stroke-dashoffset: 110em;
|
||||
}
|
||||
@ -151,7 +151,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sqr-draw-checkmark {
|
||||
@keyframes pow-draw-checkmark {
|
||||
0% {
|
||||
stroke-dashoffset: 74em;
|
||||
}
|
||||
@ -160,7 +160,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sqr-spinning-gears-small {
|
||||
@keyframes pow-spinning-gears-small {
|
||||
0% {
|
||||
transform: translate(161px, 161px) rotate(0deg) translate(-161px,-161px);
|
||||
}
|
||||
@ -169,7 +169,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes sqr-spinning-gears-large {
|
||||
@keyframes pow-spinning-gears-large {
|
||||
0% {
|
||||
transform: translate(73px, 73px) rotate(360deg) translate(-73px,-73px);
|
||||
}
|
||||
|
@ -2,47 +2,48 @@
|
||||
|
||||
const numberOfWebWorkersToCreate = 4;
|
||||
|
||||
window.sqrCaptchaReset = () => {
|
||||
window.sqrCaptchaInitDone = false;
|
||||
window.powBotDeterrentReset = () => {
|
||||
window.botBotDeterrentInitDone = false;
|
||||
};
|
||||
|
||||
window.sqrCaptchaInit = () => {
|
||||
if(window.sqrCaptchaInitDone) {
|
||||
console.error("sqrCaptchaInit was called twice!");
|
||||
window.botBotDeterrentInit = () => {
|
||||
if(window.botBotDeterrentInitDone) {
|
||||
console.error("botBotDeterrentInit was called twice!");
|
||||
return
|
||||
}
|
||||
window.sqrCaptchaInitDone = true;
|
||||
window.botBotDeterrentInitDone = true;
|
||||
|
||||
const challenges = Array.from(document.querySelectorAll("[data-sqr-captcha-challenge]"));
|
||||
const challenges = Array.from(document.querySelectorAll("[data-pow-bot-deterrent-challenge]"));
|
||||
const challengesMap = {};
|
||||
let url = null;
|
||||
let proofOfWorker = { postMessage: () => console.error("error: proofOfWorker was never loaded. ") };
|
||||
|
||||
challenges.forEach(element => {
|
||||
|
||||
data-pow-bot-deterrent
|
||||
if(!url) {
|
||||
if(!element.dataset.sqrCaptchaUrl) {
|
||||
console.error("error: element with data-sqr-captcha-challenge property is missing the data-sqr-captcha-url property");
|
||||
if(!element.dataset.powBotDeterrentAPIURL) {
|
||||
console.error("error: element with data-pow-bot-deterrent-challenge property is missing the data-pow-bot-deterrent-url property");
|
||||
}
|
||||
url = element.dataset.sqrCaptchaUrl;
|
||||
url = element.dataset.sqrpowAPIURL;
|
||||
if(url.endsWith("/")) {
|
||||
url = url.substring(0, url.length-1)
|
||||
}
|
||||
}
|
||||
|
||||
if(!element.dataset.sqrCaptchaCallback) {
|
||||
console.error("error: element with data-sqr-captcha-challenge property is missing the data-sqr-captcha-callback property");
|
||||
if(!element.dataset.powBotDeterrentCallback) {
|
||||
console.error("error: element with data-pow-bot-deterrent-challenge property is missing the data-pow-bot-deterrent-callback property");
|
||||
return
|
||||
}
|
||||
|
||||
if(typeof element.dataset.sqrCaptchaCallback != "string") {
|
||||
console.error("error: data-sqr-captcha-callback property should be of type 'string'");
|
||||
if(typeof element.dataset.powBotDeterrentCallback != "string") {
|
||||
console.error("error: data-pow-bot-deterrent-callback property should be of type 'string'");
|
||||
return
|
||||
}
|
||||
|
||||
const callback = getCallbackFromGlobalNamespace(element.dataset.sqrCaptchaCallback);
|
||||
const callback = getCallbackFromGlobalNamespace(element.dataset.powBotDeterrentCallback);
|
||||
if(!callback) {
|
||||
console.warn(`warning: data-sqr-captcha-callback '${element.dataset.sqrCaptchaCallback}' `
|
||||
console.warn(`warning: data-pow-bot-deterrent-callback '${element.dataset.powBotDeterrentCallback}' `
|
||||
+ "is not defined in the global namespace yet. It had better be defined by the time it's called!");
|
||||
}
|
||||
|
||||
@ -58,15 +59,15 @@
|
||||
parent = parent.parentElement
|
||||
}
|
||||
if(!form) {
|
||||
console.error("error: element with data-sqr-captcha-challenge property was not inside a form element");
|
||||
console.error("error: element with data-pow-bot-deterrent-challenge property was not inside a form element");
|
||||
//todo
|
||||
}
|
||||
|
||||
let cssIsAlreadyLoaded = document.querySelector(`link[href='${url}/static/captcha.css']`);
|
||||
let cssIsAlreadyLoaded = document.querySelector(`link[href='${url}/static/pow-bot-deterrent.css']`);
|
||||
|
||||
cssIsAlreadyLoaded = cssIsAlreadyLoaded || Array.from(document.styleSheets).some(x => {
|
||||
try {
|
||||
return Array.from(x.rules).some(x => x.selectorText == ".sqr-captcha")
|
||||
return Array.from(x.rules).some(x => x.selectorText == ".pow-botdeterrent")
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
@ -77,26 +78,26 @@
|
||||
"rel": "stylesheet",
|
||||
"charset": "utf8",
|
||||
});
|
||||
stylesheet.onload = () => renderCaptcha(element);
|
||||
stylesheet.setAttribute("href", `${url}/static/captcha.css`);
|
||||
stylesheet.onload = () => renderProgressInfo(element);
|
||||
stylesheet.setAttribute("href", `${url}/static/pow-bot-deterrent.css`);
|
||||
} else {
|
||||
renderCaptcha(element);
|
||||
renderProgressInfo(element);
|
||||
}
|
||||
|
||||
window.sqrCaptchaTrigger = () => {
|
||||
window.powBotDeterrentTrigger = () => {
|
||||
|
||||
const challenge = element.dataset.sqrCaptchaChallenge;
|
||||
const challenge = element.dataset.powBotDeterrentChallenge;
|
||||
if(!challengesMap[challenge]) {
|
||||
challengesMap[challenge] = {
|
||||
element: element,
|
||||
attempts: 0,
|
||||
startTime: new Date().getTime(),
|
||||
};
|
||||
const progressBarContainer = element.querySelector(".sqr-captcha-progress-bar-container");
|
||||
const progressBarContainer = element.querySelector(".pow-botdeterrent-progress-bar-container");
|
||||
progressBarContainer.style.display = "block";
|
||||
const mainElement = element.querySelector(".sqr-captcha");
|
||||
const mainElement = element.querySelector(".pow-botdeterrent");
|
||||
mainElement.style.display = "inline-block";
|
||||
const gears = element.querySelector(".sqr-gears-icon");
|
||||
const gears = element.querySelector(".pow-gears-icon");
|
||||
gears.style.display = "block";
|
||||
|
||||
challengesMap[challenge].updateProgressInterval = setInterval(() => {
|
||||
@ -107,8 +108,8 @@
|
||||
challengesMap[challenge].attempts
|
||||
);
|
||||
const element = challengesMap[challenge].element;
|
||||
const progressBar = element.querySelector(".sqr-captcha-progress-bar");
|
||||
const bestHashElement = element.querySelector(".sqr-captcha-best-hash");
|
||||
const progressBar = element.querySelector(".pow-botdeterrent-progress-bar");
|
||||
const bestHashElement = element.querySelector(".pow-botdeterrent-best-hash");
|
||||
bestHashElement.textContent = getHashProgressText(challengesMap[challenge]);
|
||||
progressBar.style.width = `${probabilityOfSuccessSoFar*100}%`;
|
||||
}
|
||||
@ -123,8 +124,8 @@
|
||||
.concat(Array.from(form.querySelectorAll("textarea")));
|
||||
|
||||
inputElements.forEach(inputElement => {
|
||||
inputElement.onchange = () => window.sqrCaptchaTrigger();
|
||||
inputElement.onkeydown = () => window.sqrCaptchaTrigger();
|
||||
inputElement.onchange = () => window.powBotDeterrentTrigger();
|
||||
inputElement.onkeydown = () => window.powBotDeterrentTrigger();
|
||||
});
|
||||
});
|
||||
|
||||
@ -167,14 +168,14 @@
|
||||
clearInterval(challengeState.updateProgressInterval);
|
||||
|
||||
const element = challengeState.element;
|
||||
const progressBar = element.querySelector(".sqr-captcha-progress-bar");
|
||||
const checkmark = element.querySelector(".sqr-checkmark-icon");
|
||||
const gears = element.querySelector(".sqr-gears-icon");
|
||||
const bestHashElement = element.querySelector(".sqr-captcha-best-hash");
|
||||
const description = element.querySelector(".sqr-captcha-description");
|
||||
const progressBar = element.querySelector(".pow-botdeterrent-progress-bar");
|
||||
const checkmark = element.querySelector(".pow-checkmark-icon");
|
||||
const gears = element.querySelector(".pow-gears-icon");
|
||||
const bestHashElement = element.querySelector(".pow-botdeterrent-best-hash");
|
||||
const description = element.querySelector(".pow-botdeterrent-description");
|
||||
challengeState.smallestHash = e.data.smallestHash;
|
||||
bestHashElement.textContent = getHashProgressText(challengeState);
|
||||
bestHashElement.classList.add("sqr-captcha-best-hash-done");
|
||||
bestHashElement.classList.add("pow-botdeterrent-best-hash-done");
|
||||
checkmark.style.display = "block";
|
||||
checkmark.style.animationPlayState = "running";
|
||||
gears.style.display = "none";
|
||||
@ -193,9 +194,9 @@
|
||||
|
||||
webWorkers.forEach(x => x.postMessage({stop: "STOP"}));
|
||||
|
||||
const callback = getCallbackFromGlobalNamespace(element.dataset.sqrCaptchaCallback);
|
||||
const callback = getCallbackFromGlobalNamespace(element.dataset.powBotDeterrentCallback);
|
||||
if(!callback) {
|
||||
console.error(`error: data-sqr-captcha-callback '${element.dataset.sqrCaptchaCallback}' `
|
||||
console.error(`error: data-pow-bot-deterrent-callback '${element.dataset.powBotDeterrentCallback}' `
|
||||
+ "is not defined in the global namespace!");
|
||||
} else {
|
||||
console.log(`firing callback for challenge ${e.data.challenge} w/ nonce ${e.data.nonce}, smallestHash: ${e.data.smallestHash}, difficulty: ${e.data.difficulty}`);
|
||||
@ -221,16 +222,16 @@
|
||||
})
|
||||
};
|
||||
|
||||
window.sqrCaptchaReset = () => {
|
||||
window.sqrCaptchaInitDone = false;
|
||||
window.powBotDeterrentReset = () => {
|
||||
window.botBotDeterrentInitDone = false;
|
||||
webWorkers.forEach(x => x.terminate());
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const challenges = Array.from(document.querySelectorAll("[data-sqr-captcha-challenge]"));
|
||||
const challenges = Array.from(document.querySelectorAll("[data-pow-bot-deterrent-challenge]"));
|
||||
if(challenges.length) {
|
||||
window.sqrCaptchaInit();
|
||||
window.botBotDeterrentInit();
|
||||
}
|
||||
|
||||
function getCallbackFromGlobalNamespace(callbackString) {
|
||||
@ -263,30 +264,30 @@
|
||||
return str.length < max ? leftPad(" " + str, max) : str;
|
||||
}
|
||||
|
||||
function renderCaptcha(parent) {
|
||||
function renderProgressInfo(parent) {
|
||||
const svgXMLNS = "http://www.w3.org/2000/svg";
|
||||
const xmlnsXMLNS = 'http://www.w3.org/2000/xmlns/';
|
||||
const xmlSpaceXMLNS = 'http://www.w3.org/XML/1998/namespace';
|
||||
|
||||
parent.innerHTML = "";
|
||||
|
||||
const main = createElement(parent, "div", {"class": "sqr-captcha sqr-captcha-hidden"});
|
||||
const mainRow = createElement(main, "div", {"class": "sqr-captcha-row"});
|
||||
const main = createElement(parent, "div", {"class": "pow-botdeterrent pow-botdeterrent-hidden"});
|
||||
const mainRow = createElement(main, "div", {"class": "pow-botdeterrent-row"});
|
||||
const mainColumn = createElement(mainRow, "div");
|
||||
const headerRow = createElement(mainColumn, "div");
|
||||
const headerLink = createElement(
|
||||
headerRow,
|
||||
"a",
|
||||
{
|
||||
"class": "sqr-captcha-link",
|
||||
"href": "https://git.sequentialread.com/forest/pow-captcha",
|
||||
"class": "pow-botdeterrent-link",
|
||||
"href": "https://git.sequentialread.com/forest/pow-bot-deterrent",
|
||||
"target": "_blank"
|
||||
},
|
||||
"💥PoW! "
|
||||
);
|
||||
createElement(headerLink, "span", null, "Captcha");
|
||||
createElement(headerRow, "div", {"class": "sqr-captcha-best-hash"}, "loading...");
|
||||
const description = createElement(mainColumn, "div", {"class": "sqr-captcha-description"});
|
||||
createElement(headerLink, "span", null, "Bot Deterrent");
|
||||
createElement(headerRow, "div", {"class": "pow-botdeterrent-best-hash"}, "loading...");
|
||||
const description = createElement(mainColumn, "div", {"class": "pow-botdeterrent-description"});
|
||||
appendFragment(description, "Please wait for your browser to calculate a ");
|
||||
createElement(
|
||||
description,
|
||||
@ -298,10 +299,10 @@
|
||||
createElement(description, "br");
|
||||
appendFragment(description, "This an accessible & privacy-respecting anti-spam measure. ");
|
||||
const progressBarContainer = createElement(main, "div", {
|
||||
"class": "sqr-captcha-progress-bar-container sqr-captcha-hidden"
|
||||
"class": "pow-botdeterrent-progress-bar-container pow-botdeterrent-hidden"
|
||||
});
|
||||
createElement(progressBarContainer, "div", {"class": "sqr-captcha-progress-bar"});
|
||||
const iconContainer = createElement(mainRow, "div", {"class": "sqr-captcha-icon-container"});
|
||||
createElement(progressBarContainer, "div", {"class": "pow-botdeterrent-progress-bar"});
|
||||
const iconContainer = createElement(mainRow, "div", {"class": "pow-botdeterrent-icon-container"});
|
||||
|
||||
|
||||
const checkmarkIcon = createElementNS(iconContainer, svgXMLNS, "svg", {
|
||||
@ -309,14 +310,14 @@
|
||||
"xml:space": [xmlSpaceXMLNS, 'preserve'],
|
||||
"version": "1.1",
|
||||
"viewBox": "0 0 512 512",
|
||||
"class": "sqr-checkmark-icon sqr-captcha-icon sqr-captcha-hidden"
|
||||
"class": "pow-checkmark-icon pow-botdeterrent-icon pow-botdeterrent-hidden"
|
||||
});
|
||||
createElementNS(checkmarkIcon, svgXMLNS, "polyline", {
|
||||
"class": "sqr-checkmark-icon-checkmark",
|
||||
"class": "pow-checkmark-icon-checkmark",
|
||||
"points": "444,110 206,343 120,252"
|
||||
});
|
||||
createElementNS(checkmarkIcon, svgXMLNS, "polyline", {
|
||||
"class": "sqr-checkmark-icon-border",
|
||||
"class": "pow-checkmark-icon-border",
|
||||
"points": "240,130 30,130 30,470 370,470 370,350"
|
||||
});
|
||||
|
||||
@ -325,14 +326,14 @@
|
||||
"xml:space": [xmlSpaceXMLNS, 'preserve'],
|
||||
"version": "1.1",
|
||||
"viewBox": "-30 -5 250 223",
|
||||
"class": "sqr-gears-icon sqr-captcha-icon sqr-captcha-hidden"
|
||||
"class": "pow-gears-icon pow-botdeterrent-icon pow-botdeterrent-hidden"
|
||||
});
|
||||
createElementNS(gearsIcon, svgXMLNS, "path", {
|
||||
"class": "sqr-gears-icon-gear-large",
|
||||
"class": "pow-gears-icon-gear-large",
|
||||
"d": "M113.595,133.642l-5.932-13.169c5.655-4.151,10.512-9.315,14.307-15.209l13.507,5.118c2.583,0.979,5.469-0.322,6.447-2.904 l4.964-13.103c0.47-1.24,0.428-2.616-0.117-3.825c-0.545-1.209-1.547-2.152-2.788-2.622l-13.507-5.118 c1.064-6.93,0.848-14.014-0.637-20.871l13.169-5.932c1.209-0.545,2.152-1.547,2.622-2.788c0.47-1.24,0.428-2.616-0.117-3.825 l-5.755-12.775c-1.134-2.518-4.096-3.638-6.612-2.505l-13.169,5.932c-4.151-5.655-9.315-10.512-15.209-14.307l5.118-13.507 c0.978-2.582-0.322-5.469-2.904-6.447L93.88,0.82c-1.239-0.469-2.615-0.428-3.825,0.117c-1.209,0.545-2.152,1.547-2.622,2.788 l-5.117,13.506c-6.937-1.07-14.033-0.849-20.872,0.636L55.513,4.699c-0.545-1.209-1.547-2.152-2.788-2.622 c-1.239-0.469-2.616-0.428-3.825,0.117L36.124,7.949c-2.518,1.134-3.639,4.094-2.505,6.612l5.932,13.169 c-5.655,4.151-10.512,9.315-14.307,15.209l-13.507-5.118c-1.239-0.469-2.615-0.427-3.825,0.117 c-1.209,0.545-2.152,1.547-2.622,2.788L0.326,53.828c-0.978,2.582,0.322,5.469,2.904,6.447l13.507,5.118 c-1.064,6.929-0.848,14.015,0.637,20.871L4.204,92.196c-1.209,0.545-2.152,1.547-2.622,2.788c-0.47,1.24-0.428,2.616,0.117,3.825 l5.755,12.775c0.544,1.209,1.547,2.152,2.787,2.622c1.241,0.47,2.616,0.429,3.825-0.117l13.169-5.932 c4.151,5.656,9.314,10.512,15.209,14.307l-5.118,13.507c-0.978,2.582,0.322,5.469,2.904,6.447l13.103,4.964 c0.571,0.216,1.172,0.324,1.771,0.324c0.701,0,1.402-0.147,2.054-0.441c1.209-0.545,2.152-1.547,2.622-2.788l5.117-13.506 c6.937,1.069,14.034,0.849,20.872-0.636l5.931,13.168c0.545,1.209,1.547,2.152,2.788,2.622c1.24,0.47,2.617,0.429,3.825-0.117 l12.775-5.754C113.607,139.12,114.729,136.16,113.595,133.642z M105.309,86.113c-4.963,13.1-17.706,21.901-31.709,21.901 c-4.096,0-8.135-0.744-12.005-2.21c-8.468-3.208-15.18-9.522-18.899-17.779c-3.719-8.256-4-17.467-0.792-25.935 c4.963-13.1,17.706-21.901,31.709-21.901c4.096,0,8.135,0.744,12.005,2.21c8.468,3.208,15.18,9.522,18.899,17.778 C108.237,68.434,108.518,77.645,105.309,86.113z"
|
||||
});
|
||||
createElementNS(gearsIcon, svgXMLNS, "path", {
|
||||
"class": "sqr-gears-icon-gear-small",
|
||||
"class": "pow-gears-icon-gear-small",
|
||||
"d": "M216.478,154.389c-0.896-0.977-2.145-1.558-3.469-1.615l-9.418-0.404 c-0.867-4.445-2.433-8.736-4.633-12.697l6.945-6.374c2.035-1.867,2.17-5.03,0.303-7.064l-6.896-7.514 c-0.896-0.977-2.145-1.558-3.47-1.615c-1.322-0.049-2.618,0.416-3.595,1.312l-6.944,6.374c-3.759-2.531-7.9-4.458-12.254-5.702 l0.404-9.418c0.118-2.759-2.023-5.091-4.782-5.209l-10.189-0.437c-2.745-0.104-5.091,2.023-5.209,4.781l-0.404,9.418 c-4.444,0.867-8.735,2.433-12.697,4.632l-6.374-6.945c-0.896-0.977-2.145-1.558-3.469-1.615c-1.324-0.054-2.618,0.416-3.595,1.312 l-7.514,6.896c-2.035,1.867-2.17,5.03-0.303,7.064l6.374,6.945c-2.531,3.759-4.458,7.899-5.702,12.254l-9.417-0.404 c-2.747-0.111-5.092,2.022-5.21,4.781l-0.437,10.189c-0.057,1.325,0.415,2.618,1.312,3.595c0.896,0.977,2.145,1.558,3.47,1.615 l9.417,0.403c0.867,4.445,2.433,8.736,4.632,12.698l-6.944,6.374c-0.977,0.896-1.558,2.145-1.615,3.469 c-0.057,1.325,0.415,2.618,1.312,3.595l6.896,7.514c0.896,0.977,2.145,1.558,3.47,1.615c1.319,0.053,2.618-0.416,3.595-1.312 l6.944-6.374c3.759,2.531,7.9,4.458,12.254,5.702l-0.404,9.418c-0.118,2.759,2.022,5.091,4.781,5.209l10.189,0.437 c0.072,0.003,0.143,0.004,0.214,0.004c1.25,0,2.457-0.468,3.381-1.316c0.977-0.896,1.558-2.145,1.615-3.469l0.404-9.418 c4.444-0.867,8.735-2.433,12.697-4.632l6.374,6.945c0.896,0.977,2.145,1.558,3.469,1.615c1.33,0.058,2.619-0.416,3.595-1.312 l7.514-6.896c2.035-1.867,2.17-5.03,0.303-7.064l-6.374-6.945c2.531-3.759,4.458-7.899,5.702-12.254l9.417,0.404 c2.756,0.106,5.091-2.022,5.21-4.781l0.437-10.189C217.847,156.659,217.375,155.366,216.478,154.389z M160.157,183.953 c-12.844-0.55-22.846-11.448-22.295-24.292c0.536-12.514,10.759-22.317,23.273-22.317c0.338,0,0.678,0.007,1.019,0.022 c12.844,0.551,22.846,11.448,22.295,24.292C183.898,174.511,173.106,184.497,160.157,183.953z"
|
||||
});
|
||||
}
|
||||
|
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user