Condition, rules, state and action refactor / rewrite
Add nested rules Add backend action, allow wildcard in backends Remove poison from tree, update README with action table Allow defining pass/fail actions on challenge, Remove redirect/referer parameters on backend pass Set challenge cookie tied to host Rewrite DNSBL condition into a challenge Allow passing an arbitrary path for assets to js challenges Optimize programs exhaustively on compilation Activation instead of map for CEL context, faster map access, new network override Return valid host on cookie setting in case Host is an IP address. bug: does not work with IPv6, see https://github.com/golang/go/issues/65521 Apply TLS fingerprinter on GetConfigForClient instead of GetCertificate Cleanup go-away cookies before passing to backend Code action for specifically replying with an HTTP code
This commit is contained in:
committed by
WeebDataHoarder
parent
1c7fe1bed9
commit
ead41055ca
@@ -111,6 +111,7 @@ type VerifyChallengeInput struct {
|
||||
|
||||
type VerifyChallengeOutput uint64
|
||||
|
||||
// TODO: expand allowed values
|
||||
const (
|
||||
VerifyChallengeOutputOK = VerifyChallengeOutput(iota)
|
||||
VerifyChallengeOutputFailed
|
||||
|
||||
186
lib/challenge/wasm/registration.go
Normal file
186
lib/challenge/wasm/registration.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"codeberg.org/meta/gzipped/v2"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.gammaspectra.live/git/go-away/embed"
|
||||
"git.gammaspectra.live/git/go-away/lib/challenge"
|
||||
_interface "git.gammaspectra.live/git/go-away/lib/challenge/wasm/interface"
|
||||
"git.gammaspectra.live/git/go-away/utils"
|
||||
"git.gammaspectra.live/git/go-away/utils/inline"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/goccy/go-yaml/ast"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
challenge.Runtimes["js"] = FillJavaScriptRegistration
|
||||
}
|
||||
|
||||
type Parameters struct {
|
||||
Path string `yaml:"path"`
|
||||
// Loader path to js/mjs file to use as challenge issuer
|
||||
Loader string `yaml:"js-loader"`
|
||||
|
||||
// Runtime path to WASM wasip1 runtime
|
||||
Runtime string `yaml:"wasm-runtime"`
|
||||
|
||||
Settings map[string]string `yaml:"wasm-runtime-settings"`
|
||||
|
||||
NativeCompiler bool `yaml:"wasm-native-compiler"`
|
||||
|
||||
VerifyProbability float64 `yaml:"verify-probability"`
|
||||
}
|
||||
|
||||
var DefaultParameters = Parameters{
|
||||
VerifyProbability: 0.1,
|
||||
NativeCompiler: true,
|
||||
}
|
||||
|
||||
func FillJavaScriptRegistration(state challenge.StateInterface, reg *challenge.Registration, parameters ast.Node) error {
|
||||
params := DefaultParameters
|
||||
|
||||
if parameters != nil {
|
||||
ymlData, err := parameters.MarshalYAML()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = yaml.Unmarshal(ymlData, ¶ms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reg.Class = challenge.ClassBlocking
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
if params.Path == "" {
|
||||
params.Path = reg.Name
|
||||
}
|
||||
|
||||
assetsFs, err := embed.GetFallbackFS(embed.ChallengeFs, params.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if params.VerifyProbability <= 0 {
|
||||
//10% default
|
||||
params.VerifyProbability = 0.1
|
||||
} else if params.VerifyProbability > 1.0 {
|
||||
params.VerifyProbability = 1.0
|
||||
}
|
||||
|
||||
reg.VerifyProbability = params.VerifyProbability
|
||||
|
||||
ob := NewRunner(params.NativeCompiler)
|
||||
reg.Object = ob
|
||||
|
||||
wasmData, err := assetsFs.ReadFile(path.Join("runtime", params.Runtime))
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load runtime: %w", err)
|
||||
}
|
||||
|
||||
err = ob.Compile("runtime", wasmData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling runtime: %w", err)
|
||||
}
|
||||
|
||||
reg.IssueChallenge = func(w http.ResponseWriter, r *http.Request, key challenge.Key, expiry time.Time) challenge.VerifyResult {
|
||||
state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{
|
||||
"EndTags": []template.HTML{
|
||||
template.HTML(fmt.Sprintf("<script async type=\"module\" src=\"%s?cacheBust=%s\"></script>", reg.Path+"/script.mjs", utils.CacheBust())),
|
||||
},
|
||||
})
|
||||
return challenge.VerifyResultNone
|
||||
}
|
||||
|
||||
reg.Verify = func(key challenge.Key, token []byte, r *http.Request) (challenge.VerifyResult, error) {
|
||||
var ok bool
|
||||
err = ob.Instantiate("runtime", func(ctx context.Context, mod api.Module) (err error) {
|
||||
in := _interface.VerifyChallengeInput{
|
||||
Key: key[:],
|
||||
Parameters: params.Settings,
|
||||
Result: token,
|
||||
}
|
||||
|
||||
out, err := VerifyChallengeCall(ctx, mod, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if out == _interface.VerifyChallengeOutputError {
|
||||
return errors.New("error checking challenge")
|
||||
}
|
||||
ok = out == _interface.VerifyChallengeOutputOK
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return challenge.VerifyResultFail, err
|
||||
}
|
||||
if ok {
|
||||
return challenge.VerifyResultOK, nil
|
||||
}
|
||||
return challenge.VerifyResultFail, nil
|
||||
}
|
||||
|
||||
// serve assets if existent
|
||||
if staticFs, err := fs.Sub(assetsFs, "static"); err != nil {
|
||||
return fmt.Errorf("no static assets: %w", err)
|
||||
} else {
|
||||
mux.Handle("GET "+reg.Path+"/static/", http.StripPrefix(reg.Path+"/static/", gzipped.FileServer(gzipped.FS(staticFs))))
|
||||
}
|
||||
|
||||
mux.HandleFunc(reg.Path+challenge.MakeChallengeUrlSuffix, func(w http.ResponseWriter, r *http.Request) {
|
||||
data := challenge.RequestDataFromContext(r.Context())
|
||||
err := ob.Instantiate("runtime", func(ctx context.Context, mod api.Module) (err error) {
|
||||
key := challenge.GetChallengeKeyForRequest(state, reg, data.Expiration(reg.Duration), r)
|
||||
|
||||
in := _interface.MakeChallengeInput{
|
||||
Key: key[:],
|
||||
Parameters: params.Settings,
|
||||
Headers: inline.MIMEHeader(r.Header),
|
||||
}
|
||||
in.Data, err = io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out, err := MakeChallengeCall(ctx, mod, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set output headers
|
||||
for k, v := range out.Headers {
|
||||
w.Header()[k] = v
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out.Data)))
|
||||
w.WriteHeader(out.Code)
|
||||
_, _ = w.Write(out.Data)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
state.ErrorPage(w, r, http.StatusInternalServerError, err, "")
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
mux.HandleFunc(reg.Path+challenge.VerifyChallengeUrlSuffix, challenge.VerifyHandlerFunc(state, reg, nil, nil))
|
||||
|
||||
mux.HandleFunc("GET "+reg.Path+"/script.mjs", func(w http.ResponseWriter, r *http.Request) {
|
||||
challenge.ServeChallengeScript(w, r, reg, params.Settings, path.Join(reg.Path, "static", params.Loader))
|
||||
})
|
||||
|
||||
reg.Handler = mux
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -94,11 +94,13 @@ func (r *Runner) Compile(key string, binary []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) Close() {
|
||||
func (r *Runner) Close() error {
|
||||
for _, module := range r.modules {
|
||||
module.Close(r.context)
|
||||
if err := module.Close(r.context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
r.runtime.Close(r.context)
|
||||
return r.runtime.Close(r.context)
|
||||
}
|
||||
|
||||
var ErrModuleNotFound = errors.New("module not found")
|
||||
|
||||
Reference in New Issue
Block a user