Skip to content

Commit

Permalink
feat: response_rewrite plugin support replace body via origin res body (
Browse files Browse the repository at this point in the history
#109)

* feat: response_rewrite plugin support replace body via origin response body

Co-authored-by: soulbird <zhaothree@gmail.com>
  • Loading branch information
soulbird and soulbird committed Sep 29, 2022
1 parent 8f8d06f commit 57208c8
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 9 deletions.
8 changes: 3 additions & 5 deletions ci/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,12 @@ services:
apisix:

web:
image: mendhak/http-https-echo
environment:
HTTP_PORT: 8888
HTTPS_PORT: 9999
image: openresty/openresty
restart: unless-stopped
volumes:
- ./openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro
ports:
- "8888:8888"
- "9999:9999"
networks:
apisix:

Expand Down
63 changes: 63 additions & 0 deletions ci/openresty/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#
# 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.
#

user root;
worker_processes auto;

pcre_jit on;


error_log logs/error.log info;

pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

# fake server, only for test
server {
listen 8888;

server_tokens off;

location / {
content_by_lua_block {
ngx.say("hello world")
}
more_clear_headers Date;
}
location /echo {
content_by_lua_block {
ngx.req.read_body()
local hdrs = ngx.req.get_headers()
for k, v in pairs(hdrs) do
ngx.header[k] = v
end
ngx.print(ngx.req.get_body_data() or "")
}
}
}
}
58 changes: 55 additions & 3 deletions cmd/go-runner/plugins/response_rewrite.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@
package plugins

import (
"bytes"
"encoding/json"
"fmt"
"regexp"

pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http"
"github.com/apache/apisix-go-plugin-runner/pkg/log"
Expand All @@ -32,6 +35,14 @@ func init() {
}
}

type RegexFilter struct {
Regex string `json:"regex"`
Scope string `json:"scope"`
Replace string `json:"replace"`

regexComplied *regexp.Regexp
}

