├── .asf.yaml ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── request-help.md └── workflows │ ├── codeql-analysis.yml │ ├── license-checker.yml │ ├── lint.yml │ ├── runner-e2e.yml │ ├── spell-checker.yml │ └── unit-test-ci.yml ├── .gitignore ├── .golangci.yml ├── .licenserc.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── ci ├── apisix │ └── config.yaml ├── docker-compose.yml └── openresty │ └── nginx.conf ├── cmd └── go-runner │ ├── main.go │ ├── main_test.go │ ├── plugins │ ├── fault_injection.go │ ├── fault_injection_test.go │ ├── limit_req.go │ ├── limit_req_test.go │ ├── request_body_rewrite.go │ ├── request_body_rewrite_test.go │ ├── response_rewrite.go │ ├── response_rewrite_test.go │ ├── say.go │ └── say_test.go │ └── version.go ├── docs ├── assets │ └── images │ │ └── runner-overview.png └── en │ └── latest │ ├── config.json │ ├── developer-guide.md │ └── getting-started.md ├── go.mod ├── go.sum ├── internal ├── http │ ├── header.go │ ├── req-response.go │ ├── req-response_test.go │ ├── request.go │ ├── request_test.go │ ├── response.go │ └── response_test.go ├── plugin │ ├── conf.go │ ├── conf_test.go │ ├── plugin.go │ └── plugin_test.go ├── server │ ├── error.go │ ├── error_test.go │ ├── server.go │ └── server_test.go └── util │ ├── msg.go │ ├── msg_test.go │ └── pool.go ├── pkg ├── common │ └── error.go ├── http │ └── http.go ├── httptest │ └── recorder.go ├── log │ └── log.go ├── plugin │ └── plugin.go └── runner │ └── runner.go └── tests └── e2e ├── go.mod ├── go.sum ├── plugins ├── plugins_fault_injection_test.go ├── plugins_limit_req_test.go ├── plugins_request_body_rewrite_test.go ├── plugins_response_rewrite_test.go ├── plugins_say_test.go └── plugins_suite_test.go └── tools └── tools.go /.asf.yaml: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one or more 2 | # contributor license agreements. See the NOTICE file distributed with 3 | # this work for additional information regarding copyright ownership. 4 | # The ASF licenses this file to You under the Apache License, Version 2.0 5 | # (the "License"); you may not use this file except in compliance with 6 | # the License. You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | github: 18 | description: Go Plugin Runner for APISIX 19 | homepage: https://apisix.apache.org/ 20 | labels: 21 | - apisix 22 | - go 23 | - gateway 24 | - plugin 25 | protected_branches: 26 | master: 27 | required_pull_request_reviews: 28 | dismiss_stale_reviews: true 29 | required_approving_review_count: 1 30 | enabled_merge_buttons: 31 | squash: true 32 | merge: false 33 | rebase: false 34 | features: 35 | issues: true 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report template 3 | about: Please use this template for reporting suspected bugs. 4 | title: 'bug: ' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Issue description 11 | 12 | ### Environment 13 | 14 | * APISIX Go Plugin Runner's version: 15 | * APISIX version: 16 | * Go version: 17 | * OS (cmd: `uname -a`): 18 | 19 | ### Minimal test code / Steps to reproduce the issue 20 | 21 | 1. 22 | 2. 23 | 3. 24 | 25 | ### What's the actual result? (including assertion message & call stack if applicable) 26 | 27 | ### What's the expected result? 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/request-help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Request help template 3 | about: Please use this template for requesting help. 4 | title: 'request help: ' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Issue description 11 | 12 | ### Environment 13 | 14 | * APISIX Go Plugin Runner's version: 15 | * APISIX version: 16 | * Go version: 17 | * OS (cmd: `uname -a`): 18 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | # For most projects, this workflow file will not need changing; you simply need 20 | # to commit it to your repository. 21 | # 22 | # You may wish to alter this file to override the set of languages analyzed, 23 | # or to provide custom queries or build logic. 24 | # 25 | # ******** NOTE ******** 26 | # We have attempted to detect the languages in your repository. Please check 27 | # the `language` matrix defined below to confirm you have the correct set of 28 | # supported CodeQL languages. 29 | # 30 | name: "CodeQL" 31 | 32 | on: 33 | push: 34 | branches: [ master ] 35 | pull_request: 36 | # The branches below must be a subset of the branches above 37 | branches: [ master ] 38 | schedule: 39 | - cron: '20 16 * * 0' 40 | 41 | jobs: 42 | analyze: 43 | name: Analyze 44 | runs-on: ubuntu-latest 45 | permissions: 46 | actions: read 47 | contents: read 48 | security-events: write 49 | 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | language: [ 'go' ] 54 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 55 | # Learn more: 56 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 57 | 58 | steps: 59 | - name: Checkout repository 60 | uses: actions/checkout@v2 61 | 62 | # Initializes the CodeQL tools for scanning. 63 | - name: Initialize CodeQL 64 | uses: github/codeql-action/init@v1 65 | with: 66 | languages: ${{ matrix.language }} 67 | # If you wish to specify custom queries, you can do so here or in a config file. 68 | # By default, queries listed here will override any specified in a config file. 69 | # Prefix the list here with "+" to use these queries and those in the config file. 70 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 71 | 72 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 73 | # If this step fails, then you should remove it and run the build manually (see below) 74 | - name: Autobuild 75 | uses: github/codeql-action/autobuild@v1 76 | 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 https://git.io/JvXDl 79 | 80 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 81 | # and modify them (or add more) to build your code if your project 82 | # uses a compiled language 83 | 84 | #- run: | 85 | # make bootstrap 86 | # make release 87 | 88 | - name: Perform CodeQL Analysis 89 | uses: github/codeql-action/analyze@v1 90 | -------------------------------------------------------------------------------- /.github/workflows/license-checker.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | name: License checker 20 | 21 | on: 22 | push: 23 | branches: 24 | - master 25 | pull_request: 26 | branches: 27 | - master 28 | 29 | jobs: 30 | check-license: 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: Check License Header 36 | uses: apache/skywalking-eyes@v0.1.0 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | name: lint 20 | 21 | on: 22 | push: 23 | branches: 24 | - master 25 | pull_request: 26 | branches: 27 | - master 28 | 29 | jobs: 30 | golang-lint: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v2 34 | - name: setup go 35 | uses: actions/setup-go@v4 36 | with: 37 | go-version: '1.17' 38 | 39 | - name: Download golangci-lint 40 | run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.39.0 41 | 42 | - name: golangci-lint 43 | run: | 44 | export PATH=$PATH:$(go env GOPATH)/bin/ 45 | make lint 46 | 47 | - name: run gofmt 48 | working-directory: ./ 49 | run: | 50 | diffs=`gofmt -l .` 51 | if [[ -n $diffs ]]; then 52 | echo "Files are not formatted by gofmt:" 53 | echo $diffs 54 | exit 1 55 | fi 56 | 57 | - name: run goimports 58 | working-directory: ./ 59 | run: | 60 | go install golang.org/x/tools/cmd/goimports@v0.15.0 61 | export PATH=$PATH:$(go env GOPATH)/bin/ 62 | diffs=`goimports -d .` 63 | if [[ -n $diffs ]]; then 64 | echo "Files are not formatted by goimport:" 65 | echo $diffs 66 | exit 1 67 | fi 68 | -------------------------------------------------------------------------------- /.github/workflows/runner-e2e.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | 20 | name: Runner E2E Test 21 | 22 | on: 23 | push: 24 | branches: 25 | - master 26 | pull_request: 27 | branches: 28 | - master 29 | 30 | jobs: 31 | run-test: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - name: setup go 37 | uses: actions/setup-go@v2.1.5 38 | with: 39 | go-version: '1.17' 40 | 41 | - name: build runner 42 | run: | 43 | make build 44 | 45 | - name: startup runner 46 | run: | 47 | APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock APISIX_CONF_EXPIRE_TIME=3600 ./go-runner run & 48 | 49 | - name: startup apisix 50 | run: | 51 | docker-compose -f ci/docker-compose.yml up -d 52 | sleep 5 53 | 54 | - name: install ginkgo cli 55 | run: go install github.com/onsi/ginkgo/ginkgo@v1.16.5 56 | 57 | - name: run tests 58 | working-directory: ./tests/e2e 59 | run: ginkgo -r -------------------------------------------------------------------------------- /.github/workflows/spell-checker.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | name: spell-checker 20 | on: 21 | push: 22 | branches: [ master ] 23 | pull_request: 24 | branches: [ master ] 25 | jobs: 26 | misspell: 27 | name: runner / misspell 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Check out code. 31 | uses: actions/checkout@v2 32 | - name: Install 33 | run: | 34 | wget -O - -q https://git.io/misspell | sh -s -- -b . 35 | - name: Misspell 36 | run: | 37 | find . -name "*.go" \ 38 | -or -name "*.md" \ 39 | -or -name "*.yml" \ 40 | -or -name "*.yaml" \ 41 | -type f | xargs ./misspell -error 42 | -------------------------------------------------------------------------------- /.github/workflows/unit-test-ci.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | name: unit-test-ci 20 | 21 | on: 22 | push: 23 | branches: 24 | - master 25 | pull_request: 26 | branches: 27 | - master 28 | jobs: 29 | run-test: 30 | runs-on: ubuntu-latest 31 | env: 32 | CODECOV_TOKEN: 892a8adc-7df4-41f8-ba74-92978fc6c561 33 | steps: 34 | - uses: actions/checkout@v2 35 | - name: setup go 36 | uses: actions/setup-go@v1 37 | with: 38 | go-version: '1.15' 39 | - name: run unit test 40 | run: | 41 | make test 42 | - name: upload coverage profile 43 | run: | 44 | bash <(curl -s https://codecov.io/bash) 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | go-runner 2 | !go-runner/ 3 | release/ 4 | *.tgz 5 | .idea 6 | .DS_Store 7 | coverage.txt 8 | logs/ 9 | *.svg 10 | .vscode 11 | go.work 12 | go.work.sum 13 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to the Apache Software Foundation (ASF) under one 3 | # or more contributor license agreements. See the NOTICE file 4 | # distributed with this work for additional information 5 | # regarding copyright ownership. The ASF licenses this file 6 | # to you under the Apache License, Version 2.0 (the 7 | # "License"); you may not use this file except in compliance 8 | # with the License. You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, 13 | # software distributed under the License is distributed on an 14 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | # KIND, either express or implied. See the License for the 16 | # specific language governing permissions and limitations 17 | # under the License. 18 | # 19 | run: 20 | skip-files: 21 | - ".*_test.go$" 22 | 23 | linters: 24 | disable-all: true 25 | enable: 26 | - deadcode 27 | - errcheck 28 | - goimports 29 | - gosimple 30 | - ineffassign 31 | - staticcheck 32 | - structcheck 33 | - unconvert 34 | - unused 35 | - varcheck 36 | - vet 37 | -------------------------------------------------------------------------------- /.licenserc.yaml: -------------------------------------------------------------------------------- 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 | header: 18 | license: 19 | spdx-id: Apache-2.0 20 | copyright-owner: Apache Software Foundation 21 | 22 | paths-ignore: 23 | - '.gitignore' 24 | - 'dist' 25 | - 'licenses' 26 | - '**/*.md' 27 | - '**/testdata/**' 28 | - '**/go.mod' 29 | - '**/go.sum' 30 | - 'LICENSE' 31 | - 'NOTICE' 32 | - '**/*.json' 33 | - '.github/ISSUE_TEMPLATE/*' 34 | - '.github/PULL_REQUEST_TEMPLATE.md' 35 | 36 | comment: on-failure 37 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | --- 4 | 5 | 23 | 24 | ## Table of Contents 25 | 26 | - [0.5.0](#050) 27 | - [0.4.0](#040) 28 | - [0.3.0](#030) 29 | - [0.2.0](#020) 30 | - [0.1.0](#010) 31 | 32 | ## 0.5.0 33 | 34 | ### Feature 35 | - :sunrise: feat: response_rewrite plugin support replace body via origin res body [#109](https://github.com/apache/apisix-go-plugin-runner/pull/109) 36 | - :sunrise: feat: support get response body by extra_info [107](https://github.com/apache/apisix-go-plugin-runner/pull/107) 37 | 38 | ### Bugfix 39 | - fix: set the correct rpc response type when meet an error [#102](https://github.com/apache/apisix-go-plugin-runner/pull/102) 40 | 41 | ## 0.4.0 42 | 43 | ### Feature 44 | 45 | - :sunrise: feat: add response-rewrite plugin [#91](https://github.com/apache/apisix-go-plugin-runner/pull/91) 46 | - :sunrise: feat: support response filter for plugin [#90](https://github.com/apache/apisix-go-plugin-runner/pull/90) 47 | - :sunrise: feat: add debugf function [#87](https://github.com/apache/apisix-go-plugin-runner/pull/87) 48 | 49 | ### Change 50 | 51 | - change: add DefaultPlugin so that we don't need to reimplement all the methods [#92](https://github.com/apache/apisix-go-plugin-runner/pull/92) 52 | 53 | ## 0.3.0 54 | 55 | ### Feature 56 | 57 | - :sunrise: feat: support upstream response header modify [#68](https://github.com/apache/apisix-go-plugin-runner/pull/68) 58 | - :sunrise: feat: support fetch request body [#70](https://github.com/apache/apisix-go-plugin-runner/pull/70) 59 | - :sunrise: feat: introduce context to plugin runner [#63](https://github.com/apache/apisix-go-plugin-runner/pull/63) 60 | - :sunrise: feat: add fault-injection plugin for benchmark [#46](https://github.com/apache/apisix-go-plugin-runner/pull/46) 61 | - :sunrise: feat: add e2e framework [#72](https://github.com/apache/apisix-go-plugin-runner/pull/72) 62 | 63 | ### Bugfix 64 | 65 | - fix: write response header break request [#65](https://github.com/apache/apisix-go-plugin-runner/pull/65) 66 | - fix: addressed blank space of GITSHA populated [#58](https://github.com/apache/apisix-go-plugin-runner/pull/58) 67 | - fix: make sure the cached conf expires after the token [#44](https://github.com/apache/apisix-go-plugin-runner/pull/44) 68 | - fix: avoid reusing nil builder [#42](https://github.com/apache/apisix-go-plugin-runner/pull/42) 69 | 70 | ## 0.2.0 71 | 72 | ### Feature 73 | 74 | - :sunrise: feat: support Var API [#31](https://github.com/apache/apisix/pull/31) 75 | - :sunrise: feat: provide default APISIX_CONF_EXPIRE_TIME to simplify 76 | thing [#30](https://github.com/apache/apisix/pull/30) 77 | - :sunrise: feat: handle idempotent key in PrepareConf [#27](https://github.com/apache/apisix/pull/27) 78 | 79 | ### Bugfix 80 | 81 | - fix: a race when reusing flatbuffers.Builder [#35](https://github.com/apache/apisix/pull/35) 82 | - fix: the default socket permission is not enough [#25](https://github.com/apache/apisix/pull/25) 83 | 84 | ## 0.1.0 85 | 86 | ### Feature 87 | 88 | - First implementation 89 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 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 | default: help 18 | 19 | VERSION ?= latest 20 | RELEASE_SRC = apisix-go-plugin-runner-${VERSION}-src 21 | 22 | GITSHA ?= $(shell git rev-parse --short=7 HEAD 2> /dev/null || echo '') 23 | OSNAME ?= $(shell uname -s | tr A-Z a-z) 24 | OSARCH ?= $(shell uname -m | tr A-Z a-z) 25 | PWD ?= $(shell pwd) 26 | ifeq ($(OSARCH), x86_64) 27 | OSARCH = amd64 28 | endif 29 | 30 | VERSYM=main._buildVersion 31 | GITSHASYM=main._buildGitRevision 32 | BUILDOSSYM=main._buildOS 33 | GO_LDFLAGS ?= "-X '$(VERSYM)=$(VERSION)' -X '$(GITSHASYM)=$(GITSHA)' -X '$(BUILDOSSYM)=$(OSNAME)/$(OSARCH)'" 34 | 35 | .PHONY: build 36 | build: 37 | cd cmd/go-runner && \ 38 | go build $(GO_BUILD_FLAGS) -ldflags $(GO_LDFLAGS) && \ 39 | mv go-runner ../.. 40 | 41 | .PHONY: lint 42 | lint: 43 | golangci-lint run --verbose ./... 44 | 45 | .PHONY: test 46 | test: 47 | go test -race -cover -coverprofile=coverage.txt ./... 48 | 49 | .PHONY: release-src 50 | release-src: compress-tar 51 | gpg --batch --yes --armor --detach-sig $(RELEASE_SRC).tgz 52 | shasum -a 512 $(RELEASE_SRC).tgz > $(RELEASE_SRC).tgz.sha512 53 | 54 | mkdir -p release 55 | mv $(RELEASE_SRC).tgz release/$(RELEASE_SRC).tgz 56 | mv $(RELEASE_SRC).tgz.asc release/$(RELEASE_SRC).tgz.asc 57 | mv $(RELEASE_SRC).tgz.sha512 release/$(RELEASE_SRC).tgz.sha512 58 | 59 | .PHONY: compress-tar 60 | compress-tar: 61 | tar -zcvf $(RELEASE_SRC).tgz \ 62 | ./cmd \ 63 | ./internal \ 64 | ./pkg \ 65 | LICENSE \ 66 | Makefile \ 67 | NOTICE \ 68 | go.mod \ 69 | go.sum \ 70 | *.md 71 | 72 | .PHONY: help 73 | help: 74 | @echo Makefile rules: 75 | @echo 76 | @grep -E '^### [-A-Za-z0-9_]+:' Makefile | sed 's/###/ /' 77 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Apache APISIX 2 | Copyright 2021-2022 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | The Apache Software Foundation (http://www.apache.org/). 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 19 | 20 | # Go Plugin Runner for Apache APISIX 21 | 22 | [![Go Report Card](https://goreportcard.com/badge/github.com/apache/apisix-go-plugin-runner)](https://goreportcard.com/report/github.com/apache/apisix-go-plugin-runner) 23 | [![Build Status](https://github.com/apache/apisix-go-plugin-runner/workflows/unit-test-ci/badge.svg?branch=master)](https://github.com/apache/apisix-go-plugin-runner/actions) 24 | [![Codecov](https://codecov.io/gh/apache/apisix-go-plugin-runner/branch/master/graph/badge.svg)](https://codecov.io/gh/apache/apisix-go-plugin-runner) 25 | [![Godoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/apache/apisix-go-plugin-runner) 26 | 27 | Runs [Apache APISIX](http://apisix.apache.org/) plugins written in Go. Implemented as a sidecar that accompanies APISIX. 28 | 29 | ## Status 30 | 31 | This project is generally available. 32 | 33 | ## Why apisix-go-plugin-runner 34 | 35 | Apache APISIX offers many full-featured plugins covering areas such as authentication, security, traffic control, serverless, analytics & monitoring, transformations, logging. 36 | 37 | It also provides highly extensible API, allowing common phases to be mounted, and users can use these API to develop their own plugins. 38 | 39 | This project is APISIX Go side implementation that supports writing plugins in Go. 40 | 41 | Currently, Go Plugin Runner is provided as a library. This is because the convention of Go is to compile all the code into an executable file. 42 | 43 | Although there is a mechanism for Go Plugin to compile the plugin code into a dynamic link library and then load it into the binary. But as far as experience is concerned, there are still some imperfections that are not so simple and direct to use. 44 | 45 | The structure of the apache/apisix-go-plugin-runner repository on GitHub is as follows: 46 | 47 | ``` 48 | . 49 | ├── cmd 50 | ├── internal 51 | ├── pkg 52 | ``` 53 | 54 | `internal` is responsible for the internal implementation, `pkg` displays the external interface, and `cmd` provides examples of the demonstration. 55 | There is a subdirectory of `go-runner` under the `cmd` directory. By reading the code in this section, you can learn how to use Go Plugin Runner in practical applications. 56 | 57 | ## How it Works 58 | 59 | At present, the communication between Go Plugin Runner and Apache APISIX is an RPC based on Unix socket. So Go Plugin Runner and Apache APISIX need to be deployed on the same machine. 60 | 61 | ### Enable Go Plugin Runner 62 | 63 | As mentioned earlier, Go Plugin Runner is managed by Apache APISIX, which runs as a child process of APISIX. So we have to configure and run this Runner in Apache APISIX. 64 | 65 | The following configuration process will take the code `cmd/go-runner` in the `apisix-go-plugin-runner` project as an example. 66 | 67 | 1. Compile the sample code. Executing `make build` generates the executable file go-runner. 68 | 2. Make the following configuration in the conf/config.yaml file of Apache APISIX: 69 | 70 | ```yaml 71 | ext-plugin: 72 | cmd: ["/path/to/apisix-go-plugin-runner/go-runner", "run"] 73 | ``` 74 | 75 | With the above configuration, Apache APISIX pulls up `go-runner` when it starts and closes `go-runner` when it stops. 76 | 77 | In view of the fact that `apisix-go-plugin-runner` is used in the form of a library in the actual development process, you need to replace the above example configuration with your own executable and startup instructions. 78 | 79 | Finally, after the startup of Apache APISIX, `go-runner` will be started along with it. 80 | 81 | ### Other configuration methods 82 | 83 | Of course, if you need to take these three steps every time you verify the functionality in the development process, it is quite tedious. So we also provide another configuration that allows apisix-go-plugin-runner to run independently during development. 84 | 85 | 1. The first thing to do is to compile the code. 86 | 2. Configure the following in the conf/config.yaml file of Apache APISIX: 87 | 88 | ```yaml 89 | ext-plugin: 90 | path_for_test: /tmp/runner.sock 91 | ``` 92 | 93 | 3. Start `go-runner` with the following code. 94 | 95 | ``` 96 | APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock ./go-runner run 97 | ``` 98 | 99 | Notice that we specify the socket address to be used for `go-runner` communication through the environment variable `APISIX_LISTEN_ADDRESS`. This address needs to be consistent with the configuration in Apache APISIX. 100 | 101 | ## License 102 | 103 | Apache 2.0 LICENSE 104 | -------------------------------------------------------------------------------- /ci/apisix/config.yaml: -------------------------------------------------------------------------------- 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 | deployment: 19 | admin: 20 | allow_admin: 21 | - 0.0.0.0/0 22 | enable_control: true 23 | control: 24 | ip: "0.0.0.0" 25 | port: 9092 26 | admin_key: 27 | - name: admin 28 | key: edd1c9f034335f136f87ad84b625c8f1 29 | role: admin 30 | etcd: 31 | host: 32 | - "http://etcd:2379" 33 | prefix: /apisix 34 | timeout: 30 35 | ext-plugin: 36 | path_for_test: /tmp/runner.sock 37 | nginx_config: 38 | user: root -------------------------------------------------------------------------------- /ci/docker-compose.yml: -------------------------------------------------------------------------------- 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 | version: "3" 19 | 20 | services: 21 | apisix: 22 | image: apache/apisix:dev 23 | restart: always 24 | volumes: 25 | - ./apisix/config.yaml:/usr/local/apisix/conf/config.yaml:ro 26 | - /tmp/runner.sock:/tmp/runner.sock 27 | depends_on: 28 | - etcd 29 | ports: 30 | - "9180:9180/tcp" 31 | - "9080:9080/tcp" 32 | - "9091:9091/tcp" 33 | - "9443:9443/tcp" 34 | - "9092:9092/tcp" 35 | networks: 36 | apisix: 37 | 38 | etcd: 39 | image: bitnami/etcd:3.4.9 40 | restart: always 41 | environment: 42 | ETCD_ENABLE_V2: "true" 43 | ALLOW_NONE_AUTHENTICATION: "yes" 44 | ETCD_ADVERTISE_CLIENT_URLS: "http://0.0.0.0:2379" 45 | ETCD_LISTEN_CLIENT_URLS: "http://0.0.0.0:2379" 46 | ports: 47 | - "2379:2379/tcp" 48 | networks: 49 | apisix: 50 | 51 | web: 52 | image: openresty/openresty 53 | restart: unless-stopped 54 | volumes: 55 | - ./openresty/nginx.conf:/usr/local/openresty/nginx/conf/nginx.conf:ro 56 | ports: 57 | - "8888:8888" 58 | networks: 59 | apisix: 60 | 61 | networks: 62 | apisix: 63 | driver: bridge -------------------------------------------------------------------------------- /ci/openresty/nginx.conf: -------------------------------------------------------------------------------- 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 | user root; 19 | worker_processes auto; 20 | 21 | pcre_jit on; 22 | 23 | 24 | error_log logs/error.log info; 25 | 26 | pid logs/nginx.pid; 27 | 28 | 29 | events { 30 | worker_connections 1024; 31 | } 32 | 33 | 34 | http { 35 | include mime.types; 36 | default_type application/octet-stream; 37 | 38 | # fake server, only for test 39 | server { 40 | listen 8888; 41 | 42 | server_tokens off; 43 | 44 | location / { 45 | content_by_lua_block { 46 | ngx.say("hello world") 47 | } 48 | 49 | more_clear_headers Date; 50 | } 51 | 52 | location /echo { 53 | content_by_lua_block { 54 | ngx.req.read_body() 55 | local hdrs = ngx.req.get_headers() 56 | for k, v in pairs(hdrs) do 57 | ngx.header[k] = v 58 | end 59 | ngx.print(ngx.req.get_body_data() or "") 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/go-runner/main.go: -------------------------------------------------------------------------------- 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 main 19 | 20 | import ( 21 | "fmt" 22 | "io" 23 | "os" 24 | "path/filepath" 25 | "runtime" 26 | "runtime/pprof" 27 | 28 | "github.com/spf13/cobra" 29 | "github.com/thediveo/enumflag" 30 | "go.uber.org/zap/zapcore" 31 | 32 | _ "github.com/apache/apisix-go-plugin-runner/cmd/go-runner/plugins" 33 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 34 | "github.com/apache/apisix-go-plugin-runner/pkg/runner" 35 | ) 36 | 37 | var ( 38 | InfoOut io.Writer = os.Stdout 39 | ) 40 | 41 | func newVersionCommand() *cobra.Command { 42 | var long bool 43 | cmd := &cobra.Command{ 44 | Use: "version", 45 | Short: "version", 46 | Run: func(cmd *cobra.Command, _ []string) { 47 | if long { 48 | fmt.Fprint(InfoOut, longVersion()) 49 | } else { 50 | fmt.Fprintf(InfoOut, "version %s\n", shortVersion()) 51 | } 52 | }, 53 | } 54 | 55 | cmd.PersistentFlags().BoolVar(&long, "long", false, "show long mode version information") 56 | return cmd 57 | } 58 | 59 | type RunMode enumflag.Flag 60 | 61 | const ( 62 | Dev RunMode = iota // Development 63 | Prod // Product 64 | Prof // Profile 65 | 66 | ProfileFilePath = "./logs/profile." 67 | LogFilePath = "./logs/runner.log" 68 | ) 69 | 70 | var RunModeIds = map[RunMode][]string{ 71 | Prod: {"prod"}, 72 | Dev: {"dev"}, 73 | Prof: {"prof"}, 74 | } 75 | 76 | func openFileToWrite(name string) (*os.File, error) { 77 | dir := filepath.Dir(name) 78 | if dir != "." { 79 | err := os.MkdirAll(dir, 0755) 80 | if err != nil { 81 | return nil, err 82 | } 83 | } 84 | f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | return f, nil 90 | } 91 | 92 | func newRunCommand() *cobra.Command { 93 | var mode RunMode 94 | cmd := &cobra.Command{ 95 | Use: "run", 96 | Short: "run", 97 | Run: func(cmd *cobra.Command, _ []string) { 98 | cfg := runner.RunnerConfig{} 99 | if mode == Prod { 100 | cfg.LogLevel = zapcore.WarnLevel 101 | f, err := openFileToWrite(LogFilePath) 102 | if err != nil { 103 | log.Fatalf("failed to open log: %s", err) 104 | } 105 | cfg.LogOutput = f 106 | } else if mode == Prof { 107 | cfg.LogLevel = zapcore.WarnLevel 108 | 109 | cpuProfileFile := ProfileFilePath + "cpu" 110 | f, err := os.Create(cpuProfileFile) 111 | if err != nil { 112 | log.Fatalf("could not create CPU profile: %s", err) 113 | } 114 | defer f.Close() 115 | if err := pprof.StartCPUProfile(f); err != nil { 116 | log.Fatalf("could not start CPU profile: %s", err) 117 | } 118 | defer pprof.StopCPUProfile() 119 | 120 | defer func() { 121 | memProfileFile := ProfileFilePath + "mem" 122 | f, err := os.Create(memProfileFile) 123 | if err != nil { 124 | log.Fatalf("could not create memory profile: %s", err) 125 | } 126 | defer f.Close() 127 | 128 | runtime.GC() 129 | if err := pprof.WriteHeapProfile(f); err != nil { 130 | log.Fatalf("could not write memory profile: %s", err) 131 | } 132 | }() 133 | } 134 | runner.Run(cfg) 135 | }, 136 | } 137 | 138 | cmd.PersistentFlags().VarP( 139 | enumflag.New(&mode, "mode", RunModeIds, enumflag.EnumCaseInsensitive), 140 | "mode", "m", 141 | "the runner's run mode; can be 'prod' or 'dev', default to 'dev'") 142 | 143 | return cmd 144 | } 145 | 146 | func NewCommand() *cobra.Command { 147 | cmd := &cobra.Command{ 148 | Use: "apisix-go-plugin-runner [command]", 149 | Long: "The Plugin runner to run Go plugins", 150 | Version: shortVersion(), 151 | } 152 | 153 | cmd.AddCommand(newRunCommand()) 154 | cmd.AddCommand(newVersionCommand()) 155 | return cmd 156 | } 157 | 158 | func main() { 159 | root := NewCommand() 160 | if err := root.Execute(); err != nil { 161 | fmt.Fprintln(os.Stderr, err.Error()) 162 | os.Exit(1) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /cmd/go-runner/main_test.go: -------------------------------------------------------------------------------- 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 main 19 | 20 | import ( 21 | "bytes" 22 | "os" 23 | "strings" 24 | "testing" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestVersion(t *testing.T) { 30 | args := []string{"version", "--long"} 31 | os.Args = append([]string{"cmd"}, args...) 32 | 33 | var b bytes.Buffer 34 | InfoOut = &b 35 | main() 36 | 37 | assert.True(t, strings.Contains(b.String(), "Building OS/Arch")) 38 | } 39 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/fault_injection.go: -------------------------------------------------------------------------------- 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 | "errors" 23 | "math/rand" 24 | "net/http" 25 | 26 | pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http" 27 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 28 | "github.com/apache/apisix-go-plugin-runner/pkg/plugin" 29 | ) 30 | 31 | const ( 32 | plugin_name = "fault-injection" 33 | ) 34 | 35 | func init() { 36 | err := plugin.RegisterPlugin(&FaultInjection{}) 37 | if err != nil { 38 | log.Fatalf("failed to register plugin %s: %s", plugin_name, err) 39 | } 40 | } 41 | 42 | // FaultInjection is used in the benchmark 43 | type FaultInjection struct { 44 | // Embed the default plugin here, 45 | // so that we don't need to reimplement all the methods. 46 | plugin.DefaultPlugin 47 | } 48 | 49 | type FaultInjectionConf struct { 50 | Body string `json:"body"` 51 | HttpStatus int `json:"http_status"` 52 | Percentage int `json:"percentage"` 53 | } 54 | 55 | func (p *FaultInjection) Name() string { 56 | return plugin_name 57 | } 58 | 59 | func (p *FaultInjection) ParseConf(in []byte) (interface{}, error) { 60 | conf := FaultInjectionConf{Percentage: -1} 61 | err := json.Unmarshal(in, &conf) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | // schema check 67 | if conf.HttpStatus < 200 { 68 | return nil, errors.New("bad http_status") 69 | } 70 | if conf.Percentage == -1 { 71 | conf.Percentage = 100 72 | } else if conf.Percentage < 0 || conf.Percentage > 100 { 73 | return nil, errors.New("bad percentage") 74 | } 75 | 76 | return conf, err 77 | } 78 | 79 | func sampleHit(percentage int) bool { 80 | return rand.Intn(100) < percentage 81 | } 82 | 83 | func (p *FaultInjection) RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) { 84 | fc := conf.(FaultInjectionConf) 85 | if !sampleHit(fc.Percentage) { 86 | return 87 | } 88 | 89 | w.WriteHeader(fc.HttpStatus) 90 | body := fc.Body 91 | if len(body) == 0 { 92 | return 93 | } 94 | 95 | _, err := w.Write([]byte(body)) 96 | if err != nil { 97 | log.Errorf("failed to write: %s", err) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/fault_injection_test.go: -------------------------------------------------------------------------------- 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 | "io/ioutil" 22 | "net/http/httptest" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestFaultInjection(t *testing.T) { 29 | in := []byte(`{"http_status":400, "body":"hello"}`) 30 | fi := &FaultInjection{} 31 | conf, err := fi.ParseConf(in) 32 | assert.Nil(t, err) 33 | 34 | w := httptest.NewRecorder() 35 | fi.RequestFilter(conf, w, nil) 36 | resp := w.Result() 37 | body, _ := ioutil.ReadAll(resp.Body) 38 | assert.Equal(t, 400, resp.StatusCode) 39 | assert.Equal(t, "hello", string(body)) 40 | } 41 | 42 | func TestFaultInjection_Percentage(t *testing.T) { 43 | in := []byte(`{"http_status":400, "percentage":0}`) 44 | fi := &FaultInjection{} 45 | conf, err := fi.ParseConf(in) 46 | assert.Nil(t, err) 47 | 48 | w := httptest.NewRecorder() 49 | fi.RequestFilter(conf, w, nil) 50 | resp := w.Result() 51 | assert.Equal(t, 200, resp.StatusCode) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/limit_req.go: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one or more 2 | // contributor license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright ownership. 4 | // The ASF licenses this file to You under the Apache License, Version 2.0 5 | // (the "License"); you may not use this file except in compliance with 6 | // the License. You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | package plugins 16 | 17 | import ( 18 | "encoding/json" 19 | "net/http" 20 | "time" 21 | 22 | "golang.org/x/time/rate" 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 | func init() { 30 | err := plugin.RegisterPlugin(&LimitReq{}) 31 | if err != nil { 32 | log.Fatalf("failed to register plugin limit-req: %s", err) 33 | } 34 | } 35 | 36 | // LimitReq is a demo for a real world plugin 37 | type LimitReq struct { 38 | // Embed the default plugin here, 39 | // so that we don't need to reimplement all the methods. 40 | plugin.DefaultPlugin 41 | } 42 | 43 | type LimitReqConf struct { 44 | Burst int `json:"burst"` 45 | Rate float64 `json:"rate"` 46 | 47 | limiter *rate.Limiter 48 | } 49 | 50 | func (p *LimitReq) Name() string { 51 | return "limit-req" 52 | } 53 | 54 | // ParseConf is called when the configuration is changed. And its output is unique per route. 55 | func (p *LimitReq) ParseConf(in []byte) (interface{}, error) { 56 | conf := LimitReqConf{} 57 | err := json.Unmarshal(in, &conf) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | limiter := rate.NewLimiter(rate.Limit(conf.Rate), conf.Burst) 63 | // the conf can be used to store route scope data 64 | conf.limiter = limiter 65 | return conf, nil 66 | } 67 | 68 | // RequestFilter is called when a request hits the route 69 | func (p *LimitReq) RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) { 70 | li := conf.(LimitReqConf).limiter 71 | rs := li.Reserve() 72 | if !rs.OK() { 73 | // limit rate exceeded 74 | log.Infof("limit req rate exceeded") 75 | // stop filters with this response 76 | w.WriteHeader(http.StatusServiceUnavailable) 77 | return 78 | } 79 | time.Sleep(rs.Delay()) 80 | } 81 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/limit_req_test.go: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one or more 2 | // contributor license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright ownership. 4 | // The ASF licenses this file to You under the Apache License, Version 2.0 5 | // (the "License"); you may not use this file except in compliance with 6 | // the License. You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | package plugins 16 | 17 | import ( 18 | "net/http" 19 | "net/http/httptest" 20 | "sync" 21 | "testing" 22 | "time" 23 | 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func TestLimitReq(t *testing.T) { 28 | in := []byte(`{"rate":5,"burst":1}`) 29 | lr := &LimitReq{} 30 | conf, err := lr.ParseConf(in) 31 | assert.Nil(t, err) 32 | 33 | start := time.Now() 34 | n := 6 35 | var wg sync.WaitGroup 36 | res := make([]*http.Response, n) 37 | for i := 0; i < n; i++ { 38 | wg.Add(1) 39 | go func(i int) { 40 | w := httptest.NewRecorder() 41 | lr.RequestFilter(conf, w, nil) 42 | resp := w.Result() 43 | res[i] = resp 44 | wg.Done() 45 | }(i) 46 | } 47 | wg.Wait() 48 | 49 | rejectN := 0 50 | for _, r := range res { 51 | if r.StatusCode == 503 { 52 | rejectN++ 53 | } 54 | } 55 | assert.Equal(t, 0, rejectN) 56 | t.Logf("Start: %v, now: %v", start, time.Now()) 57 | assert.True(t, time.Since(start) >= 1*time.Second) 58 | } 59 | 60 | func TestLimitReq_YouShouldNotPass(t *testing.T) { 61 | in := []byte(`{}`) 62 | lr := &LimitReq{} 63 | conf, err := lr.ParseConf(in) 64 | assert.Nil(t, err) 65 | 66 | w := httptest.NewRecorder() 67 | lr.RequestFilter(conf, w, nil) 68 | resp := w.Result() 69 | assert.Equal(t, 503, resp.StatusCode) 70 | } 71 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/request_body_rewrite.go: -------------------------------------------------------------------------------- 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 | } 65 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/request_body_rewrite_test.go: -------------------------------------------------------------------------------- 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 | } 133 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/response_rewrite.go: -------------------------------------------------------------------------------- 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 | "bytes" 22 | "encoding/json" 23 | "fmt" 24 | "regexp" 25 | 26 | pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http" 27 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 28 | "github.com/apache/apisix-go-plugin-runner/pkg/plugin" 29 | ) 30 | 31 | func init() { 32 | err := plugin.RegisterPlugin(&ResponseRewrite{}) 33 | if err != nil { 34 | log.Fatalf("failed to register plugin response-rewrite: %s", err) 35 | } 36 | } 37 | 38 | type RegexFilter struct { 39 | Regex string `json:"regex"` 40 | Scope string `json:"scope"` 41 | Replace string `json:"replace"` 42 | 43 | regexComplied *regexp.Regexp 44 | } 45 | 46 | // ResponseRewrite is a demo to show how to rewrite response data. 47 | type ResponseRewrite struct { 48 | // Embed the default plugin here, 49 | // so that we don't need to reimplement all the methods. 50 | plugin.DefaultPlugin 51 | } 52 | 53 | type ResponseRewriteConf struct { 54 | Status int `json:"status"` 55 | Headers map[string]string `json:"headers"` 56 | Body string `json:"body"` 57 | Filters []RegexFilter `json:"filters"` 58 | } 59 | 60 | func (p *ResponseRewrite) Name() string { 61 | return "response-rewrite" 62 | } 63 | 64 | func (p *ResponseRewrite) ParseConf(in []byte) (interface{}, error) { 65 | conf := ResponseRewriteConf{} 66 | err := json.Unmarshal(in, &conf) 67 | if err != nil { 68 | return nil, err 69 | } 70 | for i := 0; i < len(conf.Filters); i++ { 71 | if reg, err := regexp.Compile(conf.Filters[i].Regex); err != nil { 72 | return nil, fmt.Errorf("failed to compile regex `%s`: %v", 73 | conf.Filters[i].Regex, err) 74 | } else { 75 | conf.Filters[i].regexComplied = reg 76 | } 77 | } 78 | return conf, nil 79 | } 80 | 81 | func (p *ResponseRewrite) ResponseFilter(conf interface{}, w pkgHTTP.Response) { 82 | cfg := conf.(ResponseRewriteConf) 83 | if cfg.Status > 0 { 84 | w.WriteHeader(200) 85 | } 86 | 87 | w.Header().Set("X-Resp-A6-Runner", "Go") 88 | if len(cfg.Headers) > 0 { 89 | for k, v := range cfg.Headers { 90 | w.Header().Set(k, v) 91 | } 92 | } 93 | 94 | body := []byte(cfg.Body) 95 | if len(cfg.Filters) > 0 { 96 | originBody, err := w.ReadBody() 97 | if err != nil { 98 | log.Errorf("failed to read response body: ", err) 99 | return 100 | } 101 | matched := false 102 | for i := 0; i < len(cfg.Filters); i++ { 103 | f := cfg.Filters[i] 104 | found := f.regexComplied.Find(originBody) 105 | if found != nil { 106 | matched = true 107 | if f.Scope == "once" { 108 | originBody = bytes.Replace(originBody, found, []byte(f.Replace), 1) 109 | } else if f.Scope == "global" { 110 | originBody = bytes.ReplaceAll(originBody, found, []byte(f.Replace)) 111 | } 112 | } 113 | } 114 | if matched { 115 | body = originBody 116 | goto write 117 | } 118 | // When configuring the Filters field, the Body field will be invalid. 119 | return 120 | } 121 | 122 | if len(body) == 0 { 123 | return 124 | } 125 | write: 126 | _, err := w.Write(body) 127 | if err != nil { 128 | log.Errorf("failed to write: %s", err) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/response_rewrite_test.go: -------------------------------------------------------------------------------- 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 | "io/ioutil" 22 | "testing" 23 | 24 | pkgHTTPTest "github.com/apache/apisix-go-plugin-runner/pkg/httptest" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestResponseRewrite(t *testing.T) { 30 | in := []byte(`{"status":200, "headers":{"X-Server-Id":"9527"},"body":"response rewrite"}`) 31 | rr := &ResponseRewrite{} 32 | conf, err := rr.ParseConf(in) 33 | assert.Nil(t, err) 34 | assert.Equal(t, 200, conf.(ResponseRewriteConf).Status) 35 | assert.Equal(t, "9527", conf.(ResponseRewriteConf).Headers["X-Server-Id"]) 36 | assert.Equal(t, "response rewrite", conf.(ResponseRewriteConf).Body) 37 | 38 | w := pkgHTTPTest.NewRecorder() 39 | w.Code = 502 40 | w.HeaderMap.Set("X-Resp-A6-Runner", "Java") 41 | rr.ResponseFilter(conf, w) 42 | 43 | body, _ := ioutil.ReadAll(w.Body) 44 | assert.Equal(t, 200, w.StatusCode()) 45 | assert.Equal(t, "Go", w.Header().Get("X-Resp-A6-Runner")) 46 | assert.Equal(t, "9527", w.Header().Get("X-Server-Id")) 47 | assert.Equal(t, "response rewrite", string(body)) 48 | } 49 | 50 | func TestResponseRewrite_BadConf(t *testing.T) { 51 | in := []byte(``) 52 | rr := &ResponseRewrite{} 53 | _, err := rr.ParseConf(in) 54 | assert.NotNil(t, err) 55 | } 56 | 57 | func TestResponseRewrite_ConfEmpty(t *testing.T) { 58 | in := []byte(`{}`) 59 | rr := &ResponseRewrite{} 60 | conf, err := rr.ParseConf(in) 61 | assert.Nil(t, err) 62 | assert.Equal(t, 0, conf.(ResponseRewriteConf).Status) 63 | assert.Equal(t, 0, len(conf.(ResponseRewriteConf).Headers)) 64 | assert.Equal(t, "", conf.(ResponseRewriteConf).Body) 65 | 66 | w := pkgHTTPTest.NewRecorder() 67 | w.Code = 502 68 | w.HeaderMap.Set("X-Resp-A6-Runner", "Java") 69 | rr.ResponseFilter(conf, w) 70 | assert.Equal(t, 502, w.StatusCode()) 71 | assert.Equal(t, "Go", w.Header().Get("X-Resp-A6-Runner")) 72 | assert.Equal(t, "", conf.(ResponseRewriteConf).Body) 73 | } 74 | 75 | func TestResponseRewrite_ReplaceGlobal(t *testing.T) { 76 | in := []byte(`{"filters":[{"regex":"world","scope":"global","replace":"golang"}]}`) 77 | rr := &ResponseRewrite{} 78 | conf, err := rr.ParseConf(in) 79 | assert.Nil(t, err) 80 | assert.Equal(t, 1, len(conf.(ResponseRewriteConf).Filters)) 81 | 82 | w := pkgHTTPTest.NewRecorder() 83 | w.Code = 200 84 | w.OriginBody = []byte("hello world world") 85 | rr.ResponseFilter(conf, w) 86 | assert.Equal(t, 200, w.StatusCode()) 87 | body, _ := ioutil.ReadAll(w.Body) 88 | assert.Equal(t, "hello golang golang", string(body)) 89 | } 90 | 91 | func TestResponseRewrite_ReplaceOnce(t *testing.T) { 92 | in := []byte(`{"filters":[{"regex":"world","scope":"once","replace":"golang"}]}`) 93 | rr := &ResponseRewrite{} 94 | conf, err := rr.ParseConf(in) 95 | assert.Nil(t, err) 96 | assert.Equal(t, 1, len(conf.(ResponseRewriteConf).Filters)) 97 | 98 | w := pkgHTTPTest.NewRecorder() 99 | w.Code = 200 100 | w.OriginBody = []byte("hello world world") 101 | rr.ResponseFilter(conf, w) 102 | assert.Equal(t, 200, w.StatusCode()) 103 | body, _ := ioutil.ReadAll(w.Body) 104 | assert.Equal(t, "hello golang world", string(body)) 105 | } 106 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/say.go: -------------------------------------------------------------------------------- 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 | func init() { 30 | err := plugin.RegisterPlugin(&Say{}) 31 | if err != nil { 32 | log.Fatalf("failed to register plugin say: %s", err) 33 | } 34 | } 35 | 36 | // Say is a demo to show how to return data directly instead of proxying 37 | // it to the upstream. 38 | type Say struct { 39 | // Embed the default plugin here, 40 | // so that we don't need to reimplement all the methods. 41 | plugin.DefaultPlugin 42 | } 43 | 44 | type SayConf struct { 45 | Body string `json:"body"` 46 | } 47 | 48 | func (p *Say) Name() string { 49 | return "say" 50 | } 51 | 52 | func (p *Say) ParseConf(in []byte) (interface{}, error) { 53 | conf := SayConf{} 54 | err := json.Unmarshal(in, &conf) 55 | return conf, err 56 | } 57 | 58 | func (p *Say) RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) { 59 | body := conf.(SayConf).Body 60 | if len(body) == 0 { 61 | return 62 | } 63 | 64 | w.Header().Add("X-Resp-A6-Runner", "Go") 65 | _, err := w.Write([]byte(body)) 66 | if err != nil { 67 | log.Errorf("failed to write: %s", err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /cmd/go-runner/plugins/say_test.go: -------------------------------------------------------------------------------- 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 | "io/ioutil" 22 | "net/http/httptest" 23 | "testing" 24 | 25 | "github.com/stretchr/testify/assert" 26 | ) 27 | 28 | func TestSay(t *testing.T) { 29 | in := []byte(`{"body":"hello"}`) 30 | say := &Say{} 31 | conf, err := say.ParseConf(in) 32 | assert.Nil(t, err) 33 | assert.Equal(t, "hello", conf.(SayConf).Body) 34 | 35 | w := httptest.NewRecorder() 36 | say.RequestFilter(conf, w, nil) 37 | resp := w.Result() 38 | body, _ := ioutil.ReadAll(resp.Body) 39 | assert.Equal(t, 200, resp.StatusCode) 40 | assert.Equal(t, "Go", resp.Header.Get("X-Resp-A6-Runner")) 41 | assert.Equal(t, "hello", string(body)) 42 | } 43 | 44 | func TestSay_BadConf(t *testing.T) { 45 | in := []byte(``) 46 | say := &Say{} 47 | _, err := say.ParseConf(in) 48 | assert.NotNil(t, err) 49 | } 50 | 51 | func TestSay_NoBody(t *testing.T) { 52 | in := []byte(`{}`) 53 | say := &Say{} 54 | conf, err := say.ParseConf(in) 55 | assert.Nil(t, err) 56 | assert.Equal(t, "", conf.(SayConf).Body) 57 | 58 | w := httptest.NewRecorder() 59 | say.RequestFilter(conf, w, nil) 60 | resp := w.Result() 61 | assert.Equal(t, "", resp.Header.Get("X-Resp-A6-Runner")) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/go-runner/version.go: -------------------------------------------------------------------------------- 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 main 19 | 20 | import ( 21 | "bytes" 22 | "fmt" 23 | "runtime" 24 | ) 25 | 26 | var ( 27 | // The following fields are populated at build time using -ldflags -X. 28 | _buildVersion = "unknown" 29 | _buildGitRevision = "unknown" 30 | _buildOS = "unknown" 31 | 32 | _buildGoVersion = runtime.Version() 33 | _runningOS = runtime.GOOS + "/" + runtime.GOARCH 34 | ) 35 | 36 | // shortVersion produces a single-line version info with format: 37 | // -- 38 | func shortVersion() string { 39 | return fmt.Sprintf("%s-%s-%s", _buildVersion, _buildGitRevision, _buildGoVersion) 40 | } 41 | 42 | // longVersion produces a verbose version info with format: 43 | // Version: xxx 44 | // Git SHA: xxx 45 | // GO Version: xxx 46 | // Running OS/Arch: xxx/xxx 47 | // Building OS/Arch: xxx/xxx 48 | func longVersion() string { 49 | buf := bytes.NewBuffer(nil) 50 | fmt.Fprintln(buf, "Version:", _buildVersion) 51 | fmt.Fprintln(buf, "Git SHA:", _buildGitRevision) 52 | fmt.Fprintln(buf, "Go Version:", _buildGoVersion) 53 | fmt.Fprintln(buf, "Building OS/Arch:", _buildOS) 54 | fmt.Fprintln(buf, "Running OS/Arch:", _runningOS) 55 | return buf.String() 56 | } 57 | -------------------------------------------------------------------------------- /docs/assets/images/runner-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apache/apisix-go-plugin-runner/b0d6d23308bfaeb8548cbc2295d42dd3094d5202/docs/assets/images/runner-overview.png -------------------------------------------------------------------------------- /docs/en/latest/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 0.5, 3 | "sidebar": [ 4 | { 5 | "type": "doc", 6 | "id": "getting-started" 7 | }, 8 | { 9 | "type": "doc", 10 | "id": "developer-guide" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /docs/en/latest/developer-guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Developer Guide 3 | --- 4 | 5 | 23 | 24 | ## Overview 25 | 26 | This documentation explains how to develop this project. 27 | 28 | ## Build 29 | 30 | Run `make build`. Then you can run `APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock ./go-runner run` 31 | to start it. 32 | 33 | ## Test 34 | 35 | Run `make test`. 36 | -------------------------------------------------------------------------------- /docs/en/latest/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting started 3 | --- 4 | 5 | 23 | 24 | ## Prerequisites 25 | 26 | ### Compatibility with Golang 27 | * Go (>= 1.15) 28 | 29 | ### Compatibility with Apache APISIX 30 | 31 | The following table describes the compatibility between apisix-go-plugin-runner and 32 | [Apache APISIX](https://apisix.apache.org). 33 | 34 | | apisix-go-plugin-runner | Apache APISIX | 35 | |------------------------:|--------------------------------------:| 36 | | `master` | `master` is recommended. | 37 | | `0.5.0` | `>= 3.0.0`, `3.0.0` is recommended. | 38 | | `0.4.0` | `>= 2.14.1`, `2.14.1` is recommended. | 39 | | `0.3.0` | `>= 2.13.0`, `2.13.0` is recommended. | 40 | | `0.2.0` | `>= 2.9.0`, `2.9.0` is recommended. | 41 | | `0.1.0` | `>= 2.7.0`, `2.7.0` is recommended. | 42 | 43 | ## Installation 44 | 45 | For now, we need to use Go Runner as a library. `cmd/go-runner` in this project is an official example showing how to use the Go Runner SDK. 46 | We will also support loading pre-compiled plugins through the Go Plugin mechanism later. 47 | 48 | ## Development 49 | 50 | ### Developing with the Go Runner SDK 51 | 52 | ```bash 53 | $ tree cmd/go-runner 54 | cmd/go-runner 55 | ├── main.go 56 | ├── main_test.go 57 | ├── plugins 58 | │ ├── say.go 59 | │ └── say_test.go 60 | └── version.go 61 | ``` 62 | 63 | Above is the directory structure of the official example. `main.go` is the entry point, where the most critical part is: 64 | 65 | ```go 66 | cfg := runner.RunnerConfig{} 67 | ... 68 | runner.Run(cfg) 69 | ``` 70 | 71 | `RunnerConfig` can be used to control the log level and log output location. 72 | 73 | `runner.Run` will make the application listen to the target socket path, receive requests and execute the registered plugins. The application will remain in this state until it exits. 74 | 75 | Then let's look at the plugin implementation. 76 | 77 | Open `plugins/say.go`. 78 | 79 | ```go 80 | func init() { 81 | err := plugin.RegisterPlugin(&Say{}) 82 | if err ! = nil { 83 | log.Fatalf("failed to register plugin say: %s", err) 84 | } 85 | } 86 | ``` 87 | 88 | Since `main.go` imports the plugins package, 89 | 90 | ```go 91 | import ( 92 | ... 93 | _ "github.com/apache/apisix-go-plugin-runner/cmd/go-runner/plugins" 94 | ... 95 | ) 96 | ``` 97 | 98 | in this way we register `Say` with `plugin.RegisterPlugin` before executing `runner.Run`. 99 | 100 | `Say` needs to implement the following methods: 101 | 102 | The `Name` method returns the plugin name. 103 | 104 | ``` 105 | func (p *Say) Name() string { 106 | return "say" 107 | } 108 | ``` 109 | 110 | `ParseConf` will be called when the plugin configuration changes, parsing the configuration and returning the plugin specific configuration. 111 | 112 | ``` 113 | func (p *Say) ParseConf(in []byte) (interface{}, error) { 114 | conf := SayConf{} 115 | err := json.Unmarshal(in, &conf) 116 | return conf, err 117 | } 118 | ``` 119 | 120 | The configuration of the plugin looks like this. 121 | 122 | ``` 123 | type SayConf struct { 124 | Body string `json: "body"` 125 | } 126 | ``` 127 | 128 | `RequestFilter` will be executed on every request with the say plugin configured. 129 | 130 | ``` 131 | func (p *Say) RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) { 132 | body := conf.(SayConf).Body 133 | if len(body) == 0 { 134 | return 135 | } 136 | 137 | w.Header().Add("X-Resp-A6-Runner", "Go") 138 | _, err := w.Write([]byte(body)) 139 | if err ! = nil { 140 | log.Errorf("failed to write: %s", err) 141 | } 142 | } 143 | ``` 144 | 145 | We can see that the RequestFilter takes the value of the body set in the configuration as the response body. If we call `Write` or `WriteHeader` of the `http.ResponseWriter` 146 | (respond directly in the plugin), it will response directly in the APISIX without touching the upstream. We can also set response headers in the plugin and touch the upstream 147 | at the same time by set RespHeader in `pkgHTTP.Request`. 148 | 149 | `ResponseFilter` supports rewriting the response during the response phase, we can see an example of its use in the ResponseRewrite plugin: 150 | 151 | ```go 152 | type RegexFilter struct { 153 | Regex string `json:"regex"` 154 | Scope string `json:"scope"` 155 | Replace string `json:"replace"` 156 | 157 | regexComplied *regexp.Regexp 158 | } 159 | 160 | type ResponseRewriteConf struct { 161 | Status int `json:"status"` 162 | Headers map[string]string `json:"headers"` 163 | Body string `json:"body"` 164 | Filters []RegexFilter `json:"filters"` 165 | } 166 | 167 | func (p *ResponseRewrite) ResponseFilter(conf interface{}, w pkgHTTP.Response) { 168 | cfg := conf.(ResponseRewriteConf) 169 | if cfg.Status > 0 { 170 | w.WriteHeader(200) 171 | } 172 | 173 | w.Header().Set("X-Resp-A6-Runner", "Go") 174 | if len(cfg.Headers) > 0 { 175 | for k, v := range cfg.Headers { 176 | w.Header().Set(k, v) 177 | } 178 | } 179 | 180 | body := []byte(cfg.Body) 181 | if len(cfg.Filters) > 0 { 182 | originBody, err := w.ReadBody() 183 | 184 | ...... 185 | 186 | for i := 0; i < len(cfg.Filters); i++ { 187 | f := cfg.Filters[i] 188 | found := f.regexComplied.Find(originBody) 189 | if found != nil { 190 | matched = true 191 | if f.Scope == "once" { 192 | originBody = bytes.Replace(originBody, found, []byte(f.Replace), 1) 193 | } else if f.Scope == "global" { 194 | originBody = bytes.ReplaceAll(originBody, found, []byte(f.Replace)) 195 | } 196 | } 197 | } 198 | 199 | ....... 200 | 201 | } 202 | if len(cfg.Body) == 0 { 203 | return 204 | } 205 | _, err := w.Write([]byte(cfg.Body)) 206 | if err != nil { 207 | log.Errorf("failed to write: %s", err) 208 | } 209 | } 210 | ``` 211 | 212 | We can see that `ResponseFilter` will rewrite the status, header, and response body of the response phase according to the configuration. 213 | 214 | In addition, we can also get the status and headers in the original response through `pkgHTTP.Response`. 215 | 216 | For the `pkgHTTP.Request` and `pkgHTTP.Response`, you can refer to the [API documentation](https://pkg.go.dev/github.com/apache/apisix-go-plugin-runner) provided by the Go Runner SDK. 217 | 218 | After building the application (`make build` in the example), we need to set some environment variables at runtime: 219 | 220 | * `APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock` 221 | 222 | Like this: 223 | 224 | ``` 225 | APISIX_LISTEN_ADDRESS=unix:/tmp/runner.sock ./go-runner run 226 | ``` 227 | 228 | The application will listen to `/tmp/runner.sock` when it runs. 229 | 230 | ### Setting up APISIX (debugging) 231 | 232 | First you need to have APISIX on your machine, which needs to be on the same instance as Go Runner. 233 | 234 | ![runner-overview](../../assets/images/runner-overview.png) 235 | 236 | The diagram above shows the workflow of APISIX on the left, while the plugin runner on the right is responsible for running external plugins written in different languages. apisix-go-plugin-runner is one such runner that supports Go. 237 | 238 | When you configure a plugin runner in APISIX, APISIX will treat the plugin runner as a child process of its own. This sub-process belongs to the same user as the APISIX process. When we restart or reload APISIX, the plugin runner will also be restarted. 239 | 240 | If you configure the ext-plugin-* plugin for a given route, a request to hit that route will trigger APISIX to make an RPC call to the plugin runner via a unix socket. The call is broken down into two phases. 241 | 242 | - ext-plugin-pre-req: executed during handling the request, before most of the APISIX built-in plugins (Lua language plugins) 243 | - ext-plugin-post-req: executed during handling the request, after most of the APISIX built-in plugins (Lua language plugins) 244 | - ext-plugin-post-resp: executed during handling the response, after most of the APISIX built-in plugins (Lua language plugins) 245 | 246 | Configure the timing of plugin runner execution as needed. 247 | 248 | The plugin runner handles the RPC calls, creates a mock request from it, then runs the plugins written in other languages and returns the results to APISIX. 249 | 250 | The order of execution of these plugins is defined in the ext-plugin-* plugin configuration. Like other plugins, they can be enabled and disabled on the fly. 251 | 252 | Let's go back to the examples. To show how to develop Go plugins, we first set APISIX into debug mode. Add the following configuration to config.yaml. 253 | 254 | ``` 255 | ext-plugin: 256 | path_for_test: /tmp/runner.sock 257 | ``` 258 | 259 | This configuration means that after hitting a routing rule, APISIX will make an RPC request to /tmp/runner.sock. 260 | 261 | Next, prepare the routing rule. 262 | 263 | ``` 264 | curl http://127.0.0.1:9080/apisix/admin/routes/1 -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d ' 265 | { 266 | "uri": "/get", 267 | "plugins": { 268 | "ext-plugin-pre-req": { 269 | "conf": [ 270 | { "name": "say", "value":"{\"body\":\"hello\"}"} 271 | ] 272 | } 273 | }, 274 | "upstream": { 275 | "type": "roundrobin", 276 | "nodes": { 277 | "127.0.0.1:1980": 1 278 | } 279 | } 280 | } 281 | ' 282 | ``` 283 | 284 | Note that the plugin name is configured in `name` and the plugin configuration (after JSON serialization) is placed in `value`. 285 | 286 | If you see `refresh cache and try again` warning on APISIX side and `key not found` warning on Runner side during development, this is due to configuration cache inconsistency. Because the Runner is not managed by APISIX in the development state, the internal state may be inconsistent. Don't worry, APISIX will retry. 287 | 288 | Then we request: curl 127.0.0.1:9080/get 289 | 290 | We can see that the interface returns hello and does not access anything upstream. 291 | 292 | ### Setting up APISIX (running) 293 | 294 | #### Setting up directly 295 | 296 | Here's an example of go-runner, you just need to configure the command line to run it inside ext-plugin: 297 | 298 | ``` 299 | ext-plugin: 300 | # path_for_test: /tmp/runner.sock 301 | cmd: ["/path/to/apisix-go-plugin-runner/go-runner", "run"] 302 | ``` 303 | 304 | APISIX will treat the plugin runner as a child process of its own, managing its entire lifecycle. 305 | 306 | APISIX will automatically assign a unix socket address for the runner to listen to when it starts. environment variables do not need to be set manually. 307 | 308 | #### Setting up in container 309 | 310 | First you need to prepare the go-runner binary. Use this command: 311 | 312 | ```shell 313 | make build 314 | ``` 315 | 316 | :::note 317 | When you use a Linux distribution such as Alpine Linux that is not based on standard glibc, you must turn off Golang's CGO support via the `CGO_ENABLED=0` environment variable to avoid libc ABI incompatibilities. 318 | 319 | If you want to use CGO, then you must build the binaries using the Go compiler and the C compiler in the same Linux distribution. 320 | ::: 321 | 322 | Then you need to rebuild the container image to include the go-runner binary. You can use the following Dockerfile: 323 | 324 | ``` 325 | FROM apache/apisix:2.15.0-debian 326 | 327 | COPY ./go-runner /usr/local/apisix-go-plugin-runner/go-runner 328 | ``` 329 | 330 | Finally, you can push and run your custom image, just configure the binary path and commands in the configuration file via `ext-plugin.cmd` to start APISIX with go plugin runner. 331 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/apache/apisix-go-plugin-runner 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/ReneKroon/ttlcache/v2 v2.4.0 7 | github.com/api7/ext-plugin-proto v0.6.1 8 | github.com/google/flatbuffers v2.0.0+incompatible 9 | github.com/spf13/cobra v1.2.1 10 | github.com/stretchr/testify v1.7.0 11 | github.com/thediveo/enumflag v0.10.1 12 | go.uber.org/multierr v1.7.0 // indirect 13 | go.uber.org/zap v1.17.0 14 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 15 | golang.org/x/tools v0.1.9 // indirect 16 | ) 17 | 18 | replace ( 19 | github.com/miekg/dns v1.0.14 => github.com/miekg/dns v1.1.25 20 | // github.com/thediveo/enumflag@v0.10.1 depends on github.com/spf13/cobra@v0.0.7 21 | github.com/spf13/cobra v0.0.7 => github.com/spf13/cobra v1.2.1 22 | ) 23 | -------------------------------------------------------------------------------- /internal/http/header.go: -------------------------------------------------------------------------------- 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 http 19 | 20 | import ( 21 | "net/http" 22 | 23 | "github.com/api7/ext-plugin-proto/go/A6" 24 | flatbuffers "github.com/google/flatbuffers/go" 25 | ) 26 | 27 | type ReadHeader interface { 28 | HeadersLength() int 29 | Headers(*A6.TextEntry, int) bool 30 | } 31 | 32 | type Header struct { 33 | hdr http.Header 34 | rawHdr http.Header 35 | 36 | deleteField map[string]struct{} 37 | } 38 | 39 | func newHeader(r ReadHeader) *Header { 40 | hh := http.Header{} 41 | size := r.HeadersLength() 42 | obj := A6.TextEntry{} 43 | for i := 0; i < size; i++ { 44 | if r.Headers(&obj, i) { 45 | hh.Add(string(obj.Name()), string(obj.Value())) 46 | } 47 | } 48 | 49 | return &Header{ 50 | hdr: http.Header{}, 51 | rawHdr: hh, 52 | 53 | deleteField: make(map[string]struct{}), 54 | } 55 | } 56 | 57 | func (h *Header) Set(key, value string) { 58 | h.hdr.Set(key, value) 59 | delete(h.deleteField, key) 60 | } 61 | 62 | func (h *Header) Del(key string) { 63 | if h.rawHdr.Get(key) != "" { 64 | h.deleteField[key] = struct{}{} 65 | h.rawHdr.Del(key) 66 | } 67 | 68 | h.hdr.Del(key) 69 | } 70 | 71 | func (h *Header) Get(key string) string { 72 | if v := h.hdr.Get(key); v != "" { 73 | return v 74 | } 75 | 76 | return h.rawHdr.Get(key) 77 | } 78 | 79 | // View 80 | // Deprecated: refactoring 81 | func (h *Header) View() http.Header { 82 | return h.hdr 83 | } 84 | 85 | func HeaderBuild(h *Header, builder *flatbuffers.Builder) []flatbuffers.UOffsetT { 86 | var hdrs []flatbuffers.UOffsetT 87 | 88 | // deleted 89 | for d := range h.deleteField { 90 | name := builder.CreateString(d) 91 | A6.TextEntryStart(builder) 92 | A6.TextEntryAddName(builder, name) 93 | te := A6.TextEntryEnd(builder) 94 | hdrs = append(hdrs, te) 95 | } 96 | 97 | // set 98 | for hKey, hVal := range h.hdr { 99 | if raw, ok := h.rawHdr[hKey]; !ok || raw[0] != hVal[0] { 100 | name := builder.CreateString(hKey) 101 | value := builder.CreateString(hVal[0]) 102 | A6.TextEntryStart(builder) 103 | A6.TextEntryAddName(builder, name) 104 | A6.TextEntryAddValue(builder, value) 105 | te := A6.TextEntryEnd(builder) 106 | hdrs = append(hdrs, te) 107 | } 108 | } 109 | 110 | return hdrs 111 | } 112 | -------------------------------------------------------------------------------- /internal/http/req-response.go: -------------------------------------------------------------------------------- 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 http 19 | 20 | import ( 21 | "bytes" 22 | "net/http" 23 | "sync" 24 | 25 | "github.com/api7/ext-plugin-proto/go/A6" 26 | hrc "github.com/api7/ext-plugin-proto/go/A6/HTTPReqCall" 27 | flatbuffers "github.com/google/flatbuffers/go" 28 | ) 29 | 30 | type ReqResponse struct { 31 | hdr http.Header 32 | body *bytes.Buffer 33 | code int 34 | } 35 | 36 | func (r *ReqResponse) Header() http.Header { 37 | if r.hdr == nil { 38 | r.hdr = http.Header{} 39 | } 40 | return r.hdr 41 | } 42 | 43 | func (r *ReqResponse) Write(b []byte) (int, error) { 44 | if r.body == nil { 45 | r.body = &bytes.Buffer{} 46 | } 47 | 48 | // APISIX will convert code 0 to 200, so we don't need to WriteHeader(http.StatusOK) 49 | // before writing the data 50 | return r.body.Write(b) 51 | } 52 | 53 | func (r *ReqResponse) WriteHeader(statusCode int) { 54 | if r.code != 0 { 55 | // official WriteHeader can't override written status 56 | // keep the same behavior 57 | return 58 | } 59 | r.code = statusCode 60 | } 61 | 62 | func (r *ReqResponse) Reset() { 63 | r.body = nil 64 | r.code = 0 65 | r.hdr = nil 66 | } 67 | 68 | func (r *ReqResponse) HasChange() bool { 69 | return !(r.body == nil && r.code == 0) 70 | } 71 | 72 | func (r *ReqResponse) FetchChanges(id uint32, builder *flatbuffers.Builder) bool { 73 | if !r.HasChange() { 74 | return false 75 | } 76 | 77 | hdrLen := len(r.hdr) 78 | var hdrVec flatbuffers.UOffsetT 79 | if hdrLen > 0 { 80 | hdrs := []flatbuffers.UOffsetT{} 81 | for n, arr := range r.hdr { 82 | for _, v := range arr { 83 | name := builder.CreateString(n) 84 | value := builder.CreateString(v) 85 | A6.TextEntryStart(builder) 86 | A6.TextEntryAddName(builder, name) 87 | A6.TextEntryAddValue(builder, value) 88 | te := A6.TextEntryEnd(builder) 89 | hdrs = append(hdrs, te) 90 | } 91 | } 92 | size := len(hdrs) 93 | hrc.StopStartHeadersVector(builder, size) 94 | for i := size - 1; i >= 0; i-- { 95 | te := hdrs[i] 96 | builder.PrependUOffsetT(te) 97 | } 98 | hdrVec = builder.EndVector(size) 99 | } 100 | 101 | var bodyVec flatbuffers.UOffsetT 102 | if r.body != nil { 103 | b := r.body.Bytes() 104 | if len(b) > 0 { 105 | bodyVec = builder.CreateByteVector(b) 106 | } 107 | } 108 | 109 | hrc.StopStart(builder) 110 | if r.code == 0 { 111 | hrc.StopAddStatus(builder, 200) 112 | } else { 113 | hrc.StopAddStatus(builder, uint16(r.code)) 114 | } 115 | if hdrLen > 0 { 116 | hrc.StopAddHeaders(builder, hdrVec) 117 | } 118 | if r.body != nil { 119 | hrc.StopAddBody(builder, bodyVec) 120 | } 121 | stop := hrc.StopEnd(builder) 122 | 123 | hrc.RespStart(builder) 124 | hrc.RespAddId(builder, id) 125 | hrc.RespAddActionType(builder, hrc.ActionStop) 126 | hrc.RespAddAction(builder, stop) 127 | res := hrc.RespEnd(builder) 128 | builder.Finish(res) 129 | 130 | return true 131 | } 132 | 133 | var reqRespPool = sync.Pool{ 134 | New: func() interface{} { 135 | return &ReqResponse{} 136 | }, 137 | } 138 | 139 | func CreateReqResponse() *ReqResponse { 140 | return reqRespPool.Get().(*ReqResponse) 141 | } 142 | 143 | func ReuseReqResponse(r *ReqResponse) { 144 | r.Reset() 145 | reqRespPool.Put(r) 146 | } 147 | -------------------------------------------------------------------------------- /internal/http/req-response_test.go: -------------------------------------------------------------------------------- 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 http 19 | 20 | import ( 21 | "net/http" 22 | "testing" 23 | 24 | "github.com/api7/ext-plugin-proto/go/A6" 25 | hrc "github.com/api7/ext-plugin-proto/go/A6/HTTPReqCall" 26 | flatbuffers "github.com/google/flatbuffers/go" 27 | "github.com/stretchr/testify/assert" 28 | 29 | "github.com/apache/apisix-go-plugin-runner/internal/util" 30 | ) 31 | 32 | func getStopAction(t *testing.T, b *flatbuffers.Builder) *hrc.Stop { 33 | buf := b.FinishedBytes() 34 | res := hrc.GetRootAsResp(buf, 0) 35 | tab := &flatbuffers.Table{} 36 | if res.Action(tab) { 37 | assert.Equal(t, hrc.ActionStop, res.ActionType()) 38 | stop := &hrc.Stop{} 39 | stop.Init(tab.Bytes, tab.Pos) 40 | return stop 41 | } 42 | return nil 43 | } 44 | 45 | func TestFetchChanges(t *testing.T) { 46 | r := CreateReqResponse() 47 | defer ReuseReqResponse(r) 48 | r.Write([]byte("hello")) 49 | h := r.Header() 50 | h.Set("foo", "bar") 51 | h.Add("foo", "baz") 52 | h.Add("cat", "dog") 53 | r.Write([]byte(" world")) 54 | assert.Equal(t, "dog", h.Get("cat")) 55 | builder := util.GetBuilder() 56 | assert.True(t, r.FetchChanges(1, builder)) 57 | 58 | stop := getStopAction(t, builder) 59 | assert.Equal(t, uint16(200), stop.Status()) 60 | assert.Equal(t, []byte("hello world"), stop.BodyBytes()) 61 | 62 | res := http.Header{} 63 | assert.Equal(t, 3, stop.HeadersLength()) 64 | for i := 0; i < stop.HeadersLength(); i++ { 65 | e := &A6.TextEntry{} 66 | stop.Headers(e, i) 67 | res.Add(string(e.Name()), string(e.Value())) 68 | } 69 | assert.Equal(t, h, res) 70 | } 71 | 72 | func TestFetchChangesEmptyResponse(t *testing.T) { 73 | r := CreateReqResponse() 74 | builder := util.GetBuilder() 75 | assert.False(t, r.FetchChanges(1, builder)) 76 | } 77 | 78 | func TestFetchChangesStatusOnly(t *testing.T) { 79 | r := CreateReqResponse() 80 | r.WriteHeader(400) 81 | builder := util.GetBuilder() 82 | assert.True(t, r.FetchChanges(1, builder)) 83 | 84 | stop := getStopAction(t, builder) 85 | assert.Equal(t, uint16(400), stop.Status()) 86 | } 87 | 88 | func TestWriteHeaderTwice(t *testing.T) { 89 | r := CreateReqResponse() 90 | r.WriteHeader(400) 91 | r.WriteHeader(503) 92 | builder := util.GetBuilder() 93 | assert.True(t, r.FetchChanges(1, builder)) 94 | 95 | stop := getStopAction(t, builder) 96 | assert.Equal(t, uint16(400), stop.Status()) 97 | } 98 | -------------------------------------------------------------------------------- /internal/http/request.go: -------------------------------------------------------------------------------- 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 http 19 | 20 | import ( 21 | "context" 22 | "encoding/binary" 23 | "net" 24 | "net/http" 25 | "net/url" 26 | "reflect" 27 | "sync" 28 | "time" 29 | 30 | "github.com/api7/ext-plugin-proto/go/A6" 31 | ei "github.com/api7/ext-plugin-proto/go/A6/ExtraInfo" 32 | hrc "github.com/api7/ext-plugin-proto/go/A6/HTTPReqCall" 33 | flatbuffers "github.com/google/flatbuffers/go" 34 | 35 | "github.com/apache/apisix-go-plugin-runner/internal/util" 36 | "github.com/apache/apisix-go-plugin-runner/pkg/common" 37 | pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http" 38 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 39 | ) 40 | 41 | type Request struct { 42 | // the root of the flatbuffers HTTPReqCall Request msg 43 | r *hrc.Req 44 | 45 | conn net.Conn 46 | extraInfoHeader []byte 47 | 48 | path []byte 49 | 50 | hdr *Header 51 | 52 | args url.Values 53 | rawArgs url.Values 54 | 55 | vars map[string][]byte 56 | body []byte 57 | 58 | ctx context.Context 59 | cancel context.CancelFunc 60 | 61 | respHdr http.Header 62 | } 63 | 64 | func (r *Request) ConfToken() uint32 { 65 | return r.r.ConfToken() 66 | } 67 | 68 | func (r *Request) ID() uint32 { 69 | return r.r.Id() 70 | } 71 | 72 | func (r *Request) SrcIP() net.IP { 73 | return r.r.SrcIpBytes() 74 | } 75 | 76 | func (r *Request) Method() string { 77 | return r.r.Method().String() 78 | } 79 | 80 | func (r *Request) Path() []byte { 81 | if r.path == nil { 82 | return r.r.Path() 83 | } 84 | return r.path 85 | } 86 | 87 | func (r *Request) SetPath(path []byte) { 88 | r.path = path 89 | } 90 | 91 | func (r *Request) Header() pkgHTTP.Header { 92 | if r.hdr == nil { 93 | r.hdr = newHeader(r.r) 94 | } 95 | 96 | return r.hdr 97 | } 98 | 99 | func (r *Request) RespHeader() http.Header { 100 | if r.respHdr == nil { 101 | r.respHdr = http.Header{} 102 | } 103 | return r.respHdr 104 | } 105 | 106 | func cloneUrlValues(oldV url.Values) url.Values { 107 | nv := 0 108 | for _, vv := range oldV { 109 | nv += len(vv) 110 | } 111 | sv := make([]string, nv) 112 | newV := make(url.Values, len(oldV)) 113 | for k, vv := range oldV { 114 | n := copy(sv, vv) 115 | newV[k] = sv[:n:n] 116 | sv = sv[n:] 117 | } 118 | return newV 119 | } 120 | 121 | func (r *Request) Args() url.Values { 122 | if r.args == nil { 123 | args := url.Values{} 124 | size := r.r.ArgsLength() 125 | obj := A6.TextEntry{} 126 | for i := 0; i < size; i++ { 127 | if r.r.Args(&obj, i) { 128 | args.Add(string(obj.Name()), string(obj.Value())) 129 | } 130 | } 131 | r.args = args 132 | r.rawArgs = cloneUrlValues(args) 133 | } 134 | return r.args 135 | } 136 | 137 | func (r *Request) Var(name string) ([]byte, error) { 138 | if r.vars == nil { 139 | r.vars = map[string][]byte{} 140 | } 141 | 142 | var v []byte 143 | var found bool 144 | 145 | if v, found = r.vars[name]; !found { 146 | var err error 147 | 148 | builder := util.GetBuilder() 149 | varName := builder.CreateString(name) 150 | ei.VarStart(builder) 151 | ei.VarAddName(builder, varName) 152 | varInfo := ei.VarEnd(builder) 153 | v, err = r.askExtraInfo(builder, ei.InfoVar, varInfo) 154 | util.PutBuilder(builder) 155 | 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | r.vars[name] = v 161 | } 162 | return v, nil 163 | } 164 | 165 | func (r *Request) Body() ([]byte, error) { 166 | if len(r.body) > 0 { 167 | return r.body, nil 168 | } 169 | 170 | builder := util.GetBuilder() 171 | ei.ReqBodyStart(builder) 172 | bodyInfo := ei.ReqBodyEnd(builder) 173 | v, err := r.askExtraInfo(builder, ei.InfoReqBody, bodyInfo) 174 | if err != nil { 175 | return nil, err 176 | } 177 | 178 | r.body = v 179 | return v, nil 180 | } 181 | 182 | func (r *Request) SetBody(body []byte) { 183 | r.body = body 184 | } 185 | 186 | func (r *Request) Reset() { 187 | defer r.cancel() 188 | r.path = nil 189 | r.hdr = nil 190 | r.args = nil 191 | 192 | r.vars = nil 193 | r.body = nil 194 | r.conn = nil 195 | r.ctx = nil 196 | r.respHdr = nil 197 | // Keep the fields below 198 | // r.extraInfoHeader = nil 199 | } 200 | 201 | func (r *Request) FetchChanges(id uint32, builder *flatbuffers.Builder) bool { 202 | if !r.hasChanges() { 203 | return false 204 | } 205 | 206 | var path flatbuffers.UOffsetT 207 | if r.path != nil { 208 | path = builder.CreateByteString(r.path) 209 | } 210 | 211 | var body flatbuffers.UOffsetT 212 | if r.body != nil { 213 | body = builder.CreateByteVector(r.body) 214 | } 215 | 216 | var hdrVec, respHdrVec flatbuffers.UOffsetT 217 | if r.hdr != nil { 218 | hdrs := HeaderBuild(r.hdr, builder) 219 | 220 | size := len(hdrs) 221 | hrc.RewriteStartHeadersVector(builder, size) 222 | for i := size - 1; i >= 0; i-- { 223 | te := hdrs[i] 224 | builder.PrependUOffsetT(te) 225 | } 226 | 227 | hdrVec = builder.EndVector(size) 228 | } 229 | 230 | if r.respHdr != nil { 231 | respHdrs := []flatbuffers.UOffsetT{} 232 | for n, arr := range r.respHdr { 233 | for _, v := range arr { 234 | name := builder.CreateString(n) 235 | value := builder.CreateString(v) 236 | A6.TextEntryStart(builder) 237 | A6.TextEntryAddName(builder, name) 238 | A6.TextEntryAddValue(builder, value) 239 | te := A6.TextEntryEnd(builder) 240 | respHdrs = append(respHdrs, te) 241 | } 242 | } 243 | size := len(respHdrs) 244 | hrc.RewriteStartRespHeadersVector(builder, size) 245 | for i := size - 1; i >= 0; i-- { 246 | te := respHdrs[i] 247 | builder.PrependUOffsetT(te) 248 | } 249 | respHdrVec = builder.EndVector(size) 250 | } 251 | 252 | var argsVec flatbuffers.UOffsetT 253 | if r.args != nil { 254 | args := []flatbuffers.UOffsetT{} 255 | oldArgs := r.rawArgs 256 | newArgs := r.args 257 | for n := range oldArgs { 258 | if _, ok := newArgs[n]; !ok { 259 | // deleted 260 | name := builder.CreateString(n) 261 | A6.TextEntryStart(builder) 262 | A6.TextEntryAddName(builder, name) 263 | te := A6.TextEntryEnd(builder) 264 | args = append(args, te) 265 | } 266 | } 267 | for n, v := range newArgs { 268 | if raw, ok := oldArgs[n]; !ok || !reflect.DeepEqual(raw, v) { 269 | // set / add 270 | for _, vv := range v { 271 | name := builder.CreateString(n) 272 | value := builder.CreateString(vv) 273 | A6.TextEntryStart(builder) 274 | A6.TextEntryAddName(builder, name) 275 | A6.TextEntryAddValue(builder, value) 276 | te := A6.TextEntryEnd(builder) 277 | args = append(args, te) 278 | } 279 | } 280 | } 281 | size := len(args) 282 | hrc.RewriteStartArgsVector(builder, size) 283 | for i := size - 1; i >= 0; i-- { 284 | te := args[i] 285 | builder.PrependUOffsetT(te) 286 | } 287 | argsVec = builder.EndVector(size) 288 | } 289 | 290 | hrc.RewriteStart(builder) 291 | if path > 0 { 292 | hrc.RewriteAddPath(builder, path) 293 | } 294 | if body > 0 { 295 | hrc.RewriteAddBody(builder, body) 296 | } 297 | if hdrVec > 0 { 298 | hrc.RewriteAddHeaders(builder, hdrVec) 299 | } 300 | if respHdrVec > 0 { 301 | hrc.RewriteAddRespHeaders(builder, respHdrVec) 302 | } 303 | if argsVec > 0 { 304 | hrc.RewriteAddArgs(builder, argsVec) 305 | } 306 | rewrite := hrc.RewriteEnd(builder) 307 | 308 | hrc.RespStart(builder) 309 | hrc.RespAddId(builder, id) 310 | hrc.RespAddActionType(builder, hrc.ActionRewrite) 311 | hrc.RespAddAction(builder, rewrite) 312 | res := hrc.RespEnd(builder) 313 | builder.Finish(res) 314 | 315 | return true 316 | } 317 | 318 | func (r *Request) BindConn(c net.Conn) { 319 | r.conn = c 320 | } 321 | 322 | func (r *Request) Context() context.Context { 323 | if r.ctx != nil { 324 | return r.ctx 325 | } 326 | return context.Background() 327 | } 328 | 329 | func (r *Request) hasChanges() bool { 330 | return r.path != nil || r.hdr != nil || 331 | r.args != nil || r.respHdr != nil || r.body != nil 332 | } 333 | 334 | func (r *Request) askExtraInfo(builder *flatbuffers.Builder, 335 | infoType ei.Info, info flatbuffers.UOffsetT) ([]byte, error) { 336 | 337 | ei.ReqStart(builder) 338 | ei.ReqAddInfoType(builder, infoType) 339 | ei.ReqAddInfo(builder, info) 340 | eiRes := ei.ReqEnd(builder) 341 | builder.Finish(eiRes) 342 | 343 | c := r.conn 344 | if len(r.extraInfoHeader) == 0 { 345 | r.extraInfoHeader = make([]byte, util.HeaderLen) 346 | } 347 | header := r.extraInfoHeader 348 | 349 | out := builder.FinishedBytes() 350 | size := len(out) 351 | binary.BigEndian.PutUint32(header, uint32(size)) 352 | header[0] = util.RPCExtraInfo 353 | 354 | n, err := util.WriteBytes(c, header, len(header)) 355 | if err != nil { 356 | util.WriteErr(n, err) 357 | return nil, common.ErrConnClosed 358 | } 359 | 360 | n, err = util.WriteBytes(c, out, size) 361 | if err != nil { 362 | util.WriteErr(n, err) 363 | return nil, common.ErrConnClosed 364 | } 365 | 366 | n, err = util.ReadBytes(c, header, util.HeaderLen) 367 | if util.ReadErr(n, err, util.HeaderLen) { 368 | return nil, common.ErrConnClosed 369 | } 370 | 371 | ty := header[0] 372 | header[0] = 0 373 | length := binary.BigEndian.Uint32(header) 374 | 375 | log.Infof("receive rpc type: %d data length: %d", ty, length) 376 | 377 | buf := make([]byte, length) 378 | n, err = util.ReadBytes(c, buf, int(length)) 379 | if util.ReadErr(n, err, int(length)) { 380 | return nil, common.ErrConnClosed 381 | } 382 | 383 | resp := ei.GetRootAsResp(buf, 0) 384 | res := resp.ResultBytes() 385 | return res, nil 386 | } 387 | 388 | var reqPool = sync.Pool{ 389 | New: func() interface{} { 390 | return &Request{} 391 | }, 392 | } 393 | 394 | func CreateRequest(buf []byte) *Request { 395 | req := reqPool.Get().(*Request) 396 | req.r = hrc.GetRootAsReq(buf, 0) 397 | // because apisix has an implicit 60s timeout, so set the timeout to 56 seconds(smaller than 60s) 398 | // so plugin writer can still break the execution with a custom response before the apisix implicit timeout. 399 | ctx, cancel := context.WithTimeout(context.Background(), 56*time.Second) 400 | req.ctx = ctx 401 | req.cancel = cancel 402 | return req 403 | } 404 | 405 | func ReuseRequest(r *Request) { 406 | r.Reset() 407 | reqPool.Put(r) 408 | } 409 | -------------------------------------------------------------------------------- /internal/http/response.go: -------------------------------------------------------------------------------- 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 http 19 | 20 | import ( 21 | "bytes" 22 | "encoding/binary" 23 | "net" 24 | "sync" 25 | 26 | "github.com/apache/apisix-go-plugin-runner/internal/util" 27 | "github.com/apache/apisix-go-plugin-runner/pkg/common" 28 | ei "github.com/api7/ext-plugin-proto/go/A6/ExtraInfo" 29 | 30 | pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http" 31 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 32 | hrc "github.com/api7/ext-plugin-proto/go/A6/HTTPRespCall" 33 | flatbuffers "github.com/google/flatbuffers/go" 34 | ) 35 | 36 | type Response struct { 37 | r *hrc.Req 38 | 39 | conn net.Conn 40 | extraInfoHeader []byte 41 | 42 | hdr *Header 43 | 44 | statusCode int 45 | 46 | body *bytes.Buffer 47 | 48 | vars map[string][]byte 49 | // originBody is read-only 50 | originBody []byte 51 | } 52 | 53 | func (r *Response) askExtraInfo(builder *flatbuffers.Builder, 54 | infoType ei.Info, info flatbuffers.UOffsetT) ([]byte, error) { 55 | 56 | ei.ReqStart(builder) 57 | ei.ReqAddInfoType(builder, infoType) 58 | ei.ReqAddInfo(builder, info) 59 | eiRes := ei.ReqEnd(builder) 60 | builder.Finish(eiRes) 61 | 62 | c := r.conn 63 | if len(r.extraInfoHeader) == 0 { 64 | r.extraInfoHeader = make([]byte, util.HeaderLen) 65 | } 66 | header := r.extraInfoHeader 67 | 68 | out := builder.FinishedBytes() 69 | size := len(out) 70 | binary.BigEndian.PutUint32(header, uint32(size)) 71 | header[0] = util.RPCExtraInfo 72 | 73 | n, err := util.WriteBytes(c, header, len(header)) 74 | if err != nil { 75 | util.WriteErr(n, err) 76 | return nil, common.ErrConnClosed 77 | } 78 | 79 | n, err = util.WriteBytes(c, out, size) 80 | if err != nil { 81 | util.WriteErr(n, err) 82 | return nil, common.ErrConnClosed 83 | } 84 | 85 | n, err = util.ReadBytes(c, header, util.HeaderLen) 86 | if util.ReadErr(n, err, util.HeaderLen) { 87 | return nil, common.ErrConnClosed 88 | } 89 | 90 | ty := header[0] 91 | header[0] = 0 92 | length := binary.BigEndian.Uint32(header) 93 | 94 | log.Infof("receive rpc type: %d data length: %d", ty, length) 95 | 96 | buf := make([]byte, length) 97 | n, err = util.ReadBytes(c, buf, int(length)) 98 | if util.ReadErr(n, err, int(length)) { 99 | return nil, common.ErrConnClosed 100 | } 101 | 102 | resp := ei.GetRootAsResp(buf, 0) 103 | res := resp.ResultBytes() 104 | return res, nil 105 | } 106 | 107 | func (r *Response) ID() uint32 { 108 | return r.r.Id() 109 | } 110 | 111 | func (r *Response) StatusCode() int { 112 | if r.statusCode == 0 { 113 | return int(r.r.Status()) 114 | } 115 | return r.statusCode 116 | } 117 | 118 | func (r *Response) Header() pkgHTTP.Header { 119 | if r.hdr == nil { 120 | r.hdr = newHeader(r.r) 121 | } 122 | 123 | return r.hdr 124 | } 125 | 126 | func (r *Response) Write(b []byte) (int, error) { 127 | if r.body == nil { 128 | r.body = &bytes.Buffer{} 129 | } 130 | 131 | return r.body.Write(b) 132 | } 133 | 134 | func (r *Response) Var(name string) ([]byte, error) { 135 | if r.vars == nil { 136 | r.vars = map[string][]byte{} 137 | } 138 | 139 | var v []byte 140 | var found bool 141 | 142 | if v, found = r.vars[name]; !found { 143 | var err error 144 | 145 | builder := util.GetBuilder() 146 | varName := builder.CreateString(name) 147 | ei.VarStart(builder) 148 | ei.VarAddName(builder, varName) 149 | varInfo := ei.VarEnd(builder) 150 | v, err = r.askExtraInfo(builder, ei.InfoVar, varInfo) 151 | util.PutBuilder(builder) 152 | 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | r.vars[name] = v 158 | } 159 | return v, nil 160 | } 161 | 162 | func (r *Response) ReadBody() ([]byte, error) { 163 | if len(r.originBody) > 0 { 164 | return r.originBody, nil 165 | } 166 | 167 | builder := util.GetBuilder() 168 | ei.ReqBodyStart(builder) 169 | bodyInfo := ei.ReqBodyEnd(builder) 170 | v, err := r.askExtraInfo(builder, ei.InfoRespBody, bodyInfo) 171 | util.PutBuilder(builder) 172 | 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | r.originBody = v 178 | return v, nil 179 | } 180 | 181 | func (r *Response) WriteHeader(statusCode int) { 182 | if r.statusCode != 0 { 183 | // official WriteHeader can't override written status 184 | // keep the same behavior 185 | return 186 | } 187 | r.statusCode = statusCode 188 | } 189 | 190 | func (r *Response) ConfToken() uint32 { 191 | return r.r.ConfToken() 192 | } 193 | 194 | func (r *Response) HasChange() bool { 195 | return !(r.body == nil && r.hdr == nil && r.statusCode == 0) 196 | } 197 | 198 | func (r *Response) FetchChanges(builder *flatbuffers.Builder) bool { 199 | if !r.HasChange() { 200 | return false 201 | } 202 | 203 | var hdrVec flatbuffers.UOffsetT 204 | if r.hdr != nil { 205 | hdrs := HeaderBuild(r.hdr, builder) 206 | 207 | size := len(hdrs) 208 | hrc.RespStartHeadersVector(builder, size) 209 | for i := size - 1; i >= 0; i-- { 210 | te := hdrs[i] 211 | builder.PrependUOffsetT(te) 212 | } 213 | 214 | hdrVec = builder.EndVector(size) 215 | } 216 | 217 | var bodyVec flatbuffers.UOffsetT 218 | if r.body != nil { 219 | b := r.body.Bytes() 220 | if len(b) > 0 { 221 | bodyVec = builder.CreateByteVector(b) 222 | } 223 | } 224 | 225 | hrc.RespStart(builder) 226 | if r.statusCode != 0 { 227 | hrc.RespAddStatus(builder, uint16(r.statusCode)) 228 | } 229 | if hdrVec > 0 { 230 | hrc.RespAddHeaders(builder, hdrVec) 231 | } 232 | if bodyVec > 0 { 233 | hrc.RespAddBody(builder, bodyVec) 234 | } 235 | hrc.RespAddId(builder, r.r.Id()) 236 | res := hrc.RespEnd(builder) 237 | builder.Finish(res) 238 | 239 | return true 240 | } 241 | 242 | func (r *Response) BindConn(c net.Conn) { 243 | r.conn = c 244 | } 245 | 246 | func (r *Response) Reset() { 247 | r.body = nil 248 | r.statusCode = 0 249 | r.hdr = nil 250 | r.conn = nil 251 | r.vars = nil 252 | r.originBody = nil 253 | } 254 | 255 | var respPool = sync.Pool{ 256 | New: func() interface{} { 257 | return &Response{} 258 | }, 259 | } 260 | 261 | func CreateResponse(buf []byte) *Response { 262 | resp := respPool.Get().(*Response) 263 | 264 | resp.r = hrc.GetRootAsReq(buf, 0) 265 | return resp 266 | } 267 | 268 | func ReuseResponse(r *Response) { 269 | r.Reset() 270 | respPool.Put(r) 271 | } 272 | -------------------------------------------------------------------------------- /internal/http/response_test.go: -------------------------------------------------------------------------------- 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 http 19 | 20 | import ( 21 | "encoding/binary" 22 | "net" 23 | "net/http" 24 | "testing" 25 | 26 | "github.com/apache/apisix-go-plugin-runner/pkg/common" 27 | ei "github.com/api7/ext-plugin-proto/go/A6/ExtraInfo" 28 | 29 | "github.com/apache/apisix-go-plugin-runner/internal/util" 30 | "github.com/api7/ext-plugin-proto/go/A6" 31 | hrc "github.com/api7/ext-plugin-proto/go/A6/HTTPRespCall" 32 | flatbuffers "github.com/google/flatbuffers/go" 33 | "github.com/stretchr/testify/assert" 34 | ) 35 | 36 | type respReqOpt struct { 37 | id int 38 | statusCode int 39 | headers []pair 40 | token int 41 | } 42 | 43 | func buildRespReq(opt respReqOpt) []byte { 44 | builder := flatbuffers.NewBuilder(1024) 45 | 46 | hdrLen := len(opt.headers) 47 | var hdrVec flatbuffers.UOffsetT 48 | if hdrLen > 0 { 49 | hdrs := []flatbuffers.UOffsetT{} 50 | for _, v := range opt.headers { 51 | name := builder.CreateString(v.name) 52 | value := builder.CreateString(v.value) 53 | A6.TextEntryStart(builder) 54 | A6.TextEntryAddName(builder, name) 55 | A6.TextEntryAddValue(builder, value) 56 | te := A6.TextEntryEnd(builder) 57 | hdrs = append(hdrs, te) 58 | } 59 | size := len(hdrs) 60 | hrc.ReqStartHeadersVector(builder, size) 61 | for i := size - 1; i >= 0; i-- { 62 | te := hdrs[i] 63 | builder.PrependUOffsetT(te) 64 | } 65 | hdrVec = builder.EndVector(size) 66 | } 67 | 68 | hrc.ReqStart(builder) 69 | hrc.ReqAddId(builder, uint32(opt.id)) 70 | hrc.ReqAddConfToken(builder, uint32(opt.token)) 71 | 72 | if opt.statusCode != 0 { 73 | hrc.ReqAddStatus(builder, uint16(opt.statusCode)) 74 | } 75 | if hdrVec > 0 { 76 | hrc.ReqAddHeaders(builder, hdrVec) 77 | } 78 | r := hrc.ReqEnd(builder) 79 | builder.Finish(r) 80 | return builder.FinishedBytes() 81 | } 82 | 83 | func TestResponse_ID(t *testing.T) { 84 | out := buildRespReq(respReqOpt{id: 1234}) 85 | r := CreateResponse(out) 86 | assert.Equal(t, 1234, int(r.ID())) 87 | ReuseResponse(r) 88 | } 89 | 90 | func TestResponse_ConfToken(t *testing.T) { 91 | out := buildRespReq(respReqOpt{token: 1234}) 92 | r := CreateResponse(out) 93 | assert.Equal(t, 1234, int(r.ConfToken())) 94 | ReuseResponse(r) 95 | } 96 | 97 | func TestResponse_StatusCode(t *testing.T) { 98 | out := buildRespReq(respReqOpt{statusCode: 200}) 99 | r := CreateResponse(out) 100 | assert.Equal(t, 200, r.StatusCode()) 101 | ReuseResponse(r) 102 | } 103 | 104 | func TestResponse_WriteHeader(t *testing.T) { 105 | out := buildRespReq(respReqOpt{statusCode: 200}) 106 | r := CreateResponse(out) 107 | 108 | r.WriteHeader(304) 109 | assert.Equal(t, 304, r.StatusCode()) 110 | 111 | builder := util.GetBuilder() 112 | assert.True(t, r.FetchChanges(builder)) 113 | resp := hrc.GetRootAsResp(builder.FinishedBytes(), 0) 114 | assert.Equal(t, 304, int(resp.Status())) 115 | ReuseResponse(r) 116 | } 117 | 118 | func TestResponse_TwiceWriteHeader(t *testing.T) { 119 | out := buildRespReq(respReqOpt{statusCode: 200}) 120 | r := CreateResponse(out) 121 | 122 | r.WriteHeader(304) 123 | r.WriteHeader(502) 124 | 125 | builder := util.GetBuilder() 126 | assert.True(t, r.FetchChanges(builder)) 127 | resp := hrc.GetRootAsResp(builder.FinishedBytes(), 0) 128 | assert.Equal(t, 304, int(resp.Status())) 129 | ReuseResponse(r) 130 | } 131 | 132 | func TestResponse_Header(t *testing.T) { 133 | out := buildRespReq(respReqOpt{headers: []pair{ 134 | {"k", "v"}, 135 | {"cache-control", "no-cache"}, 136 | {"cache-control", "no-store"}, 137 | {"cat", "dog"}, 138 | }}) 139 | r := CreateResponse(out) 140 | hdr := r.Header() 141 | assert.Equal(t, "v", hdr.Get("k")) 142 | assert.Equal(t, "no-cache", hdr.Get("Cache-Control")) 143 | assert.Equal(t, "no-cache", hdr.Get("cache-control")) 144 | 145 | hdr.Del("empty") 146 | hdr.Del("k") 147 | assert.Equal(t, "", hdr.Get("k")) 148 | 149 | hdr.Set("cache-control", "max-age=10s") 150 | assert.Equal(t, "max-age=10s", hdr.Get("Cache-Control")) 151 | hdr.Del("cache-Control") 152 | assert.Equal(t, "", hdr.Get("cache-control")) 153 | 154 | hdr.Set("k", "v2") 155 | hdr.Del("cat") 156 | 157 | builder := util.GetBuilder() 158 | assert.True(t, r.FetchChanges(builder)) 159 | resp := hrc.GetRootAsResp(builder.FinishedBytes(), 0) 160 | assert.Equal(t, 3, resp.HeadersLength()) 161 | 162 | exp := http.Header{} 163 | exp.Set("Cache-Control", "") 164 | exp.Set("cat", "") 165 | exp.Set("k", "v2") 166 | res := http.Header{} 167 | for i := 0; i < resp.HeadersLength(); i++ { 168 | e := &A6.TextEntry{} 169 | resp.Headers(e, i) 170 | res.Add(string(e.Name()), string(e.Value())) 171 | } 172 | assert.Equal(t, exp, res) 173 | ReuseResponse(r) 174 | } 175 | 176 | func TestResponse_Write(t *testing.T) { 177 | out := buildRespReq(respReqOpt{ 178 | id: 1234, 179 | statusCode: 200, 180 | headers: []pair{{"k", "v"}}, 181 | }) 182 | r := CreateResponse(out) 183 | r.Write([]byte("hello ")) 184 | r.Write([]byte("world")) 185 | 186 | builder := util.GetBuilder() 187 | assert.True(t, r.FetchChanges(builder)) 188 | resp := hrc.GetRootAsResp(builder.FinishedBytes(), 0) 189 | assert.Equal(t, 1234, int(resp.Id())) 190 | assert.Equal(t, 0, int(resp.Status())) 191 | assert.Equal(t, 0, resp.HeadersLength()) 192 | assert.Equal(t, []byte("hello world"), resp.BodyBytes()) 193 | ReuseResponse(r) 194 | } 195 | 196 | func TestResponse_Var(t *testing.T) { 197 | out := buildRespReq(respReqOpt{}) 198 | r := CreateResponse(out) 199 | 200 | cc, sc := net.Pipe() 201 | r.BindConn(cc) 202 | 203 | go func() { 204 | header := make([]byte, util.HeaderLen) 205 | n, err := util.ReadBytes(sc, header, util.HeaderLen) 206 | if util.ReadErr(n, err, util.HeaderLen) { 207 | return 208 | } 209 | 210 | ty := header[0] 211 | assert.Equal(t, byte(util.RPCExtraInfo), ty) 212 | header[0] = 0 213 | length := binary.BigEndian.Uint32(header) 214 | 215 | buf := make([]byte, length) 216 | n, err = util.ReadBytes(sc, buf, int(length)) 217 | if util.ReadErr(n, err, int(length)) { 218 | return 219 | } 220 | 221 | req := ei.GetRootAsReq(buf, 0) 222 | info := getVarInfo(t, req) 223 | assert.Equal(t, "request_time", string(info.Name())) 224 | 225 | builder := util.GetBuilder() 226 | res := builder.CreateByteVector([]byte("1.0")) 227 | ei.RespStart(builder) 228 | ei.RespAddResult(builder, res) 229 | eiRes := ei.RespEnd(builder) 230 | builder.Finish(eiRes) 231 | out := builder.FinishedBytes() 232 | size := len(out) 233 | binary.BigEndian.PutUint32(header, uint32(size)) 234 | header[0] = util.RPCExtraInfo 235 | 236 | n, err = util.WriteBytes(sc, header, len(header)) 237 | if err != nil { 238 | util.WriteErr(n, err) 239 | return 240 | } 241 | 242 | n, err = util.WriteBytes(sc, out, size) 243 | if err != nil { 244 | util.WriteErr(n, err) 245 | return 246 | } 247 | }() 248 | 249 | for i := 0; i < 2; i++ { 250 | v, err := r.Var("request_time") 251 | assert.Nil(t, err) 252 | assert.Equal(t, "1.0", string(v)) 253 | } 254 | } 255 | 256 | func TestResponse_Var_FailedToSendExtraInfoReq(t *testing.T) { 257 | out := buildRespReq(respReqOpt{}) 258 | r := CreateResponse(out) 259 | 260 | cc, sc := net.Pipe() 261 | r.BindConn(cc) 262 | 263 | go func() { 264 | header := make([]byte, util.HeaderLen) 265 | n, err := util.ReadBytes(sc, header, util.HeaderLen) 266 | if util.ReadErr(n, err, util.HeaderLen) { 267 | return 268 | } 269 | sc.Close() 270 | }() 271 | 272 | _, err := r.Var("request_time") 273 | assert.Equal(t, common.ErrConnClosed, err) 274 | } 275 | 276 | func TestResponse_FailedToReadExtraInfoResp(t *testing.T) { 277 | out := buildRespReq(respReqOpt{}) 278 | r := CreateResponse(out) 279 | 280 | cc, sc := net.Pipe() 281 | r.BindConn(cc) 282 | 283 | go func() { 284 | header := make([]byte, util.HeaderLen) 285 | n, err := util.ReadBytes(sc, header, util.HeaderLen) 286 | if util.ReadErr(n, err, util.HeaderLen) { 287 | return 288 | } 289 | 290 | ty := header[0] 291 | assert.Equal(t, byte(util.RPCExtraInfo), ty) 292 | header[0] = 0 293 | length := binary.BigEndian.Uint32(header) 294 | 295 | buf := make([]byte, length) 296 | n, err = util.ReadBytes(sc, buf, int(length)) 297 | if util.ReadErr(n, err, int(length)) { 298 | return 299 | } 300 | 301 | sc.Close() 302 | }() 303 | 304 | _, err := r.Var("request_time") 305 | assert.Equal(t, common.ErrConnClosed, err) 306 | } 307 | 308 | func TestRead(t *testing.T) { 309 | out := buildRespReq(respReqOpt{}) 310 | r := CreateResponse(out) 311 | 312 | cc, sc := net.Pipe() 313 | r.BindConn(cc) 314 | 315 | go func() { 316 | header := make([]byte, util.HeaderLen) 317 | n, err := util.ReadBytes(sc, header, util.HeaderLen) 318 | if util.ReadErr(n, err, util.HeaderLen) { 319 | return 320 | } 321 | 322 | ty := header[0] 323 | assert.Equal(t, byte(util.RPCExtraInfo), ty) 324 | header[0] = 0 325 | length := binary.BigEndian.Uint32(header) 326 | 327 | buf := make([]byte, length) 328 | n, err = util.ReadBytes(sc, buf, int(length)) 329 | if util.ReadErr(n, err, int(length)) { 330 | return 331 | } 332 | 333 | req := ei.GetRootAsReq(buf, 0) 334 | assert.Equal(t, ei.InfoRespBody, req.InfoType()) 335 | 336 | builder := util.GetBuilder() 337 | res := builder.CreateByteVector([]byte("Hello, Go Runner")) 338 | ei.RespStart(builder) 339 | ei.RespAddResult(builder, res) 340 | eiRes := ei.RespEnd(builder) 341 | builder.Finish(eiRes) 342 | out := builder.FinishedBytes() 343 | size := len(out) 344 | binary.BigEndian.PutUint32(header, uint32(size)) 345 | header[0] = util.RPCExtraInfo 346 | 347 | n, err = util.WriteBytes(sc, header, len(header)) 348 | if err != nil { 349 | util.WriteErr(n, err) 350 | return 351 | } 352 | 353 | n, err = util.WriteBytes(sc, out, size) 354 | if err != nil { 355 | util.WriteErr(n, err) 356 | return 357 | } 358 | }() 359 | 360 | v, err := r.ReadBody() 361 | assert.Nil(t, err) 362 | assert.Equal(t, "Hello, Go Runner", string(v)) 363 | } 364 | -------------------------------------------------------------------------------- /internal/plugin/conf.go: -------------------------------------------------------------------------------- 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 plugin 19 | 20 | import ( 21 | "strconv" 22 | "sync" 23 | "time" 24 | 25 | "github.com/ReneKroon/ttlcache/v2" 26 | A6 "github.com/api7/ext-plugin-proto/go/A6" 27 | pc "github.com/api7/ext-plugin-proto/go/A6/PrepareConf" 28 | flatbuffers "github.com/google/flatbuffers/go" 29 | 30 | "github.com/apache/apisix-go-plugin-runner/internal/util" 31 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 32 | ) 33 | 34 | var ( 35 | cache *ConfCache 36 | ) 37 | 38 | type ConfEntry struct { 39 | Name string 40 | Value interface{} 41 | } 42 | type RuleConf []ConfEntry 43 | 44 | type ConfCache struct { 45 | lock sync.Mutex 46 | 47 | tokenCache *ttlcache.Cache 48 | keyCache *ttlcache.Cache 49 | 50 | tokenCounter uint32 51 | } 52 | 53 | func newConfCache(ttl time.Duration) *ConfCache { 54 | cc := &ConfCache{ 55 | tokenCounter: 0, 56 | } 57 | for _, c := range []**ttlcache.Cache{&cc.tokenCache, &cc.keyCache} { 58 | cache := ttlcache.NewCache() 59 | err := cache.SetTTL(ttl) 60 | if err != nil { 61 | log.Fatalf("failed to set global ttl for cache: %s", err) 62 | } 63 | cache.SkipTTLExtensionOnHit(false) 64 | *c = cache 65 | } 66 | return cc 67 | } 68 | 69 | func (cc *ConfCache) Set(req *pc.Req) (uint32, error) { 70 | cc.lock.Lock() 71 | defer cc.lock.Unlock() 72 | 73 | key := string(req.Key()) 74 | // APISIX < 2.9 doesn't send the idempotent key 75 | if key != "" { 76 | res, err := cc.keyCache.Get(key) 77 | if err == nil { 78 | return res.(uint32), nil 79 | } 80 | 81 | if err != ttlcache.ErrNotFound { 82 | log.Errorf("failed to get cached token with key: %s", err) 83 | // recreate the token 84 | } 85 | } 86 | 87 | entries := RuleConf{} 88 | te := A6.TextEntry{} 89 | for i := 0; i < req.ConfLength(); i++ { 90 | if req.Conf(&te, i) { 91 | name := string(te.Name()) 92 | plugin := findPlugin(name) 93 | if plugin == nil { 94 | log.Warnf("can't find plugin %s, skip", name) 95 | continue 96 | } 97 | 98 | log.Infof("prepare conf for plugin %s", name) 99 | 100 | v := te.Value() 101 | conf, err := plugin.ParseConf(v) 102 | if err != nil { 103 | log.Errorf( 104 | "failed to parse configuration for plugin %s, configuration: %s, err: %v", 105 | name, string(v), err) 106 | continue 107 | } 108 | 109 | entries = append(entries, ConfEntry{ 110 | Name: name, 111 | Value: conf, 112 | }) 113 | } 114 | } 115 | 116 | cc.tokenCounter++ 117 | token := cc.tokenCounter 118 | err := cc.tokenCache.Set(strconv.FormatInt(int64(token), 10), entries) 119 | if err != nil { 120 | return 0, err 121 | } 122 | 123 | err = cc.keyCache.Set(key, token) 124 | return token, err 125 | } 126 | 127 | func (cc *ConfCache) SetInTest(token uint32, entries RuleConf) error { 128 | return cc.tokenCache.Set(strconv.FormatInt(int64(token), 10), entries) 129 | } 130 | 131 | func (cc *ConfCache) Get(token uint32) (RuleConf, error) { 132 | res, err := cc.tokenCache.Get(strconv.FormatInt(int64(token), 10)) 133 | if err != nil { 134 | return nil, err 135 | } 136 | return res.(RuleConf), err 137 | } 138 | 139 | func InitConfCache(ttl time.Duration) { 140 | cache = newConfCache(ttl) 141 | } 142 | 143 | func PrepareConf(buf []byte) (*flatbuffers.Builder, error) { 144 | req := pc.GetRootAsReq(buf, 0) 145 | 146 | token, err := cache.Set(req) 147 | if err != nil { 148 | return nil, err 149 | } 150 | 151 | builder := util.GetBuilder() 152 | pc.RespStart(builder) 153 | pc.RespAddConfToken(builder, token) 154 | root := pc.RespEnd(builder) 155 | builder.Finish(root) 156 | return builder, nil 157 | } 158 | 159 | func GetRuleConf(token uint32) (RuleConf, error) { 160 | return cache.Get(token) 161 | } 162 | 163 | func SetRuleConfInTest(token uint32, conf RuleConf) error { 164 | return cache.SetInTest(token, conf) 165 | } 166 | -------------------------------------------------------------------------------- /internal/plugin/conf_test.go: -------------------------------------------------------------------------------- 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 plugin 19 | 20 | import ( 21 | "errors" 22 | "sort" 23 | "strconv" 24 | "sync" 25 | "testing" 26 | "time" 27 | 28 | "github.com/ReneKroon/ttlcache/v2" 29 | A6 "github.com/api7/ext-plugin-proto/go/A6" 30 | pc "github.com/api7/ext-plugin-proto/go/A6/PrepareConf" 31 | flatbuffers "github.com/google/flatbuffers/go" 32 | "github.com/stretchr/testify/assert" 33 | ) 34 | 35 | func TestPrepareConf(t *testing.T) { 36 | InitConfCache(10 * time.Millisecond) 37 | 38 | builder := flatbuffers.NewBuilder(1024) 39 | pc.ReqStart(builder) 40 | root := pc.ReqEnd(builder) 41 | builder.Finish(root) 42 | b := builder.FinishedBytes() 43 | 44 | bd, _ := PrepareConf(b) 45 | out := bd.FinishedBytes() 46 | resp := pc.GetRootAsResp(out, 0) 47 | assert.Equal(t, uint32(1), resp.ConfToken()) 48 | 49 | bd, _ = PrepareConf(b) 50 | out = bd.FinishedBytes() 51 | resp = pc.GetRootAsResp(out, 0) 52 | assert.Equal(t, uint32(2), resp.ConfToken()) 53 | } 54 | 55 | func prepareConfWithData(builder *flatbuffers.Builder, arg ...flatbuffers.UOffsetT) { 56 | tes := []flatbuffers.UOffsetT{} 57 | for i := 0; i < len(arg); i += 2 { 58 | A6.TextEntryStart(builder) 59 | name := arg[i] 60 | value := arg[i+1] 61 | A6.TextEntryAddName(builder, name) 62 | A6.TextEntryAddValue(builder, value) 63 | te := A6.TextEntryEnd(builder) 64 | tes = append(tes, te) 65 | } 66 | 67 | pc.ReqStartConfVector(builder, len(tes)) 68 | for i := len(tes) - 1; i >= 0; i-- { 69 | builder.PrependUOffsetT(tes[i]) 70 | } 71 | v := builder.EndVector(len(tes)) 72 | 73 | pc.ReqStart(builder) 74 | pc.ReqAddConf(builder, v) 75 | root := pc.ReqEnd(builder) 76 | builder.Finish(root) 77 | b := builder.FinishedBytes() 78 | 79 | PrepareConf(b) 80 | } 81 | 82 | func TestPrepareConfUnknownPlugin(t *testing.T) { 83 | InitConfCache(1 * time.Millisecond) 84 | builder := flatbuffers.NewBuilder(1024) 85 | 86 | name := builder.CreateString("xxx") 87 | value := builder.CreateString(`{"body":"yes"}`) 88 | prepareConfWithData(builder, name, value) 89 | res, _ := GetRuleConf(1) 90 | assert.Equal(t, 0, len(res)) 91 | } 92 | 93 | func TestPrepareConfBadConf(t *testing.T) { 94 | InitConfCache(1 * time.Millisecond) 95 | builder := flatbuffers.NewBuilder(1024) 96 | 97 | f := func(in []byte) (conf interface{}, err error) { 98 | return nil, errors.New("ouch") 99 | } 100 | RegisterPlugin("bad_conf", f, emptyRequestFilter, emptyResponseFilter) 101 | name := builder.CreateString("bad_conf") 102 | value := builder.CreateString(`{"body":"yes"}`) 103 | prepareConfWithData(builder, name, value) 104 | res, _ := GetRuleConf(1) 105 | assert.Equal(t, 0, len(res)) 106 | } 107 | 108 | func TestPrepareConfConcurrentlyWithoutKey(t *testing.T) { 109 | InitConfCache(10 * time.Millisecond) 110 | 111 | builder := flatbuffers.NewBuilder(1024) 112 | pc.ReqStart(builder) 113 | root := pc.ReqEnd(builder) 114 | builder.Finish(root) 115 | b := builder.FinishedBytes() 116 | 117 | n := 10 118 | var wg sync.WaitGroup 119 | res := make([][]byte, n) 120 | for i := 0; i < n; i++ { 121 | wg.Add(1) 122 | go func(i int) { 123 | bd, err := PrepareConf(b) 124 | assert.Nil(t, err) 125 | res[i] = bd.FinishedBytes()[:] 126 | wg.Done() 127 | }(i) 128 | } 129 | wg.Wait() 130 | 131 | tokens := make([]int, n) 132 | for i := 0; i < n; i++ { 133 | resp := pc.GetRootAsResp(res[i], 0) 134 | tokens[i] = int(resp.ConfToken()) 135 | } 136 | 137 | sort.Ints(tokens) 138 | for i := 0; i < n; i++ { 139 | assert.Equal(t, i+1, tokens[i]) 140 | } 141 | } 142 | 143 | func TestPrepareConfConcurrentlyWithTheSameKey(t *testing.T) { 144 | InitConfCache(10 * time.Millisecond) 145 | 146 | builder := flatbuffers.NewBuilder(1024) 147 | key := builder.CreateString("key") 148 | pc.ReqStart(builder) 149 | pc.ReqAddKey(builder, key) 150 | root := pc.ReqEnd(builder) 151 | builder.Finish(root) 152 | b := builder.FinishedBytes() 153 | 154 | n := 10 155 | var wg sync.WaitGroup 156 | res := make([][]byte, n) 157 | for i := 0; i < n; i++ { 158 | wg.Add(1) 159 | go func(i int) { 160 | bd, err := PrepareConf(b) 161 | assert.Nil(t, err) 162 | res[i] = bd.FinishedBytes()[:] 163 | wg.Done() 164 | }(i) 165 | } 166 | wg.Wait() 167 | 168 | tokens := make([]int, n) 169 | for i := 0; i < n; i++ { 170 | resp := pc.GetRootAsResp(res[i], 0) 171 | tokens[i] = int(resp.ConfToken()) 172 | } 173 | 174 | sort.Ints(tokens) 175 | for i := 0; i < n; i++ { 176 | assert.Equal(t, 1, tokens[i]) 177 | } 178 | } 179 | 180 | func TestPrepareConfConcurrentlyWithTheDifferentKey(t *testing.T) { 181 | InitConfCache(10 * time.Millisecond) 182 | 183 | builder := flatbuffers.NewBuilder(1024) 184 | n := 10 185 | var wg sync.WaitGroup 186 | var lock sync.Mutex 187 | res := make([][]byte, n) 188 | for i := 0; i < n; i++ { 189 | wg.Add(1) 190 | go func(i int) { 191 | lock.Lock() 192 | key := builder.CreateString(strconv.Itoa(i)) 193 | pc.ReqStart(builder) 194 | pc.ReqAddKey(builder, key) 195 | root := pc.ReqEnd(builder) 196 | builder.Finish(root) 197 | b := builder.FinishedBytes() 198 | lock.Unlock() 199 | 200 | bd, err := PrepareConf(b) 201 | assert.Nil(t, err) 202 | res[i] = bd.FinishedBytes()[:] 203 | wg.Done() 204 | }(i) 205 | } 206 | wg.Wait() 207 | 208 | tokens := make([]int, n) 209 | for i := 0; i < n; i++ { 210 | resp := pc.GetRootAsResp(res[i], 0) 211 | tokens[i] = int(resp.ConfToken()) 212 | } 213 | 214 | sort.Ints(tokens) 215 | for i := 0; i < n; i++ { 216 | assert.Equal(t, i+1, tokens[i]) 217 | } 218 | } 219 | 220 | func TestGetRuleConf(t *testing.T) { 221 | InitConfCache(1 * time.Millisecond) 222 | builder := flatbuffers.NewBuilder(1024) 223 | pc.ReqStart(builder) 224 | root := pc.ReqEnd(builder) 225 | builder.Finish(root) 226 | b := builder.FinishedBytes() 227 | 228 | bd, _ := PrepareConf(b) 229 | out := bd.FinishedBytes() 230 | resp := pc.GetRootAsResp(out, 0) 231 | assert.Equal(t, uint32(1), resp.ConfToken()) 232 | 233 | res, _ := GetRuleConf(1) 234 | assert.Equal(t, 0, len(res)) 235 | 236 | time.Sleep(2 * time.Millisecond) 237 | _, err := GetRuleConf(1) 238 | assert.Equal(t, ttlcache.ErrNotFound, err) 239 | } 240 | 241 | func TestGetRuleConfCheckConf(t *testing.T) { 242 | RegisterPlugin("echo", emptyParseConf, emptyRequestFilter, emptyResponseFilter) 243 | InitConfCache(1 * time.Millisecond) 244 | builder := flatbuffers.NewBuilder(1024) 245 | 246 | name := builder.CreateString("echo") 247 | value := builder.CreateString(`{"body":"yes"}`) 248 | A6.TextEntryStart(builder) 249 | A6.TextEntryAddName(builder, name) 250 | A6.TextEntryAddValue(builder, value) 251 | te := A6.TextEntryEnd(builder) 252 | 253 | pc.ReqStartConfVector(builder, 1) 254 | builder.PrependUOffsetT(te) 255 | v := builder.EndVector(1) 256 | 257 | pc.ReqStart(builder) 258 | pc.ReqAddConf(builder, v) 259 | root := pc.ReqEnd(builder) 260 | builder.Finish(root) 261 | b := builder.FinishedBytes() 262 | 263 | PrepareConf(b) 264 | res, _ := GetRuleConf(1) 265 | assert.Equal(t, 1, len(res)) 266 | assert.Equal(t, "echo", res[0].Name) 267 | } 268 | -------------------------------------------------------------------------------- /internal/plugin/plugin.go: -------------------------------------------------------------------------------- 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 plugin 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "net" 24 | "net/http" 25 | "sync" 26 | 27 | hreqc "github.com/api7/ext-plugin-proto/go/A6/HTTPReqCall" 28 | hrespc "github.com/api7/ext-plugin-proto/go/A6/HTTPRespCall" 29 | flatbuffers "github.com/google/flatbuffers/go" 30 | 31 | inHTTP "github.com/apache/apisix-go-plugin-runner/internal/http" 32 | "github.com/apache/apisix-go-plugin-runner/internal/util" 33 | pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http" 34 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 35 | ) 36 | 37 | type ParseConfFunc func(in []byte) (conf interface{}, err error) 38 | type RequestFilterFunc func(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) 39 | type ResponseFilterFunc func(conf interface{}, w pkgHTTP.Response) 40 | 41 | type pluginOpts struct { 42 | ParseConf ParseConfFunc 43 | RequestFilter RequestFilterFunc 44 | ResponseFilter ResponseFilterFunc 45 | } 46 | 47 | type pluginRegistries struct { 48 | sync.Mutex 49 | opts map[string]*pluginOpts 50 | } 51 | 52 | type ErrPluginRegistered struct { 53 | name string 54 | } 55 | 56 | func (err ErrPluginRegistered) Error() string { 57 | return fmt.Sprintf("plugin %s registered", err.name) 58 | } 59 | 60 | var ( 61 | pluginRegistry = pluginRegistries{opts: map[string]*pluginOpts{}} 62 | 63 | ErrMissingName = errors.New("missing name") 64 | ErrMissingParseConfMethod = errors.New("missing ParseConf method") 65 | ErrMissingRequestFilterMethod = errors.New("missing RequestFilter method") 66 | ErrMissingResponseFilterMethod = errors.New("missing ResponseFilter method") 67 | 68 | RequestPhase = requestPhase{} 69 | ResponsePhase = responsePhase{} 70 | ) 71 | 72 | func RegisterPlugin(name string, pc ParseConfFunc, sv RequestFilterFunc, rsv ResponseFilterFunc) error { 73 | log.Infof("register plugin %s", name) 74 | 75 | if name == "" { 76 | return ErrMissingName 77 | } 78 | if pc == nil { 79 | return ErrMissingParseConfMethod 80 | } 81 | if sv == nil { 82 | return ErrMissingRequestFilterMethod 83 | } 84 | if rsv == nil { 85 | return ErrMissingResponseFilterMethod 86 | } 87 | 88 | opt := &pluginOpts{ 89 | ParseConf: pc, 90 | RequestFilter: sv, 91 | ResponseFilter: rsv, 92 | } 93 | pluginRegistry.Lock() 94 | defer pluginRegistry.Unlock() 95 | if _, found := pluginRegistry.opts[name]; found { 96 | return ErrPluginRegistered{name} 97 | } 98 | pluginRegistry.opts[name] = opt 99 | return nil 100 | } 101 | 102 | func findPlugin(name string) *pluginOpts { 103 | if opt, found := pluginRegistry.opts[name]; found { 104 | return opt 105 | } 106 | return nil 107 | } 108 | 109 | type requestPhase struct { 110 | } 111 | 112 | func (ph *requestPhase) filter(conf RuleConf, w *inHTTP.ReqResponse, r *inHTTP.Request) error { 113 | for _, c := range conf { 114 | plugin := findPlugin(c.Name) 115 | if plugin == nil { 116 | log.Warnf("can't find plugin %s, skip", c.Name) 117 | continue 118 | } 119 | 120 | log.Infof("run plugin %s", c.Name) 121 | 122 | plugin.RequestFilter(c.Value, w, r) 123 | 124 | if w.HasChange() { 125 | // response is generated, no need to continue 126 | break 127 | } 128 | } 129 | return nil 130 | } 131 | 132 | func (ph *requestPhase) builder(id uint32, resp *inHTTP.ReqResponse, req *inHTTP.Request) *flatbuffers.Builder { 133 | builder := util.GetBuilder() 134 | 135 | if resp != nil && resp.FetchChanges(id, builder) { 136 | return builder 137 | } 138 | 139 | if req != nil && req.FetchChanges(id, builder) { 140 | return builder 141 | } 142 | 143 | hreqc.RespStart(builder) 144 | hreqc.RespAddId(builder, id) 145 | res := hreqc.RespEnd(builder) 146 | builder.Finish(res) 147 | return builder 148 | } 149 | 150 | func HTTPReqCall(buf []byte, conn net.Conn) (*flatbuffers.Builder, error) { 151 | req := inHTTP.CreateRequest(buf) 152 | req.BindConn(conn) 153 | defer inHTTP.ReuseRequest(req) 154 | 155 | resp := inHTTP.CreateReqResponse() 156 | defer inHTTP.ReuseReqResponse(resp) 157 | 158 | token := req.ConfToken() 159 | conf, err := GetRuleConf(token) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | err = RequestPhase.filter(conf, resp, req) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | id := req.ID() 170 | builder := RequestPhase.builder(id, resp, req) 171 | return builder, nil 172 | } 173 | 174 | type responsePhase struct { 175 | } 176 | 177 | func (ph *responsePhase) filter(conf RuleConf, w *inHTTP.Response) error { 178 | for _, c := range conf { 179 | plugin := findPlugin(c.Name) 180 | if plugin == nil { 181 | log.Warnf("can't find plugin %s, skip", c.Name) 182 | continue 183 | } 184 | 185 | log.Infof("run plugin %s", c.Name) 186 | 187 | plugin.ResponseFilter(c.Value, w) 188 | 189 | if w.HasChange() { 190 | // response is generated, no need to continue 191 | break 192 | } 193 | } 194 | return nil 195 | } 196 | 197 | func (ph *responsePhase) builder(id uint32, resp *inHTTP.Response) *flatbuffers.Builder { 198 | builder := util.GetBuilder() 199 | if resp != nil && resp.FetchChanges(builder) { 200 | return builder 201 | } 202 | 203 | hrespc.RespStart(builder) 204 | hrespc.RespAddId(builder, id) 205 | res := hrespc.RespEnd(builder) 206 | builder.Finish(res) 207 | 208 | return builder 209 | } 210 | 211 | func HTTPRespCall(buf []byte, conn net.Conn) (*flatbuffers.Builder, error) { 212 | resp := inHTTP.CreateResponse(buf) 213 | resp.BindConn(conn) 214 | defer inHTTP.ReuseResponse(resp) 215 | 216 | token := resp.ConfToken() 217 | conf, err := GetRuleConf(token) 218 | if err != nil { 219 | return nil, err 220 | } 221 | 222 | err = ResponsePhase.filter(conf, resp) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | id := resp.ID() 228 | return ResponsePhase.builder(id, resp), nil 229 | } 230 | -------------------------------------------------------------------------------- /internal/server/error.go: -------------------------------------------------------------------------------- 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 server 19 | 20 | import ( 21 | "fmt" 22 | 23 | "github.com/ReneKroon/ttlcache/v2" 24 | A6Err "github.com/api7/ext-plugin-proto/go/A6/Err" 25 | flatbuffers "github.com/google/flatbuffers/go" 26 | 27 | "github.com/apache/apisix-go-plugin-runner/internal/util" 28 | ) 29 | 30 | type UnknownType struct { 31 | ty byte 32 | } 33 | 34 | func (err UnknownType) Error() string { 35 | return fmt.Sprintf("unknown type %d", err.ty) 36 | } 37 | 38 | func ReportError(err error) *flatbuffers.Builder { 39 | builder := util.GetBuilder() 40 | A6Err.RespStart(builder) 41 | 42 | var code A6Err.Code 43 | switch err { 44 | case ttlcache.ErrNotFound: 45 | code = A6Err.CodeCONF_TOKEN_NOT_FOUND 46 | default: 47 | switch err.(type) { 48 | case UnknownType: 49 | code = A6Err.CodeBAD_REQUEST 50 | default: 51 | code = A6Err.CodeSERVICE_UNAVAILABLE 52 | } 53 | } 54 | 55 | A6Err.RespAddCode(builder, code) 56 | resp := A6Err.RespEnd(builder) 57 | builder.Finish(resp) 58 | return builder 59 | } 60 | -------------------------------------------------------------------------------- /internal/server/error_test.go: -------------------------------------------------------------------------------- 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 server 19 | 20 | import ( 21 | "io" 22 | "testing" 23 | "time" 24 | 25 | "github.com/apache/apisix-go-plugin-runner/internal/plugin" 26 | A6Err "github.com/api7/ext-plugin-proto/go/A6/Err" 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | func TestReportErrorCacheToken(t *testing.T) { 31 | plugin.InitConfCache(10 * time.Millisecond) 32 | 33 | _, err := plugin.GetRuleConf(uint32(999999)) 34 | b := ReportError(err) 35 | out := b.FinishedBytes() 36 | resp := A6Err.GetRootAsResp(out, 0) 37 | assert.Equal(t, A6Err.CodeCONF_TOKEN_NOT_FOUND, resp.Code()) 38 | } 39 | 40 | func TestReportErrorUnknownType(t *testing.T) { 41 | b := ReportError(UnknownType{23}) 42 | out := b.FinishedBytes() 43 | resp := A6Err.GetRootAsResp(out, 0) 44 | assert.Equal(t, A6Err.CodeBAD_REQUEST, resp.Code()) 45 | } 46 | 47 | func TestReportErrorUnknownErr(t *testing.T) { 48 | b := ReportError(io.EOF) 49 | out := b.FinishedBytes() 50 | resp := A6Err.GetRootAsResp(out, 0) 51 | assert.Equal(t, A6Err.CodeSERVICE_UNAVAILABLE, resp.Code()) 52 | } 53 | -------------------------------------------------------------------------------- /internal/server/server.go: -------------------------------------------------------------------------------- 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 server 19 | 20 | import ( 21 | "encoding/binary" 22 | "fmt" 23 | "net" 24 | "os" 25 | "os/signal" 26 | "strconv" 27 | "strings" 28 | "syscall" 29 | "time" 30 | 31 | "github.com/ReneKroon/ttlcache/v2" 32 | flatbuffers "github.com/google/flatbuffers/go" 33 | 34 | "github.com/apache/apisix-go-plugin-runner/internal/plugin" 35 | "github.com/apache/apisix-go-plugin-runner/internal/util" 36 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 37 | ) 38 | 39 | const ( 40 | SockAddrEnv = "APISIX_LISTEN_ADDRESS" 41 | ConfCacheTTLEnv = "APISIX_CONF_EXPIRE_TIME" 42 | ) 43 | 44 | type handler func(buf []byte, conn net.Conn) (*flatbuffers.Builder, error) 45 | 46 | var ( 47 | typeHandlerMap = map[byte]handler{ 48 | util.RPCPrepareConf: func(buf []byte, conn net.Conn) (*flatbuffers.Builder, error) { 49 | return plugin.PrepareConf(buf) 50 | }, 51 | util.RPCHTTPReqCall: func(buf []byte, conn net.Conn) (*flatbuffers.Builder, error) { 52 | return plugin.HTTPReqCall(buf, conn) 53 | }, 54 | util.RPCHTTPRespCall: func(buf []byte, conn net.Conn) (*flatbuffers.Builder, error) { 55 | return plugin.HTTPRespCall(buf, conn) 56 | }, 57 | } 58 | ) 59 | 60 | func generateErrorReport(err error) *flatbuffers.Builder { 61 | if err == ttlcache.ErrNotFound { 62 | log.Warnf("%s", err) 63 | } else { 64 | log.Errorf("%s", err) 65 | } 66 | 67 | return ReportError(err) 68 | } 69 | 70 | func recoverPanic() { 71 | if err := recover(); err != nil { 72 | log.Errorf("panic recovered: %s", err) 73 | } 74 | } 75 | 76 | func dispatchRPC(ty byte, in []byte, conn net.Conn) (*flatbuffers.Builder, byte) { 77 | var err error 78 | var bd *flatbuffers.Builder 79 | hl, ok := typeHandlerMap[ty] 80 | if !ok { 81 | log.Warnf("unknown rpc type: %d", ty) 82 | return generateErrorReport(UnknownType{ty}), util.RPCError 83 | } 84 | 85 | bd, err = hl(in, conn) 86 | if err != nil { 87 | return generateErrorReport(err), util.RPCError 88 | } 89 | 90 | replaced, ok := checkIfDataTooLarge(bd) 91 | if !ok { 92 | return replaced, util.RPCError 93 | } 94 | 95 | return bd, ty 96 | } 97 | 98 | func checkIfDataTooLarge(bd *flatbuffers.Builder) (*flatbuffers.Builder, bool) { 99 | out := bd.FinishedBytes() 100 | size := len(out) 101 | if size < util.MaxDataSize { 102 | return bd, true 103 | } 104 | 105 | err := fmt.Errorf("the max length of data is %d but got %d", util.MaxDataSize, size) 106 | util.PutBuilder(bd) 107 | bd = generateErrorReport(err) 108 | 109 | return bd, false 110 | } 111 | 112 | func handleConn(c net.Conn) { 113 | defer recoverPanic() 114 | 115 | log.Infof("Client connected (%s)", c.RemoteAddr().Network()) 116 | defer c.Close() 117 | 118 | header := make([]byte, util.HeaderLen) 119 | for { 120 | n, err := util.ReadBytes(c, header, util.HeaderLen) 121 | if util.ReadErr(n, err, util.HeaderLen) { 122 | break 123 | } 124 | 125 | ty := header[0] 126 | // we only use last 3 bytes to store the length, so the first byte is 127 | // consider zero 128 | header[0] = 0 129 | length := binary.BigEndian.Uint32(header) 130 | 131 | log.Infof("receive rpc type: %d data length: %d", ty, length) 132 | 133 | buf := make([]byte, length) 134 | n, err = util.ReadBytes(c, buf, int(length)) 135 | if util.ReadErr(n, err, int(length)) { 136 | break 137 | } 138 | 139 | bd, respTy := dispatchRPC(ty, buf, c) 140 | out := bd.FinishedBytes() 141 | size := len(out) 142 | binary.BigEndian.PutUint32(header, uint32(size)) 143 | header[0] = respTy 144 | 145 | n, err = util.WriteBytes(c, header, len(header)) 146 | if err != nil { 147 | util.WriteErr(n, err) 148 | break 149 | } 150 | 151 | n, err = util.WriteBytes(c, out, size) 152 | if err != nil { 153 | util.WriteErr(n, err) 154 | break 155 | } 156 | 157 | util.PutBuilder(bd) 158 | } 159 | } 160 | 161 | func getConfCacheTTL() time.Duration { 162 | // ensure the conf cached in the runner expires after the token in APISIX 163 | amplificationFactor := 1.2 164 | ttl := os.Getenv(ConfCacheTTLEnv) 165 | if ttl == "" { 166 | return time.Duration(3600*amplificationFactor) * time.Second 167 | } 168 | 169 | n, err := strconv.Atoi(ttl) 170 | if err != nil || n <= 0 { 171 | log.Errorf("invalid cache ttl: %s", ttl) 172 | return 0 173 | } 174 | return time.Duration(float64(n)*amplificationFactor) * time.Second 175 | } 176 | 177 | func getSockAddr() string { 178 | path := os.Getenv(SockAddrEnv) 179 | if !strings.HasPrefix(path, "unix:") { 180 | log.Errorf("invalid socket address: %s", path) 181 | return "" 182 | } 183 | return path[len("unix:"):] 184 | } 185 | 186 | func Run() { 187 | ttl := getConfCacheTTL() 188 | if ttl == 0 { 189 | log.Fatalf("A valid conf cache ttl should be set via environment variable %s", 190 | ConfCacheTTLEnv) 191 | } 192 | log.Warnf("conf cache ttl is %v", ttl) 193 | 194 | plugin.InitConfCache(ttl) 195 | 196 | sockAddr := getSockAddr() 197 | if sockAddr == "" { 198 | log.Fatalf("A valid socket address should be set via environment variable %s", SockAddrEnv) 199 | } 200 | log.Warnf("listening to %s", sockAddr) 201 | 202 | // clean up sock file created by others 203 | if err := os.RemoveAll(sockAddr); err != nil { 204 | log.Fatalf("remove file %s: %s", sockAddr, err) 205 | } 206 | // clean up sock file created by me 207 | defer func() { 208 | if err := os.RemoveAll(sockAddr); err != nil { 209 | log.Errorf("remove file %s: %s", sockAddr, err) 210 | } 211 | }() 212 | 213 | l, err := net.Listen("unix", sockAddr) 214 | if err != nil { 215 | log.Fatalf("listen %s: %s", sockAddr, err) 216 | } 217 | defer l.Close() 218 | 219 | // the default socket permission is 0755, which prevents the 'nobody' worker process 220 | // from writing to it if the APISIX is run under root. 221 | err = os.Chmod(sockAddr, 0766) 222 | if err != nil { 223 | log.Fatalf("can't change mod for file %s: %s", sockAddr, err) 224 | } 225 | 226 | done := make(chan struct{}) 227 | quit := make(chan os.Signal, 1) 228 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 229 | 230 | go func() { 231 | for { 232 | conn, err := l.Accept() 233 | 234 | select { 235 | case <-done: 236 | // don't report the "use of closed network connection" error when the server 237 | // is exiting. 238 | return 239 | default: 240 | } 241 | 242 | if err != nil { 243 | log.Errorf("accept: %s", err) 244 | continue 245 | } 246 | 247 | go handleConn(conn) 248 | } 249 | }() 250 | 251 | sig := <-quit 252 | log.Warnf("server receive %s and exit", sig.String()) 253 | close(done) 254 | } 255 | -------------------------------------------------------------------------------- /internal/server/server_test.go: -------------------------------------------------------------------------------- 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 server 19 | 20 | import ( 21 | "bytes" 22 | "encoding/binary" 23 | "net" 24 | "os" 25 | "syscall" 26 | "testing" 27 | "time" 28 | 29 | hrc "github.com/api7/ext-plugin-proto/go/A6/HTTPReqCall" 30 | "github.com/stretchr/testify/assert" 31 | 32 | "github.com/apache/apisix-go-plugin-runner/internal/plugin" 33 | "github.com/apache/apisix-go-plugin-runner/internal/util" 34 | ) 35 | 36 | func TestGetSockAddr(t *testing.T) { 37 | os.Unsetenv(SockAddrEnv) 38 | assert.Equal(t, "", getSockAddr()) 39 | 40 | os.Setenv(SockAddrEnv, "unix:/tmp/x.sock") 41 | assert.Equal(t, "/tmp/x.sock", getSockAddr()) 42 | } 43 | 44 | func TestGetConfCacheTTL(t *testing.T) { 45 | os.Unsetenv(ConfCacheTTLEnv) 46 | assert.Equal(t, 4320*time.Second, getConfCacheTTL()) 47 | 48 | os.Setenv(ConfCacheTTLEnv, "12") 49 | assert.Equal(t, 14*time.Second, getConfCacheTTL()) 50 | 51 | os.Setenv(ConfCacheTTLEnv, "1a") 52 | assert.Equal(t, time.Duration(0), getConfCacheTTL()) 53 | } 54 | 55 | func TestDispatchRPC_UnknownType(t *testing.T) { 56 | bd, ty := dispatchRPC(126, []byte(""), nil) 57 | err := UnknownType{126} 58 | expectBd := ReportError(err) 59 | assert.Equal(t, expectBd.FinishedBytes(), bd.FinishedBytes()) 60 | assert.Equal(t, ty, byte(util.RPCError)) 61 | } 62 | 63 | func TestDispatchRPC_KnownType(t *testing.T) { 64 | bd := util.GetBuilder() 65 | hrc.ReqStart(bd) 66 | hrc.ReqAddConfToken(bd, 1) 67 | r := hrc.ReqEnd(bd) 68 | bd.Finish(r) 69 | 70 | _, ty := dispatchRPC(util.RPCHTTPReqCall, bd.FinishedBytes(), nil) 71 | assert.Equal(t, ty, byte(util.RPCError)) 72 | } 73 | 74 | func TestDispatchRPC_OutTooLarge(t *testing.T) { 75 | builder := util.GetBuilder() 76 | bodyVec := builder.CreateByteVector(make([]byte, util.MaxDataSize+1)) 77 | hrc.StopStart(builder) 78 | hrc.StopAddBody(builder, bodyVec) 79 | stop := hrc.StopEnd(builder) 80 | 81 | hrc.RespStart(builder) 82 | hrc.RespAddId(builder, 1) 83 | hrc.RespAddActionType(builder, hrc.ActionStop) 84 | hrc.RespAddAction(builder, stop) 85 | res := hrc.RespEnd(builder) 86 | builder.Finish(res) 87 | } 88 | 89 | func TestRun(t *testing.T) { 90 | path := "/tmp/x.sock" 91 | addr := "unix:" + path 92 | os.Setenv(SockAddrEnv, addr) 93 | os.Setenv(ConfCacheTTLEnv, "60") 94 | 95 | go func() { 96 | Run() 97 | }() 98 | 99 | time.Sleep(100 * time.Millisecond) 100 | 101 | stat, err := os.Stat(path) 102 | assert.True(t, stat.Mode().Perm() == 0766) 103 | 104 | header := make([]byte, 4) 105 | binary.BigEndian.PutUint32(header, uint32(32)) 106 | header[0] = 1 107 | cases := []struct { 108 | header []byte 109 | }{ 110 | // dad header 111 | {[]byte("a")}, 112 | // header without body 113 | {header}, 114 | // header without body truncated 115 | {append(header, 32)}, 116 | // header with bad body 117 | {append(header, bytes.Repeat([]byte{1, 2}, 16)...)}, 118 | } 119 | 120 | for _, c := range cases { 121 | conn, err := net.DialTimeout("unix", addr[len("unix:"):], 1*time.Second) 122 | assert.NotNil(t, conn, err) 123 | defer conn.Close() 124 | util.WriteBytes(conn, c.header, len(c.header)) 125 | } 126 | 127 | syscall.Kill(syscall.Getpid(), syscall.SIGINT) 128 | time.Sleep(10 * time.Millisecond) 129 | 130 | _, err = os.Stat(path) 131 | assert.True(t, os.IsNotExist(err)) 132 | } 133 | 134 | func init() { 135 | plugin.InitConfCache(time.Second) 136 | } 137 | -------------------------------------------------------------------------------- /internal/util/msg.go: -------------------------------------------------------------------------------- 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 util 19 | 20 | import ( 21 | "fmt" 22 | "io" 23 | "net" 24 | 25 | flatbuffers "github.com/google/flatbuffers/go" 26 | 27 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 28 | ) 29 | 30 | const ( 31 | HeaderLen = 4 32 | MaxDataSize = 2<<24 - 1 33 | ) 34 | 35 | const ( 36 | RPCError = iota 37 | RPCPrepareConf 38 | RPCHTTPReqCall 39 | RPCExtraInfo 40 | RPCHTTPRespCall 41 | ) 42 | 43 | type RPCResult struct { 44 | Err error 45 | Builder *flatbuffers.Builder 46 | } 47 | 48 | // Use struct if the result is not only []byte 49 | type ExtraInfoResult []byte 50 | 51 | func ReadErr(n int, err error, required int) bool { 52 | if 0 < n && n < required { 53 | err = fmt.Errorf("truncated, only get the first %d bytes", n) 54 | } 55 | if err != nil { 56 | if err != io.EOF { 57 | log.Errorf("read: %s", err) 58 | } 59 | return true 60 | } 61 | return false 62 | } 63 | 64 | func WriteErr(n int, err error) { 65 | if err != nil { 66 | log.Errorf("write: %s", err) 67 | } 68 | } 69 | 70 | func ReadBytes(c net.Conn, b []byte, n int) (int, error) { 71 | l := 0 72 | for l < n { 73 | tmp, err := c.Read(b[l:]) 74 | if err != nil { 75 | return l + tmp, err 76 | } 77 | l += tmp 78 | } 79 | return l, nil 80 | } 81 | 82 | func WriteBytes(c net.Conn, b []byte, n int) (int, error) { 83 | l := 0 84 | for l < n { 85 | tmp, err := c.Write(b[l:]) 86 | if err != nil { 87 | return l + tmp, err 88 | } 89 | l += tmp 90 | } 91 | return l, nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/util/msg_test.go: -------------------------------------------------------------------------------- 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 util 19 | 20 | import ( 21 | "math/rand" 22 | "net" 23 | "testing" 24 | "time" 25 | 26 | "github.com/stretchr/testify/assert" 27 | ) 28 | 29 | func TestReadAndWriteBytes(t *testing.T) { 30 | path := "/tmp/test.sock" 31 | server, err := net.Listen("unix", path) 32 | assert.NoError(t, err) 33 | defer server.Close() 34 | 35 | // transfer large enough data 36 | n := 10000000 37 | 38 | const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 39 | in := make([]byte, n) 40 | for i := range in { 41 | in[i] = letterBytes[rand.Intn(len(letterBytes))] 42 | } 43 | 44 | go func() { 45 | client, err := net.DialTimeout("unix", path, 1*time.Second) 46 | assert.NoError(t, err) 47 | defer client.Close() 48 | WriteBytes(client, in, len(in)) 49 | }() 50 | 51 | fd, err := server.Accept() 52 | assert.NoError(t, err) 53 | out := make([]byte, n) 54 | ReadBytes(fd, out, n) 55 | assert.Equal(t, in, out) 56 | } 57 | -------------------------------------------------------------------------------- /internal/util/pool.go: -------------------------------------------------------------------------------- 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 util 19 | 20 | import ( 21 | "sync" 22 | 23 | flatbuffers "github.com/google/flatbuffers/go" 24 | ) 25 | 26 | var builderPool = sync.Pool{ 27 | New: func() interface{} { 28 | return flatbuffers.NewBuilder(256) 29 | }, 30 | } 31 | 32 | func GetBuilder() *flatbuffers.Builder { 33 | return builderPool.Get().(*flatbuffers.Builder) 34 | } 35 | 36 | func PutBuilder(b *flatbuffers.Builder) { 37 | b.Reset() 38 | builderPool.Put(b) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/common/error.go: -------------------------------------------------------------------------------- 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 common 19 | 20 | import "errors" 21 | 22 | var ( 23 | ErrConnClosed = errors.New("The connection is closed") 24 | ) 25 | -------------------------------------------------------------------------------- /pkg/http/http.go: -------------------------------------------------------------------------------- 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 http 19 | 20 | import ( 21 | "context" 22 | "net" 23 | "net/http" 24 | "net/url" 25 | ) 26 | 27 | // Request represents the HTTP request received by APISIX. 28 | // We don't use net/http's Request because it doesn't suit our purpose. 29 | // Take `Request.Header` as an example: 30 | // 31 | // 1. We need to record any change to the request headers. As the Request.Header 32 | // is not an interface, there is not way to inject our special tracker. 33 | // 34 | // 2. As the author of fasthttp pointed out, "headers are stored in a map[string][]string. 35 | // So the server must parse all the headers, ...". The official API is suboptimal, which 36 | // is even worse in our case as it is not a real HTTP server. 37 | type Request interface { 38 | // ID returns the request id 39 | ID() uint32 40 | 41 | // SrcIP returns the client's IP 42 | SrcIP() net.IP 43 | // Method returns the HTTP method (GET, POST, PUT, etc.) 44 | Method() string 45 | // Path returns the path part of the client's URI (without query string and the other parts) 46 | // It won't be equal to the one in the Request-Line sent by the client if it has 47 | // been rewritten by APISIX 48 | Path() []byte 49 | // SetPath is the setter for Path 50 | SetPath([]byte) 51 | // Header returns the HTTP headers 52 | Header() Header 53 | // Args returns the query string 54 | Args() url.Values 55 | 56 | // Var returns the value of a Nginx variable, like `r.Var("request_time")` 57 | // 58 | // To fetch the value, the runner will look up the request's cache first. If not found, 59 | // the runner will ask it from the APISIX. If the RPC call is failed, an error in 60 | // pkg/common.ErrConnClosed type is returned. 61 | Var(name string) ([]byte, error) 62 | 63 | // Body returns HTTP request body 64 | // 65 | // To fetch the value, the runner will look up the request's cache first. If not found, 66 | // the runner will ask it from the APISIX. If the RPC call is failed, an error in 67 | // pkg/common.ErrConnClosed type is returned. 68 | Body() ([]byte, error) 69 | 70 | // SetBody rewrites the original request body 71 | SetBody([]byte) 72 | 73 | // Context returns the request's context. 74 | // 75 | // The returned context is always non-nil; it defaults to the 76 | // background context. 77 | // 78 | // For run plugin, the context controls cancellation. 79 | Context() context.Context 80 | // RespHeader returns an http.Header which allows you to add or set response headers before reaching the upstream. 81 | // Some built-in headers would not take effect, like `connection`,`content-length`,`transfer-encoding`,`location,server`,`www-authenticate`,`content-encoding`,`content-type`,`content-location` and `content-language` 82 | RespHeader() http.Header 83 | } 84 | 85 | // Response represents the HTTP response from the upstream received by APISIX. 86 | // In order to avoid semantic misunderstanding, 87 | // we also use Response to represent the rewritten response from Plugin Runner. 88 | // Therefore, any instance that implements the Response interface will be readable and rewritable. 89 | type Response interface { 90 | // ID returns the request id 91 | ID() uint32 92 | 93 | // StatusCode returns the response code 94 | StatusCode() int 95 | 96 | // Header returns the response header. 97 | // 98 | // It allows you to add or set response headers before reaching the client. 99 | Header() Header 100 | 101 | // Var returns the value of a Nginx variable, like `r.Var("request_time")` 102 | // 103 | // To fetch the value, the runner will look up the request's cache first. If not found, 104 | // the runner will ask it from the APISIX. If the RPC call is failed, an error in 105 | // pkg/common.ErrConnClosed type is returned. 106 | Var(name string) ([]byte, error) 107 | 108 | // ReadBody returns origin HTTP response body 109 | // 110 | // To fetch the value, the runner will look up the request's cache first. If not found, 111 | // the runner will ask it from the APISIX. If the RPC call is failed, an error in 112 | // pkg/common.ErrConnClosed type is returned. 113 | // 114 | // It was not named `Body` 115 | // because `Body` was already occupied in earlier interface implementations. 116 | ReadBody() ([]byte, error) 117 | 118 | // Write rewrites the origin response data. 119 | // 120 | // Unlike `ResponseWriter.Write`, we don't need to WriteHeader(http.StatusOK) 121 | // before writing the data 122 | // Because APISIX will convert code 0 to 200. 123 | Write(b []byte) (int, error) 124 | 125 | // WriteHeader rewrites the origin response StatusCode 126 | // 127 | // WriteHeader can't override written status. 128 | WriteHeader(statusCode int) 129 | } 130 | 131 | // Header is like http.Header, but only implements the subset of its methods 132 | type Header interface { 133 | // Set sets the header entries associated with key to the single element value. 134 | // It replaces any existing values associated with key. 135 | // The key is case insensitive 136 | Set(key, value string) 137 | 138 | // Del deletes the values associated with key. The key is case insensitive 139 | Del(key string) 140 | 141 | // Get gets the first value associated with the given key. 142 | // If there are no values associated with the key, Get returns "". 143 | // It is case insensitive 144 | Get(key string) string 145 | 146 | // View returns the internal structure. It is expected for read operations. Any write operation 147 | // won't be recorded 148 | //Deprecated: refactoring 149 | View() http.Header 150 | 151 | // TODO: support Add 152 | } 153 | -------------------------------------------------------------------------------- /pkg/httptest/recorder.go: -------------------------------------------------------------------------------- 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 httptest 19 | 20 | import ( 21 | "bytes" 22 | "net/http" 23 | 24 | pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http" 25 | ) 26 | 27 | // ResponseRecorder is an implementation of pkgHTTP.Response that 28 | // records its mutations for later inspection in tests. 29 | type ResponseRecorder struct { 30 | // Code is the HTTP response code set at initialization. 31 | Code int 32 | 33 | // HeaderMap contains the headers explicitly set by the Handler. 34 | // It is an internal detail. 35 | HeaderMap pkgHTTP.Header 36 | 37 | // Body is the buffer to which the Handler's Write calls are sent. 38 | // If nil, the Writes are silently discarded. 39 | Body *bytes.Buffer 40 | 41 | // OriginBody is the response body received by APISIX from upstream. 42 | OriginBody []byte 43 | 44 | Vars map[string][]byte 45 | 46 | statusCode int 47 | id uint32 48 | } 49 | 50 | // NewRecorder returns an initialized ResponseRecorder. 51 | func NewRecorder() *ResponseRecorder { 52 | return &ResponseRecorder{ 53 | HeaderMap: newHeader(), 54 | Body: new(bytes.Buffer), 55 | } 56 | } 57 | 58 | // ID is APISIX rpc's id. 59 | func (rw *ResponseRecorder) ID() uint32 { 60 | return rw.id 61 | } 62 | 63 | // StatusCode returns the response code. 64 | // 65 | // Note that if a Handler never calls WriteHeader, 66 | // this will be initial status code, rather than the implicit 67 | // http.StatusOK. 68 | func (rw *ResponseRecorder) StatusCode() int { 69 | if rw.statusCode == 0 { 70 | return rw.Code 71 | } 72 | 73 | return rw.statusCode 74 | } 75 | 76 | // Header implements pkgHTTP.Response. It returns the response 77 | // headers to mutate within a handler. 78 | func (rw *ResponseRecorder) Header() pkgHTTP.Header { 79 | m := rw.HeaderMap 80 | if m == nil { 81 | rw.HeaderMap = newHeader() 82 | } 83 | return m 84 | } 85 | 86 | // Write implements pkgHTTP.Response. 87 | // The data in buf is written to rw.Body, if not nil. 88 | func (rw *ResponseRecorder) Write(buf []byte) (int, error) { 89 | if rw.Body == nil { 90 | rw.Body = &bytes.Buffer{} 91 | } 92 | return rw.Body.Write(buf) 93 | } 94 | 95 | // Var implements pkgHTTP.Response. 96 | func (rw *ResponseRecorder) Var(key string) ([]byte, error) { 97 | if rw.Vars == nil { 98 | rw.Vars = make(map[string][]byte) 99 | } 100 | return rw.Vars[key], nil 101 | } 102 | 103 | // ReadBody implements pkgHTTP.Response. 104 | func (rw *ResponseRecorder) ReadBody() ([]byte, error) { 105 | if rw.OriginBody == nil { 106 | rw.OriginBody = make([]byte, 0) 107 | } 108 | return rw.OriginBody, nil 109 | } 110 | 111 | // WriteHeader implements pkgHTTP.Response. 112 | // The statusCode is only allowed to be written once. 113 | func (rw *ResponseRecorder) WriteHeader(code int) { 114 | if rw.statusCode != 0 { 115 | return 116 | } 117 | 118 | rw.statusCode = code 119 | } 120 | 121 | type Header struct { 122 | http.Header 123 | } 124 | 125 | func (h *Header) View() http.Header { 126 | return h.Header 127 | } 128 | 129 | func newHeader() *Header { 130 | return &Header{ 131 | Header: http.Header{}, 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /pkg/log/log.go: -------------------------------------------------------------------------------- 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 log 19 | 20 | import ( 21 | "os" 22 | "sync" 23 | 24 | "go.uber.org/zap" 25 | "go.uber.org/zap/zapcore" 26 | ) 27 | 28 | var ( 29 | logger *zap.SugaredLogger 30 | 31 | loggerInit sync.Once 32 | ) 33 | 34 | func SetLogger(l *zap.SugaredLogger) { 35 | logger = l 36 | } 37 | 38 | func NewLogger(level zapcore.Level, out zapcore.WriteSyncer) { 39 | var atomicLevel = zap.NewAtomicLevel() 40 | atomicLevel.SetLevel(level) 41 | 42 | core := zapcore.NewCore( 43 | zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()), 44 | out, 45 | atomicLevel) 46 | lg := zap.New(core, zap.AddStacktrace(zap.ErrorLevel), zap.AddCaller(), zap.AddCallerSkip(1)) 47 | logger = lg.Sugar() 48 | } 49 | 50 | func GetLogger() *zap.SugaredLogger { 51 | loggerInit.Do(func() { 52 | if logger == nil { 53 | // logger is not initialized, for example, running `go test` 54 | NewLogger(zapcore.InfoLevel, os.Stdout) 55 | } 56 | }) 57 | return logger 58 | } 59 | 60 | func Debugf(template string, args ...interface{}) { 61 | GetLogger().Debugf(template, args...) 62 | } 63 | 64 | func Infof(template string, args ...interface{}) { 65 | GetLogger().Infof(template, args...) 66 | } 67 | 68 | func Warnf(template string, args ...interface{}) { 69 | GetLogger().Warnf(template, args...) 70 | } 71 | 72 | func Errorf(template string, args ...interface{}) { 73 | GetLogger().Errorf(template, args...) 74 | } 75 | 76 | func Fatalf(template string, args ...interface{}) { 77 | GetLogger().Fatalf(template, args...) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/plugin/plugin.go: -------------------------------------------------------------------------------- 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 plugin 19 | 20 | import ( 21 | "net/http" 22 | 23 | "github.com/apache/apisix-go-plugin-runner/internal/plugin" 24 | pkgHTTP "github.com/apache/apisix-go-plugin-runner/pkg/http" 25 | ) 26 | 27 | // Plugin represents the Plugin 28 | type Plugin interface { 29 | // Name returns the plugin name 30 | Name() string 31 | 32 | // ParseConf is the method to parse given plugin configuration. When the 33 | // configuration can't be parsed, it will be skipped. 34 | ParseConf(in []byte) (conf interface{}, err error) 35 | 36 | // RequestFilter is the method to handle request. 37 | // It is like the `http.ServeHTTP`, plus the ctx and the configuration created by 38 | // ParseConf. 39 | // 40 | // When the `w` is written, the execution of plugin chain will be stopped. 41 | // We don't use onion model like Gin/Caddy because we don't serve the whole request lifecycle 42 | // inside the runner. The plugin is only a filter running at one stage. 43 | RequestFilter(conf interface{}, w http.ResponseWriter, r pkgHTTP.Request) 44 | 45 | // ResponseFilter is the method to handle response. 46 | // This filter is currently only pre-defined and has not been implemented. 47 | ResponseFilter(conf interface{}, w pkgHTTP.Response) 48 | } 49 | 50 | // RegisterPlugin register a plugin. Plugin which has the same name can't be registered twice. 51 | // This method should be called before calling `runner.Run`. 52 | func RegisterPlugin(p Plugin) error { 53 | return plugin.RegisterPlugin(p.Name(), p.ParseConf, p.RequestFilter, p.ResponseFilter) 54 | } 55 | 56 | // DefaultPlugin provides the no-op implementation of the Plugin interface. 57 | type DefaultPlugin struct{} 58 | 59 | func (*DefaultPlugin) RequestFilter(interface{}, http.ResponseWriter, pkgHTTP.Request) {} 60 | func (*DefaultPlugin) ResponseFilter(interface{}, pkgHTTP.Response) {} 61 | -------------------------------------------------------------------------------- /pkg/runner/runner.go: -------------------------------------------------------------------------------- 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 runner 19 | 20 | import ( 21 | "os" 22 | 23 | "go.uber.org/zap" 24 | "go.uber.org/zap/zapcore" 25 | 26 | "github.com/apache/apisix-go-plugin-runner/internal/server" 27 | "github.com/apache/apisix-go-plugin-runner/pkg/log" 28 | ) 29 | 30 | // RunnerConfig is the configuration of the runner 31 | type RunnerConfig struct { 32 | // LogLevel is the level of log, default to `zapcore.InfoLevel` 33 | LogLevel zapcore.Level 34 | // LogOutput is the output of log, default to `os.Stdout` 35 | LogOutput zapcore.WriteSyncer 36 | // Logger will be reused by the framework when it is not nil. 37 | Logger *zap.SugaredLogger 38 | } 39 | 40 | // Run starts the runner and listen the socket configured by environment variable "APISIX_LISTEN_ADDRESS" 41 | func Run(cfg RunnerConfig) { 42 | if cfg.LogOutput == nil { 43 | cfg.LogOutput = os.Stdout 44 | } 45 | 46 | if cfg.Logger == nil { 47 | log.NewLogger(cfg.LogLevel, cfg.LogOutput) 48 | } else { 49 | log.SetLogger(cfg.Logger) 50 | } 51 | 52 | server.Run() 53 | } 54 | -------------------------------------------------------------------------------- /tests/e2e/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/apache/apisix-go-plugin-runner/tests/e2e 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/gavv/httpexpect/v2 v2.3.1 7 | github.com/onsi/ginkgo v1.16.5 8 | github.com/onsi/gomega v1.18.1 9 | ) 10 | 11 | require ( 12 | github.com/ajg/form v1.5.1 // indirect 13 | github.com/andybalholm/brotli v1.0.4 // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/fatih/structs v1.0.0 // indirect 16 | github.com/fsnotify/fsnotify v1.4.9 // indirect 17 | github.com/google/go-querystring v1.0.0 // indirect 18 | github.com/gorilla/websocket v1.4.2 // indirect 19 | github.com/imkira/go-interpol v1.0.0 // indirect 20 | github.com/klauspost/compress v1.15.0 // indirect 21 | github.com/nxadm/tail v1.4.8 // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | github.com/sergi/go-diff v1.0.0 // indirect 24 | github.com/stretchr/testify v1.5.1 // indirect 25 | github.com/valyala/bytebufferpool v1.0.0 // indirect 26 | github.com/valyala/fasthttp v1.34.0 // indirect 27 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect 28 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 29 | github.com/xeipuuv/gojsonschema v1.1.0 // indirect 30 | github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect 31 | github.com/yudai/gojsondiff v1.0.0 // indirect 32 | github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect 33 | golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect 34 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f // indirect 35 | golang.org/x/text v0.3.8 // indirect 36 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 37 | gopkg.in/yaml.v2 v2.4.0 // indirect 38 | moul.io/http2curl v1.0.1-0.20190925090545-5cd742060b0e // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /tests/e2e/plugins/plugins_fault_injection_test.go: -------------------------------------------------------------------------------- 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("Fault-injection plugin", func() { 30 | table.DescribeTable("tries to test fault-injection 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":"/test/go/runner/faultinjection", 40 | "plugins":{ 41 | "ext-plugin-pre-req":{ 42 | "conf":[ 43 | { 44 | "name":"fault-injection", 45 | "value":"{\"http_status\":400,\"body\":\"hello\"}" 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("Test if fault-injection plugin work.", tools.HttpTestCase{ 61 | Object: tools.GetA6DPExpect(), 62 | Method: http.MethodGet, 63 | Path: "/test/go/runner/faultinjection", 64 | ExpectBody: []string{"hello"}, 65 | ExpectStatus: http.StatusBadRequest, 66 | }), 67 | ) 68 | }) 69 | -------------------------------------------------------------------------------- /tests/e2e/plugins/plugins_limit_req_test.go: -------------------------------------------------------------------------------- 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("Limit-req Plugin", func() { 30 | table.DescribeTable("tries to test limit-req 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":"/test/go/runner/limitreq", 40 | "plugins":{ 41 | "ext-plugin-pre-req":{ 42 | "conf":[ 43 | { 44 | "name":"limit-req", 45 | "value":"{\"rate\":5,\"burst\":1}" 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("Test if limit-req plugin work.", tools.HttpTestCase{ 61 | Object: tools.GetA6DPExpect(), 62 | Method: http.MethodGet, 63 | Path: "/test/go/runner/limitreq", 64 | ExpectStatus: http.StatusOK, 65 | }), 66 | ) 67 | }) 68 | -------------------------------------------------------------------------------- /tests/e2e/plugins/plugins_request_body_rewrite_test.go: -------------------------------------------------------------------------------- 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 | }) 70 | -------------------------------------------------------------------------------- /tests/e2e/plugins/plugins_response_rewrite_test.go: -------------------------------------------------------------------------------- 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("ResponseRewrite Plugin", func() { 30 | table.DescribeTable("tries to test ResponseRewrite 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":"/test/go/runner/say", 40 | "plugins":{ 41 | "ext-plugin-post-resp":{ 42 | "conf":[ 43 | { 44 | "name":"response-rewrite", 45 | "value":"{\"headers\":{\"X-Server-Id\":\"9527\"},\"body\":\"response 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 response.", tools.HttpTestCase{ 61 | Object: tools.GetA6DPExpect(), 62 | Method: http.MethodGet, 63 | Path: "/test/go/runner/say", 64 | ExpectBody: []string{"response rewrite"}, 65 | ExpectStatus: http.StatusOK, 66 | ExpectHeaders: map[string]string{ 67 | "X-Resp-A6-Runner": "Go", 68 | "X-Server-Id": "9527", 69 | }, 70 | }), 71 | /* 72 | { 73 | .... 74 | "filters": [ 75 | { 76 | "regex": "world", 77 | "scope": "global", 78 | "replace": "golang" 79 | }, 80 | { 81 | "regex": "hello", 82 | "scope": "once", 83 | "replace": "nice" 84 | } 85 | ] 86 | "body":"response rewrite" 87 | } 88 | */ 89 | table.Entry("Config APISIX.", tools.HttpTestCase{ 90 | Object: tools.GetA6CPExpect(), 91 | Method: http.MethodPut, 92 | Path: "/apisix/admin/routes/1", 93 | Body: `{ 94 | "uri":"/echo", 95 | "plugins":{ 96 | "ext-plugin-post-resp":{ 97 | "conf":[ 98 | { 99 | "name":"response-rewrite", 100 | "value":"{\"headers\":{\"X-Server-Id\":\"9527\"},\"filters\":[{\"regex\":\"world\",\"scope\":\"global\",\"replace\":\"golang\"},{\"regex\":\"hello\",\"scope\":\"once\",\"replace\":\"nice\"}],\"body\":\"response rewrite\"}" 101 | } 102 | ] 103 | } 104 | }, 105 | "upstream":{ 106 | "nodes":{ 107 | "web:8888":1 108 | }, 109 | "type":"roundrobin" 110 | } 111 | }`, 112 | Headers: map[string]string{"X-API-KEY": tools.GetAdminToken()}, 113 | ExpectStatusRange: httpexpect.Status2xx, 114 | }), 115 | table.Entry("Should replace response.", tools.HttpTestCase{ 116 | Object: tools.GetA6DPExpect(), 117 | Method: http.MethodGet, 118 | Path: "/echo", 119 | Body: "hello hello world world", 120 | ExpectBody: []string{"nice hello golang golang"}, 121 | ExpectStatus: http.StatusOK, 122 | ExpectHeaders: map[string]string{ 123 | "X-Resp-A6-Runner": "Go", 124 | "X-Server-Id": "9527", 125 | }, 126 | }), 127 | ) 128 | }) 129 | -------------------------------------------------------------------------------- /tests/e2e/plugins/plugins_say_test.go: -------------------------------------------------------------------------------- 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("Say Plugin", func() { 30 | table.DescribeTable("tries to test say 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":"/test/go/runner/say", 40 | "plugins":{ 41 | "ext-plugin-pre-req":{ 42 | "conf":[ 43 | { 44 | "name":"say", 45 | "value":"{\"body\":\"hello\"}" 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 return hello.", tools.HttpTestCase{ 61 | Object: tools.GetA6DPExpect(), 62 | Method: http.MethodGet, 63 | Path: "/test/go/runner/say", 64 | ExpectBody: []string{"hello"}, 65 | ExpectStatus: http.StatusOK, 66 | }), 67 | ) 68 | }) 69 | -------------------------------------------------------------------------------- /tests/e2e/plugins/plugins_suite_test.go: -------------------------------------------------------------------------------- 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 | "testing" 22 | 23 | "github.com/onsi/ginkgo" 24 | "github.com/onsi/gomega" 25 | ) 26 | 27 | func TestPlugins(t *testing.T) { 28 | gomega.RegisterFailHandler(ginkgo.Fail) 29 | ginkgo.RunSpecs(t, "Plugins Suite") 30 | } 31 | -------------------------------------------------------------------------------- /tests/e2e/tools/tools.go: -------------------------------------------------------------------------------- 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 tools 19 | 20 | import ( 21 | "net/http" 22 | "strings" 23 | "time" 24 | 25 | "github.com/gavv/httpexpect/v2" 26 | "github.com/onsi/ginkgo" 27 | ) 28 | 29 | var ( 30 | token = "edd1c9f034335f136f87ad84b625c8f1" 31 | A6DPHost = "http://127.0.0.1:9080" 32 | A6CPHost = "http://127.0.0.1:9180" 33 | ) 34 | 35 | func GetAdminToken() string { 36 | return token 37 | } 38 | 39 | func GetA6DPExpect() *httpexpect.Expect { 40 | t := ginkgo.GinkgoT() 41 | return httpexpect.New(t, A6DPHost) 42 | } 43 | 44 | func GetA6CPExpect() *httpexpect.Expect { 45 | t := ginkgo.GinkgoT() 46 | return httpexpect.New(t, A6CPHost) 47 | } 48 | 49 | type HttpTestCase struct { 50 | Object *httpexpect.Expect 51 | Method string 52 | Path string 53 | Query string 54 | Body string 55 | Headers map[string]string 56 | ExpectStatus int 57 | ExpectStatusRange httpexpect.StatusRange 58 | ExpectCode int 59 | ExpectBody interface{} 60 | ExpectHeaders map[string]string 61 | Sleep time.Duration //ms 62 | } 63 | 64 | func RunTestCase(htc HttpTestCase) { 65 | var req *httpexpect.Request 66 | expect := htc.Object 67 | switch htc.Method { 68 | case http.MethodGet: 69 | req = expect.GET(htc.Path) 70 | case http.MethodPost: 71 | req = expect.POST(htc.Path) 72 | case http.MethodPut: 73 | req = expect.PUT(htc.Path) 74 | case http.MethodDelete: 75 | req = expect.DELETE(htc.Path) 76 | case http.MethodOptions: 77 | req = expect.OPTIONS(htc.Path) 78 | default: 79 | } 80 | 81 | if req == nil { 82 | panic("init request failed") 83 | } 84 | 85 | if htc.Sleep == 0 { 86 | time.Sleep(time.Duration(100) * time.Millisecond) 87 | } else { 88 | time.Sleep(htc.Sleep) 89 | } 90 | 91 | if len(htc.Query) > 0 { 92 | req.WithQueryString(htc.Query) 93 | } 94 | 95 | setContentType := false 96 | for hk, hv := range htc.Headers { 97 | req.WithHeader(hk, hv) 98 | if strings.ToLower(hk) == "content-type" { 99 | setContentType = true 100 | } 101 | } 102 | 103 | if !setContentType { 104 | req.WithHeader("Content-Type", "application/json") 105 | } 106 | 107 | if len(htc.Body) > 0 { 108 | req.WithText(htc.Body) 109 | } 110 | 111 | resp := req.Expect() 112 | 113 | if htc.ExpectStatus != 0 { 114 | resp.Status(htc.ExpectStatus) 115 | } 116 | 117 | if htc.ExpectStatusRange > 0 { 118 | resp.StatusRange(htc.ExpectStatusRange) 119 | } 120 | 121 | if htc.ExpectHeaders != nil { 122 | for hk, hv := range htc.ExpectHeaders { 123 | resp.Header(hk).Equal(hv) 124 | } 125 | } 126 | 127 | if htc.ExpectBody != nil { 128 | if body, ok := htc.ExpectBody.(string); ok { 129 | if len(body) == 0 { 130 | resp.Body().Empty() 131 | } else { 132 | resp.Body().Contains(body) 133 | } 134 | } 135 | 136 | if bodies, ok := htc.ExpectBody.([]string); ok && len(bodies) > 0 { 137 | for _, b := range bodies { 138 | resp.Body().Contains(b) 139 | } 140 | } 141 | } 142 | } 143 | --------------------------------------------------------------------------------