2023-11-30 20:42:06 +00:00
|
|
|
package plugins
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"reflect"
|
2024-10-04 14:36:04 +00:00
|
|
|
"runtime"
|
2024-06-25 07:58:04 +00:00
|
|
|
"strings"
|
2023-11-30 20:42:06 +00:00
|
|
|
|
|
|
|
"github.com/http-wasm/http-wasm-host-go/handler"
|
|
|
|
wasm "github.com/http-wasm/http-wasm-host-go/handler/nethttp"
|
|
|
|
"github.com/tetratelabs/wazero"
|
|
|
|
"github.com/traefik/traefik/v3/pkg/logs"
|
|
|
|
"github.com/traefik/traefik/v3/pkg/middlewares"
|
|
|
|
)
|
|
|
|
|
|
|
|
type wasmMiddlewareBuilder struct {
|
2024-06-25 07:58:04 +00:00
|
|
|
path string
|
|
|
|
cache wazero.CompilationCache
|
|
|
|
settings Settings
|
2023-11-30 20:42:06 +00:00
|
|
|
}
|
|
|
|
|
2024-06-25 07:58:04 +00:00
|
|
|
func newWasmMiddlewareBuilder(goPath, moduleName, wasmPath string, settings Settings) (*wasmMiddlewareBuilder, error) {
|
|
|
|
ctx := context.Background()
|
|
|
|
path := filepath.Join(goPath, "src", moduleName, wasmPath)
|
|
|
|
cache := wazero.NewCompilationCache()
|
|
|
|
|
|
|
|
code, err := os.ReadFile(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("loading Wasm binary: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCompilationCache(cache))
|
|
|
|
if _, err = rt.CompileModule(ctx, code); err != nil {
|
|
|
|
return nil, fmt.Errorf("compiling guest module: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &wasmMiddlewareBuilder{path: path, cache: cache, settings: settings}, nil
|
2023-11-30 20:42:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (b wasmMiddlewareBuilder) newMiddleware(config map[string]interface{}, middlewareName string) (pluginMiddleware, error) {
|
|
|
|
return &WasmMiddleware{
|
|
|
|
middlewareName: middlewareName,
|
|
|
|
config: reflect.ValueOf(config),
|
|
|
|
builder: b,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b wasmMiddlewareBuilder) newHandler(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, error) {
|
2024-06-25 07:58:04 +00:00
|
|
|
h, applyCtx, err := b.buildMiddleware(ctx, next, cfg, middlewareName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("building Wasm middleware: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
h.ServeHTTP(rw, req.WithContext(applyCtx(req.Context())))
|
|
|
|
}), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *wasmMiddlewareBuilder) buildMiddleware(ctx context.Context, next http.Handler, cfg reflect.Value, middlewareName string) (http.Handler, func(ctx context.Context) context.Context, error) {
|
2023-11-30 20:42:06 +00:00
|
|
|
code, err := os.ReadFile(b.path)
|
|
|
|
if err != nil {
|
2024-06-25 07:58:04 +00:00
|
|
|
return nil, nil, fmt.Errorf("loading binary: %w", err)
|
|
|
|
}
|
|
|
|
|
2024-09-16 09:12:04 +00:00
|
|
|
rt := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCompilationCache(b.cache))
|
2024-06-25 07:58:04 +00:00
|
|
|
|
|
|
|
guestModule, err := rt.CompileModule(ctx, code)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("compiling guest module: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
applyCtx, err := InstantiateHost(ctx, rt, guestModule, b.settings)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, fmt.Errorf("instantiating host module: %w", err)
|
2023-11-30 20:42:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
logger := middlewares.GetLogger(ctx, middlewareName, "wasm")
|
|
|
|
|
2024-09-16 09:12:04 +00:00
|
|
|
config := wazero.NewModuleConfig().WithSysWalltime().WithStartFunctions("_start", "_initialize")
|
2024-06-25 07:58:04 +00:00
|
|
|
for _, env := range b.settings.Envs {
|
|
|
|
config.WithEnv(env, os.Getenv(env))
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(b.settings.Mounts) > 0 {
|
|
|
|
fsConfig := wazero.NewFSConfig()
|
|
|
|
for _, mount := range b.settings.Mounts {
|
|
|
|
withDir := fsConfig.WithDirMount
|
|
|
|
prefix, readOnly := strings.CutSuffix(mount, ":ro")
|
|
|
|
if readOnly {
|
|
|
|
withDir = fsConfig.WithReadOnlyDirMount
|
|
|
|
}
|
|
|
|
parts := strings.Split(prefix, ":")
|
|
|
|
switch {
|
|
|
|
case len(parts) == 1:
|
|
|
|
withDir(parts[0], parts[0])
|
|
|
|
case len(parts) == 2:
|
|
|
|
withDir(parts[0], parts[1])
|
|
|
|
default:
|
|
|
|
return nil, nil, fmt.Errorf("invalid directory %q", mount)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
config.WithFSConfig(fsConfig)
|
|
|
|
}
|
|
|
|
|
2023-11-30 20:42:06 +00:00
|
|
|
opts := []handler.Option{
|
2024-06-25 07:58:04 +00:00
|
|
|
handler.ModuleConfig(config),
|
2023-11-30 20:42:06 +00:00
|
|
|
handler.Logger(logs.NewWasmLogger(logger)),
|
|
|
|
}
|
|
|
|
|
|
|
|
i := cfg.Interface()
|
|
|
|
if i != nil {
|
|
|
|
config, ok := i.(map[string]interface{})
|
|
|
|
if !ok {
|
2024-06-25 07:58:04 +00:00
|
|
|
return nil, nil, fmt.Errorf("could not type assert config: %T", i)
|
2023-11-30 20:42:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
data, err := json.Marshal(config)
|
|
|
|
if err != nil {
|
2024-06-25 07:58:04 +00:00
|
|
|
return nil, nil, fmt.Errorf("marshaling config: %w", err)
|
2023-11-30 20:42:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
opts = append(opts, handler.GuestConfig(data))
|
|
|
|
}
|
|
|
|
|
2024-06-25 07:58:04 +00:00
|
|
|
opts = append(opts, handler.Runtime(func(ctx context.Context) (wazero.Runtime, error) {
|
|
|
|
return rt, nil
|
|
|
|
}))
|
|
|
|
|
|
|
|
mw, err := wasm.NewMiddleware(applyCtx(ctx), code, opts...)
|
2023-11-30 20:42:06 +00:00
|
|
|
if err != nil {
|
2024-06-25 07:58:04 +00:00
|
|
|
return nil, nil, fmt.Errorf("creating middleware: %w", err)
|
2023-11-30 20:42:06 +00:00
|
|
|
}
|
|
|
|
|
2024-10-04 14:36:04 +00:00
|
|
|
h := mw.NewHandler(ctx, next)
|
|
|
|
|
|
|
|
// Traefik does not Close the middleware when creating a new instance on a configuration change.
|
|
|
|
// When the middleware is marked to be GC, we need to close it so the wasm instance is properly closed.
|
|
|
|
// Reference: https://github.com/traefik/traefik/issues/11119
|
|
|
|
runtime.SetFinalizer(h, func(_ http.Handler) {
|
|
|
|
if err := mw.Close(ctx); err != nil {
|
|
|
|
logger.Err(err).Msg("[wasm] middleware Close failed")
|
|
|
|
} else {
|
|
|
|
logger.Debug().Msg("[wasm] middleware Close ok")
|
|
|
|
}
|
|
|
|
})
|
|
|
|
return h, applyCtx, nil
|
2023-11-30 20:42:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// WasmMiddleware is an HTTP handler plugin wrapper.
|
|
|
|
type WasmMiddleware struct {
|
|
|
|
middlewareName string
|
|
|
|
config reflect.Value
|
|
|
|
builder wasmMiddlewareBuilder
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewHandler creates a new HTTP handler.
|
|
|
|
func (m WasmMiddleware) NewHandler(ctx context.Context, next http.Handler) (http.Handler, error) {
|
|
|
|
return m.builder.newHandler(ctx, next, m.config, m.middlewareName)
|
|
|
|
}
|