feat: support request body rewrite #127 (#151)
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 093445b..c1a3b12 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -32,9 +32,9 @@
steps:
- uses: actions/checkout@v2
- name: setup go
- uses: actions/setup-go@v1
+ uses: actions/setup-go@v4
with:
- go-version: '1.15'
+ go-version: '1.17'
- name: Download golangci-lint
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.39.0
diff --git a/.gitignore b/.gitignore
index 590e1b6..7fffd5c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,6 @@
coverage.txt
logs/
*.svg
+.vscode
+go.work
+go.work.sum
diff --git a/cmd/go-runner/plugins/limit_req.go b/cmd/go-runner/plugins/limit_req.go
index 6632da3..bdebaaf 100644
--- a/cmd/go-runner/plugins/limit_req.go
+++ b/cmd/go-runner/plugins/limit_req.go
@@ -5,7 +5,7 @@
// (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
+// 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,
diff --git a/cmd/go-runner/plugins/request_body_rewrite.go b/cmd/go-runner/plugins/request_body_rewrite.go
new file mode 100644
index 0000000..3351b6f
--- /dev/null
+++ b/cmd/go-runner/plugins/request_body_rewrite.go
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package plugins
+
+import (
+ "encoding/json"
+ "net/http"
+
+ pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http"
+ "github.com/apache/apisix-go-plugin-runner/pkg/log"
+ "github.com/apache/apisix-go-plugin-runner/pkg/plugin"
+)
+
+const requestBodyRewriteName = "request-body-rewrite"
+
+func init() {
+ if err := plugin.RegisterPlugin(&RequestBodyRewrite{}); err != nil {
+ log.Fatalf("failed to register plugin %s: %s", requestBodyRewriteName, err.Error())
+ }
+}
+
+type RequestBodyRewrite struct {
+ plugin.DefaultPlugin
+}
+
+type RequestBodyRewriteConfig struct {
+ NewBody string `json:"new_body"`
+}
+
+func (*RequestBodyRewrite) Name() string {
+ return requestBodyRewriteName
+}
+
+func (p *RequestBodyRewrite) ParseConf(in []byte) (interface{}, error) {
+ conf := RequestBodyRewriteConfig{}
+ err := json.Unmarshal(in, &conf)
+ if err != nil {
+ log.Errorf("failed to parse config for plugin %s: %s", p.Name(), err.Error())
+ }
+ return conf, err
+}
+
+func (*RequestBodyRewrite) RequestFilter(conf interface{}, _ http.ResponseWriter, r pkgHTTP.Request) {
+ newBody := conf.(RequestBodyRewriteConfig).NewBody
+ if newBody == "" {
+ return
+ }
+ r.SetBody([]byte(newBody))
+}
diff --git a/cmd/go-runner/plugins/request_body_rewrite_test.go b/cmd/go-runner/plugins/request_body_rewrite_test.go
new file mode 100644
index 0000000..8dbf6b0
--- /dev/null
+++ b/cmd/go-runner/plugins/request_body_rewrite_test.go
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package plugins
+
+import (
+ "context"
+ "net"
+ "net/http"
+ "net/url"
+ "testing"
+
+ pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRequestBodyRewrite_ParseConf(t *testing.T) {
+ testCases := []struct {
+ name string
+ in []byte
+ expect string
+ wantErr bool
+ }{
+ {
+ "happy path",
+ []byte(`{"new_body":"hello"}`),
+ "hello",
+ false,
+ },
+ {
+ "empty conf",
+ []byte(``),
+ "",
+ true,
+ },
+ {
+ "empty body",
+ []byte(`{"new_body":""}`),
+ "",
+ false,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ p := new(RequestBodyRewrite)
+ conf, err := p.ParseConf(tc.in)
+ if tc.wantErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+ require.Equal(t, tc.expect, conf.(RequestBodyRewriteConfig).NewBody)
+ })
+ }
+}
+
+func TestRequestBodyRewrite_RequestFilter(t *testing.T) {
+ req := &mockHTTPRequest{body: []byte("hello")}
+ p := new(RequestBodyRewrite)
+ conf, err := p.ParseConf([]byte(`{"new_body":"See ya"}`))
+ require.NoError(t, err)
+ p.RequestFilter(conf, nil, req)
+ require.Equal(t, []byte("See ya"), req.body)
+}
+
+// mockHTTPRequest implements pkgHTTP.Request
+type mockHTTPRequest struct {
+ body []byte
+}
+
+func (r *mockHTTPRequest) SetBody(body []byte) {
+ r.body = body
+}
+
+func (*mockHTTPRequest) Args() url.Values {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) Body() ([]byte, error) {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) Context() context.Context {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) Header() pkgHTTP.Header {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) ID() uint32 {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) Method() string {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) Path() []byte {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) RespHeader() http.Header {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) SetPath([]byte) {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) SrcIP() net.IP {
+ panic("unimplemented")
+}
+
+func (*mockHTTPRequest) Var(string) ([]byte, error) {
+ panic("unimplemented")
+}
diff --git a/go.mod b/go.mod
index 4f7915f..80186c7 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@
require (
github.com/ReneKroon/ttlcache/v2 v2.4.0
- github.com/api7/ext-plugin-proto v0.6.0
+ github.com/api7/ext-plugin-proto v0.6.1
github.com/google/flatbuffers v2.0.0+incompatible
github.com/spf13/cobra v1.2.1
github.com/stretchr/testify v1.7.0
diff --git a/go.sum b/go.sum
index ff5ba33..088e956 100644
--- a/go.sum
+++ b/go.sum
@@ -43,8 +43,8 @@
github.com/ReneKroon/ttlcache/v2 v2.4.0/go.mod h1:zbo6Pv/28e21Z8CzzqgYRArQYGYtjONRxaAKGxzQvG4=
github.com/alvaroloes/enumer v1.1.2/go.mod h1:FxrjvuXoDAx9isTJrv4c+T410zFi0DtXIT0m65DJ+Wo=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/api7/ext-plugin-proto v0.6.0 h1:xmgcKwWRiM9EpBIs1wYJ7Ife/YnLl4IL2NEy4417g60=
-github.com/api7/ext-plugin-proto v0.6.0/go.mod h1:8dbdAgCESeqwZ0IXirbjLbshEntmdrAX3uet+LW3jVU=
+github.com/api7/ext-plugin-proto v0.6.1 h1:eQN0oHacL97ezVGWVmsRigt+ClcpgjipUq0rmW8BG4g=
+github.com/api7/ext-plugin-proto v0.6.1/go.mod h1:8dbdAgCESeqwZ0IXirbjLbshEntmdrAX3uet+LW3jVU=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
diff --git a/internal/http/request.go b/internal/http/request.go
index 5c00172..005c5f2 100644
--- a/internal/http/request.go
+++ b/internal/http/request.go
@@ -189,6 +189,10 @@
return v, nil
}
+func (r *Request) SetBody(body []byte) {
+ r.body = body
+}
+
func (r *Request) Reset() {
defer r.cancel()
r.path = nil
@@ -205,7 +209,7 @@
}
func (r *Request) FetchChanges(id uint32, builder *flatbuffers.Builder) bool {
- if r.path == nil && r.hdr == nil && r.args == nil && r.respHdr == nil {
+ if !r.hasChanges() {
return false
}
@@ -214,6 +218,11 @@
path = builder.CreateByteString(r.path)
}
+ var body flatbuffers.UOffsetT
+ if r.body != nil {
+ body = builder.CreateByteVector(r.body)
+ }
+
var hdrVec, respHdrVec flatbuffers.UOffsetT
if r.hdr != nil {
hdrs := []flatbuffers.UOffsetT{}
@@ -314,6 +323,9 @@
if path > 0 {
hrc.RewriteAddPath(builder, path)
}
+ if body > 0 {
+ hrc.RewriteAddBody(builder, body)
+ }
if hdrVec > 0 {
hrc.RewriteAddHeaders(builder, hdrVec)
}
@@ -346,6 +358,11 @@
return context.Background()
}
+func (r *Request) hasChanges() bool {
+ return r.path != nil || r.hdr != nil ||
+ r.args != nil || r.respHdr != nil || r.body != nil
+}
+
func (r *Request) askExtraInfo(builder *flatbuffers.Builder,
infoType ei.Info, info flatbuffers.UOffsetT) ([]byte, error) {
diff --git a/internal/http/request_test.go b/internal/http/request_test.go
index d6b58ed..00c1e69 100644
--- a/internal/http/request_test.go
+++ b/internal/http/request_test.go
@@ -502,6 +502,17 @@
}()
v, err := r.Body()
- assert.Nil(t, err)
+ assert.NoError(t, err)
assert.Equal(t, "Hello, Go Runner", string(v))
+
+ const newBody = "Hello, Rust Runner"
+ r.SetBody([]byte(newBody))
+ v, err = r.Body()
+ assert.NoError(t, err)
+ assert.Equal(t, []byte(newBody), v)
+
+ builder := util.GetBuilder()
+ assert.True(t, r.FetchChanges(1, builder))
+ rewrite := getRewriteAction(t, builder)
+ assert.Equal(t, []byte(newBody), rewrite.BodyBytes())
}
diff --git a/pkg/http/http.go b/pkg/http/http.go
index 5b4e93c..fdb0294 100644
--- a/pkg/http/http.go
+++ b/pkg/http/http.go
@@ -67,6 +67,9 @@
// pkg/common.ErrConnClosed type is returned.
Body() ([]byte, error)
+ // SetBody rewrites the original request body
+ SetBody([]byte)
+
// Context returns the request's context.
//
// The returned context is always non-nil; it defaults to the
diff --git a/tests/e2e/plugins/plugins_request_body_rewrite_test.go b/tests/e2e/plugins/plugins_request_body_rewrite_test.go
new file mode 100644
index 0000000..9a404ac
--- /dev/null
+++ b/tests/e2e/plugins/plugins_request_body_rewrite_test.go
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package plugins_test
+
+import (
+ "net/http"
+
+ "github.com/apache/apisix-go-plugin-runner/tests/e2e/tools"
+ "github.com/gavv/httpexpect/v2"
+ "github.com/onsi/ginkgo"
+ "github.com/onsi/ginkgo/extensions/table"
+)
+
+var _ = ginkgo.Describe("RequestBodyRewrite Plugin", func() {
+ table.DescribeTable("tries to test request body rewrite feature",
+ func(tc tools.HttpTestCase) {
+ tools.RunTestCase(tc)
+ },
+ table.Entry("config APISIX", tools.HttpTestCase{
+ Object: tools.GetA6CPExpect(),
+ Method: http.MethodPut,
+ Path: "/apisix/admin/routes/1",
+ Body: `{
+ "uri":"/echo",
+ "plugins":{
+ "ext-plugin-pre-req":{
+ "conf":[
+ {
+ "name":"request-body-rewrite",
+ "value":"{\"new_body\":\"request body rewrite\"}"
+ }
+ ]
+ }
+ },
+ "upstream":{
+ "nodes":{
+ "web:8888":1
+ },
+ "type":"roundrobin"
+ }
+ }`,
+ Headers: map[string]string{"X-API-KEY": tools.GetAdminToken()},
+ ExpectStatusRange: httpexpect.Status2xx,
+ }),
+ table.Entry("should rewrite request body", tools.HttpTestCase{
+ Object: tools.GetA6DPExpect(),
+ Method: http.MethodGet,
+ Path: "/echo",
+ Body: "hello hello world world",
+ ExpectBody: "request body rewrite",
+ ExpectStatus: http.StatusOK,
+ }),
+ )
+})