diff --git a/bindings/wasm/output.go b/bindings/wasm/output.go index 40f2b18c93..29fb6ed937 100644 --- a/bindings/wasm/output.go +++ b/bindings/wasm/output.go @@ -23,6 +23,8 @@ import ( "strings" "sync/atomic" + "github.com/stealthrocket/wasi-go/imports/wasi_http" + "github.com/stealthrocket/wasi-go/imports/wasi_http/default_http" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" @@ -77,16 +79,27 @@ func (out *outputBinding) Init(ctx context.Context, metadata bindings.Metadata) return fmt.Errorf("wasm: error compiling binary: %w", err) } - switch detectImports(out.module.ImportedFunctions()) { - case modeWasiP1: + imports := detectImports(out.module.ImportedFunctions()) + + if _, found := imports[modeWasiP1]; found { _, err = wasi_snapshot_preview1.Instantiate(ctx, out.runtime) } - if err != nil { _ = out.runtime.Close(context.Background()) - return fmt.Errorf("wasm: error instantiating host functions: %w", err) + return fmt.Errorf("wasm: error instantiating host wasi functions: %w", err) } - return + if _, found := imports[modeWasiHTTP]; found { + if out.meta.StrictSandbox { + _ = out.runtime.Close(context.Background()) + return fmt.Errorf("can not instantiate wasi-http with strict sandbox") + } + err = wasi_http.Instantiate(ctx, out.runtime) + } + if err != nil { + _ = out.runtime.Close(context.Background()) + return fmt.Errorf("wasm: error instantiating host wasi-http functions: %w", err) + } + return nil } func (out *outputBinding) Invoke(ctx context.Context, req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) { @@ -152,19 +165,23 @@ func (out *outputBinding) Close() error { const ( modeDefault importMode = iota modeWasiP1 + modeWasiHTTP ) type importMode uint -func detectImports(imports []api.FunctionDefinition) importMode { +func detectImports(imports []api.FunctionDefinition) map[importMode]bool { + result := make(map[importMode]bool) for _, f := range imports { moduleName, _, _ := f.Import() switch moduleName { case wasi_snapshot_preview1.ModuleName: - return modeWasiP1 + result[modeWasiP1] = true + case default_http.ModuleName: + result[modeWasiHTTP] = true } } - return modeDefault + return result } // GetComponentMetadata returns the metadata of the component. diff --git a/bindings/wasm/output_test.go b/bindings/wasm/output_test.go index 69e5167bd8..f065df863f 100644 --- a/bindings/wasm/output_test.go +++ b/bindings/wasm/output_test.go @@ -18,6 +18,8 @@ import ( "context" _ "embed" "io" + "net/http" + "net/http/httptest" "testing" "github.com/stretchr/testify/require" @@ -31,6 +33,7 @@ const ( urlArgsFile = "file://testdata/args/main.wasm" urlExampleFile = "file://testdata/example/main.wasm" urlLoopFile = "file://testdata/loop/main.wasm" + urlHTTPFile = "file://testdata/http/main.wasm" ) func Test_outputBinding_Init(t *testing.T) { @@ -179,3 +182,89 @@ func Test_Invoke(t *testing.T) { }) } } + +type handler struct{} + +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/unknown" { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("Not found")) + return + } + w.WriteHeader(http.StatusOK) + w.Write([]byte(r.URL.Path)) +} + +func Test_InvokeHttp(t *testing.T) { + type testCase struct { + name string + url string + request *bindings.InvokeRequest + ctx context.Context + reqURL string + expectedData string + expectedErr string + } + + s := httptest.NewServer(&handler{}) + defer s.Close() + + tests := []testCase{ + { + name: "http", + url: urlHTTPFile, + request: &bindings.InvokeRequest{ + Operation: ExecuteOperation, + }, + reqURL: s.URL, + expectedData: "Status: 200\nBody: \n/\n", + }, + { + name: "unknown", + url: urlHTTPFile, + request: &bindings.InvokeRequest{ + Operation: ExecuteOperation, + }, + reqURL: s.URL + "/unknown", + expectedData: "Status: 404\nBody: \nNot found\n", + }, + } + + for _, tt := range tests { + tc := tt + t.Run(tc.name, func(t *testing.T) { + l := logger.NewLogger(t.Name()) + var buf bytes.Buffer + l.SetOutput(&buf) + + meta := metadata.Base{Properties: map[string]string{"url": tc.url}} + + output := NewWasmOutput(l) + defer output.(io.Closer).Close() + + ctx := context.Background() + + err := output.Init(ctx, bindings.Metadata{Base: meta}) + require.NoError(t, err) + + reqCtx := tc.ctx + if reqCtx == nil { + reqCtx = ctx + } + + tc.request.Metadata = map[string]string{"args": tc.reqURL} + + if tc.expectedErr == "" { + // execute twice to prove idempotency + for i := 0; i < 2; i++ { + resp, outputErr := output.Invoke(reqCtx, tc.request) + require.NoError(t, outputErr) + require.Equal(t, tc.expectedData, string(resp.Data)) + } + } else { + _, err = output.Invoke(reqCtx, tc.request) + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} diff --git a/bindings/wasm/testdata/http/Makefile b/bindings/wasm/testdata/http/Makefile new file mode 100644 index 0000000000..f82b0ebfd6 --- /dev/null +++ b/bindings/wasm/testdata/http/Makefile @@ -0,0 +1,4 @@ +default: main.wasm + +main.wasm: main.go + tinygo build -target wasi -o main.wasm main.go \ No newline at end of file diff --git a/bindings/wasm/testdata/http/main.go b/bindings/wasm/testdata/http/main.go new file mode 100644 index 0000000000..653ece8869 --- /dev/null +++ b/bindings/wasm/testdata/http/main.go @@ -0,0 +1,35 @@ +package main + +// Building main.wasm: +// go get github.com/dev-wasm/dev-wasm-go/http/client +// tinygo build -target wasi -o main.wasm main.go + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + + wasiclient "github.com/dev-wasm/dev-wasm-go/http/client" +) + +func printResponse(r *http.Response) { + fmt.Printf("Status: %d\n", r.StatusCode) + body, err := ioutil.ReadAll(r.Body) + if err != nil { + panic(err.Error()) + } + fmt.Printf("Body: \n%s\n", body) +} + +func main() { + client := http.Client{ + Transport: wasiclient.WasiRoundTripper{}, + } + res, err := client.Get(os.Args[1]) + if err != nil { + panic(err.Error()) + } + defer res.Body.Close() + printResponse(res) +} diff --git a/bindings/wasm/testdata/http/main.wasm b/bindings/wasm/testdata/http/main.wasm new file mode 100755 index 0000000000..7d65972dd5 Binary files /dev/null and b/bindings/wasm/testdata/http/main.wasm differ diff --git a/go.mod b/go.mod index cb9ec16fbd..3ad6bd2b6d 100644 --- a/go.mod +++ b/go.mod @@ -99,6 +99,7 @@ require ( github.com/sendgrid/sendgrid-go v3.12.0+incompatible github.com/sijms/go-ora/v2 v2.7.9 github.com/spf13/cast v1.5.1 + github.com/stealthrocket/wasi-go v0.7.6-0.20230718231108-c3d30af59057 github.com/stretchr/testify v1.8.4 github.com/supplyon/gremcos v0.1.40 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.608 diff --git a/go.sum b/go.sum index b8cd36797a..6d8fabcffb 100644 --- a/go.sum +++ b/go.sum @@ -1861,6 +1861,9 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/stealthrocket/wasi-go v0.7.6-0.20230718231108-c3d30af59057 h1:BaBBX206PM1+qF5WQx7Ug7mbKqzizBONDMv4ST5EVNg= +github.com/stealthrocket/wasi-go v0.7.6-0.20230718231108-c3d30af59057/go.mod h1:PJ5oVs2E1ciOJnsTnav4nvTtEcJ4D1jUZAewS9pzuZg= +github.com/stealthrocket/wazergo v0.19.1 h1:BPrITETPgSFwiytwmToO0MbUC/+RGC39JScz1JmmG6c= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=