mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-15 04:58:43 +00:00
feat(web/js): add wasm client side runner
Signed-off-by: Xe Iaso <me@xeiaso.net>
This commit is contained in:
@@ -20,11 +20,7 @@ services:
|
|||||||
dockerfile: .devcontainer/Dockerfile
|
dockerfile: .devcontainer/Dockerfile
|
||||||
volumes:
|
volumes:
|
||||||
- ../:/workspace/anubis:cached
|
- ../:/workspace/anubis:cached
|
||||||
- cargo-target:/workspace/anubis/target:cached
|
|
||||||
environment:
|
environment:
|
||||||
VALKEY_URL: redis://valkey:6379/0
|
VALKEY_URL: redis://valkey:6379/0
|
||||||
#entrypoint: ["/usr/bin/sleep", "infinity"]
|
#entrypoint: ["/usr/bin/sleep", "infinity"]
|
||||||
user: vscode
|
user: vscode
|
||||||
|
|
||||||
volumes:
|
|
||||||
cargo-target:
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import (
|
|||||||
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
_ "github.com/TecharoHQ/anubis/lib/challenge/metarefresh"
|
||||||
_ "github.com/TecharoHQ/anubis/lib/challenge/preact"
|
_ "github.com/TecharoHQ/anubis/lib/challenge/preact"
|
||||||
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
_ "github.com/TecharoHQ/anubis/lib/challenge/proofofwork"
|
||||||
|
_ "github.com/TecharoHQ/anubis/lib/challenge/wasm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { render, h, Fragment } from "preact";
|
import { render, h, Fragment } from "preact";
|
||||||
import { useState, useEffect } from "preact/hooks";
|
import { useState, useEffect } from "preact/hooks";
|
||||||
import { g, j, r, u, x } from "./xeact.js";
|
import { g, j, r, u, x } from "../../../../web/lib/xeact";
|
||||||
import { Sha256 } from "@aws-crypto/sha256-js";
|
import { Sha256 } from "@aws-crypto/sha256-js";
|
||||||
|
|
||||||
/** @jsx h */
|
/** @jsx h */
|
||||||
|
|||||||
81
lib/challenge/wasm/proofofwork.go
Normal file
81
lib/challenge/wasm/proofofwork.go
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
package wasm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
|
chall "github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/localization"
|
||||||
|
"github.com/a-h/templ"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:generate go tool github.com/a-h/templ/cmd/templ generate
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
chall.Register("argon2id", &Impl{Algorithm: "argon2id"})
|
||||||
|
chall.Register("sha256", &Impl{Algorithm: "sha256"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Impl struct {
|
||||||
|
Algorithm string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Impl) Setup(mux *http.ServeMux) {}
|
||||||
|
|
||||||
|
func (i *Impl) Issue(w http.ResponseWriter, r *http.Request, lg *slog.Logger, in *chall.IssueInput) (templ.Component, error) {
|
||||||
|
loc := localization.GetLocalizer(r)
|
||||||
|
return page(loc), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Impl) Validate(r *http.Request, lg *slog.Logger, in *chall.ValidateInput) error {
|
||||||
|
rule := in.Rule
|
||||||
|
challenge := in.Challenge.RandomData
|
||||||
|
|
||||||
|
nonceStr := r.FormValue("nonce")
|
||||||
|
if nonceStr == "" {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w nonce", chall.ErrMissingField))
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, err := strconv.Atoi(nonceStr)
|
||||||
|
if err != nil {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: nonce: %w", chall.ErrInvalidFormat, err))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedTimeStr := r.FormValue("elapsedTime")
|
||||||
|
if elapsedTimeStr == "" {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w elapsedTime", chall.ErrMissingField))
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsedTime, err := strconv.ParseFloat(elapsedTimeStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: elapsedTime: %w", chall.ErrInvalidFormat, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
response := r.FormValue("response")
|
||||||
|
if response == "" {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w response", chall.ErrMissingField))
|
||||||
|
}
|
||||||
|
|
||||||
|
calcString := fmt.Sprintf("%s%d", challenge, nonce)
|
||||||
|
calculated := internal.SHA256sum(calcString)
|
||||||
|
|
||||||
|
if subtle.ConstantTimeCompare([]byte(response), []byte(calculated)) != 1 {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted response %s but got %s", chall.ErrFailed, calculated, response))
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare the leading zeroes
|
||||||
|
if !strings.HasPrefix(response, strings.Repeat("0", rule.Challenge.Difficulty)) {
|
||||||
|
return chall.NewError("validate", "invalid response", fmt.Errorf("%w: wanted %d leading zeros but got %s", chall.ErrFailed, rule.Challenge.Difficulty, response))
|
||||||
|
}
|
||||||
|
|
||||||
|
lg.Debug("challenge took", "elapsedTime", elapsedTime)
|
||||||
|
chall.TimeTaken.WithLabelValues(i.Algorithm).Observe(elapsedTime)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
44
lib/challenge/wasm/proofofwork.templ
Normal file
44
lib/challenge/wasm/proofofwork.templ
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package wasm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/localization"
|
||||||
|
)
|
||||||
|
|
||||||
|
templ page(localizer *localization.SimpleLocalizer) {
|
||||||
|
<div class="centered-div">
|
||||||
|
<img id="image" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version }/>
|
||||||
|
<img style="display:none;" style="width:100%;max-width:256px;" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version }/>
|
||||||
|
<p id="status">{ localizer.T("loading") }</p>
|
||||||
|
<script async type="module" src={ anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version }></script>
|
||||||
|
<div id="progress" role="progressbar" aria-labelledby="status">
|
||||||
|
<div class="bar-inner"></div>
|
||||||
|
</div>
|
||||||
|
<details>
|
||||||
|
if anubis.UseSimplifiedExplanation {
|
||||||
|
<p>
|
||||||
|
{ localizer.T("simplified_explanation") }
|
||||||
|
</p>
|
||||||
|
} else {
|
||||||
|
<p>
|
||||||
|
{ localizer.T("ai_companies_explanation") }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ localizer.T("anubis_compromise") }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ localizer.T("hack_purpose") }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ localizer.T("jshelter_note") }
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</details>
|
||||||
|
<noscript>
|
||||||
|
<p>
|
||||||
|
{ localizer.T("javascript_required") }
|
||||||
|
</p>
|
||||||
|
</noscript>
|
||||||
|
<div id="testarea"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
190
lib/challenge/wasm/proofofwork_templ.go
generated
Normal file
190
lib/challenge/wasm/proofofwork_templ.go
generated
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
// Code generated by templ - DO NOT EDIT.
|
||||||
|
|
||||||
|
// templ: version: v0.3.924
|
||||||
|
package wasm
|
||||||
|
|
||||||
|
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||||
|
|
||||||
|
import "github.com/a-h/templ"
|
||||||
|
import templruntime "github.com/a-h/templ/runtime"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TecharoHQ/anubis"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/localization"
|
||||||
|
)
|
||||||
|
|
||||||
|
func page(localizer *localization.SimpleLocalizer) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var1 == nil {
|
||||||
|
templ_7745c5c3_Var1 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"centered-div\"><img id=\"image\" style=\"width:100%;max-width:256px;\" src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var2 string
|
||||||
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/pensive.webp?cacheBuster=" + anubis.Version)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 10, Col: 165}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\"> <img style=\"display:none;\" style=\"width:100%;max-width:256px;\" src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var3 string
|
||||||
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/img/happy.webp?cacheBuster=" + anubis.Version)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 11, Col: 174}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"><p id=\"status\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("loading"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 12, Col: 41}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</p><script async type=\"module\" src=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(anubis.BasePrefix + "/.within.website/x/cmd/anubis/static/js/main.mjs?cacheBuster=" + anubis.Version)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 13, Col: 136}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"></script><div id=\"progress\" role=\"progressbar\" aria-labelledby=\"status\"><div class=\"bar-inner\"></div></div><details>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if anubis.UseSimplifiedExplanation {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var6 string
|
||||||
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("simplified_explanation"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 20, Col: 44}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var7 string
|
||||||
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("ai_companies_explanation"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 24, Col: 46}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</p><p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("anubis_compromise"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 27, Col: 39}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</p><p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var9 string
|
||||||
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("hack_purpose"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 30, Col: 34}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p><p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var10 string
|
||||||
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("jshelter_note"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 33, Col: 35}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</details><noscript><p>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var11 string
|
||||||
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(localizer.T("javascript_required"))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `proofofwork.templ`, Line: 39, Col: 40}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p></noscript><div id=\"testarea\"></div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = templruntime.GeneratedTemplate
|
||||||
151
lib/challenge/wasm/proofofwork_test.go
Normal file
151
lib/challenge/wasm/proofofwork_test.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package wasm
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TecharoHQ/anubis/lib/challenge"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy"
|
||||||
|
"github.com/TecharoHQ/anubis/lib/policy/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mkRequest(t *testing.T, values map[string]string) *http.Request {
|
||||||
|
t.Helper()
|
||||||
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
q := req.URL.Query()
|
||||||
|
|
||||||
|
for k, v := range values {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.URL.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasic(t *testing.T) {
|
||||||
|
i := &Impl{Algorithm: "fast"}
|
||||||
|
bot := &policy.Bot{
|
||||||
|
Challenge: &config.ChallengeRules{
|
||||||
|
Algorithm: "fast",
|
||||||
|
Difficulty: 0,
|
||||||
|
ReportAs: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const challengeStr = "hunter"
|
||||||
|
const response = "2652bdba8fb4d2ab39ef28d8534d7694c557a4ae146c1e9237bd8d950280500e"
|
||||||
|
|
||||||
|
for _, cs := range []struct {
|
||||||
|
name string
|
||||||
|
req *http.Request
|
||||||
|
err error
|
||||||
|
challengeStr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "allgood",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: nil,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no-params",
|
||||||
|
req: mkRequest(t, map[string]string{}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-nonce",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-elapsedTime",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing-response",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
}),
|
||||||
|
err: challenge.ErrMissingField,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong-nonce-format",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "taco",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrInvalidFormat,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong-elapsedTime-format",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "taco",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrInvalidFormat,
|
||||||
|
challengeStr: challengeStr,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid-response",
|
||||||
|
req: mkRequest(t, map[string]string{
|
||||||
|
"nonce": "0",
|
||||||
|
"elapsedTime": "69",
|
||||||
|
"response": response,
|
||||||
|
}),
|
||||||
|
err: challenge.ErrFailed,
|
||||||
|
challengeStr: "Tacos are tasty",
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(cs.name, func(t *testing.T) {
|
||||||
|
lg := slog.With()
|
||||||
|
|
||||||
|
i.Setup(http.NewServeMux())
|
||||||
|
|
||||||
|
inp := &challenge.IssueInput{
|
||||||
|
Rule: bot,
|
||||||
|
Challenge: &challenge.Challenge{
|
||||||
|
RandomData: cs.challengeStr,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := i.Issue(httptest.NewRecorder(), cs.req, lg, inp); err != nil {
|
||||||
|
t.Errorf("can't issue challenge: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := i.Validate(cs.req, lg, &challenge.ValidateInput{
|
||||||
|
Rule: bot,
|
||||||
|
Challenge: &challenge.Challenge{
|
||||||
|
RandomData: cs.challengeStr,
|
||||||
|
},
|
||||||
|
}); !errors.Is(err, cs.err) {
|
||||||
|
t.Errorf("got wrong error from Validate, got %v but wanted %v", err, cs.err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
"package": "yeet",
|
"package": "yeet",
|
||||||
"lint": "make lint"
|
"lint": "make lint"
|
||||||
},
|
},
|
||||||
|
"imports": {
|
||||||
|
"lib/*": "./web/lib/*"
|
||||||
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ for file in js/**/*.ts js/**/*.mjs; do
|
|||||||
|
|
||||||
mkdir -p "$(dirname "$out")"
|
mkdir -p "$(dirname "$out")"
|
||||||
|
|
||||||
esbuild "$file" --sourcemap --bundle --minify --outfile="$out" --banner:js="$LICENSE"
|
esbuild "$file" --sourcemap --bundle --outfile="$out" --banner:js="$LICENSE"
|
||||||
gzip -f -k -n "$out"
|
gzip -f -k -n "$out"
|
||||||
zstd -f -k --ultra -22 "$out"
|
zstd -f -k --ultra -22 "$out"
|
||||||
brotli -fZk "$out"
|
brotli -fZk "$out"
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import fast from "./fast";
|
import fast from "./fast";
|
||||||
|
import wasm from "./wasm";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fast: fast,
|
fast: fast,
|
||||||
slow: fast, // XXX(Xe): slow is deprecated, but keep this around in case anything goes bad
|
slow: fast, // XXX(Xe): slow is deprecated, but keep this around in case anything goes bad
|
||||||
|
|
||||||
|
argon2id: wasm,
|
||||||
|
sha256: wasm,
|
||||||
}
|
}
|
||||||
87
web/js/algorithms/wasm.ts
Normal file
87
web/js/algorithms/wasm.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { u } from "../../lib/xeact";
|
||||||
|
|
||||||
|
type ProgressCallback = (nonce: number) => void;
|
||||||
|
|
||||||
|
interface ProcessOptions {
|
||||||
|
basePrefix: string;
|
||||||
|
version: string;
|
||||||
|
algorithm: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHardwareConcurrency = () =>
|
||||||
|
navigator.hardwareConcurrency !== undefined ? navigator.hardwareConcurrency : 1;
|
||||||
|
|
||||||
|
export default function process(
|
||||||
|
options: ProcessOptions,
|
||||||
|
data: string,
|
||||||
|
difficulty: number = 5,
|
||||||
|
signal: AbortSignal | null = null,
|
||||||
|
progressCallback?: ProgressCallback,
|
||||||
|
threads: number = Math.trunc(Math.max(getHardwareConcurrency() / 2, 1)),
|
||||||
|
): Promise<string> {
|
||||||
|
console.debug(options);
|
||||||
|
const { basePrefix, version, algorithm } = options;
|
||||||
|
|
||||||
|
let wasmFeatures = "baseline";
|
||||||
|
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
const module = await fetch(u(`${basePrefix}/.within.website/x/cmd/anubis/static/wasm/${wasmFeatures}/${algorithm}.wasm?cacheBuster=${version}`))
|
||||||
|
.then(x => WebAssembly.compileStreaming(x));
|
||||||
|
|
||||||
|
const webWorkerURL = `${options.basePrefix}/.within.website/x/cmd/anubis/static/js/worker/wasm.mjs?cacheBuster=${version}`;
|
||||||
|
|
||||||
|
const workers: Worker[] = [];
|
||||||
|
let settled = false;
|
||||||
|
|
||||||
|
const onAbort = () => {
|
||||||
|
console.log("PoW aborted");
|
||||||
|
cleanup();
|
||||||
|
reject(new DOMException("Aborted", "AbortError"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
workers.forEach((w) => w.terminate());
|
||||||
|
if (signal != null) {
|
||||||
|
signal.removeEventListener("abort", onAbort);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (signal != null) {
|
||||||
|
if (signal.aborted) {
|
||||||
|
return onAbort();
|
||||||
|
}
|
||||||
|
signal.addEventListener("abort", onAbort, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < threads; i++) {
|
||||||
|
let worker = new Worker(webWorkerURL);
|
||||||
|
|
||||||
|
worker.onmessage = (event) => {
|
||||||
|
if (typeof event.data === "number") {
|
||||||
|
progressCallback?.(event.data);
|
||||||
|
} else {
|
||||||
|
cleanup();
|
||||||
|
resolve(event.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.onerror = (event) => {
|
||||||
|
cleanup();
|
||||||
|
reject(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.postMessage({
|
||||||
|
data,
|
||||||
|
difficulty,
|
||||||
|
nonce: i,
|
||||||
|
threads,
|
||||||
|
module,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ const t = (key) => translations[`js_${key}`] || translations[key] || key;
|
|||||||
try {
|
try {
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
const { hash, nonce } = await process(
|
const { hash, nonce } = await process(
|
||||||
{ basePrefix, version: anubisVersion },
|
{ basePrefix, version: anubisVersion, algorithm: rules.algorithm },
|
||||||
challenge.randomData,
|
challenge.randomData,
|
||||||
rules.difficulty,
|
rules.difficulty,
|
||||||
null,
|
null,
|
||||||
|
|||||||
106
web/js/worker/wasm.ts
Normal file
106
web/js/worker/wasm.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
export interface Args {
|
||||||
|
data: string;
|
||||||
|
difficulty: number;
|
||||||
|
nonce: number;
|
||||||
|
threads: number;
|
||||||
|
module: BufferSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AnubisExports {
|
||||||
|
anubis_work: (difficulty: number, initialNonce: number, threads: number) => number;
|
||||||
|
data_ptr: () => number;
|
||||||
|
result_hash_ptr: () => number;
|
||||||
|
result_hash_size: () => number;
|
||||||
|
set_data_length: (len: number) => void;
|
||||||
|
memory: WebAssembly.Memory;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener("message", async (event: MessageEvent<Args>) => {
|
||||||
|
console.log(event.data);
|
||||||
|
const { data, difficulty, threads, module } = event.data;
|
||||||
|
let { nonce } = event.data;
|
||||||
|
|
||||||
|
const importObject = {
|
||||||
|
anubis: {
|
||||||
|
anubis_update_nonce: (nonce: number) => postMessage(nonce),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nonce !== 0) {
|
||||||
|
importObject.anubis.anubis_update_nonce = (_) => { };
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = await WebAssembly.instantiate(module, importObject);
|
||||||
|
|
||||||
|
const {
|
||||||
|
anubis_work,
|
||||||
|
data_ptr,
|
||||||
|
result_hash_ptr,
|
||||||
|
result_hash_size,
|
||||||
|
set_data_length,
|
||||||
|
memory
|
||||||
|
} = (obj as unknown as any).exports as unknown as AnubisExports;
|
||||||
|
function uint8ArrayToHex(arr: Uint8Array) {
|
||||||
|
return Array.from(arr)
|
||||||
|
.map((c) => c.toString(16).padStart(2, "0"))
|
||||||
|
.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToUint8Array(hexString: string): Uint8Array {
|
||||||
|
// Remove whitespace and optional '0x' prefix
|
||||||
|
hexString = hexString.replace(/\s+/g, '').replace(/^0x/, '');
|
||||||
|
|
||||||
|
// Check for valid length
|
||||||
|
if (hexString.length % 2 !== 0) {
|
||||||
|
throw new Error('Invalid hex string length');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for valid characters
|
||||||
|
if (!/^[0-9a-fA-F]+$/.test(hexString)) {
|
||||||
|
throw new Error('Invalid hex characters');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to Uint8Array
|
||||||
|
const byteArray = new Uint8Array(hexString.length / 2);
|
||||||
|
for (let i = 0; i < byteArray.length; i++) {
|
||||||
|
const byteValue = parseInt(hexString.substr(i * 2, 2), 16);
|
||||||
|
byteArray[i] = byteValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return byteArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write data to buffer
|
||||||
|
function writeToBuffer(data: Uint8Array) {
|
||||||
|
if (data.length > 1024) throw new Error("Data exceeds buffer size");
|
||||||
|
|
||||||
|
// Get pointer and create view
|
||||||
|
const offset = data_ptr();
|
||||||
|
const buffer = new Uint8Array(memory.buffer, offset, data.length);
|
||||||
|
|
||||||
|
// Copy data
|
||||||
|
buffer.set(data);
|
||||||
|
|
||||||
|
// Set data length
|
||||||
|
set_data_length(data.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFromChallenge() {
|
||||||
|
const offset = result_hash_ptr();
|
||||||
|
const buffer = new Uint8Array(memory.buffer, offset, result_hash_size());
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeToBuffer(hexToUint8Array(data));
|
||||||
|
|
||||||
|
nonce = anubis_work(difficulty, nonce, threads);
|
||||||
|
const challenge = readFromChallenge();
|
||||||
|
const result = uint8ArrayToHex(challenge);
|
||||||
|
|
||||||
|
postMessage({
|
||||||
|
hash: result,
|
||||||
|
difficulty,
|
||||||
|
nonce,
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user