189 lines
4.9 KiB
Go
189 lines
4.9 KiB
Go
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.StaticCacheBust())),
|
|
},
|
|
})
|
|
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)))
|
|
|
|
data.ResponseHeaders(w)
|
|
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
|
|
}
|