updates for pow-bot-deterrent-rp -- fixing cross domain webworker issue

This commit is contained in:
Your Name 2025-03-15 18:48:47 -05:00
parent 86a1d903dc
commit 9f8fc11ed5
9 changed files with 229 additions and 201 deletions

View File

@ -16,6 +16,5 @@ WORKDIR /app
COPY --from=build /build/pow-bot-deterrent /app/pow-bot-deterrent
COPY static /app/static
COPY PoW_Bot_Deterrent_API_Tokens /app/PoW_Bot_Deterrent_API_Tokens
RUN chmod +x /app/pow-bot-deterrent
ENTRYPOINT ["/app/pow-bot-deterrent"]

View File

@ -142,7 +142,7 @@ Return type: `text/plain` (error/status messages only)
Otherwise it returns 404, 400, or 500.
#### `GET /static/<filename>`
#### `GET /pow-bot-deterrent-static/<filename>`
Return type: depends on file
@ -236,7 +236,7 @@ When `pow-bot-deterrent.js` runs, if it finds an element with `data-pow-bot-dete
> 💬 *INFO* the element with the `pow-bot-deterrent` 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.botBotDeterrentInit`
#### `window.powBotDeterrentInit`
The bot deterrent event listeners, elements, css, & webworkers **won't be loaded until this function is called**.
@ -248,15 +248,15 @@ For example:
```
<script>
window.botBotDeterrentInit();
window.powBotDeterrentInit();
</script>
```
#### `window.powBotDeterrentReset`
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.
Resets the bot deterrent(s), stops the webworkers, etc. Use this if you have updated the page and you need to call `window.powBotDeterrentInit` again.
#### `window.botBotDeterrentInitDone`
#### `window.powBotDeterrentInitDone`
A boolean variable that `pow-bot-deterrent.js` uses internally, so it can know if it has already been initialized or not.
@ -296,10 +296,10 @@ function MyComponent({botDeterrentURL, challenge}) {
// Maybe less clear than the above, but JavaScript heads might enjoy this more:
// window[uniqueCallback] = setNonce;
if(window.botBotDeterrentInitDone) {
if(window.powBotDeterrentInitDone) {
window.powBotDeterrentReset();
}
window.botBotDeterrentInit();
window.powBotDeterrentInit();
}, [uniqueCallback]);
return (
@ -484,7 +484,7 @@ There are two main important parts, the form and the javascript at the bottom:
document.querySelector("form input[type='submit']").disabled = false;
};
</script>
<script src="{{ .PowAPIURL }}/static/pow-bot-deterrent.js"></script>
<script src="{{ .PowAPIURL }}/pow-bot-deterrent-static/pow-bot-deterrent.js"></script>
```
⚠️ **NOTE** that the element with the `pow-bot-deterrent` 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!

View File

@ -1,6 +1,10 @@
#!/bin/bash -e
VERSION="0.0.13"
tag="0.0.0"
if git describe --tags --abbrev=0 > /dev/null 2>&1 ; then
tag="$(git describe --tags --abbrev=0)"
fi
VERSION="$tag-$(git rev-parse --short HEAD)-$(hexdump -n 2 -ve '1/1 "%.2x"' /dev/urandom)"
rm -rf dockerbuild || true
mkdir dockerbuild
@ -9,17 +13,17 @@ cp Dockerfile dockerbuild/Dockerfile-amd64
cp Dockerfile dockerbuild/Dockerfile-arm
cp Dockerfile dockerbuild/Dockerfile-arm64
sed -E 's|FROM alpine|FROM amd64/alpine|' -i dockerbuild/Dockerfile-amd64
sed -E 's|FROM alpine|FROM arm32v7/alpine|' -i dockerbuild/Dockerfile-arm
sed -E 's|FROM alpine|FROM arm64v8/alpine|' -i dockerbuild/Dockerfile-arm64
sed -E 's|FROM alpine|FROM --platform=linux/amd64 alpine|' -i dockerbuild/Dockerfile-amd64
sed -E 's|FROM alpine|FROM --platform=linux/arm/v7 alpine|' -i dockerbuild/Dockerfile-arm
sed -E 's|FROM alpine|FROM --platform=linux/arm64/v8 alpine|' -i dockerbuild/Dockerfile-arm64
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-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 build --progress=plain -f dockerbuild/Dockerfile-amd64 -t sequentialread/pow-bot-deterrent:$VERSION-amd64 .
docker build --progress=plain -f dockerbuild/Dockerfile-arm -t sequentialread/pow-bot-deterrent:$VERSION-arm .
docker build --progress=plain -f dockerbuild/Dockerfile-arm64 -t sequentialread/pow-bot-deterrent:$VERSION-arm64 .
docker push sequentialread/pow-bot-deterrent:$VERSION-amd64
docker push sequentialread/pow-bot-deterrent:$VERSION-arm

View File

@ -47,31 +47,6 @@
document.querySelector("form input[type='submit']").disabled = false;
};
</script>
<script src="/static/pow-bot-deterrent.js"></script>
<!-- <script src='./static/scrypt_wasm.js'></script>
<script>
const { scrypt } = wasm_bindgen;
async function run() {
console.log("a");
await wasm_bindgen();
console.log(scrypt(hexEncode('password in hex'), hexEncode('password in hex'), 4096, 8, 1, 16))
}
run();
function hexEncode(s){
var hex, i;
var result = "";
for (i=0; i<s.length; i++) {
hex = s.charCodeAt(i).toString(16);
result += ("000"+hex).slice(-4);
}
return result
}
</script> -->
<script src="/pow-bot-deterrent-static/pow-bot-deterrent.js"></script>
</body>
</html>

170
main.go
View File

@ -14,15 +14,34 @@ import (
"os/exec"
"path"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"time"
configlite "git.sequentialread.com/forest/config-lite"
errors "git.sequentialread.com/forest/pkg-errors"
"golang.org/x/crypto/scrypt"
)
type Config struct {
ListenPort int `json:"listen_port"`
BatchSize int `json:"batch_size"`
DeprecateAfterBatches int `json:"deprecate_after_batches"`
ScryptCPUAndMemoryCost int `json:"scrypt_cpu_and_memory_cost"`
AdminAPIToken string `json:"admin_api_token"`
EmailAddress string `json:"email_address"`
// port 993 (IMAPS)
// port 143 (STARTTLS) [deprecated!]
ImapHost string `json:"imap_host"`
ImapPort int `json:"imap_port"`
ImapEncryption string `json:"imap_encryption"`
ImapUsername string `json:"imap_username"`
ImapPassword string `json:"imap_password"`
}
// https://en.wikipedia.org/wiki/Scrypt
type ScryptParameters struct {
CPUAndMemoryCost int `json:"N"`
@ -38,6 +57,9 @@ type Challenge struct {
DifficultyLevel int `json:"dl"`
}
var config Config
var appDirectory string
var scryptParameters ScryptParameters
var currentChallengesGeneration = map[string]int{}
var challenges = map[string]map[string]int{}
@ -45,51 +67,7 @@ func main() {
var err error
batchSize := 1000
deprecateAfterBatches := 10
portNumber := 2370
scryptCPUAndMemoryCost := 16384
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_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_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_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_BOT_DETERRENT_SCRYPT_CPU_AND_MEMORY_COST '%s' can't be converted to an integer", scryptCPUAndMemoryCostEnv))
}
}
apiTokensFolder := locateAPITokensFolder()
adminAPIToken := os.ExpandEnv("$POW_BOT_DETERRENT_ADMIN_API_TOKEN")
if adminAPIToken == "" {
panic(errors.New("can't start the app, the POW_BOT_DETERRENT_ADMIN_API_TOKEN environment variable is required"))
}
scryptParameters := ScryptParameters{
CPUAndMemoryCost: scryptCPUAndMemoryCost,
BlockSize: 8,
Paralellization: 1,
KeyLength: 16,
}
apiTokensFolder := readConfiguration()
requireMethod := func(method string) func(http.ResponseWriter, *http.Request) bool {
return func(responseWriter http.ResponseWriter, request *http.Request) bool {
@ -103,7 +81,7 @@ func main() {
}
requireAdmin := func(responseWriter http.ResponseWriter, request *http.Request) bool {
if request.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", adminAPIToken) {
if request.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", config.AdminAPIToken) {
http.Error(responseWriter, "401 Unauthorized", http.StatusUnauthorized)
return true
}
@ -262,8 +240,8 @@ func main() {
return true
}
toReturn := make([]string, batchSize)
for i := 0; i < batchSize; i++ {
toReturn := make([]string, config.BatchSize)
for i := 0; i < config.BatchSize; i++ {
preimageBytes := make([]byte, 8)
_, err := rand.Read(preimageBytes)
if err != nil {
@ -309,7 +287,7 @@ func main() {
}
toRemove := []string{}
for k, generation := range challenges[token] {
if generation+deprecateAfterBatches < currentChallengesGeneration[token] {
if generation+config.DeprecateAfterBatches < currentChallengesGeneration[token] {
toRemove = append(toRemove, k)
}
}
@ -417,11 +395,23 @@ func main() {
return true
})
http.HandleFunc("/static/captcha.css", func(responseWriter http.ResponseWriter, request *http.Request) {
bytez, _ := os.ReadFile("./static/pow-bot-deterrent.css")
responseWriter.Header().Set("Content-Type", "text/css")
responseWriter.Write(bytez)
})
http.HandleFunc("/static/captcha.js", func(responseWriter http.ResponseWriter, request *http.Request) {
bytez, _ := os.ReadFile("./static/pow-bot-deterrent.js")
responseWriter.Header().Set("Content-Type", "application/javascript")
responseWriter.Write(bytez)
})
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
http.Handle("/pow-bot-deterrent-static/", http.StripPrefix("/pow-bot-deterrent-static/", http.FileServer(http.Dir("./static/"))))
log.Printf("💥 PoW! Bot Deterrent server listening on port %d", portNumber)
log.Printf("💥 PoW! Bot Deterrent server listening on port %d", config.ListenPort)
err = http.ListenAndServe(fmt.Sprintf(":%d", portNumber), nil)
err = http.ListenAndServe(fmt.Sprintf(":%d", config.ListenPort), nil)
// if got this far it means server crashed!
panic(err)
@ -489,3 +479,81 @@ func getCurrentExecDir() (dir string, err error) {
return dir, nil
}
func readConfiguration() string {
apiTokensFolderPath := locateAPITokensFolder()
appDirectory = filepath.Dir(apiTokensFolderPath)
configJsonPath := filepath.Join(appDirectory, "config.json")
err := configlite.ReadConfiguration(configJsonPath, "POW_BOT_DETERRENT", []string{}, reflect.ValueOf(&config))
if err != nil {
panic(errors.Wrap(err, "ReadConfiguration returned"))
}
errors := []string{}
if config.ListenPort == 0 {
config.ListenPort = 2370
}
if config.BatchSize == 0 {
config.BatchSize = 1000
}
if config.DeprecateAfterBatches == 0 {
config.DeprecateAfterBatches = 10
}
if config.ScryptCPUAndMemoryCost == 0 {
config.ScryptCPUAndMemoryCost = 16384
}
if config.AdminAPIToken == "" {
errors = append(errors, "the POW_BOT_DETERRENT_ADMIN_API_TOKEN environment variable is required")
}
if config.EmailAddress != "" {
if config.ImapHost == "" {
config.ImapHost = "localhost"
}
if config.ImapPort == 0 {
config.ImapPort = 993
}
if config.ImapEncryption == "" {
config.ImapEncryption = "SMTPS"
}
if config.ImapEncryption != "STARTTLS" && config.ImapEncryption != "IMAPS" && config.ImapEncryption != "NONE" {
errors = append(errors, fmt.Sprintf("ImapEncryption '%s' must be IMAPS, STARTTLS or NONE", config.ImapEncryption))
}
if config.ImapUsername == "" {
errors = append(errors, "ImapUsername is required")
}
if config.ImapPassword == "" {
errors = append(errors, "ImapPassword is required")
}
}
if len(errors) > 0 {
log.Fatalln("💥 PoW Bot Deterrent can't start because there are configuration issues:")
log.Fatalln(strings.Join(errors, "\n"))
}
scryptParameters = ScryptParameters{
CPUAndMemoryCost: config.ScryptCPUAndMemoryCost,
BlockSize: 8,
Paralellization: 1,
KeyLength: 16,
}
log.Println("💥 PoW Bot Deterrent starting up with config:")
configToLogBytes, _ := json.MarshalIndent(config, "", " ")
configToLogString := regexp.MustCompile(
`("admin_api_token": ")[^"]+(",)`,
).ReplaceAllString(
string(configToLogBytes),
"$1******$2",
)
configToLogString = regexp.MustCompile(
`("imap_password": ")[^"]+(",?)`,
).ReplaceAllString(
configToLogString,
"$1******$2",
)
log.Println(configToLogString)
return apiTokensFolderPath
}

