Skip to content

Commit a265bcd

Browse files
authored
feat: support request body rewrite #127 (#151)
1 parent f049304 commit a265bcd

File tree

11 files changed

+307
-8
lines changed

11 files changed

+307
-8
lines changed

.github/workflows/lint.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ jobs:
3232
steps:
3333
- uses: actions/checkout@v2
3434
- name: setup go
35-
uses: actions/setup-go@v1
35+
uses: actions/setup-go@v4
3636
with:
37-
go-version: '1.15'
37+
go-version: '1.17'
3838

3939
- name: Download golangci-lint
4040
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.39.0

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ release/
77
coverage.txt
88
logs/
99
*.svg
10+
.vscode
11+
go.work
12+
go.work.sum

cmd/go-runner/plugins/limit_req.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// (the "License"); you may not use this file except in compliance with
66
// the License. You may obtain a copy of the License at
77
//
8-
// http://www.apache.org/licenses/LICENSE-2.0
8+
// http://www.apache.org/licenses/LICENSE-2.0
99
//
1010
// Unless required by applicable law or agreed to in writing, software
1111
// distributed under the License is distributed on an "AS IS" BASIS,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package plugins
19+
20+
import (
21+
"encoding/json"
22+
"net/http"
23+
24+
pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http"
25+
"github.com/apache/apisix-go-plugin-runner/pkg/log"
26+
"github.com/apache/apisix-go-plugin-runner/pkg/plugin"
27+
)
28+
29+
const requestBodyRewriteName = "request-body-rewrite"
30+
31+
func init() {
32+
if err := plugin.RegisterPlugin(&RequestBodyRewrite{}); err != nil {
33+
log.Fatalf("failed to register plugin %s: %s", requestBodyRewriteName, err.Error())
34+
}
35+
}
36+
37+
type RequestBodyRewrite struct {
38+
plugin.DefaultPlugin
39+
}
40+
41+
type RequestBodyRewriteConfig struct {
42+
NewBody string `json:"new_body"`
43+
}
44+
45+
func (*RequestBodyRewrite) Name() string {
46+
return requestBodyRewriteName
47+
}
48+
49+
func (p *RequestBodyRewrite) ParseConf(in []byte) (interface{}, error) {
50+
conf := RequestBodyRewriteConfig{}
51+
err := json.Unmarshal(in, &conf)
52+
if err != nil {
53+
log.Errorf("failed to parse config for plugin %s: %s", p.Name(), err.Error())
54+
}
55+
return conf, err
56+
}
57+
58+
func (*RequestBodyRewrite) RequestFilter(conf interface{}, _ http.ResponseWriter, r pkgHTTP.Request) {
59+
newBody := conf.(RequestBodyRewriteConfig).NewBody
60+
if newBody == "" {
61+
return
62+
}
63+
r.SetBody([]byte(newBody))
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package plugins
19+
20+
import (
21+
"context"
22+
"net"
23+
"net/http"
24+
"net/url"
25+
"testing"
26+
27+
pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http"
28+
"github.com/stretchr/testify/require"
29+
)
30+
31+
func TestRequestBodyRewrite_ParseConf(t *testing.T) {
32+
testCases := []struct {
33+
name string
34+
in []byte
35+
expect string
36+
wantErr bool
37+
}{
38+
{
39+
"happy path",
40+
[]byte(`{"new_body":"hello"}`),
41+
"hello",
42+
false,
43+
},
44+
{
45+
"empty conf",
46+
[]byte(``),
47+
"",
48+
true,
49+
},
50+
{
51+
"empty body",
52+
[]byte(`{"new_body":""}`),
53+
"",
54+
false,
55+
},
56+
}
57+
58+
for _, tc := range testCases {
59+
t.Run(tc.name, func(t *testing.T) {
60+
p := new(RequestBodyRewrite)
61+
conf, err := p.ParseConf(tc.in)
62+
if tc.wantErr {
63+
require.Error(t, err)
64+
} else {
65+
require.NoError(t, err)
66+
}
67+
require.Equal(t, tc.expect, conf.(RequestBodyRewriteConfig).NewBody)
68+
})
69+
}
70+
}
71+
72+
func TestRequestBodyRewrite_RequestFilter(t *testing.T) {
73+
req := &mockHTTPRequest{body: []byte("hello")}
74+
p := new(RequestBodyRewrite)
75+
conf, err := p.ParseConf([]byte(`{"new_body":"See ya"}`))
76+
require.NoError(t, err)
77+
p.RequestFilter(conf, nil, req)
78+
require.Equal(t, []byte("See ya"), req.body)
79+
}
80+
81+
// mockHTTPRequest implements pkgHTTP.Request
82+
type mockHTTPRequest struct {
83+
body []byte
84+
}
85+
86+
func (r *mockHTTPRequest) SetBody(body []byte) {
87+
r.body = body
88+
}
89+
90+
func (*mockHTTPRequest) Args() url.Values {
91+
panic("unimplemented")
92+
}
93+
94+
func (*mockHTTPRequest) Body() ([]byte, error) {
95+
panic("unimplemented")
96+
}
97+
98+
func (*mockHTTPRequest) Context() context.Context {
99+
panic("unimplemented")
100+
}
101+
102+
func (*mockHTTPRequest) Header() pkgHTTP.Header {
103+
panic("unimplemented")
104+
}
105+
106+
func (*mockHTTPRequest) ID() uint32 {
107+
panic("unimplemented")
108+
}
109+
110+
func (*mockHTTPRequest) Method() string {
111+
panic("unimplemented")
112+
}
113+
114+
func (*mockHTTPRequest) Path() []byte {
115+
panic("unimplemented")
116+
}
117+
118+
func (*mockHTTPRequest) RespHeader() http.Header {
119+
panic("unimplemented")
120+
}
121+
122+
func (*mockHTTPRequest) SetPath([]byte) {
123+
panic("unimplemented")
124+
}
125+
126+
func (*mockHTTPRequest) SrcIP() net.IP {
127+
panic("unimplemented")
128+
}
129+
130+
func (*mockHTTPRequest) Var(string) ([]byte, error) {
131+
panic("unimplemented")
132+
}

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.15
44

55
require (
66
github.com/ReneKroon/ttlcache/v2 v2.4.0
7-
github.com/api7/ext-plugin-proto v0.6.0
7+
github.com/api7/ext-plugin-proto v0.6.1
88
github.com/google/flatbuffers v2.0.0+incompatible
99
github.com/spf13/cobra v1.2.1
1010
github.com/stretchr/testify v1.7.0

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ github.com/ReneKroon/ttlcache/v2 v2.4.0 h1:KywGhjik+ZFTDXMNLiPECSzmdx2yNvAlDNKES
4343
github.com/ReneKroon/ttlcache/v2 v2.4.0/go.mod h1:zbo6Pv/28e21Z8CzzqgYRArQYGYtjONRxaAKGxzQvG4=
4444
github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
4545
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
46-
github.com/api7/ext-plugin-proto v0.6.0 h1:xmgcKwWRiM9EpBIs1wYJ7Ife/YnLl4IL2NEy4417g60=
47-
github.com/api7/ext-plugin-proto v0.6.0/go.mod h1:8dbdAgCESeqwZ0IXirbjLbshEntmdrAX3uet+LW3jVU=
46+
github.com/api7/ext-plugin-proto v0.6.1 h1:eQN0oHacL97ezVGWVmsRigt+ClcpgjipUq0rmW8BG4g=
47+
github.com/api7/ext-plugin-proto v0.6.1/go.mod h1:8dbdAgCESeqwZ0IXirbjLbshEntmdrAX3uet+LW3jVU=
4848
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
4949
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
5050
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=

internal/http/request.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,10 @@ func (r *Request) Body() ([]byte, error) {
189189
return v, nil
190190
}
191191

192+
func (r *Request) SetBody(body []byte) {
193+
r.body = body
194+
}
195+
192196
func (r *Request) Reset() {
193197
defer r.cancel()
194198
r.path = nil
@@ -205,7 +209,7 @@ func (r *Request) Reset() {
205209
}
206210

207211
func (r *Request) FetchChanges(id uint32, builder *flatbuffers.Builder) bool {
208-
if r.path == nil && r.hdr == nil && r.args == nil && r.respHdr == nil {
212+
if !r.hasChanges() {
209213
return false
210214
}
211215

@@ -214,6 +218,11 @@ func (r *Request) FetchChanges(id uint32, builder *flatbuffers.Builder) bool {
214218
path = builder.CreateByteString(r.path)
215219
}
216220

221+
var body flatbuffers.UOffsetT
222+
if r.body != nil {
223+
body = builder.CreateByteVector(r.body)
224+
}
225+
217226
var hdrVec, respHdrVec flatbuffers.UOffsetT
218227
if r.hdr != nil {
219228
hdrs := []flatbuffers.UOffsetT{}
@@ -314,6 +323,9 @@ func (r *Request) FetchChanges(id uint32, builder *flatbuffers.Builder) bool {
314323
if path > 0 {
315324
hrc.RewriteAddPath(builder, path)
316325
}
326+
if body > 0 {
327+
hrc.RewriteAddBody(builder, body)
328+
}
317329
if hdrVec > 0 {
318330
hrc.RewriteAddHeaders(builder, hdrVec)
319331
}
@@ -346,6 +358,11 @@ func (r *Request) Context() context.Context {
346358
return context.Background()
347359
}
348360

361+
func (r *Request) hasChanges() bool {
362+
return r.path != nil || r.hdr != nil ||
363+
r.args != nil || r.respHdr != nil || r.body != nil
364+
}
365+
349366
func (r *Request) askExtraInfo(builder *flatbuffers.Builder,
350367
infoType ei.Info, info flatbuffers.UOffsetT) ([]byte, error) {
351368

internal/http/request_test.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,17 @@ func TestBody(t *testing.T) {
502502
}()
503503

504504
v, err := r.Body()
505-
assert.Nil(t, err)
505+
assert.NoError(t, err)
506506
assert.Equal(t, "Hello, Go Runner", string(v))
507+
508+
const newBody = "Hello, Rust Runner"
509+
r.SetBody([]byte(newBody))
510+
v, err = r.Body()
511+
assert.NoError(t, err)
512+
assert.Equal(t, []byte(newBody), v)
513+
514+
builder := util.GetBuilder()
515+
assert.True(t, r.FetchChanges(1, builder))
516+
rewrite := getRewriteAction(t, builder)
517+
assert.Equal(t, []byte(newBody), rewrite.BodyBytes())
507518
}

pkg/http/http.go

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ type Request interface {
6767
// pkg/common.ErrConnClosed type is returned.
6868
Body() ([]byte, error)
6969

70+
// SetBody rewrites the original request body
71+
SetBody([]byte)
72+
7073
// Context returns the request's context.
7174
//
7275
// The returned context is always non-nil; it defaults to the
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package plugins_test
19+
20+
import (
21+
"net/http"
22+
23+
"github.com/apache/apisix-go-plugin-runner/tests/e2e/tools"
24+
"github.com/gavv/httpexpect/v2"
25+
"github.com/onsi/ginkgo"
26+
"github.com/onsi/ginkgo/extensions/table"
27+
)
28+
29+
var _ = ginkgo.Describe("RequestBodyRewrite Plugin", func() {
30+
table.DescribeTable("tries to test request body rewrite feature",
31+
func(tc tools.HttpTestCase) {
32+
tools.RunTestCase(tc)
33+
},
34+
table.Entry("config APISIX", tools.HttpTestCase{
35+
Object: tools.GetA6CPExpect(),
36+
Method: http.MethodPut,
37+
Path: "/apisix/admin/routes/1",
38+
Body: `{
39+
"uri":"/echo",
40+
"plugins":{
41+
"ext-plugin-pre-req":{
42+
"conf":[
43+
{
44+
"name":"request-body-rewrite",
45+
"value":"{\"new_body\":\"request body rewrite\"}"
46+
}
47+
]
48+
}
49+
},
50+
"upstream":{
51+
"nodes":{
52+
"web:8888":1
53+
},
54+
"type":"roundrobin"
55+
}
56+
}`,
57+
Headers: map[string]string{"X-API-KEY": tools.GetAdminToken()},
58+
ExpectStatusRange: httpexpect.Status2xx,
59+
}),
60+
table.Entry("should rewrite request body", tools.HttpTestCase{
61+
Object: tools.GetA6DPExpect(),
62+
Method: http.MethodGet,
63+
Path: "/echo",
64+
Body: "hello hello world world",
65+
ExpectBody: "request body rewrite",
66+
ExpectStatus: http.StatusOK,
67+
}),
68+
)
69+
})

0 commit comments

Comments
 (0)