diff --git a/apisix/wasm.lua b/apisix/wasm.lua index 9e3e9293f939..939549a2ad9f 100644 --- a/apisix/wasm.lua +++ b/apisix/wasm.lua @@ -65,7 +65,7 @@ end local function access_wrapper(self, conf, ctx) local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin) if not plugin_ctx then - core.log.error("failed to init wasm plugin ctx: ", err) + core.log.error("failed to fetch wasm plugin ctx: ", err) return 503 end @@ -77,6 +77,21 @@ local function access_wrapper(self, conf, ctx) end +local function header_filter_wrapper(self, conf, ctx) + local plugin_ctx, err = fetch_plugin_ctx(conf, ctx, self.plugin) + if not plugin_ctx then + core.log.error("failed to fetch wasm plugin ctx: ", err) + return 503 + end + + local ok, err = wasm.on_http_response_headers(plugin_ctx) + if not ok then + core.log.error("failed to run wasm plugin: ", err) + return 503 + end +end + + function _M.require(attrs) if not support_wasm then return nil, "need to build APISIX-OpenResty to support wasm" @@ -101,6 +116,9 @@ function _M.require(attrs) mod.access = function (conf, ctx) return access_wrapper(mod, conf, ctx) end + mod.header_filter = function (conf, ctx) + return header_filter_wrapper(mod, conf, ctx) + end -- the returned values need to be the same as the Lua's 'require' return true, mod diff --git a/docs/en/latest/wasm.md b/docs/en/latest/wasm.md index 9606bd8f5b92..8d81d69a2e00 100644 --- a/docs/en/latest/wasm.md +++ b/docs/en/latest/wasm.md @@ -93,4 +93,11 @@ Attributes below can be configured in the plugin: | Name | Type | Requirement | Default | Valid | Description | | --------------------------------------| ------------| -------------- | -------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| conf | string | required | | != "" | the plugin ctx configuration which can be fetched via Proxy WASM SDK | +| conf | string | required | | != "" | the plugin ctx configuration which can be fetched via Proxy WASM SDK | + +Here is the mapping between Proxy WASM callbacks and APISIX's phases: + +* `proxy_on_configure`: run once there is not PluginContext for the new configuration. +For example, when the first request hits the route which has WASM plugin configured. +* `proxy_on_http_request_headers`: run in the access phase. +* `proxy_on_http_response_headers`: run in the header_filter phase. diff --git a/t/wasm/response-rewrite.t b/t/wasm/response-rewrite.t new file mode 100644 index 000000000000..250a07c10fce --- /dev/null +++ b/t/wasm/response-rewrite.t @@ -0,0 +1,94 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +use t::APISIX; + +my $nginx_binary = $ENV{'TEST_NGINX_BINARY'} || 'nginx'; +my $version = eval { `$nginx_binary -V 2>&1` }; + +if ($version !~ m/\/apisix-nginx-module/) { + plan(skip_all => "apisix-nginx-module not installed"); +} else { + plan('no_plan'); +} + +add_block_preprocessor(sub { + my ($block) = @_; + + if ((!defined $block->error_log) && (!defined $block->no_error_log)) { + $block->set_value("no_error_log", "[error]"); + } + + if (!defined $block->request) { + $block->set_value("request", "GET /t"); + } + + my $extra_yaml_config = <<_EOC_; +wasm: + plugins: + - name: wasm-response-rewrite + priority: 7997 + file: t/wasm/response-rewrite/main.go.wasm +_EOC_ + $block->set_value("extra_yaml_config", $extra_yaml_config); +}); + +run_tests(); + +__DATA__ + +=== TEST 1: response rewrite headers +--- config + location /t { + content_by_lua_block { + local t = require("lib.test_admin").test + local code, body = t('/apisix/admin/routes/1', + ngx.HTTP_PUT, + [[{ + "uri": "/hello", + "upstream": { + "type": "roundrobin", + "nodes": { + "127.0.0.1:1980": 1 + } + }, + "plugins": { + "wasm-response-rewrite": { + "conf": "{\"headers\":[{\"name\":\"x-wasm\",\"value\":\"apisix\"}]}" + } + } + }]] + ) + + if code >= 300 then + ngx.status = code + ngx.say(body) + return + end + + ngx.say(body) + } + } +--- response_body +passed + + + +=== TEST 2: hit +--- request +GET /hello +--- response_headers +x-wasm: apisix diff --git a/t/wasm/response-rewrite/main.go b/t/wasm/response-rewrite/main.go new file mode 100644 index 000000000000..1c469aa22d39 --- /dev/null +++ b/t/wasm/response-rewrite/main.go @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" + "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" + + "github.com/valyala/fastjson" +) + +func main() { + proxywasm.SetVMContext(&vmContext{}) +} + +type vmContext struct { + types.DefaultVMContext +} + +func (*vmContext) NewPluginContext(contextID uint32) types.PluginContext { + return &pluginContext{} +} + +type header struct { + Name string + Value string +} + +type pluginContext struct { + types.DefaultPluginContext + Headers []header +} + +func (ctx *pluginContext) OnPluginStart(pluginConfigurationSize int) types.OnPluginStartStatus { + data, err := proxywasm.GetPluginConfiguration() + if err != nil { + proxywasm.LogErrorf("error reading plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + + var p fastjson.Parser + v, err := p.ParseBytes(data) + if err != nil { + proxywasm.LogErrorf("erorr decoding plugin configuration: %v", err) + return types.OnPluginStartStatusFailed + } + headers := v.GetArray("headers") + ctx.Headers = make([]header, len(headers)) + for i, hdr := range headers { + ctx.Headers[i] = header{ + Name: string(hdr.GetStringBytes("name")), + Value: string(hdr.GetStringBytes("value")), + } + } + + return types.OnPluginStartStatusOK +} + +func (ctx *pluginContext) NewHttpContext(contextID uint32) types.HttpContext { + return &httpContext{parent: ctx} +} + +type httpContext struct { + types.DefaultHttpContext + parent *pluginContext +} + +func (ctx *httpContext) OnHttpResponseHeaders(numHeaders int, endOfStream bool) types.Action { + plugin := ctx.parent + for _, hdr := range plugin.Headers { + proxywasm.ReplaceHttpResponseHeader(hdr.Name, hdr.Value) + } + return types.ActionContinue +}