View File

@ -7,6 +7,7 @@
let scrypt;
let scryptPromise;
let wasm;
let working = false;
const batchSize = 8;
@ -125,7 +126,7 @@ onmessage = function(e) {
}
};
if(scrypt) {
if(wasm && scrypt) {
doWork();
} else {
scryptPromise.then(() => {

View File

@ -3,31 +3,27 @@
const numberOfWebWorkersToCreate = 4;
window.powBotDeterrentReset = () => {
window.botBotDeterrentInitDone = false;
window.powBotDeterrentInitDone = false;
};
window.botBotDeterrentInit = () => {
if(window.botBotDeterrentInitDone) {
console.error("botBotDeterrentInit was called twice!");
const trimSlashes = x => x.replace(/^\/|\/$/g, '');
window.powBotDeterrentInit = () => {
if(window.powBotDeterrentInitDone) {
console.error("powBotDeterrentInit was called twice!");
return
}
window.botBotDeterrentInitDone = true;
window.powBotDeterrentInitDone = true;
const challenges = Array.from(document.querySelectorAll("[data-pow-bot-deterrent-challenge]"));
const challengesMap = {};
let url = null;
let staticAssetsPath = trimSlashes("/pow-bot-deterrent-static/")
let proofOfWorker = { postMessage: () => console.error("error: proofOfWorker was never loaded. ") };
challenges.forEach(element => {
if(!url) {
if(!element.dataset.powBotDeterrentUrl) {
console.error("error: element with data-pow-bot-deterrent-challenge property is missing the data-pow-bot-deterrent-url property");
}
url = element.dataset.powBotDeterrentUrl;
if(url.endsWith("/")) {
url = url.substring(0, url.length-1)
}
if(element.dataset.powBotDeterrentStaticAssetsPath) {
staticAssetsPath = trimSlashes(element.dataset.powBotDeterrentStaticAssetsPath);
}
if(!element.dataset.powBotDeterrentCallback) {
@ -62,7 +58,7 @@
//todo
}
let cssIsAlreadyLoaded = document.querySelector(`link[href='${url}/static/pow-bot-deterrent.css']`);
let cssIsAlreadyLoaded = document.querySelector(`link[href='/${staticAssetsPath}/pow-bot-deterrent.css']`);
cssIsAlreadyLoaded = cssIsAlreadyLoaded || Array.from(document.styleSheets).some(x => {
try {
@ -78,7 +74,7 @@
"charset": "utf8",
});
stylesheet.onload = () => renderProgressInfo(element);
stylesheet.setAttribute("href", `${url}/static/pow-bot-deterrent.css`);
stylesheet.setAttribute("href", `${staticAssetsPath}/pow-bot-deterrent.css`);
} else {
renderProgressInfo(element);
}
@ -133,22 +129,9 @@
//todo
}
if(url) {
// // https://stackoverflow.com/questions/21913673/execute-web-worker-from-different-origin/62914052#62914052
// const webWorkerUrlWhichIsProbablyCrossOrigin = `${url}/static/proofOfWorker.js`;
// const webWorkerPointerDataURL = URL.createObjectURL(
// new Blob(
// [ `importScripts( "${ webWorkerUrlWhichIsProbablyCrossOrigin }" );` ],
// { type: "text/javascript" }
// )
// );
// return
let webWorkers;
webWorkers = [...Array(numberOfWebWorkersToCreate)].map((_, i) => {
const webWorker = new Worker('/static/proofOfWorker.js');
const webWorker = new Worker(`/${staticAssetsPath}/proofOfWorker.js`);
webWorker.onmessage = function(e) {
const challengeState = challengesMap[e.data.challenge]
if(!challengeState) {
@ -213,8 +196,6 @@
return webWorker;
});
// URL.revokeObjectURL(webWorkerPointerDataURL);
proofOfWorker = {
postMessage: arg => webWorkers.forEach((x, i) => {
x.postMessage({ ...arg, workerId: i })
@ -222,15 +203,14 @@
};
window.powBotDeterrentReset = () => {
window.botBotDeterrentInitDone = false;
window.powBotDeterrentInitDone = false;
webWorkers.forEach(x => x.terminate());
};
}
};
const challenges = Array.from(document.querySelectorAll("[data-pow-bot-deterrent-challenge]"));
if(challenges.length) {
window.botBotDeterrentInit();
window.powBotDeterrentInit();
}
function getCallbackFromGlobalNamespace(callbackString) {

View File

@ -7,6 +7,7 @@
let scrypt;
let scryptPromise;
let wasm = undefined;
let working = false;
const batchSize = 4;
@ -149,7 +150,6 @@ let wasm_bindgen;
if (typeof document !== 'undefined' && document.currentScript !== null) {
script_src = new URL(document.currentScript.src, location.href).toString();
}
let wasm = undefined;
let WASM_VECTOR_LEN = 0;
@ -365,11 +365,11 @@ let wasm_bindgen;
return __wbg_finalize_init(instance, module);
}
wasm_bindgen = Object.assign(__wbg_init, { initSync }, __exports);
/pow-bot-deterrent-static/_bindgen = Object.assign(__wbg_init, { initSync }, __exports);
})();
scrypt = wasm_bindgen.scrypt;
scryptPromise = wasm_bindgen({module_or_path: "/static/scrypt.wasm"});
scryptPromise = wasm_bindgen({module_or_path: "/pow-bot-deterrent-static/scrypt.wasm"});

View File

@ -46,7 +46,8 @@ echo '
cat ../proofOfWorkerStub.js | tail -n +6 >> ../static/proofOfWorker.js
cat scrypt-wasm/pkg/scrypt_wasm.js >> ../static/proofOfWorker.js
# wasm was defined at the top of proofOfWorker.js, so don't define it again.
cat scrypt-wasm/pkg/scrypt_wasm.js | grep -v 'let wasm = ' >> ../static/proofOfWorker.js
# see: https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html
echo '