You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by "kingluo (via GitHub)" <gi...@apache.org> on 2023/02/01 06:17:45 UTC
[GitHub] [apisix] kingluo opened a new pull request, #8766: feat: Body transformer plugin
kingluo opened a new pull request, #8766:
URL: https://github.com/apache/apisix/pull/8766
### Description
<!-- Please include a summary of the change and which issue is fixed. -->
<!-- Please also include relevant motivation and context. -->
This plugin is used to transform the request or response body from one
format to another format, e.g. JSON to XML.
It depends on xml2lua and lua-resty-template.
Use cases:
* simple SOAP proxy
* generic template-based transform
### Checklist
- [x] I have explained the need for this PR and the problem it solves
- [x] I have explained the changes or the new features added to this PR
- [ ] I have added tests corresponding to this change
- [ ] I have updated the documentation to reflect this change
- [x] I have verified that this change is backward compatible (If not, please discuss on the [APISIX mailing list](https://github.com/apache/apisix/tree/master#community) first)
<!--
Note
1. Mark the PR as draft until it's ready to be reviewed.
2. Always add/update tests for any changes unless you have a good reason.
3. Always update the documentation to reflect the changes made in the PR.
4. Make a new commit to resolve conversations instead of `push -f`.
5. To resolve merge conflicts, merge master instead of rebasing.
6. Use "request review" to notify the reviewer after making changes.
7. Only a reviewer can mark a conversation as resolved.
-->
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] kingluo commented on a diff in pull request #8766: feat: Body transformer plugin
Posted by "kingluo (via GitHub)" <gi...@apache.org>.
kingluo commented on code in PR #8766:
URL: https://github.com/apache/apisix/pull/8766#discussion_r1098248435
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ out._ctx = ctx
+ ok, out = pcall(render, out)
+ if not ok then
+ local err = str_format("%s template rendering: %s", typ, out)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ core.log.info(typ, " body transform output=", out)
+ return out
+end
+
+
+local function set_input_format(conf, typ, ct)
+ if conf[typ].input_format == nil and ct then
+ if ct:find("text/xml") then
+ conf[typ].input_format = "xml"
+ elseif ct:find("application/json") then
+ conf[typ].input_format = "json"
+ end
+ end
+end
+
+
+function _M.rewrite(conf, ctx)
Review Comment:
And this plugin is just doing rewrite job, just like proxy-rewrite plugin.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] spacewander commented on a diff in pull request #8766: feat: Body transformer plugin
Posted by "spacewander (via GitHub)" <gi...@apache.org>.
spacewander commented on code in PR #8766:
URL: https://github.com/apache/apisix/pull/8766#discussion_r1099706074
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
Review Comment:
The solution is not about making luacheck happy, but limiting the scope of the injection. If we don't deregister the methods, it will affect other modules including those not written by us.
Anyway, it is not a good idea to allow the monkey patch of the std lib outside our scope.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] spacewander commented on a diff in pull request #8766: feat: Body transformer plugin
Posted by "spacewander (via GitHub)" <gi...@apache.org>.
spacewander commented on code in PR #8766:
URL: https://github.com/apache/apisix/pull/8766#discussion_r1098118373
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
Review Comment:
Could we register these methods before we use them and deregister once the job is done? So we can avoid affecting other modules.
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
Review Comment:
It seems this code just inserts a new k
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ out._ctx = ctx
+ ok, out = pcall(render, out)
+ if not ok then
+ local err = str_format("%s template rendering: %s", typ, out)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ core.log.info(typ, " body transform output=", out)
+ return out
+end
+
+
+local function set_input_format(conf, typ, ct)
+ if conf[typ].input_format == nil and ct then
+ if ct:find("text/xml") then
+ conf[typ].input_format = "xml"
+ elseif ct:find("application/json") then
+ conf[typ].input_format = "json"
+ end
+ end
+end
+
+
+function _M.rewrite(conf, ctx)
Review Comment:
`rewrite` is only for plugins that require running before other plugins. For example, plugins to select consumers, plugins for tracing, plugins to set real IP, and so on. Most of the other plugin use `access` phase.
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ out._ctx = ctx
+ ok, out = pcall(render, out)
+ if not ok then
+ local err = str_format("%s template rendering: %s", typ, out)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ core.log.info(typ, " body transform output=", out)
+ return out
+end
+
+
+local function set_input_format(conf, typ, ct)
+ if conf[typ].input_format == nil and ct then
+ if ct:find("text/xml") then
+ conf[typ].input_format = "xml"
+ elseif ct:find("application/json") then
+ conf[typ].input_format = "json"
+ end
+ end
+end
+
+
+function _M.rewrite(conf, ctx)
+ if conf.request then
+ local body = core.request.get_body()
+ set_input_format(conf, "request", ctx.var.http_content_type)
+ local out, status, err = transform(conf, body, "request", ctx)
+ if not out then
+ return status, { message = err }
+ end
+ req_set_body_data(out)
+ end
+end
+
+
+function _M.header_filter(conf, ctx)
+ if conf.response then
+ set_input_format(conf, "response", ngx.header.content_type)
+ core.response.clear_header_as_body_modified()
+ end
+end
+
+
+function _M.body_filter(conf, ctx)
+ if conf.response then
+ local body = core.response.hold_body_chunk(ctx)
+ if ngx.arg[2] == false and not body then
Review Comment:
```suggestion
if not body then
```
hold_body_chunk returns a body only when ngx.arg[2] is true
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ out._ctx = ctx
+ ok, out = pcall(render, out)
+ if not ok then
+ local err = str_format("%s template rendering: %s", typ, out)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ core.log.info(typ, " body transform output=", out)
+ return out
+end
+
+
+local function set_input_format(conf, typ, ct)
+ if conf[typ].input_format == nil and ct then
Review Comment:
The conf is shared between reqs. Is there a race if the previous request set the input format different from the current one?
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 500, err
Review Comment:
```suggestion
return nil, 503, err
```
`500` is reserved to indicate bug in APISIX
##########
t/plugin/body-transformer.t:
##########
@@ -0,0 +1,658 @@
+#
+# 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 'no_plan';
+
+no_long_string();
+no_shuffle();
+no_root_location();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: simulate simple SOAP proxy
+--- config
+location /demo {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local body = core.request.get_body()
+ local xml2lua = require("xml2lua")
+ local xmlhandler = require("xmlhandler.tree")
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ parser:parse(body)
+ --core.log.error(core.json.encode(handler.root))
+
+ ngx.print(string.format([[
+<SOAP-ENV:Envelope
+ xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+ <SOAP-ENV:Header/>
+ <SOAP-ENV:Body>
+ <ns2:getCountryResponse
+ xmlns:ns2="http://spring.io/guides/gs-producing-web-service">
+ <ns2:country>
+ <ns2:name>%s</ns2:name>
+ <ns2:population>46704314</ns2:population>
+ <ns2:capital>Madrid</ns2:capital>
+ <ns2:currency>EUR</ns2:currency>
+ </ns2:country>
+ </ns2:getCountryResponse>
+ </SOAP-ENV:Body>
+</SOAP-ENV:Envelope>
+ ]], handler.root["soap-env:Envelope"]["soap-env:Body"]["ns0:getCountryRequest"]["ns0:name"]))
+ }
+}
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+
+ local req_template = ngx.encode_base64[[
+<?xml version="1.0"?>
+<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap-env:Body>
+ <ns0:getCountryRequest xmlns:ns0="http://spring.io/guides/gs-producing-web-service">
+ <ns0:name>{{name:escape_xml()}}</ns0:name>
+ </ns0:getCountryRequest>
+ </soap-env:Body>
+</soap-env:Envelope>
+ ]]
+
+ local rsp_template = ngx.encode_base64[[
+{% if Envelope.Body.Fault == nil then %}
+{
+ "status":"{{_ctx.var.status}}",
+ "currency":"{{Envelope.Body.getCountryResponse.country.currency}}",
+ "population":{{Envelope.Body.getCountryResponse.country.population}},
+ "capital":"{{Envelope.Body.getCountryResponse.country.capital}}",
+ "name":"{{Envelope.Body.getCountryResponse.country.name}}"
+}
+{% else %}
+{
+ "message":"{{Envelope.Body.Fault.faultstring[1]:escape_json()}}",
+ "code":"{{Envelope.Body.Fault.faultcode}}"
+ {% if Envelope.Body.Fault.faultactor ~= nil then %}
+ , "actor":"{{Envelope.Body.Fault.faultactor}}"
+ {% end %}
+}
+{% end %}
+ ]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/ws",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "request": {
+ "template": "%s"
+ },
+ "response": {
+ "input_format": "xml",
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template, rsp_template, ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/ws"
+ local body = [[{"name": "Spain"}]]
+ local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}}
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, opt)
+ if not res then
+ ngx.say(err)
Review Comment:
We should check `--- response_body` to ensure no err happens. Please update other similar places.
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ out._ctx = ctx
+ ok, out = pcall(render, out)
Review Comment:
Better to avoid overriding a variable with a value in different type
##########
docs/en/latest/plugins/body-transformer.md:
##########
@@ -0,0 +1,277 @@
+---
+title: body-transformer
+keywords:
+ - APISIX
+ - Plugin
+ - BODY TRANSFORMER
+ - body-transformer
+description: This document contains information about the Apache APISIX body-transformer Plugin.
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+## Description
+
+This plugin is used to transform the request and/or response body from one
+format to another format, e.g. JSON to XML.
+
+Use cases:
+
+- simple SOAP proxy
+- generic template-based transform, e.g. JSON to JSON, JSON to HTML, XML to YAML
+
+## Attributes
+
+| Name | Type | Required | Description |
+| ----------- | ----------- | ----------- | ----------- |
+| `request` | object | False | request body transformation configuration |
+| `request.input_format` | string | False | request body original format, if not specified, it would be determined from `Content-Type` header. |
+| `request.template` | string | False | request body transformation template |
Review Comment:
The template is required
##########
t/plugin/body-transformer.t:
##########
@@ -0,0 +1,658 @@
+#
+# 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 'no_plan';
+
+no_long_string();
+no_shuffle();
+no_root_location();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: simulate simple SOAP proxy
+--- config
+location /demo {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local body = core.request.get_body()
+ local xml2lua = require("xml2lua")
+ local xmlhandler = require("xmlhandler.tree")
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ parser:parse(body)
+ --core.log.error(core.json.encode(handler.root))
+
+ ngx.print(string.format([[
+<SOAP-ENV:Envelope
+ xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+ <SOAP-ENV:Header/>
+ <SOAP-ENV:Body>
+ <ns2:getCountryResponse
+ xmlns:ns2="http://spring.io/guides/gs-producing-web-service">
+ <ns2:country>
+ <ns2:name>%s</ns2:name>
+ <ns2:population>46704314</ns2:population>
+ <ns2:capital>Madrid</ns2:capital>
+ <ns2:currency>EUR</ns2:currency>
+ </ns2:country>
+ </ns2:getCountryResponse>
+ </SOAP-ENV:Body>
+</SOAP-ENV:Envelope>
+ ]], handler.root["soap-env:Envelope"]["soap-env:Body"]["ns0:getCountryRequest"]["ns0:name"]))
+ }
+}
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+
+ local req_template = ngx.encode_base64[[
+<?xml version="1.0"?>
+<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap-env:Body>
+ <ns0:getCountryRequest xmlns:ns0="http://spring.io/guides/gs-producing-web-service">
+ <ns0:name>{{name:escape_xml()}}</ns0:name>
+ </ns0:getCountryRequest>
+ </soap-env:Body>
+</soap-env:Envelope>
+ ]]
+
+ local rsp_template = ngx.encode_base64[[
+{% if Envelope.Body.Fault == nil then %}
+{
+ "status":"{{_ctx.var.status}}",
+ "currency":"{{Envelope.Body.getCountryResponse.country.currency}}",
+ "population":{{Envelope.Body.getCountryResponse.country.population}},
+ "capital":"{{Envelope.Body.getCountryResponse.country.capital}}",
+ "name":"{{Envelope.Body.getCountryResponse.country.name}}"
+}
+{% else %}
+{
+ "message":"{{Envelope.Body.Fault.faultstring[1]:escape_json()}}",
+ "code":"{{Envelope.Body.Fault.faultcode}}"
+ {% if Envelope.Body.Fault.faultactor ~= nil then %}
+ , "actor":"{{Envelope.Body.Fault.faultactor}}"
+ {% end %}
+}
+{% end %}
+ ]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/ws",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "request": {
+ "template": "%s"
+ },
+ "response": {
+ "input_format": "xml",
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template, rsp_template, ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/ws"
+ local body = [[{"name": "Spain"}]]
+ local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}}
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, opt)
+ if not res then
+ ngx.say(err)
+ return
+ end
+ assert(res.status == 200)
+ local data1 = core.json.decode(res.body)
+ local data2 = core.json.decode[[{"status":"200","currency":"EUR","population":46704314,"capital":"Madrid","name":"Spain"}]]
+ assert(core.json.stably_encode(data1), core.json.stably_encode(data2))
+ }
+ }
+
+
+
+=== TEST 2: test JSON-to-JSON
+--- config
+ location /demo {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local body = core.request.get_body()
+ local data = core.json.decode(body)
+ assert(data.foo == "hello world" and data.bar == 30)
+ }
+ }
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+ local core = require("apisix.core")
+ local req_template = [[{"foo":"{{name .. " world"}}","bar":{{age+10}}}]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/foobar",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "request": {
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template:gsub('"', '\\"'), ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/foobar"
+ local body = [[{"name":"hello","age":20}]]
+ local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}}
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, opt)
+ if not res then
+ ngx.say(err)
+ return
+ end
+ assert(res.status == 200)
+ }
+ }
+
+
+
+=== TEST 3: specify wrong input_format
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+ local core = require("apisix.core")
+ local req_template = [[{"foo":"{{name .. " world"}}","bar":{{age+10}}}]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/foobar",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "request": {
+ "input_format": "xml",
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template:gsub('"', '\\"'), ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/foobar"
+ local body = [[{"name":"hello","age":20}]]
+ local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}}
+ local httpc = http.new()
+ httpc:request_uri(uri, opt)
+ }
+ }
+--- error_log
+Error Parsing XML
+
+
+
+=== TEST 4: invalid reference in template
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+ local core = require("apisix.core")
+ local req_template = [[{"foo":"{{name() .. " world"}}","bar":{{age+10}}}]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/foobar",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "request": {
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template:gsub('"', '\\"'), ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/foobar"
+ local body = [[{"name":"hello","age":20}]]
+ local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}}
+ local httpc = http.new()
+ httpc:request_uri(uri, opt)
+ }
+ }
+--- error_log
+attempt to call global 'name' (a string value)
Review Comment:
Please add log prefix to ensure it comes from an expected log
##########
t/plugin/body-transformer.t:
##########
@@ -0,0 +1,658 @@
+#
+# 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 'no_plan';
+
+no_long_string();
+no_shuffle();
+no_root_location();
+
+add_block_preprocessor(sub {
+ my ($block) = @_;
+
+ if (!$block->request) {
+ $block->set_value("request", "GET /t");
+ }
+});
+
+run_tests;
+
+__DATA__
+
+=== TEST 1: simulate simple SOAP proxy
+--- config
+location /demo {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local body = core.request.get_body()
+ local xml2lua = require("xml2lua")
+ local xmlhandler = require("xmlhandler.tree")
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ parser:parse(body)
+ --core.log.error(core.json.encode(handler.root))
+
+ ngx.print(string.format([[
+<SOAP-ENV:Envelope
+ xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
+ <SOAP-ENV:Header/>
+ <SOAP-ENV:Body>
+ <ns2:getCountryResponse
+ xmlns:ns2="http://spring.io/guides/gs-producing-web-service">
+ <ns2:country>
+ <ns2:name>%s</ns2:name>
+ <ns2:population>46704314</ns2:population>
+ <ns2:capital>Madrid</ns2:capital>
+ <ns2:currency>EUR</ns2:currency>
+ </ns2:country>
+ </ns2:getCountryResponse>
+ </SOAP-ENV:Body>
+</SOAP-ENV:Envelope>
+ ]], handler.root["soap-env:Envelope"]["soap-env:Body"]["ns0:getCountryRequest"]["ns0:name"]))
+ }
+}
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+
+ local req_template = ngx.encode_base64[[
+<?xml version="1.0"?>
+<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">
+ <soap-env:Body>
+ <ns0:getCountryRequest xmlns:ns0="http://spring.io/guides/gs-producing-web-service">
+ <ns0:name>{{name:escape_xml()}}</ns0:name>
+ </ns0:getCountryRequest>
+ </soap-env:Body>
+</soap-env:Envelope>
+ ]]
+
+ local rsp_template = ngx.encode_base64[[
+{% if Envelope.Body.Fault == nil then %}
+{
+ "status":"{{_ctx.var.status}}",
+ "currency":"{{Envelope.Body.getCountryResponse.country.currency}}",
+ "population":{{Envelope.Body.getCountryResponse.country.population}},
+ "capital":"{{Envelope.Body.getCountryResponse.country.capital}}",
+ "name":"{{Envelope.Body.getCountryResponse.country.name}}"
+}
+{% else %}
+{
+ "message":"{{Envelope.Body.Fault.faultstring[1]:escape_json()}}",
+ "code":"{{Envelope.Body.Fault.faultcode}}"
+ {% if Envelope.Body.Fault.faultactor ~= nil then %}
+ , "actor":"{{Envelope.Body.Fault.faultactor}}"
+ {% end %}
+}
+{% end %}
+ ]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/ws",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "request": {
+ "template": "%s"
+ },
+ "response": {
+ "input_format": "xml",
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template, rsp_template, ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/ws"
+ local body = [[{"name": "Spain"}]]
+ local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}}
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, opt)
+ if not res then
+ ngx.say(err)
+ return
+ end
+ assert(res.status == 200)
+ local data1 = core.json.decode(res.body)
+ local data2 = core.json.decode[[{"status":"200","currency":"EUR","population":46704314,"capital":"Madrid","name":"Spain"}]]
+ assert(core.json.stably_encode(data1), core.json.stably_encode(data2))
+ }
+ }
+
+
+
+=== TEST 2: test JSON-to-JSON
+--- config
+ location /demo {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local body = core.request.get_body()
+ local data = core.json.decode(body)
+ assert(data.foo == "hello world" and data.bar == 30)
+ }
+ }
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+ local core = require("apisix.core")
+ local req_template = [[{"foo":"{{name .. " world"}}","bar":{{age+10}}}]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/foobar",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "request": {
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template:gsub('"', '\\"'), ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/foobar"
+ local body = [[{"name":"hello","age":20}]]
+ local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}}
+ local httpc = http.new()
+ local res, err = httpc:request_uri(uri, opt)
+ if not res then
+ ngx.say(err)
+ return
+ end
+ assert(res.status == 200)
+ }
+ }
+
+
+
+=== TEST 3: specify wrong input_format
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+ local core = require("apisix.core")
+ local req_template = [[{"foo":"{{name .. " world"}}","bar":{{age+10}}}]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/foobar",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "request": {
+ "input_format": "xml",
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template:gsub('"', '\\"'), ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/foobar"
+ local body = [[{"name":"hello","age":20}]]
+ local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}}
+ local httpc = http.new()
+ httpc:request_uri(uri, opt)
+ }
+ }
+--- error_log
+Error Parsing XML
+
+
+
+=== TEST 4: invalid reference in template
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+ local core = require("apisix.core")
+ local req_template = [[{"foo":"{{name() .. " world"}}","bar":{{age+10}}}]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/foobar",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo"
+ },
+ "body-transformer": {
+ "request": {
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template:gsub('"', '\\"'), ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/foobar"
+ local body = [[{"name":"hello","age":20}]]
+ local opt = {method = "POST", body = body, headers = {["Content-Type"] = "application/json"}}
+ local httpc = http.new()
+ httpc:request_uri(uri, opt)
+ }
+ }
+--- error_log
+attempt to call global 'name' (a string value)
+
+
+
+=== TEST 5: generate request body from scratch
+--- config
+ location /demo {
+ content_by_lua_block {
+ local core = require("apisix.core")
+ local body = core.request.get_body()
+ local data = core.json.decode(body)
+ assert(data.foo == "hello world")
+ }
+ }
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin")
+ local core = require("apisix.core")
+ local req_template = [[{
+ "foo":"{{_ctx.var.arg_name .. " world"}}"
+ }]]
+
+ local code, body = t.test('/apisix/admin/routes/1',
+ ngx.HTTP_PUT,
+ string.format([[{
+ "uri": "/foobar",
+ "plugins": {
+ "proxy-rewrite": {
+ "uri": "/demo",
+ "method": "POST"
+ },
+ "body-transformer": {
+ "request": {
+ "template": "%s"
+ }
+ }
+ },
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {
+ "127.0.0.1:%d": 1
+ }
+ }
+ }]], req_template:gsub('"', '\\"'), ngx.var.server_port)
+ )
+
+ if code >= 300 then
+ ngx.status = code
+ return
+ end
+ ngx.sleep(0.5)
+
+ local core = require("apisix.core")
+ local http = require("resty.http")
+ local uri = "http://127.0.0.1:" .. ngx.var.server_port .. "/foobar?name=hello"
+ local opt = {method = "GET"}
+ local httpc = http.new()
+ httpc:request_uri(uri, opt)
Review Comment:
We need to check the res here?
##########
docs/en/latest/plugins/body-transformer.md:
##########
@@ -0,0 +1,277 @@
+---
+title: body-transformer
+keywords:
+ - APISIX
+ - Plugin
+ - BODY TRANSFORMER
+ - body-transformer
+description: This document contains information about the Apache APISIX body-transformer Plugin.
+---
+
+<!--
+#
+# 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.
+#
+-->
+
+## Description
+
+This plugin is used to transform the request and/or response body from one
+format to another format, e.g. JSON to XML.
+
+Use cases:
+
+- simple SOAP proxy
+- generic template-based transform, e.g. JSON to JSON, JSON to HTML, XML to YAML
+
+## Attributes
+
+| Name | Type | Required | Description |
+| ----------- | ----------- | ----------- | ----------- |
+| `request` | object | False | request body transformation configuration |
+| `request.input_format` | string | False | request body original format, if not specified, it would be determined from `Content-Type` header. |
+| `request.template` | string | False | request body transformation template |
+| `response` | object | False | response body transformation configuration |
+| `response.input_format` | string | False | response body original format |
+| `response.template` | string | False | response body transformation template, if not specified, it would be determined from `Content-Type` header. |
Review Comment:
Do you mean determining the input_format from `Content-Type` header?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] kingluo commented on a diff in pull request #8766: feat: Body transformer plugin
Posted by "kingluo (via GitHub)" <gi...@apache.org>.
kingluo commented on code in PR #8766:
URL: https://github.com/apache/apisix/pull/8766#discussion_r1098241831
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ out._ctx = ctx
+ ok, out = pcall(render, out)
+ if not ok then
+ local err = str_format("%s template rendering: %s", typ, out)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ core.log.info(typ, " body transform output=", out)
+ return out
+end
+
+
+local function set_input_format(conf, typ, ct)
+ if conf[typ].input_format == nil and ct then
+ if ct:find("text/xml") then
+ conf[typ].input_format = "xml"
+ elseif ct:find("application/json") then
+ conf[typ].input_format = "json"
+ end
+ end
+end
+
+
+function _M.rewrite(conf, ctx)
Review Comment:
This plugin needs to use original content-type header before proxy-rewrite plugin overrrdes it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] kingluo commented on a diff in pull request #8766: feat: Body transformer plugin
Posted by "kingluo (via GitHub)" <gi...@apache.org>.
kingluo commented on code in PR #8766:
URL: https://github.com/apache/apisix/pull/8766#discussion_r1098378327
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
Review Comment:
luacheck has no inline option to depress "setting undefine field of global" warning. But depress the whole lua file is not proper. And `escape_*` has no conflict so far, so I think it's ok to use these names.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] spacewander commented on a diff in pull request #8766: feat: Body transformer plugin
Posted by "spacewander (via GitHub)" <gi...@apache.org>.
spacewander commented on code in PR #8766:
URL: https://github.com/apache/apisix/pull/8766#discussion_r1101000894
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,206 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+local function escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+local function escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ local newk = k:match(".*:(.*)")
+ if newk then
+ tbl[newk] = v
+ tbl[k] = nil
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 503, err
+ end
+
+ out._ctx = ctx
+ out._escape_xml = escape_xml
+ out._escape_json = escape_json
+ local ok, render_out = pcall(render, out)
+ if not ok then
+ local err = str_format("%s template rendering: %s", typ, render_out)
+ core.log.error(err)
+ return nil, 503, err
+ end
+
+ core.log.info(typ, " body transform output=", render_out)
+ return render_out
+end
+
+
+local function set_input_format(conf, typ, ct)
+ if conf[typ].input_format == nil and ct then
+ if ct:find("text/xml") then
+ conf[typ].input_format = "xml"
+ elseif ct:find("application/json") then
+ conf[typ].input_format = "json"
+ end
+ end
+end
+
+
+function _M.rewrite(conf, ctx)
+ if conf.request then
+ conf = core.table.deepcopy(conf)
+ ctx.body_transformer_conf = conf
+ local body = core.request.get_body()
+ set_input_format(conf, "request", ctx.var.http_content_type)
+ local out, status, err = transform(conf, body, "request", ctx)
+ if not out then
+ return status, { message = err }
+ end
+ req_set_body_data(out)
+ end
+end
+
+
+function _M.header_filter(conf, ctx)
+ if conf.response then
+ if not ctx.body_transformer_conf then
+ conf = core.table.deepcopy(conf)
+ ctx.body_transformer_conf = conf
+ end
+ set_input_format(conf, "response", ngx.header.content_type)
+ core.response.clear_header_as_body_modified()
+ end
+end
+
+
+function _M.body_filter(_, ctx)
+ local conf = ctx.body_transformer_conf
+ if conf.response then
Review Comment:
It seems if neither conf.request nor conf.response are given, the conf will be nil here.
We should check if one of them is configured at least in check_schema.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] spacewander merged pull request #8766: feat: Body transformer plugin
Posted by "spacewander (via GitHub)" <gi...@apache.org>.
spacewander merged PR #8766:
URL: https://github.com/apache/apisix/pull/8766
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] spacewander commented on a diff in pull request #8766: feat: Body transformer plugin
Posted by "spacewander (via GitHub)" <gi...@apache.org>.
spacewander commented on code in PR #8766:
URL: https://github.com/apache/apisix/pull/8766#discussion_r1099707593
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ out._ctx = ctx
+ ok, out = pcall(render, out)
+ if not ok then
+ local err = str_format("%s template rendering: %s", typ, out)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ core.log.info(typ, " body transform output=", out)
+ return out
+end
+
+
+local function set_input_format(conf, typ, ct)
+ if conf[typ].input_format == nil and ct then
+ if ct:find("text/xml") then
+ conf[typ].input_format = "xml"
+ elseif ct:find("application/json") then
+ conf[typ].input_format = "json"
+ end
+ end
+end
+
+
+function _M.rewrite(conf, ctx)
Review Comment:
We used to have a discussion to unify the "rewrite/access" methods in 2020.
Since this is not the only plugin that has inconsistent "rewrite/access" methods, and you insist on it, I am OK to accept it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] kingluo commented on a diff in pull request #8766: feat: Body transformer plugin
Posted by "kingluo (via GitHub)" <gi...@apache.org>.
kingluo commented on code in PR #8766:
URL: https://github.com/apache/apisix/pull/8766#discussion_r1099710089
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
Review Comment:
Yes, I know. But setting methods on demand on the global object will make luacheck warns: `setting undefined field escape_xml of global string`, which could not be depressed via inline comment, so ignore the whole file in `.luacheckrc` instead?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
[GitHub] [apisix] kingluo commented on a diff in pull request #8766: feat: Body transformer plugin
Posted by "kingluo (via GitHub)" <gi...@apache.org>.
kingluo commented on code in PR #8766:
URL: https://github.com/apache/apisix/pull/8766#discussion_r1098240320
##########
apisix/plugins/body-transformer.lua:
##########
@@ -0,0 +1,198 @@
+--
+-- 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.
+--
+local core = require("apisix.core")
+local xml2lua = require("xml2lua")
+local xmlhandler = require("xmlhandler.tree")
+local template = require("resty.template")
+local ngx = ngx
+local decode_base64 = ngx.decode_base64
+local req_set_body_data = ngx.req.set_body_data
+local str_format = string.format
+local type = type
+local pcall = pcall
+local pairs = pairs
+
+
+local transform_schema = {
+ type = "object",
+ properties = {
+ input_format = { type = "string", enum = {"xml", "json"} },
+ template = { type = "string" },
+ },
+ required = {"template"},
+}
+
+local schema = {
+ type = "object",
+ properties = {
+ request = transform_schema,
+ response = transform_schema,
+ },
+}
+
+
+local _M = {
+ version = 0.1,
+ priority = 1080,
+ name = "body-transformer",
+ schema = schema,
+}
+
+
+-- luacheck: ignore
+function string.escape_xml(s)
+ return s:gsub("&", "&")
+ :gsub("<", "<")
+ :gsub(">", ">")
+ :gsub("'", "'")
+ :gsub('"', """)
+end
+
+
+-- luacheck: ignore
+function string.escape_json(s)
+ return core.json.encode(s)
+end
+
+
+local function remove_namespace(tbl)
+ for k, v in pairs(tbl) do
+ if type(k) == "string" then
+ k = k:match(".*:(.*)")
+ if k then
+ tbl[k] = v
+ end
+ if type(v) == "table" then
+ remove_namespace(v)
+ end
+ end
+ end
+ return tbl
+end
+
+
+local decoders = {
+ xml = function(data)
+ local handler = xmlhandler:new()
+ local parser = xml2lua.parser(handler)
+ local ok, err = pcall(parser.parse, parser, data)
+ if ok then
+ return remove_namespace(handler.root)
+ else
+ return nil, err
+ end
+ end,
+ json = function(data)
+ return core.json.decode(data)
+ end,
+}
+
+
+function _M.check_schema(conf)
+ return core.schema.check(schema, conf)
+end
+
+
+local function transform(conf, body, typ, ctx)
+ local out = {_body = body}
+ if body then
+ local err
+ local format = conf[typ].input_format
+ if format then
+ out, err = decoders[format](body)
+ if not out then
+ err = str_format("%s body decode: %s", typ, err)
+ core.log.error(err, ", body=", body)
+ return nil, 400, err
+ end
+ end
+ end
+
+ local text = conf[typ].template
+ text = decode_base64(text) or text
+ local ok, render = pcall(template.compile, text)
+ if not ok then
+ local err = render
+ err = str_format("%s template compile: %s", typ, err)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ out._ctx = ctx
+ ok, out = pcall(render, out)
+ if not ok then
+ local err = str_format("%s template rendering: %s", typ, out)
+ core.log.error(err)
+ return nil, 500, err
+ end
+
+ core.log.info(typ, " body transform output=", out)
+ return out
+end
+
+
+local function set_input_format(conf, typ, ct)
+ if conf[typ].input_format == nil and ct then
+ if ct:find("text/xml") then
+ conf[typ].input_format = "xml"
+ elseif ct:find("application/json") then
+ conf[typ].input_format = "json"
+ end
+ end
+end
+
+
+function _M.rewrite(conf, ctx)
+ if conf.request then
+ local body = core.request.get_body()
+ set_input_format(conf, "request", ctx.var.http_content_type)
+ local out, status, err = transform(conf, body, "request", ctx)
+ if not out then
+ return status, { message = err }
+ end
+ req_set_body_data(out)
+ end
+end
+
+
+function _M.header_filter(conf, ctx)
+ if conf.response then
+ set_input_format(conf, "response", ngx.header.content_type)
+ core.response.clear_header_as_body_modified()
+ end
+end
+
+
+function _M.body_filter(conf, ctx)
+ if conf.response then
+ local body = core.response.hold_body_chunk(ctx)
+ if ngx.arg[2] == false and not body then
Review Comment:
No, this plugiin needs to handle `body=nil` case.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: notifications-unsubscribe@apisix.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org