// ResponseRewrite is a demo to show how to rewrite response data.
type ResponseRewrite struct {
// Embed the default plugin here,
Expand All @@ -43,6 +54,7 @@ type ResponseRewriteConf struct {
Status int `json:"status"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
Filters []RegexFilter `json:"filters"`
}

func (p *ResponseRewrite) Name() string {
Expand All @@ -52,7 +64,18 @@ func (p *ResponseRewrite) Name() string {
func (p *ResponseRewrite) ParseConf(in []byte) (interface{}, error) {
conf := ResponseRewriteConf{}
err := json.Unmarshal(in, &conf)
return conf, err
if err != nil {
return nil, err
}
for i := 0; i < len(conf.Filters); i++ {
if reg, err := regexp.Compile(conf.Filters[i].Regex); err != nil {
return nil, fmt.Errorf("failed to compile regex `%s`: %v",
conf.Filters[i].Regex, err)
} else {
conf.Filters[i].regexComplied = reg
}
}
return conf, nil
}

func (p *ResponseRewrite) ResponseFilter(conf interface{}, w pkgHTTP.Response) {
Expand All @@ -68,10 +91,39 @@ func (p *ResponseRewrite) ResponseFilter(conf interface{}, w pkgHTTP.Response) {
}
}

if len(cfg.Body) == 0 {
body := []byte(cfg.Body)
if len(cfg.Filters) > 0 {
originBody, err := w.ReadBody()
if err != nil {
log.Errorf("failed to read response body: ", err)
return
}
matched := false
for i := 0; i < len(cfg.Filters); i++ {
f := cfg.Filters[i]
found := f.regexComplied.Find(originBody)
if found != nil {
matched = true
if f.Scope == "once" {
originBody = bytes.Replace(originBody, found, []byte(f.Replace), 1)
} else if f.Scope == "global" {
originBody = bytes.ReplaceAll(originBody, found, []byte(f.Replace))
}
}
}
if matched {
body = originBody
goto write
}
// When configuring the Filters field, the Body field will be invalid.
return
}

if len(body) == 0 {
return
}
_, err := w.Write([]byte(cfg.Body))
write:
_, err := w.Write(body)
if err != nil {
log.Errorf("failed to write: %s", err)
}
Expand Down
32 changes: 32 additions & 0 deletions cmd/go-runner/plugins/response_rewrite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,35 @@ func TestResponseRewrite_ConfEmpty(t *testing.T) {
assert.Equal(t, "Go", w.Header().Get("X-Resp-A6-Runner"))
assert.Equal(t, "", conf.(ResponseRewriteConf).Body)
}

func TestResponseRewrite_ReplaceGlobal(t *testing.T) {
in := []byte(`{"filters":[{"regex":"world","scope":"global","replace":"golang"}]}`)
rr := &ResponseRewrite{}
conf, err := rr.ParseConf(in)
assert.Nil(t, err)
assert.Equal(t, 1, len(conf.(ResponseRewriteConf).Filters))

w := pkgHTTPTest.NewRecorder()
w.Code = 200
w.OriginBody = []byte("hello world world")
rr.ResponseFilter(conf, w)
assert.Equal(t, 200, w.StatusCode())
body, _ := ioutil.ReadAll(w.Body)
assert.Equal(t, "hello golang golang", string(body))
}

func TestResponseRewrite_ReplaceOnce(t *testing.T) {
in := []byte(`{"filters":[{"regex":"world","scope":"once","replace":"golang"}]}`)
rr := &ResponseRewrite{}
conf, err := rr.ParseConf(in)
assert.Nil(t, err)
assert.Equal(t, 1, len(conf.(ResponseRewriteConf).Filters))

w := pkgHTTPTest.NewRecorder()
w.Code = 200
w.OriginBody = []byte("hello world world")
rr.ResponseFilter(conf, w)
assert.Equal(t, 200, w.StatusCode())
body, _ := ioutil.ReadAll(w.Body)
assert.Equal(t, "hello golang world", string(body))
}
33 changes: 32 additions & 1 deletion docs/en/latest/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ The following table describes the compatibility between apisix-go-plugin-runner

| apisix-go-plugin-runner | Apache APISIX |
|------------------------:|--------------------------------------:|
| `master` | `>= 2.14.1`, `2.14.1` is recommended. |
| `master` | `master` is recommended. |
| `0.4.0` | `>= 2.14.1`, `2.14.1` is recommended. |
| `0.3.0` | `>= 2.13.0`, `2.13.0` is recommended. |
| `0.2.0` | `>= 2.9.0`, `2.9.0` is recommended. |
Expand Down Expand Up @@ -148,10 +148,19 @@ at the same time by set RespHeader in `pkgHTTP.Request`.
`ResponseFilter` supports rewriting the response during the response phase, we can see an example of its use in the ResponseRewrite plugin:

```go
type RegexFilter struct {
Regex string `json:"regex"`
Scope string `json:"scope"`
Replace string `json:"replace"`

regexComplied *regexp.Regexp
}

type ResponseRewriteConf struct {
Status int `json:"status"`
Headers map[string]string `json:"headers"`
Body string `json:"body"`
Filters []RegexFilter `json:"filters"`
}

func (p *ResponseRewrite) ResponseFilter(conf interface{}, w pkgHTTP.Response) {
Expand All @@ -167,6 +176,28 @@ func (p *ResponseRewrite) ResponseFilter(conf interface{}, w pkgHTTP.Response) {
}
}

body := []byte(cfg.Body)
if len(cfg.Filters) > 0 {
originBody, err := w.ReadBody()

......

for i := 0; i < len(cfg.Filters); i++ {
f := cfg.Filters[i]
found := f.regexComplied.Find(originBody)
if found != nil {
matched = true
if f.Scope == "once" {
originBody = bytes.Replace(originBody, found, []byte(f.Replace), 1)
} else if f.Scope == "global" {
originBody = bytes.ReplaceAll(originBody, found, []byte(f.Replace))
}
}
}

.......

}
if len(cfg.Body) == 0 {
return
}
Expand Down
56 changes: 56 additions & 0 deletions tests/e2e/plugins/plugins_response_rewrite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,61 @@ var _ = ginkgo.Describe("ResponseRewrite Plugin", func() {
"X-Server-Id": "9527",
},
}),
/*
{
....
"filters": [
{
"regex": "world",
"scope": "global",
"replace": "golang"
},
{
"regex": "hello",
"scope": "once",
"replace": "nice"
}
]
"body":"response rewrite"
}
*/
table.Entry("Config APISIX.", tools.HttpTestCase{
Object: tools.GetA6CPExpect(),
Method: http.MethodPut,
Path: "/apisix/admin/routes/1",
Body: `{
"uri":"/echo",
"plugins":{
"ext-plugin-post-resp":{
"conf":[
{
"name":"response-rewrite",
"value":"{\"headers\":{\"X-Server-Id\":\"9527\"},\"filters\":[{\"regex\":\"world\",\"scope\":\"global\",\"replace\":\"golang\"},{\"regex\":\"hello\",\"scope\":\"once\",\"replace\":\"nice\"}],\"body\":\"response rewrite\"}"
}
]
}
},
"upstream":{
"nodes":{
"web:8888":1
},
"type":"roundrobin"
}
}`,
Headers: map[string]string{"X-API-KEY": tools.GetAdminToken()},
ExpectStatusRange: httpexpect.Status2xx,
}),
table.Entry("Should replace response.", tools.HttpTestCase{
Object: tools.GetA6DPExpect(),
Method: http.MethodGet,
Path: "/echo",
Body: "hello hello world world",
ExpectBody: []string{"nice hello golang golang"},
ExpectStatus: http.StatusOK,
ExpectHeaders: map[string]string{
"X-Resp-A6-Runner": "Go",
"X-Server-Id": "9527",
},
}),
)
})

0 comments on commit 57208c8

Please sign in to comment.